Merge nucleic/olive-iris-yak-q2qm into dev
This commit is contained in:
@@ -875,7 +875,8 @@ final class RemoteStore: ObservableObject {
|
||||
private func isQueueableIntent(_ msg: ClientMsg) -> Bool {
|
||||
switch msg {
|
||||
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,
|
||||
.setSessionAutoShip, .setSessionShipBranch, .approvalRespond, .resolveProcessStall:
|
||||
return true
|
||||
@@ -1261,7 +1262,8 @@ final class RemoteStore: ObservableObject {
|
||||
hostName = "Andrew's Mac"
|
||||
grantedScope = .control
|
||||
capabilities = WireCapabilities(
|
||||
canModifyToolInput: true, allowAlwaysScopes: [.session, .toolName], canFetchDiff: true)
|
||||
canModifyToolInput: true, allowAlwaysScopes: [.session, .toolName], canFetchDiff: true,
|
||||
canMarkSessionDone: true)
|
||||
modelCatalog = WireModelCatalog(
|
||||
groups: [
|
||||
[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 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) {
|
||||
send(.deleteSession(id))
|
||||
if id == openSessionID { closeOpen() }
|
||||
@@ -2845,7 +2867,8 @@ final class RemoteStore: ObservableObject {
|
||||
break
|
||||
case .unsubscribe(let id), .interrupt(let id), .deleteSession(let id), .discard(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, _),
|
||||
.sendInput(let id, _), .cancelQueuedMessage(let id, _), .fetchDiff(let id),
|
||||
.resolveProcessStall(let id, _, _),
|
||||
@@ -3066,6 +3089,10 @@ final class RemoteStore: ObservableObject {
|
||||
demoUpdateSession(id) { $0.demoCopy(favorite: favorite) }
|
||||
case .setArchived(let id, let 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):
|
||||
sessions.removeAll { $0.sessionID == id }
|
||||
NotificationRouter.shared.updateBadge(needsYouCount)
|
||||
|
||||
@@ -399,6 +399,16 @@ private struct SplitSidebar: View {
|
||||
} label: {
|
||||
Label("Favorite", systemImage: summary.favorite ? "star.slash" : "star.fill")
|
||||
}.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) {
|
||||
Button(role: .destructive) {
|
||||
|
||||
@@ -322,6 +322,14 @@ struct SessionDetailView: View {
|
||||
|
||||
@ViewBuilder
|
||||
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: {
|
||||
Label("Rename", systemImage: "pencil")
|
||||
}
|
||||
|
||||
@@ -127,8 +127,9 @@ struct SessionsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// One tappable session row with its favorite / delete / archive swipe actions. Shared by
|
||||
/// the live sections (Needs you / Running / Done) and the collapsible Archived section.
|
||||
/// One tappable session row with its mark-done / favorite / delete / archive swipe actions.
|
||||
/// Shared by the live sections (Needs you / Running / Done) and the collapsible Archived
|
||||
/// section.
|
||||
@ViewBuilder
|
||||
private func sessionRow(_ summary: WireSessionSummary) -> some View {
|
||||
NavigationLink(value: summary.sessionID) {
|
||||
@@ -141,6 +142,17 @@ struct SessionsView: View {
|
||||
} label: {
|
||||
Label("Favorite", systemImage: summary.favorite ? "star.slash" : "star.fill")
|
||||
}.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) {
|
||||
Button(role: .destructive) {
|
||||
|
||||
Reference in New Issue
Block a user