nvrsion: Add: Fix truncation of “Run now” button text in New idea composer, add model selector support for Claude Fable 5, refactor Project Workflow Enhancement, add Wait Button Logic, update iOS remote to match Chat Message Queue Display, block sandboxed/controlled agents from running git commands via mcp__nucleic__host_exec using container-level shims, add chat icon animation to bounce when chat is blocked for >60 seconds, add “Cmd+Shift+N” shortcut to open a floating chat composer over the window, remove “Run now” button from “New idea” composer, and shield Git command execution by scanning inside ‘-c’ strings.

Nucleic-Promote: 1
Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
2026-07-01 19:01:20 -07:00
co-authored by Nucleic
parent 85d753d2f7
commit 79a28605b3
2 changed files with 61 additions and 10 deletions
@@ -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 {