Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions macos/M1K3App/MessageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
// Review: Kev + claude-opus-5, 2026-09-14 — a "Private Cloud Compute" label under every answer PCC produced
// (`answerOrigin`, ADR 0006). Confidence 0.85 (verified by launch with the Debug echo backend: a PCC answer,
// a stopped PCC partial, and no label on the local answer after a fallback).
// Review: Kev + claude-opus-4-6, 2026-09-22 — toolTraceFooter now visible DURING streaming (was gated
// on status != .streaming); tools show live as each dispatches, with a content transition. Confidence 0.85.

import AppKit
import M1K3Chat
Expand Down Expand Up @@ -163,20 +165,21 @@ struct MessageView: View {
}
}

/// Persisted provenance: which tools served this turn. The live activity
/// label vanishes the moment tokens stream — this line is what survives in
/// the transcript, so tool use (and what left the device) is never
/// invisible after the fact. Rendered on failed turns too, not just
/// complete ones — a turn that searched the web and THEN failed is exactly
/// when "what left the device" matters most (bot review, PR #132). Leaked
/// Persisted provenance: which tools served this turn. Visible LIVE as
/// each tool dispatches — the activity label vanishes the moment tokens
/// stream, but this line stays so tool use (and what left the device) is
/// never invisible, during OR after the turn. Rendered on failed turns
/// too — a turn that searched the web and THEN failed is exactly when
/// "what left the device" matters most (bot review, PR #132). Leaked
/// turns can't reach here with a trace: the guard nils it with the rest.
@ViewBuilder
private var toolTraceFooter: some View {
if message.status != .streaming, let tools = message.toolsUsed, !tools.isEmpty {
if let tools = message.toolsUsed, !tools.isEmpty {
Label(ActivityLabeler.traceLabel(for: tools), systemImage: "wrench.and.screwdriver")
.font(.caption2)
.foregroundStyle(.secondary)
.textSelection(.enabled)
.animation(.easeOut(duration: 0.2), value: tools)
}
}

Expand Down
15 changes: 15 additions & 0 deletions macos/M1K3iOSApp/MessageBubble.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
// the FOLLOWUPS chips the shared ChatSession was already populating are finally
// rendered — tap-to-send via `onSendFollowUp`, mirroring MessageView.
// Review: Kev + claude-fable-5.1, 2026-09-08 — "Stopped" caption for an `interrupted` answer. Confidence now 0.8.
// Review: Kev + claude-opus-4-6, 2026-09-22 — added toolTraceRow (the persisted
// "Used web search · date & time" provenance line), visible live during and after
// the turn. iOS had no tool trace at all. Confidence 0.85.
//

import M1K3Chat
Expand Down Expand Up @@ -87,6 +90,7 @@ struct MessageBubble: View {
if !message.sources.isEmpty {
sourcesRow(message.sources)
}
toolTraceRow
followUpChips
}
.frame(maxWidth: .infinity, alignment: .leading)
Expand All @@ -102,6 +106,17 @@ struct MessageBubble: View {
}
}

@ViewBuilder
private var toolTraceRow: some View {
if let tools = message.toolsUsed, !tools.isEmpty {
Label(ActivityLabeler.traceLabel(for: tools), systemImage: "wrench.and.screwdriver")
.font(.caption2)
.foregroundStyle(.secondary)
.animation(.easeOut(duration: 0.2), value: tools)
.padding(.top, 2)
}
}

private func sourcesRow(_ sources: [ChunkHit]) -> some View {
let titles = Array(Set(sources.map(\.itemTitle))).prefix(4)
return HStack(spacing: 6) {
Expand Down
17 changes: 16 additions & 1 deletion macos/Sources/M1K3Chat/ResponderActivity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
// web search always shows its query, so nothing leaves the device invisibly.
//
// Signed: Kev + claude-fable-5, 2026-06-09, Confidence 0.85, Prior: Unknown
// Review: Kev + claude-opus-4-6, 2026-09-22 — live tool labels for 6 tools that
// fell through to the raw default; default fallback humanises via displayName.
// Confidence 0.85.

import Foundation

Expand Down Expand Up @@ -74,8 +77,20 @@ public enum ActivityLabeler {
"Checking the date & time…"
case "system_status":
"Checking system status…"
case "delegate_deep":
"Starting a deep dive…"
case "list_documents":
"Scanning your documents…"
case "get_document":
"Opening a document…"
case "open_link":
"Opening a link…"
case "lookup_fact":
"Looking that up…"
case "recent_activity":
"Looking back over the week…"
default:
"Using \(name)…"
"Using \(displayName(forTool: name))…"
}
}

Expand Down
14 changes: 13 additions & 1 deletion macos/Tests/M1K3ChatTests/ActivityTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ struct ActivityLabelerTests {
== "Checking the date & time…")
#expect(ActivityLabeler.label(for: .usingTool(name: "system_status", argument: ""))
== "Checking system status…")
#expect(ActivityLabeler.label(for: .usingTool(name: "delegate_deep", argument: ""))
== "Starting a deep dive…")
#expect(ActivityLabeler.label(for: .usingTool(name: "list_documents", argument: ""))
== "Scanning your documents…")
#expect(ActivityLabeler.label(for: .usingTool(name: "get_document", argument: ""))
== "Opening a document…")
#expect(ActivityLabeler.label(for: .usingTool(name: "open_link", argument: ""))
== "Opening a link…")
#expect(ActivityLabeler.label(for: .usingTool(name: "lookup_fact", argument: ""))
== "Looking that up…")
#expect(ActivityLabeler.label(for: .usingTool(name: "recent_activity", argument: ""))
== "Looking back over the week…")
}

@Test("fetch_page shows which site is being read")
Expand All @@ -45,7 +57,7 @@ struct ActivityLabelerTests {
#expect(ActivityLabeler.label(for: .retrieving) == "Recalling what I know…")
#expect(ActivityLabeler.label(for: .thinking(iteration: 0)) == "Thinking…")
#expect(ActivityLabeler.label(for: .usingTool(name: "query_graph", argument: "x"))
== "Using query_graph…")
== "Using query graph…")
}

@Test("tools have short display names for the transcript trace")
Expand Down
Loading