Merge nucleic/lucid-drift-otter-zrlk into dev
This commit is contained in:
@@ -473,7 +473,11 @@ struct SessionDetailView: View {
|
|||||||
if running {
|
if running {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
ProgressView().controlSize(.small)
|
ProgressView().controlSize(.small)
|
||||||
Text("Agent is working…").font(.footnote).foregroundStyle(.primary)
|
Text(progressText).font(.footnote).foregroundStyle(.primary)
|
||||||
|
// Cross-fade the label as the agent moves between steps, so a
|
||||||
|
// fast run of tool calls doesn't snap the row's text.
|
||||||
|
.contentTransition(.opacity)
|
||||||
|
.animation(.easeInOut(duration: 0.15), value: progressText)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
@@ -629,6 +633,83 @@ struct SessionDetailView: View {
|
|||||||
if !message.text.isEmpty { return message.text }
|
if !message.text.isEmpty { return message.text }
|
||||||
return message.attachments.map(\.filename).joined(separator: ", ")
|
return message.attachments.map(\.filename).joined(separator: ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A live, human label for what the agent is doing right now — "Reading…", "Running on a macOS
|
||||||
|
/// VM…", "Thinking…" — derived from the newest transcript event, replacing a fixed "Agent is
|
||||||
|
/// working…" that said nothing about a turn that may run for minutes.
|
||||||
|
///
|
||||||
|
/// Derived on-device from the host's own event stream rather than read off a field in the
|
||||||
|
/// summary: the phone already holds those events (they're what the transcript above renders),
|
||||||
|
/// so the label tracks the turn at streaming speed instead of at summary-refresh speed, needs
|
||||||
|
/// no wire field, and works against a host of any version. Mirrors the Mac's
|
||||||
|
/// `SessionDetailView.progressText`; keep the two in step.
|
||||||
|
private var progressText: String {
|
||||||
|
// Blocked on a human, not the agent. The composer's working row is hidden behind the
|
||||||
|
// approval card in that state, but a resolved-but-not-yet-cleared gate can flash through
|
||||||
|
// here — say what's actually being waited on rather than claiming the agent is working.
|
||||||
|
if let approval = store.openApprovals.first {
|
||||||
|
return approval.toolName == AskUserQuestion.toolName
|
||||||
|
? "Waiting for answers…"
|
||||||
|
: "Waiting for approval…"
|
||||||
|
}
|
||||||
|
for event in store.openEvents.reversed() {
|
||||||
|
// Extended-thinking progress pings (`system/thinking_tokens`) arrive as raw events
|
||||||
|
// during the model's reasoning pause — often the long gap after a tool result, before
|
||||||
|
// any visible output. Without this the walk skips every ping and lands back on the last
|
||||||
|
// tool call, freezing the row on e.g. "Running…" for the whole pause, which reads as a
|
||||||
|
// stall even though the model is working.
|
||||||
|
if Self.isThinkingPing(event) { return "Thinking…" }
|
||||||
|
switch event.kind {
|
||||||
|
case .toolCallStarted(let call), .toolCallCompleted(let call):
|
||||||
|
return Self.gerund(for: call.name)
|
||||||
|
case .thinking:
|
||||||
|
return "Thinking…"
|
||||||
|
case .assistantText(let chunk) where chunk.isPartial:
|
||||||
|
return "Writing…"
|
||||||
|
case .toolResult:
|
||||||
|
// The tool has finished (its result landed); the model is now working on what comes
|
||||||
|
// next. Stop here rather than walking back to the call behind it and reporting a
|
||||||
|
// completed tool as still "Running…".
|
||||||
|
return "Working…"
|
||||||
|
case .userText:
|
||||||
|
// Nothing from the agent since the user's message.
|
||||||
|
return "Working…"
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "Working…"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this is a Claude `system/thinking_tokens` progress ping — the mobile echo of
|
||||||
|
/// `TranscriptProjection.parseThinkingTokens`, reduced to the "is it one" half (the phone's
|
||||||
|
/// working row shows no token meter, so the counts themselves go unused).
|
||||||
|
private static func isThinkingPing(_ event: AgentEvent) -> Bool {
|
||||||
|
guard case .raw(let raw) = event.kind else { return false }
|
||||||
|
return event.nativeType == "system/thinking_tokens"
|
||||||
|
|| raw.native["subtype"]?.stringValue == "thinking_tokens"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turns a tool name into a present-progressive status line — "Read" → "Reading…". Unknown
|
||||||
|
/// tools fall back to "Running <name>…" so we never guess a malformed gerund. Mirrors the Mac's
|
||||||
|
/// `SessionDetailView.gerund(for:)`.
|
||||||
|
private static func gerund(for name: String) -> String {
|
||||||
|
switch name {
|
||||||
|
case "Read": return "Reading…"
|
||||||
|
case "Write": return "Writing…"
|
||||||
|
case "Edit", "MultiEdit", "NotebookEdit": return "Editing…"
|
||||||
|
case "Bash", "BashOutput": return "Running…"
|
||||||
|
case "Grep", "Glob": return "Searching…"
|
||||||
|
case "WebFetch": return "Fetching…"
|
||||||
|
case "WebSearch": return "Searching the web…"
|
||||||
|
case "Task": return "Delegating…"
|
||||||
|
case HostCommandSummary.hostExecToolName: return "Running a command on host…"
|
||||||
|
// The VM/container tools name where the work is happening ("Running on a macOS VM…")
|
||||||
|
// rather than echoing their `mcp__nucleic__…` wire name.
|
||||||
|
default: return SandboxToolDisplay.gerund(for: name)
|
||||||
|
?? "Running \(name)…"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TranscriptList: View {
|
struct TranscriptList: View {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import NucleicProtocol
|
|||||||
/// `mcp__nucleic__mac_vm_exec` wire name followed by a JSON gist.
|
/// `mcp__nucleic__mac_vm_exec` wire name followed by a JSON gist.
|
||||||
///
|
///
|
||||||
/// The mobile echo of `NucleicCore/SandboxToolDisplay.swift`, trimmed to what this app shows (a
|
/// The mobile echo of `NucleicCore/SandboxToolDisplay.swift`, trimmed to what this app shows (a
|
||||||
/// label, a glyph, a one-line body — iOS has no per-tool working row or tool-block verb). It
|
/// label, a glyph, a one-line body, the composer's working line — iOS has no tool-block verb). It
|
||||||
/// carries the tool names as literals because the remote links only `NucleicProtocol`, not
|
/// carries the tool names as literals because the remote links only `NucleicProtocol`, not
|
||||||
/// `NucleicCore` where they're declared; keep the two in step when a tool is added.
|
/// `NucleicCore` where they're declared; keep the two in step when a tool is added.
|
||||||
///
|
///
|
||||||
@@ -50,6 +50,25 @@ enum SandboxToolDisplay {
|
|||||||
label(for: toolName) ?? bareName(toolName)
|
label(for: toolName) ?? bareName(toolName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The present-progressive working-row line — "Running on a macOS VM…". Nil for tools this
|
||||||
|
/// file doesn't speak for, so the caller keeps its own "Running <name>…" fallback. Mirrors
|
||||||
|
/// `NucleicCore/SandboxToolDisplay.gerund(for:)` so the phone's composer names a call exactly
|
||||||
|
/// the way the Mac's status line does.
|
||||||
|
static func gerund(for toolName: String) -> String? {
|
||||||
|
switch bareName(toolName) {
|
||||||
|
case "mac_vm_exec": "Running on a macOS VM…"
|
||||||
|
case "linux_vm_exec": "Running on a Linux VM…"
|
||||||
|
case "linux_container": "Running in a Linux container…"
|
||||||
|
case "mac_vm_computer", "mac_vm_computer_batch": "Operating a macOS VM…"
|
||||||
|
case "linux_vm_computer", "linux_vm_computer_batch": "Operating a Linux VM…"
|
||||||
|
case "mac_vm_clear_notifications": "Clearing VM notifications…"
|
||||||
|
case "mac_vm_request_operator": "Waiting for you to drive the VM…"
|
||||||
|
case "mac_vm_control": "Managing the macOS VM…"
|
||||||
|
case "linux_vm_control": "Managing the Linux VM…"
|
||||||
|
default: nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// SF Symbol for the tool — a screen for computer-use, a box for a container, a power switch
|
/// SF Symbol for the tool — a screen for computer-use, a box for a container, a power switch
|
||||||
/// for a lifecycle op — so the glyph carries the same distinction the label does.
|
/// for a lifecycle op — so the glyph carries the same distinction the label does.
|
||||||
static func icon(for toolName: String) -> String? {
|
static func icon(for toolName: String) -> String? {
|
||||||
|
|||||||
Reference in New Issue
Block a user