Merge nucleic/olive-iris-yak-q2qm into dev

This commit is contained in:
2026-07-28 19:24:22 -07:00
parent 1b022fd835
commit 04590918c0
5 changed files with 67 additions and 11 deletions
@@ -1,5 +1,5 @@
{ {
"originHash" : "6a454662a64d0761bc14f81f12302560e4ac3eba029e64f1e0b0b7f004590631", "originHash" : "290676a4813b700fd9b3b86cca9dedcd7359ac250b5ce400dbc58caed6255b4a",
"pins" : [ "pins" : [
{ {
"identity" : "async-http-client", "identity" : "async-http-client",
@@ -177,8 +177,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio.git", "location" : "https://github.com/apple/swift-nio.git",
"state" : { "state" : {
"revision" : "cd3e1152083706d77b223fb29110e590efcc70c0", "revision" : "0b18836bd8b0162e7e17a995a3fbee20ed8f3b2b",
"version" : "2.101.2" "version" : "2.101.3"
} }
}, },
{ {
@@ -202,10 +202,9 @@
{ {
"identity" : "swift-nio-ssl", "identity" : "swift-nio-ssl",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-nio-ssl.git", "location" : "https://github.com/Joannis/swift-nio-ssl.git",
"state" : { "state" : {
"revision" : "407d82d5b6cc00e1c3fb83a81b1539b70c788c5e", "revision" : "bc3bd90098701c6c1d7aa6275b6911860fe9d439"
"version" : "2.37.1"
} }
}, },
{ {
@@ -875,7 +875,8 @@ final class RemoteStore: ObservableObject {
private func isQueueableIntent(_ msg: ClientMsg) -> Bool { private func isQueueableIntent(_ msg: ClientMsg) -> Bool {
switch msg { switch msg {
case .sendInput, .startChat, .captureTodo, .dispatchTodo, .setTodoStatus, .deleteTodo, case .sendInput, .startChat, .captureTodo, .dispatchTodo, .setTodoStatus, .deleteTodo,
.renameSession, .setFavorite, .setArchived, .deleteSession, .discard, .integrate, .renameSession, .setFavorite, .setArchived, .markSessionDone, .deleteSession,
.discard, .integrate,
.interrupt, .cancelQueuedMessage, .setSessionModel, .setSessionEffort, .setSessionAuto, .interrupt, .cancelQueuedMessage, .setSessionModel, .setSessionEffort, .setSessionAuto,
.setSessionAutoShip, .setSessionShipBranch, .approvalRespond, .resolveProcessStall: .setSessionAutoShip, .setSessionShipBranch, .approvalRespond, .resolveProcessStall:
return true return true
@@ -1261,7 +1262,8 @@ final class RemoteStore: ObservableObject {
hostName = "Andrew's Mac" hostName = "Andrew's Mac"
grantedScope = .control grantedScope = .control
capabilities = WireCapabilities( capabilities = WireCapabilities(
canModifyToolInput: true, allowAlwaysScopes: [.session, .toolName], canFetchDiff: true) canModifyToolInput: true, allowAlwaysScopes: [.session, .toolName], canFetchDiff: true,
canMarkSessionDone: true)
modelCatalog = WireModelCatalog( modelCatalog = WireModelCatalog(
groups: [ groups: [
[WireModelCatalog.Model(sku: "claude-opus-4-8[1m]", displayName: "Opus 4.8", backend: .claudeCode, [WireModelCatalog.Model(sku: "claude-opus-4-8[1m]", displayName: "Opus 4.8", backend: .claudeCode,
@@ -2696,6 +2698,26 @@ final class RemoteStore: ObservableObject {
} }
func setFavorite(_ id: SessionID, _ favorite: Bool) { send(.setFavorite(id, favorite)) } func setFavorite(_ id: SessionID, _ favorite: Bool) { send(.setFavorite(id, favorite)) }
func setArchived(_ id: SessionID, _ archived: Bool) { send(.setArchived(id, archived)) } func setArchived(_ id: SessionID, _ archived: Bool) { send(.setArchived(id, archived)) }
/// Hand-mark a chat "Done" — the phone's counterpart of the Mac sidebar's "Mark Done", for a
/// chat stuck reading "Awaiting Input" whose work is actually finished. The host flips the last
/// turn's disposition to `.completed` (and finalizes the chat the same way its own classifier
/// would, so an autoship-armed chat ships), then broadcasts the refreshed summary. Guarded on
/// `canMarkDone` so a tap can't fire against a chat the host would reject — and so a stale
/// button (the row changed state between render and tap) is inert rather than confusing.
func markSessionDone(_ id: SessionID) {
guard let summary = sessions.first(where: { $0.sessionID == id }), canMarkDone(summary) else { return }
send(.markSessionDone(id))
}
/// Whether "Mark Done" applies to this chat: the row reads "Awaiting Input" (the only state the
/// disposition refines, so anything else is a host-side no-op), it isn't already Done, this
/// device holds control scope, and the owning Mac speaks the verb. Mirrors the Mac sidebar's
/// own gate; a moved-away tombstone is excluded — its chat lives on another Mac now.
func canMarkDone(_ summary: WireSessionSummary) -> Bool {
canControl && capabilities.canMarkSessionDone && summary.movedTo == nil
&& summary.status == .awaitingInput && summary.disposition != .completed
}
func deleteSession(_ id: SessionID) { func deleteSession(_ id: SessionID) {
send(.deleteSession(id)) send(.deleteSession(id))
if id == openSessionID { closeOpen() } if id == openSessionID { closeOpen() }
@@ -2845,7 +2867,8 @@ final class RemoteStore: ObservableObject {
break break
case .unsubscribe(let id), .interrupt(let id), .deleteSession(let id), .discard(let id), case .unsubscribe(let id), .interrupt(let id), .deleteSession(let id), .discard(let id),
.integrate(let id, _), .renameSession(let id, _), .setFavorite(let id, _), .integrate(let id, _), .renameSession(let id, _), .setFavorite(let id, _),
.setArchived(let id, _), .setSessionModel(let id, _), .setSessionEffort(let id, _), .setArchived(let id, _), .markSessionDone(let id),
.setSessionModel(let id, _), .setSessionEffort(let id, _),
.setSessionAuto(let id, _), .setSessionAutoShip(let id, _), .setSessionShipBranch(let id, _), .setSessionAuto(let id, _), .setSessionAutoShip(let id, _), .setSessionShipBranch(let id, _),
.sendInput(let id, _), .cancelQueuedMessage(let id, _), .fetchDiff(let id), .sendInput(let id, _), .cancelQueuedMessage(let id, _), .fetchDiff(let id),
.resolveProcessStall(let id, _, _), .resolveProcessStall(let id, _, _),
@@ -3066,6 +3089,10 @@ final class RemoteStore: ObservableObject {
demoUpdateSession(id) { $0.demoCopy(favorite: favorite) } demoUpdateSession(id) { $0.demoCopy(favorite: favorite) }
case .setArchived(let id, let archived): case .setArchived(let id, let archived):
demoUpdateSession(id) { $0.demoCopy(archived: archived) } demoUpdateSession(id) { $0.demoCopy(archived: archived) }
case .markSessionDone(let id):
// Same shape as the host's: the row keeps its `.awaitingInput` status and just stops
// reading "needs you", because the disposition — not the status — is what refines it.
demoUpdateSession(id) { $0.demoCopy(disposition: .some(.completed)) }
case .deleteSession(let id): case .deleteSession(let id):
sessions.removeAll { $0.sessionID == id } sessions.removeAll { $0.sessionID == id }
NotificationRouter.shared.updateBadge(needsYouCount) NotificationRouter.shared.updateBadge(needsYouCount)
@@ -399,6 +399,16 @@ private struct SplitSidebar: View {
} label: { } label: {
Label("Favorite", systemImage: summary.favorite ? "star.slash" : "star.fill") Label("Favorite", systemImage: summary.favorite ? "star.slash" : "star.fill")
}.tint(.yellow) }.tint(.yellow)
// Behind Favorite, for the same reason as the compact list:
// the first leading button is the full-swipe action, and
// marking done can enqueue an autoship merge.
if store.canMarkDone(summary) {
Button {
store.markSessionDone(summary.sessionID)
} label: {
Label("Mark Done", systemImage: "checkmark.circle")
}.tint(Palette.success)
}
} }
.swipeActions(edge: .trailing) { .swipeActions(edge: .trailing) {
Button(role: .destructive) { Button(role: .destructive) {
@@ -322,6 +322,14 @@ struct SessionDetailView: View {
@ViewBuilder @ViewBuilder
private func sessionActions(_ summary: WireSessionSummary) -> some View { private func sessionActions(_ summary: WireSessionSummary) -> some View {
// "Awaiting Input" that's actually done: flip it to "Done" by hand so a chat stuck on the
// wrong state clears without spending another turn on it. Offered only while the gate holds
// (`store.canMarkDone`), mirroring the Mac sidebar's own condition.
if store.canMarkDone(summary) {
Button { store.markSessionDone(sessionID) } label: {
Label("Mark Done", systemImage: "checkmark.circle")
}
}
Button { renameDraft = summary.title; showRename = true } label: { Button { renameDraft = summary.title; showRename = true } label: {
Label("Rename", systemImage: "pencil") Label("Rename", systemImage: "pencil")
} }
@@ -127,8 +127,9 @@ struct SessionsView: View {
} }
} }
/// One tappable session row with its favorite / delete / archive swipe actions. Shared by /// One tappable session row with its mark-done / favorite / delete / archive swipe actions.
/// the live sections (Needs you / Running / Done) and the collapsible Archived section. /// Shared by the live sections (Needs you / Running / Done) and the collapsible Archived
/// section.
@ViewBuilder @ViewBuilder
private func sessionRow(_ summary: WireSessionSummary) -> some View { private func sessionRow(_ summary: WireSessionSummary) -> some View {
NavigationLink(value: summary.sessionID) { NavigationLink(value: summary.sessionID) {
@@ -141,6 +142,17 @@ struct SessionsView: View {
} label: { } label: {
Label("Favorite", systemImage: summary.favorite ? "star.slash" : "star.fill") Label("Favorite", systemImage: summary.favorite ? "star.slash" : "star.fill")
}.tint(.yellow) }.tint(.yellow)
// "Mark Done" rides behind Favorite deliberately. The leading edge's *first* button is
// also its full-swipe action, and marking done is the same declaration the classifier
// makes — on an autoship-armed chat it enqueues the merge — so it must take a
// deliberate tap, not a flick. Favorite keeps the full swipe it always had.
if store.canMarkDone(summary) {
Button {
store.markSessionDone(summary.sessionID)
} label: {
Label("Mark Done", systemImage: "checkmark.circle")
}.tint(Palette.success)
}
} }
.swipeActions(edge: .trailing) { .swipeActions(edge: .trailing) {
Button(role: .destructive) { Button(role: .destructive) {