From 79a28605b3ff3db2adbece09326024d3412d5b02 Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Wed, 1 Jul 2026 19:01:20 -0700 Subject: [PATCH] =?UTF-8?q?nvrsion:=20Add:=20Fix=20truncation=20of=20?= =?UTF-8?q?=E2=80=9CRun=20now=E2=80=9D=20button=20text=20in=20New=20idea?= =?UTF-8?q?=20composer,=20add=20model=20selector=20support=20for=20Claude?= =?UTF-8?q?=20Fable=205,=20refactor=20Project=20Workflow=20Enhancement,=20?= =?UTF-8?q?add=20Wait=20Button=20Logic,=20update=20iOS=20remote=20to=20mat?= =?UTF-8?q?ch=20Chat=20Message=20Queue=20Display,=20block=20sandboxed/cont?= =?UTF-8?q?rolled=20agents=20from=20running=20git=20commands=20via=20mcp?= =?UTF-8?q?=5F=5Fnucleic=5F=5Fhost=5Fexec=20using=20container-level=20shim?= =?UTF-8?q?s,=20add=20chat=20icon=20animation=20to=20bounce=20when=20chat?= =?UTF-8?q?=20is=20blocked=20for=20>60=20seconds,=20add=20=E2=80=9CCmd+Shi?= =?UTF-8?q?ft+N=E2=80=9D=20shortcut=20to=20open=20a=20floating=20chat=20co?= =?UTF-8?q?mposer=20over=20the=20window,=20remove=20=E2=80=9CRun=20now?= =?UTF-8?q?=E2=80=9D=20button=20from=20=E2=80=9CNew=20idea=E2=80=9D=20comp?= =?UTF-8?q?oser,=20and=20shield=20Git=20command=20execution=20by=20scannin?= =?UTF-8?q?g=20inside=20=E2=80=98-c=E2=80=99=20strings.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nucleic-Promote: 1 Co-authored-by: Nucleic --- .../NucleicRemote/Models/RemoteStore.swift | 2 + .../Views/SessionDetailView.swift | 69 ++++++++++++++++--- 2 files changed, 61 insertions(+), 10 deletions(-) diff --git a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift index b05b117..1698841 100644 --- a/NucleicRemote/NucleicRemote/Models/RemoteStore.swift +++ b/NucleicRemote/NucleicRemote/Models/RemoteStore.swift @@ -291,6 +291,8 @@ final class RemoteStore: ObservableObject { } func integrate(_ id: SessionID, _ mode: IntegrationMode) { send(.integrate(id, mode)) } func interrupt(_ id: SessionID) { send(.interrupt(id)) } + /// Cancel one queued (not-yet-sent) follow-up by id — the phone's per-message ✕. + func cancelQueuedMessage(_ id: SessionID, _ messageID: UUID) { send(.cancelQueuedMessage(id, messageID)) } // Mid-session model / reasoning / automation — the same affordances the Mac header exposes. // `nil` model/effort resets the session to the host/app default. diff --git a/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift b/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift index 0b95763..1fd2c07 100644 --- a/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift +++ b/NucleicRemote/NucleicRemote/Views/SessionDetailView.swift @@ -170,17 +170,16 @@ struct SessionDetailView: View { let running = status == .running let canCompose = running || status == .awaitingInput || status.isTerminal VStack(spacing: 8) { - // A follow-up submitted mid-turn, held by the host to send when the current - // turn finishes. - if let queued = summary?.queuedMessage, !queued.isEmpty { - HStack(spacing: 6) { - Image(systemName: "clock").foregroundStyle(.secondary) - Text(queued).lineLimit(1).truncationMode(.tail) - .foregroundStyle(.secondary) - Spacer(minLength: 8) + // Follow-ups submitted mid-turn, held by the host to send when the current turn + // finishes. Each is shown as its own pill (with its attachments) and — at control + // scope — can be cancelled individually, matching the Mac composer. (They still + // send as one combined turn.) + if let queued = summary?.queuedMessages, !queued.isEmpty { + VStack(spacing: 6) { + ForEach(queued) { message in + queuedMessageRow(message) + } } - .font(.footnote) - .frame(maxWidth: .infinity, alignment: .leading) } if running { HStack(spacing: 8) { @@ -211,6 +210,56 @@ struct SessionDetailView: View { .padding() } } + + /// A queued follow-up shown above the composer: its text (or attachment names) with the + /// attachments as chips, and — at control scope — a ✕ that cancels just this message. + @ViewBuilder + private func queuedMessageRow(_ message: QueuedMessage) -> some View { + VStack(alignment: .leading, spacing: 4) { + HStack(spacing: 6) { + Image(systemName: "clock").foregroundStyle(.secondary) + Text(queuedRowText(message)).lineLimit(1).truncationMode(.tail) + .foregroundStyle(.secondary) + Spacer(minLength: 8) + if store.canControl { + Button { + // Refill the composer for editing (text only — the phone can't stage the + // host's files) unless the user has already started typing; then drop it. + if draft.trimmingCharacters(in: .whitespaces).isEmpty { draft = message.text } + store.cancelQueuedMessage(sessionID, message.id) + } label: { + Image(systemName: "xmark.circle.fill").foregroundStyle(.secondary) + } + .buttonStyle(.plain) + .disabled(!store.connectivity.isLive) + } + } + .font(.footnote) + if !message.attachments.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 6) { + ForEach(message.attachments) { attachment in + Label(attachment.filename, systemImage: "doc") + .font(.caption2) + .lineLimit(1) + .padding(.horizontal, 7).padding(.vertical, 3) + .background(.quaternary, in: .capsule) + } + } + } + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 8).padding(.vertical, 6) + .background(.quaternary.opacity(0.4), in: .rect(cornerRadius: 10)) + } + + /// The one-line label for a queued pill: the message text, or — for an attachment-only + /// message — the attachment names (the chips below repeat them), so it's never blank. + private func queuedRowText(_ message: QueuedMessage) -> String { + if !message.text.isEmpty { return message.text } + return message.attachments.map(\.filename).joined(separator: ", ") + } } struct TranscriptList: View {