M4+: mirror the desktop design language onto mobile (Home/Projects/To-Dos/Sessions)
Design system: Palette (teal accent + the Mac's categorical status colors, status→color refined by turn disposition), StatusStyle glyphs/labels, a card surface modifier, app-wide teal tint. Fixes the reported bug: Sessions now hides archived chats and uses disposition to split NEEDS YOU from DONE (a finished-the-work turn no longer shows as needing you). Rows gain status dots, favorite star, approval-count badge, diffstat; swipe to favorite/archive/delete. New surfaces, full control-scope parity with the Mac: - Home: greeting + day-streak flame, GitHub-style activity grid, stat cards, quick to-dos, and a start-chat composer (project picker + Auto). - Projects: per-project session counts → project detail with a scoped composer. - To-dos: capture, group by project, complete/dispatch/delete. - Session detail: a control menu (rename, favorite, interrupt, integrate merge/squash/rebase, archive, delete). RemoteStore handles the dashboard event + exposes all control intents; claims control scope. A NUCLEIC_DEMO/NUCLEIC_TAB env seam seeds mock state for offline UI preview. iOS app builds for the iOS 27 simulator; Home + Sessions verified rendering via screenshots. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
@@ -8,6 +8,9 @@ struct SessionDetailView: View {
|
||||
let sessionID: SessionID
|
||||
@State private var tab = 0
|
||||
@State private var draft = ""
|
||||
@State private var showRename = false
|
||||
@State private var renameDraft = ""
|
||||
@State private var showIntegrate = false
|
||||
|
||||
private var summary: WireSessionSummary? {
|
||||
store.sessions.first { $0.sessionID == sessionID }
|
||||
@@ -35,10 +38,54 @@ struct SessionDetailView: View {
|
||||
}
|
||||
.navigationTitle(summary?.title ?? "Session")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
if let summary, store.canControl {
|
||||
ToolbarItem(placement: .topBarTrailing) { sessionMenu(summary) }
|
||||
}
|
||||
}
|
||||
.alert("Rename chat", isPresented: $showRename) {
|
||||
TextField("Title", text: $renameDraft)
|
||||
Button("Cancel", role: .cancel) {}
|
||||
Button("Rename") { store.renameSession(sessionID, to: renameDraft) }
|
||||
}
|
||||
.confirmationDialog("Integrate this branch", isPresented: $showIntegrate, titleVisibility: .visible) {
|
||||
Button("Merge") { store.integrate(sessionID, .merge) }
|
||||
Button("Squash & merge") { store.integrate(sessionID, .squash) }
|
||||
Button("Rebase") { store.integrate(sessionID, .rebase) }
|
||||
Button("Cancel", role: .cancel) {}
|
||||
}
|
||||
.onAppear { store.open(sessionID) }
|
||||
.onDisappear { store.closeOpen() }
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func sessionMenu(_ summary: WireSessionSummary) -> some View {
|
||||
Menu {
|
||||
Button { renameDraft = summary.title; showRename = true } label: {
|
||||
Label("Rename", systemImage: "pencil")
|
||||
}
|
||||
Button { store.setFavorite(sessionID, !summary.favorite) } label: {
|
||||
Label(summary.favorite ? "Unfavorite" : "Favorite",
|
||||
systemImage: summary.favorite ? "star.slash" : "star")
|
||||
}
|
||||
if summary.status == .running {
|
||||
Button { store.interrupt(sessionID) } label: { Label("Interrupt", systemImage: "stop.circle") }
|
||||
}
|
||||
Section {
|
||||
Button { showIntegrate = true } label: { Label("Integrate…", systemImage: "arrow.triangle.merge") }
|
||||
Button { store.setArchived(sessionID, !summary.archived) } label: {
|
||||
Label(summary.archived ? "Unarchive" : "Archive",
|
||||
systemImage: summary.archived ? "tray.and.arrow.up" : "archivebox")
|
||||
}
|
||||
}
|
||||
Button(role: .destructive) { store.deleteSession(sessionID) } label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var actionArea: some View {
|
||||
Divider()
|
||||
|
||||
Reference in New Issue
Block a user