Files
nucleic-remote-ios/NucleicRemote/NucleicRemote/Views/SessionsView.swift
T
abkslmandClaude Opus 4.8 a70b21cd3e 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]>
2026-06-13 02:20:53 -07:00

119 lines
5.6 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
import NucleicProtocol
/// Attention-first home (UX_IOS §3): NEEDS YOU pinned at top, then RUNNING, then DONE. Archived
/// chats are hidden (matching the Mac sidebar); disposition splits "needs you" from "done".
struct SessionsView: View {
@EnvironmentObject var store: RemoteStore
@State private var showArchived = false
private var grouped: [(title: String, rows: [WireSessionSummary])] {
let pool = (showArchived ? store.sessions : store.liveSessions)
.sorted {
let a = StatusStyle.sortRank($0), b = StatusStyle.sortRank($1)
return a == b ? $0.updatedAt > $1.updatedAt : a < b
}
let needs = pool.filter { $0.status.needsYou($0.disposition) }
let running = pool.filter { $0.status == .running || $0.status == .provisioning || $0.status == .idle }
let done = pool.filter { !needs.contains($0) && !running.contains($0) }
return [("Needs you", needs), ("Running", running), ("Done", done)].filter { !$0.rows.isEmpty }
}
var body: some View {
NavigationStack {
Group {
if store.liveSessions.isEmpty && !showArchived {
ContentUnavailableView(
"No sessions", systemImage: "square.stack.3d.up",
description: Text(store.connectivity.isLive
? "Start a session from Home or the Mac to see it here."
: "Waiting to connect to \(store.hostName)…"))
} else {
List {
ForEach(grouped, id: \.title) { group in
Section(group.title.uppercased()) {
ForEach(group.rows, id: \.sessionID) { summary in
NavigationLink {
SessionDetailView(sessionID: summary.sessionID)
} label: {
SessionRow(summary: summary)
}
.swipeActions(edge: .leading) {
Button {
store.setFavorite(summary.sessionID, !summary.favorite)
} label: {
Label("Favorite", systemImage: summary.favorite ? "star.slash" : "star.fill")
}.tint(.yellow)
}
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
store.deleteSession(summary.sessionID)
} label: { Label("Delete", systemImage: "trash") }
Button {
store.setArchived(summary.sessionID, !summary.archived)
} label: {
Label(summary.archived ? "Unarchive" : "Archive",
systemImage: summary.archived ? "tray.and.arrow.up" : "archivebox")
}.tint(.gray)
}
}
}
}
}
.listStyle(.insetGrouped)
.refreshable { store.refreshSessions() }
}
}
.navigationTitle("Sessions")
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
showArchived.toggle()
} label: {
Image(systemName: showArchived ? "archivebox.fill" : "archivebox")
}
}
}
.safeAreaInset(edge: .bottom) { ConnectionChip().padding(.bottom, 8) }
}
}
}
struct SessionRow: View {
let summary: WireSessionSummary
var body: some View {
HStack(spacing: 12) {
Image(systemName: StatusStyle.symbol(summary.status, disposition: summary.disposition))
.foregroundStyle(Palette.status(summary.status, disposition: summary.disposition))
.font(.title3)
.frame(width: 24)
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 5) {
if summary.favorite {
Image(systemName: "star.fill").font(.caption2).foregroundStyle(.yellow)
}
Text(summary.title).font(.body.weight(.medium)).lineLimit(1)
}
Text(summary.projectName).font(.caption).foregroundStyle(.secondary)
}
Spacer()
VStack(alignment: .trailing, spacing: 2) {
if summary.pendingApprovalCount > 0 {
Text("\(summary.pendingApprovalCount)")
.font(.caption2.bold())
.padding(.horizontal, 6).padding(.vertical, 2)
.background(Palette.attention, in: Capsule())
.foregroundStyle(.white)
}
if let diff = summary.diffStat, diff.filesChanged > 0 {
Text("+\(diff.added)/−\(diff.removed)")
.font(.caption.monospacedDigit())
.foregroundStyle(.secondary)
}
}
}
.padding(.vertical, 2)
}
}