@markusylisiurunen/tau 0.2.129 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/README.md +111 -59
  2. package/dist/core/async/cli.js +12 -2
  3. package/dist/core/async/cli.js.map +1 -1
  4. package/dist/core/async/http_protocol.js +7 -1
  5. package/dist/core/async/http_protocol.js.map +1 -1
  6. package/dist/core/async/http_server.js +9 -1
  7. package/dist/core/async/http_server.js.map +1 -1
  8. package/dist/core/async/index.js.map +1 -1
  9. package/dist/core/async/session_manager.js +94 -51
  10. package/dist/core/async/session_manager.js.map +1 -1
  11. package/dist/core/cli.js +12 -15
  12. package/dist/core/cli.js.map +1 -1
  13. package/dist/core/commands/registry.js +9 -55
  14. package/dist/core/commands/registry.js.map +1 -1
  15. package/dist/core/config/index.js.map +1 -1
  16. package/dist/core/config/runtime.js +0 -1
  17. package/dist/core/config/runtime.js.map +1 -1
  18. package/dist/core/config/schema.js +102 -17
  19. package/dist/core/config/schema.js.map +1 -1
  20. package/dist/core/debug.js +1 -20
  21. package/dist/core/debug.js.map +1 -1
  22. package/dist/core/diff_review/{session.js → bridge.js} +57 -114
  23. package/dist/core/diff_review/bridge.js.map +1 -0
  24. package/dist/core/diff_review/index.js +2 -3
  25. package/dist/core/diff_review/index.js.map +1 -1
  26. package/dist/core/diff_review/review_thread.js +48 -23
  27. package/dist/core/diff_review/review_thread.js.map +1 -1
  28. package/dist/core/diff_review/snapshot.js +40 -11
  29. package/dist/core/diff_review/snapshot.js.map +1 -1
  30. package/dist/core/events/parser.js +43 -12
  31. package/dist/core/events/parser.js.map +1 -1
  32. package/dist/core/index.js +0 -1
  33. package/dist/core/index.js.map +1 -1
  34. package/dist/core/modes/index.js +1 -2
  35. package/dist/core/modes/index.js.map +1 -1
  36. package/dist/core/modes/rpc_server.js +15 -321
  37. package/dist/core/modes/rpc_server.js.map +1 -1
  38. package/dist/core/modes/websocket_server.js +181 -0
  39. package/dist/core/modes/websocket_server.js.map +1 -0
  40. package/dist/core/personas.js +11 -1
  41. package/dist/core/personas.js.map +1 -1
  42. package/dist/core/runtime/chat_runtime.js +19 -12
  43. package/dist/core/runtime/chat_runtime.js.map +1 -1
  44. package/dist/core/runtime/conversation_turn_runtime.js +16 -1
  45. package/dist/core/runtime/conversation_turn_runtime.js.map +1 -1
  46. package/dist/core/runtime/runtime_bootstrap.js +326 -0
  47. package/dist/core/runtime/runtime_bootstrap.js.map +1 -1
  48. package/dist/core/session/compaction.js +13 -11
  49. package/dist/core/session/compaction.js.map +1 -1
  50. package/dist/core/session/core_session.js +9 -0
  51. package/dist/core/session/core_session.js.map +1 -1
  52. package/dist/core/session/direct_bash.js +34 -0
  53. package/dist/core/session/direct_bash.js.map +1 -0
  54. package/dist/core/session/pruning.js +595 -0
  55. package/dist/core/session/pruning.js.map +1 -0
  56. package/dist/core/session/runner.js +45 -5
  57. package/dist/core/session/runner.js.map +1 -1
  58. package/dist/core/session/session_engine.js +84 -10
  59. package/dist/core/session/session_engine.js.map +1 -1
  60. package/dist/core/static/prompts/diff-review-wrapper.md +1 -1
  61. package/dist/core/subagents/subagent_engine.js +1 -1
  62. package/dist/core/subagents/subagent_engine.js.map +1 -1
  63. package/dist/core/tools/catalog.js.map +1 -1
  64. package/dist/core/tools/diff_review.js +12 -12
  65. package/dist/core/tools/diff_review.js.map +1 -1
  66. package/dist/core/tools/execution_backend.js +9 -2
  67. package/dist/core/tools/execution_backend.js.map +1 -1
  68. package/dist/core/tools/registry.js.map +1 -1
  69. package/dist/core/usage/logs.js.map +1 -1
  70. package/dist/core/utils/context.js +1 -1
  71. package/dist/core/utils/context.js.map +1 -1
  72. package/dist/core/utils/context_builder.js +1 -18
  73. package/dist/core/utils/context_builder.js.map +1 -1
  74. package/dist/core/utils/gemini_speech.js +1 -1
  75. package/dist/core/utils/gemini_speech.js.map +1 -1
  76. package/dist/core/utils/project_files.js +11 -16
  77. package/dist/core/utils/project_files.js.map +1 -1
  78. package/dist/core/utils/spawn_capture.js +2 -2
  79. package/dist/core/utils/spawn_capture.js.map +1 -1
  80. package/dist/core/version.js +1 -1
  81. package/dist/core/version.js.map +1 -1
  82. package/dist/execution/cloudflare_sandbox_execution_environment.js +400 -0
  83. package/dist/execution/cloudflare_sandbox_execution_environment.js.map +1 -0
  84. package/dist/execution/execution_environment.js +24 -0
  85. package/dist/execution/execution_environment.js.map +1 -0
  86. package/dist/execution/fly_sprite_execution_environment.js +559 -0
  87. package/dist/execution/fly_sprite_execution_environment.js.map +1 -0
  88. package/dist/execution/local_execution_environment.js +93 -0
  89. package/dist/execution/local_execution_environment.js.map +1 -0
  90. package/dist/execution/runtime_config_snapshot.js +209 -0
  91. package/dist/execution/runtime_config_snapshot.js.map +1 -0
  92. package/dist/execution/sandbox_tool_helpers.js +55 -0
  93. package/dist/execution/sandbox_tool_helpers.js.map +1 -0
  94. package/dist/execution/tool_backend_execution_environment.js +55 -0
  95. package/dist/execution/tool_backend_execution_environment.js.map +1 -0
  96. package/dist/host/hosted_ephemeral_agent_session.js +286 -0
  97. package/dist/host/hosted_ephemeral_agent_session.js.map +1 -0
  98. package/dist/host/index.js +3 -0
  99. package/dist/host/index.js.map +1 -0
  100. package/dist/host/local_session_host.js +1464 -0
  101. package/dist/host/local_session_host.js.map +1 -0
  102. package/dist/host/session_host.js +2 -0
  103. package/dist/host/session_host.js.map +1 -0
  104. package/dist/host/session_protocol_handler.js +873 -0
  105. package/dist/host/session_protocol_handler.js.map +1 -0
  106. package/dist/main.js +537 -77
  107. package/dist/main.js.map +1 -1
  108. package/dist/protocol/index.d.ts +2 -0
  109. package/dist/protocol/index.js +2 -0
  110. package/dist/protocol/index.js.map +1 -0
  111. package/dist/protocol/session_protocol.d.ts +691 -0
  112. package/dist/protocol/session_protocol.js +2117 -0
  113. package/dist/protocol/session_protocol.js.map +1 -0
  114. package/dist/sdk/client.d.ts +2 -1
  115. package/dist/sdk/client.js +136 -392
  116. package/dist/sdk/client.js.map +1 -1
  117. package/dist/sdk/index.d.ts +7 -4
  118. package/dist/sdk/index.js +4 -2
  119. package/dist/sdk/index.js.map +1 -1
  120. package/dist/sdk/session.d.ts +5 -0
  121. package/dist/sdk/session.js +406 -0
  122. package/dist/sdk/session.js.map +1 -0
  123. package/dist/sdk/types.d.ts +97 -78
  124. package/dist/store/file_session_store.js +157 -0
  125. package/dist/store/file_session_store.js.map +1 -0
  126. package/dist/store/memory_session_store.js +24 -0
  127. package/dist/store/memory_session_store.js.map +1 -0
  128. package/dist/store/session_store.js +32 -0
  129. package/dist/store/session_store.js.map +1 -0
  130. package/dist/transport/errors.d.ts +29 -0
  131. package/dist/{sdk → transport}/errors.js +5 -5
  132. package/dist/transport/errors.js.map +1 -0
  133. package/dist/transport/in_process_session_transport.js +102 -0
  134. package/dist/transport/in_process_session_transport.js.map +1 -0
  135. package/dist/transport/index.d.ts +7 -0
  136. package/dist/transport/index.js +4 -0
  137. package/dist/transport/index.js.map +1 -0
  138. package/dist/transport/pending_session_protocol_requests.js +67 -0
  139. package/dist/transport/pending_session_protocol_requests.js.map +1 -0
  140. package/dist/transport/session_protocol_transport_helpers.js +98 -0
  141. package/dist/transport/session_protocol_transport_helpers.js.map +1 -0
  142. package/dist/transport/session_transport.d.ts +11 -0
  143. package/dist/transport/session_transport.js +2 -0
  144. package/dist/transport/session_transport.js.map +1 -0
  145. package/dist/transport/stdio_session_transport.d.ts +38 -0
  146. package/dist/transport/stdio_session_transport.js +243 -0
  147. package/dist/transport/stdio_session_transport.js.map +1 -0
  148. package/dist/transport/websocket_session_transport.d.ts +52 -0
  149. package/dist/transport/websocket_session_transport.js +235 -0
  150. package/dist/transport/websocket_session_transport.js.map +1 -0
  151. package/dist/tui/chat_controller/assistant_clipboard.js +39 -0
  152. package/dist/tui/chat_controller/assistant_clipboard.js.map +1 -0
  153. package/dist/tui/chat_controller/command_hints.js +43 -0
  154. package/dist/tui/chat_controller/command_hints.js.map +1 -0
  155. package/dist/tui/chat_controller/diff_review_service.js +9 -9
  156. package/dist/tui/chat_controller/diff_review_service.js.map +1 -1
  157. package/dist/tui/chat_controller/diff_review_user_message.js +17 -0
  158. package/dist/tui/chat_controller/diff_review_user_message.js.map +1 -0
  159. package/dist/tui/chat_controller/history_labels.js +8 -0
  160. package/dist/tui/chat_controller/history_labels.js.map +1 -0
  161. package/dist/tui/chat_controller/history_message_model.js +36 -0
  162. package/dist/tui/chat_controller/history_message_model.js.map +1 -0
  163. package/dist/tui/chat_controller/status_format.js +22 -0
  164. package/dist/tui/chat_controller/status_format.js.map +1 -0
  165. package/dist/tui/chat_view.js +0 -1
  166. package/dist/tui/chat_view.js.map +1 -1
  167. package/dist/tui/index.js +1 -1
  168. package/dist/tui/index.js.map +1 -1
  169. package/dist/tui/listen_capture.js +87 -0
  170. package/dist/tui/listen_capture.js.map +1 -0
  171. package/dist/tui/memory_mode.js +23 -0
  172. package/dist/tui/memory_mode.js.map +1 -0
  173. package/dist/tui/session_chat_app.js +227 -0
  174. package/dist/tui/session_chat_app.js.map +1 -0
  175. package/dist/tui/session_chat_controller.js +2176 -0
  176. package/dist/tui/session_chat_controller.js.map +1 -0
  177. package/dist/tui/session_tool_execution_backend.js +170 -0
  178. package/dist/tui/session_tool_execution_backend.js.map +1 -0
  179. package/dist/tui/speech_playback.js +107 -0
  180. package/dist/tui/speech_playback.js.map +1 -0
  181. package/dist/tui/ui/assistant_message.js +27 -21
  182. package/dist/tui/ui/assistant_message.js.map +1 -1
  183. package/dist/tui/ui/chat_container.js +4 -0
  184. package/dist/tui/ui/chat_container.js.map +1 -1
  185. package/dist/tui/ui/custom_editor.js +0 -5
  186. package/dist/tui/ui/custom_editor.js.map +1 -1
  187. package/dist/tui/ui/slash_autocomplete.js +14 -36
  188. package/dist/tui/ui/slash_autocomplete.js.map +1 -1
  189. package/dist/tui/ui/tool_ui_registry.js +21 -0
  190. package/dist/tui/ui/tool_ui_registry.js.map +1 -1
  191. package/package.json +6 -3
  192. package/dist/core/config/bash_commands.js +0 -44
  193. package/dist/core/config/bash_commands.js.map +0 -1
  194. package/dist/core/diff_review/session.js.map +0 -1
  195. package/dist/core/modes/rpc_adapter.js +0 -17
  196. package/dist/core/modes/rpc_adapter.js.map +0 -1
  197. package/dist/core/modes/rpc_protocol.js +0 -500
  198. package/dist/core/modes/rpc_protocol.js.map +0 -1
  199. package/dist/core/session/checkpoint.js +0 -34
  200. package/dist/core/session/checkpoint.js.map +0 -1
  201. package/dist/sdk/errors.d.ts +0 -33
  202. package/dist/sdk/errors.js.map +0 -1
  203. package/dist/tui/app.js +0 -64
  204. package/dist/tui/app.js.map +0 -1
  205. package/dist/tui/chat_controller/session_maintenance_service.js +0 -634
  206. package/dist/tui/chat_controller/session_maintenance_service.js.map +0 -1
  207. package/dist/tui/chat_controller.js +0 -2579
  208. package/dist/tui/chat_controller.js.map +0 -1
@@ -0,0 +1,2176 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { dirname } from "node:path/posix";
3
+ import { createCommandRegistry, getRiskLevelDescription, } from "../core/commands/index.js";
4
+ import { getGoogleApiKey } from "../core/config/index.js";
5
+ import { captureDiffReviewSnapshot, DiffReviewBridge, } from "../core/diff_review/index.js";
6
+ import { buildDiffReviewInstructions } from "../core/diff_review/review_thread.js";
7
+ import { createDefaultCoreDeps } from "../core/runtime/deps.js";
8
+ import { runDirectBashCommand } from "../core/session/direct_bash.js";
9
+ import { parseSessionPruneFraction, parseSessionPruneFractionAndGuidance, } from "../core/session/pruning.js";
10
+ import { REASONING_LEVELS } from "../core/types.js";
11
+ import { formatAdaptiveNumber, formatTokenWindow } from "../core/utils/format.js";
12
+ import { extractAssistantText } from "../core/utils/messages.js";
13
+ import { getAutoCompactionMetadataFromMessage } from "../core/utils/user_metadata.js";
14
+ import { APP_VERSION } from "../core/version.js";
15
+ import { shellQuote } from "../execution/sandbox_tool_helpers.js";
16
+ import { applySessionProtocolDelta } from "../protocol/session_protocol.js";
17
+ import { copyAssistantCodeToClipboard, copyAssistantTextToClipboard, } from "./chat_controller/assistant_clipboard.js";
18
+ import { getCommandHint } from "./chat_controller/command_hints.js";
19
+ import { DiffReviewService, } from "./chat_controller/diff_review_service.js";
20
+ import { formatDiffReviewUserMessage } from "./chat_controller/diff_review_user_message.js";
21
+ import { formatRewindCandidateLabel } from "./chat_controller/history_labels.js";
22
+ import { buildHistoryMessageModel, extractHistoryUserText, } from "./chat_controller/history_message_model.js";
23
+ import { InterruptLifecycle } from "./chat_controller/interrupt_lifecycle.js";
24
+ import { QueuedUserMessages } from "./chat_controller/queued_user_messages.js";
25
+ import { formatDurationMs, formatSessionCost } from "./chat_controller/status_format.js";
26
+ import { copyTextToClipboard } from "./clipboard.js";
27
+ import { DOUBLE_PRESS_WINDOW_MS } from "./constants.js";
28
+ import { cleanupListenTempFile, createListenTempFilePath, getSpeechToTextApiKey, getSpeechToTextApiKeyErrorMessage, LISTEN_RECORDING_MAX_DURATION_MS, LISTEN_RECORDING_MIN_BYTES, readListenAudio, startListenAudioCapture, transcribeListenAudio, } from "./listen_capture.js";
29
+ import { formatDefaultMemoryModeFilePath, formatMemoryModeUserMessage } from "./memory_mode.js";
30
+ import { createSdkToolExecutionBackend } from "./session_tool_execution_backend.js";
31
+ import { runSpeechPlaybackTask } from "./speech_playback.js";
32
+ const CAFFEINATE_COMMAND = "/usr/bin/caffeinate";
33
+ export class SessionChatController {
34
+ view;
35
+ session;
36
+ createSession;
37
+ targetLabel;
38
+ config;
39
+ defaultDiffTool;
40
+ diffToolLauncher;
41
+ deps;
42
+ themeIds;
43
+ commandRegistry;
44
+ commandHandlers;
45
+ interruptLifecycle = new InterruptLifecycle();
46
+ diffReviewService;
47
+ snapshot;
48
+ renderedMessageIds = [];
49
+ hiddenHistoryEntryIds = new Set();
50
+ clientRenderedUserMessages = new Map();
51
+ observedSessionRevision;
52
+ eventUnsubscribe;
53
+ snapshotRecovery;
54
+ snapshotRecoveryDeltas = [];
55
+ isStreaming = false;
56
+ submittedTurnInProgress = false;
57
+ isBashMode = false;
58
+ isBashIncognito = false;
59
+ isMemoryMode = false;
60
+ showThinking = false;
61
+ compactToolUi = true;
62
+ commandHint;
63
+ currentTurnStartedAt;
64
+ lastTurnDurationMs = 0;
65
+ turnTimer;
66
+ lastEmptySubmitAt;
67
+ queuedMessageBuffer;
68
+ assistantMessages = [];
69
+ listenRecording;
70
+ listenTransition;
71
+ isTranscribingListen = false;
72
+ speechStatusHint;
73
+ caffeinated;
74
+ turnCaffeinate;
75
+ disableCaffeinateForSession = false;
76
+ speakTask;
77
+ constructor(options) {
78
+ this.view = options.view;
79
+ this.session = options.session;
80
+ this.createSession = options.createSession;
81
+ this.snapshot = options.snapshot;
82
+ this.targetLabel = options.targetLabel;
83
+ this.config = options.config ?? {};
84
+ this.defaultDiffTool = options.defaultDiffTool;
85
+ this.diffToolLauncher = options.diffToolLauncher;
86
+ this.deps = options.deps ?? createDefaultCoreDeps();
87
+ this.themeIds = options.themeIds ?? [];
88
+ this.caffeinated = options.caffeinated ?? false;
89
+ this.observedSessionRevision = options.snapshot.revision;
90
+ this.queuedMessageBuffer = new QueuedUserMessages(options.queuedUserMessages ?? []);
91
+ this.commandRegistry = createCommandRegistry();
92
+ this.commandHandlers = {
93
+ help: () => this.showHelp(),
94
+ copyText: () => this.copyLastAssistantText(),
95
+ copyCode: () => this.copyLastAssistantCode(),
96
+ newSession: () => this.createNewSession(),
97
+ rewind: () => this.startRewindFlow(),
98
+ diff: (argsText) => this.startDiffReview(argsText),
99
+ compactSummaryOnly: (extra) => this.compactSession("summary-only", extra),
100
+ compactSummaryAndLast: (extra) => this.compactSession("summary-and-last", extra),
101
+ pruneEarliest: (extra) => this.pruneSession("earliest", extra),
102
+ pruneLargest: (extra) => this.pruneSession("largest", extra),
103
+ pruneSmart: (extra) => this.pruneSession("smart", extra),
104
+ reload: () => this.reloadContent(),
105
+ listen: () => this.startListenCaptureFromCommand(),
106
+ speak: () => this.speakLastAssistantMessage(),
107
+ risk: (level) => this.setRiskLevel(level),
108
+ persona: (id) => this.setPersona(id),
109
+ prompt: (id) => this.insertPrompt(id),
110
+ theme: (id) => this.switchTheme(id),
111
+ unknown: () => this.view.addSystemMessage("unknown command. type /help.", "error"),
112
+ };
113
+ this.diffReviewService = new DiffReviewService({
114
+ view: this.view,
115
+ interruptLifecycle: this.interruptLifecycle,
116
+ refreshStatus: () => this.refreshStatus(),
117
+ startTurnTimer: () => this.startTurnTimer(),
118
+ stopTurnTimer: () => this.stopTurnTimer(),
119
+ getDiffToolConfig: () => this.resolveDiffToolConfig(),
120
+ startSession: (args) => this.startDiffReviewBridge(args),
121
+ onReviewReturned: (review) => void this.handleReturnedDiffReview(review),
122
+ });
123
+ }
124
+ start() {
125
+ this.view.setThinkingVisibility(this.showThinking);
126
+ this.view.setCompactToolUi(this.compactToolUi);
127
+ this.view.addMessage({
128
+ type: "app_intro",
129
+ title: this.buildStartupIntroTitle(),
130
+ body: this.buildStartupIntroBody().join("\n"),
131
+ });
132
+ this.renderSnapshot(this.snapshot);
133
+ this.hydrateRunningSnapshotState();
134
+ this.eventUnsubscribe = this.session.onDelta((delta) => this.onSdkDelta(delta));
135
+ this.refreshStatus();
136
+ }
137
+ async dispose() {
138
+ this.eventUnsubscribe?.();
139
+ if (this.listenTransition) {
140
+ await this.listenTransition;
141
+ }
142
+ if (this.listenRecording) {
143
+ await this.cancelListenCapture();
144
+ }
145
+ await this.diffReviewService.cancel();
146
+ if (this.speakTask) {
147
+ this.speakTask.abortController.abort();
148
+ await this.speakTask.completion;
149
+ }
150
+ await this.stopTurnCaffeinate();
151
+ this.stopTurnTimer();
152
+ try {
153
+ await this.session.unobserve();
154
+ }
155
+ catch {
156
+ // The transport may already be closing; detach is best-effort during UI disposal.
157
+ }
158
+ }
159
+ getInputHandlers() {
160
+ return {
161
+ onCtrlT: () => this.toggleThinkingVisibility(),
162
+ onCtrlO: () => this.toggleCompactToolUi(),
163
+ onShiftTab: () => void this.cycleReasoningLevel(),
164
+ onCtrlR: () => void this.cycleRiskLevel(),
165
+ onCtrlP: () => void this.cyclePersonality(),
166
+ onCtrlS: () => void this.stashEditorToClipboard(),
167
+ onCtrlY: () => void this.toggleListenCapture(),
168
+ onCtrlG: () => this.terminateSelectedSubagent(),
169
+ onEscape: () => void this.interrupt(),
170
+ beforeSubmit: (text) => this.beforeSubmit(text),
171
+ onChange: (text) => this.handleEditorChange(text),
172
+ onSubmit: (text) => void this.handleSubmit(text),
173
+ onSteerSubmit: (text) => void this.submitSteeringMessage(text),
174
+ onFlushQueueAsSteer: () => void this.flushQueuedMessagesAsSteering(),
175
+ onAltUp: () => this.popQueuedMessageIntoEditor(),
176
+ onAltDown: () => this.cycleSubagentSelection(),
177
+ onAltC: () => this.collapseQueuedMessages(),
178
+ };
179
+ }
180
+ beforeSubmit(text) {
181
+ if (!this.isSessionOperationActive()) {
182
+ return true;
183
+ }
184
+ const trimmed = text.trimStart();
185
+ if (trimmed.startsWith("!")) {
186
+ return false;
187
+ }
188
+ if (trimmed.startsWith("/") && this.isSingleLineInput(text)) {
189
+ const parsed = this.commandRegistry.parse(trimmed);
190
+ if (parsed.type === "unknown") {
191
+ return true;
192
+ }
193
+ if (!this.isStreaming) {
194
+ return false;
195
+ }
196
+ return this.commandRegistry.allowsDuringStreaming(parsed);
197
+ }
198
+ return true;
199
+ }
200
+ async handleSubmit(text) {
201
+ const trimmed = text.trim();
202
+ if (!trimmed) {
203
+ if (this.isStreaming || this.submittedTurnInProgress) {
204
+ return;
205
+ }
206
+ const now = Date.now();
207
+ if (this.lastEmptySubmitAt !== undefined &&
208
+ now - this.lastEmptySubmitAt <= DOUBLE_PRESS_WINDOW_MS) {
209
+ this.lastEmptySubmitAt = undefined;
210
+ await this.retryTurn();
211
+ }
212
+ else {
213
+ this.lastEmptySubmitAt = now;
214
+ }
215
+ return;
216
+ }
217
+ this.lastEmptySubmitAt = undefined;
218
+ if (this.isSessionOperationActive()) {
219
+ if (trimmed.startsWith("/") && this.isSingleLineInput(text)) {
220
+ const parsed = this.commandRegistry.parse(trimmed);
221
+ if (parsed.type !== "unknown") {
222
+ if (this.isStreaming && this.commandRegistry.allowsDuringStreaming(parsed)) {
223
+ await this.commandRegistry.dispatch(parsed, this.commandHandlers);
224
+ }
225
+ else {
226
+ this.view.addSystemMessage("wait for tau to become idle before running commands", "warn");
227
+ }
228
+ return;
229
+ }
230
+ }
231
+ if (trimmed.startsWith("!")) {
232
+ return;
233
+ }
234
+ this.queuedMessageBuffer.enqueue(trimmed, () => this.view.requestRender());
235
+ this.view.addSystemMessage("message queued", "success", {
236
+ persist: false,
237
+ });
238
+ return;
239
+ }
240
+ if (trimmed.startsWith("/") && this.isSingleLineInput(text)) {
241
+ const parsed = this.commandRegistry.parse(trimmed);
242
+ if (parsed.type !== "unknown") {
243
+ await this.commandRegistry.dispatch(parsed, this.commandHandlers);
244
+ return;
245
+ }
246
+ }
247
+ if (trimmed.startsWith("!")) {
248
+ if (trimmed.startsWith("!!")) {
249
+ const command = trimmed.slice(2).trim();
250
+ if (command) {
251
+ await this.runSessionBashCommand(command, {
252
+ addToContext: false,
253
+ labelOverride: "incognito",
254
+ });
255
+ }
256
+ return;
257
+ }
258
+ const command = trimmed.slice(1).trim();
259
+ if (command) {
260
+ await this.runSessionBashCommand(command, {
261
+ addToContext: true,
262
+ labelOverride: "you ran",
263
+ });
264
+ }
265
+ return;
266
+ }
267
+ if (this.isSingleLineInput(text) && trimmed.startsWith("#")) {
268
+ const request = trimmed.slice(1).trim();
269
+ if (!request) {
270
+ this.view.addSystemMessage("memory mode request was empty.", "warn");
271
+ return;
272
+ }
273
+ const agentsFilePath = formatDefaultMemoryModeFilePath(this.snapshot.executionEnvironment.cwd);
274
+ await this.submitUserText(request, {
275
+ textForModel: formatMemoryModeUserMessage(agentsFilePath, request),
276
+ kind: "memory",
277
+ });
278
+ return;
279
+ }
280
+ await this.submitUserText(trimmed);
281
+ }
282
+ async onUserInput(text) {
283
+ await this.handleSubmit(text);
284
+ }
285
+ showHelp() {
286
+ this.view.addSystemMessage(this.commandRegistry.buildHelpText({
287
+ agentsFiles: this.getAgentsFilePaths(),
288
+ skills: this.snapshot.catalog.skills,
289
+ riskLevels: ["read-only", "read-write"],
290
+ themes: this.themeIds,
291
+ formatPath: (path) => formatPathForSessionDisplay(path, this.snapshot.executionEnvironment.home),
292
+ }), "muted");
293
+ }
294
+ switchTheme(themeId) {
295
+ this.view.updateTheme({ themeId });
296
+ this.view.addSystemMessage(`theme set to ${themeId}`, "success");
297
+ }
298
+ handleEditorChange(text) {
299
+ const wasBash = this.isBashMode;
300
+ const wasBashIncognito = this.isBashIncognito;
301
+ const wasMemory = this.isMemoryMode;
302
+ const previousCommandHint = this.commandHint;
303
+ if (text.trim().length > 0) {
304
+ this.lastEmptySubmitAt = undefined;
305
+ }
306
+ const trimmed = text.trimStart();
307
+ const isSingleLine = this.isSingleLineInput(text);
308
+ const isIncognito = trimmed.startsWith("!!");
309
+ this.isBashIncognito = isIncognito;
310
+ this.isBashMode = trimmed.startsWith("!") && !isIncognito;
311
+ this.isMemoryMode = isSingleLine && trimmed.startsWith("#");
312
+ this.commandHint = this.getCommandHintForInput(text);
313
+ if (wasBash !== this.isBashMode ||
314
+ wasBashIncognito !== this.isBashIncognito ||
315
+ wasMemory !== this.isMemoryMode ||
316
+ previousCommandHint !== this.commandHint) {
317
+ this.refreshStatus();
318
+ }
319
+ }
320
+ getCommandHintForInput(text) {
321
+ const trimmed = text.trimStart();
322
+ if (!trimmed.startsWith("/") || !this.isSingleLineInput(text)) {
323
+ return undefined;
324
+ }
325
+ const parsed = this.commandRegistry.parse(trimmed);
326
+ if (parsed.type === "unknown") {
327
+ return undefined;
328
+ }
329
+ return getCommandHint(parsed);
330
+ }
331
+ getAutocompleteSources() {
332
+ return {
333
+ personas: () => this.snapshot.catalog.personas.map((persona) => ({
334
+ id: persona.id,
335
+ label: persona.label,
336
+ })),
337
+ prompts: () => this.snapshot.catalog.prompts.map((prompt) => ({
338
+ id: prompt.id,
339
+ label: prompt.label,
340
+ })),
341
+ themes: () => this.themeIds.map((id) => ({ id })),
342
+ autocompletePaths: async (query, limit) => (await this.session.autocompletePaths({ query, limit })).paths,
343
+ skills: () => this.snapshot.catalog.skills.map((skill) => skill.name),
344
+ subagents: () => Object.keys(this.getCurrentPersonaSnapshot()?.subagents ?? {}),
345
+ riskLevels: () => ["read-only", "read-write"],
346
+ };
347
+ }
348
+ buildStartupIntroTitle() {
349
+ const parts = [`tau v${APP_VERSION}`];
350
+ const agentsFiles = this.getAgentsFilePaths();
351
+ if (agentsFiles.length > 0) {
352
+ parts.push(`${agentsFiles.length} AGENTS.md`);
353
+ }
354
+ if (this.snapshot.catalog.skills.length > 0) {
355
+ parts.push(`${this.snapshot.catalog.skills.length} skills`);
356
+ }
357
+ return parts.join(" · ");
358
+ }
359
+ buildStartupIntroBody() {
360
+ const lines = [
361
+ "type `/help` for commands and keybindings",
362
+ "mention files with `@`, agents and skills with `@@`",
363
+ "run bash commands with `!` or `!!`",
364
+ "use `#` to update AGENTS.md",
365
+ ];
366
+ if (this.snapshot.catalog.skills.length > 0) {
367
+ lines.push("", "skills:");
368
+ for (const skill of this.snapshot.catalog.skills) {
369
+ const skillsRoot = formatPathForSessionDisplay(dirname(dirname(skill.path)), this.snapshot.executionEnvironment.home);
370
+ lines.push(` ${skill.name} (${skillsRoot})`);
371
+ }
372
+ }
373
+ const agentsFiles = this.getAgentsFilePaths().map((path) => formatPathForSessionDisplay(path, this.snapshot.executionEnvironment.home));
374
+ if (agentsFiles.length > 0) {
375
+ lines.push("", "context:");
376
+ for (const agentsFile of agentsFiles) {
377
+ lines.push(` ${agentsFile}`);
378
+ }
379
+ }
380
+ lines.push("", this.formatSessionIdentityText());
381
+ return lines;
382
+ }
383
+ async submitUserText(text, options = {}) {
384
+ const historyEntryId = `session-user-${randomUUID()}`;
385
+ const model = {
386
+ type: "user",
387
+ text,
388
+ ...(options.kind ? { kind: options.kind } : {}),
389
+ };
390
+ this.clientRenderedUserMessages.set(historyEntryId, model);
391
+ this.view.addMessage(model, historyEntryId);
392
+ this.renderedMessageIds.push(historyEntryId);
393
+ await this.runSessionTurn(() => this.session.submit(options.textForModel ?? text, { historyEntryId }));
394
+ }
395
+ async retryTurn() {
396
+ await this.runSessionTurn(() => this.session.retry());
397
+ }
398
+ async runSessionBashCommand(command, options) {
399
+ if (this.isSessionOperationActive()) {
400
+ return;
401
+ }
402
+ const toolCallId = `bash-user-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
403
+ const headerTarget = command.split(/\r?\n/, 1)[0] ?? command;
404
+ this.isStreaming = true;
405
+ this.startTurnTimer();
406
+ this.view.startWorkingIcon();
407
+ this.view.handleToolUiEvent({
408
+ type: "bash_started",
409
+ toolCallId,
410
+ command,
411
+ headerTarget,
412
+ });
413
+ this.refreshStatus();
414
+ try {
415
+ const backend = createSdkToolExecutionBackend({
416
+ session: this.session,
417
+ cwd: this.snapshot.executionEnvironment.cwd,
418
+ });
419
+ const result = await runDirectBashCommand({
420
+ command,
421
+ backend,
422
+ addToContext: options.addToContext,
423
+ addUserText: async (text) => {
424
+ const recorded = await this.session.record(text, {
425
+ historyEntryId: toolCallId,
426
+ });
427
+ return recorded.userHistoryEntryId;
428
+ },
429
+ });
430
+ if (result.userHistoryEntryId) {
431
+ this.hiddenHistoryEntryIds.add(result.userHistoryEntryId);
432
+ this.renderedMessageIds.push(result.userHistoryEntryId);
433
+ }
434
+ this.view.handleToolUiEvent({
435
+ type: "bash_execution",
436
+ toolCallId,
437
+ command: result.command,
438
+ headerTarget,
439
+ exitCode: result.exitCode,
440
+ truncationInfo: result.truncationInfo,
441
+ uiText: result.uiText,
442
+ durationMs: result.durationMs,
443
+ labelOverride: options.labelOverride,
444
+ });
445
+ await this.syncFromSessionSnapshot();
446
+ }
447
+ catch (error) {
448
+ this.view.handleToolUiEvent({
449
+ type: "bash_blocked",
450
+ toolCallId,
451
+ command,
452
+ headerTarget,
453
+ reason: error.message || "bash failed",
454
+ });
455
+ await this.syncFromSessionSnapshot();
456
+ }
457
+ finally {
458
+ this.isStreaming = false;
459
+ this.view.stopWorkingIcon();
460
+ this.stopTurnTimer();
461
+ this.refreshStatus();
462
+ this.view.requestRender();
463
+ await this.drainQueuedMessages();
464
+ }
465
+ }
466
+ async submitSteeringMessage(text) {
467
+ const trimmed = text.trim();
468
+ if (!trimmed) {
469
+ return;
470
+ }
471
+ if (!this.isStreaming) {
472
+ if (this.submittedTurnInProgress) {
473
+ this.queuedMessageBuffer.enqueue(trimmed, () => this.view.requestRender());
474
+ this.view.addSystemMessage("message queued", "success", {
475
+ persist: false,
476
+ });
477
+ return;
478
+ }
479
+ await this.submitUserText(trimmed);
480
+ return;
481
+ }
482
+ if (!this.submittedTurnInProgress) {
483
+ this.queuedMessageBuffer.enqueue(trimmed, () => this.view.requestRender());
484
+ this.view.addSystemMessage("current task cannot be steered; message queued", "warn");
485
+ return;
486
+ }
487
+ this.view.addSystemMessage("steering queued for next turn boundary", "success");
488
+ void this.session
489
+ .steer(trimmed, { historyEntryId: `session-steer-${randomUUID()}` })
490
+ .catch((error) => {
491
+ this.view.addSystemMessage(`steering failed: ${error.message}`, "error");
492
+ });
493
+ }
494
+ async flushQueuedMessagesAsSteering() {
495
+ if (this.queuedMessageBuffer.length === 0) {
496
+ this.view.addSystemMessage("no queued messages to steer", "warn");
497
+ return;
498
+ }
499
+ if (!this.isStreaming || !this.submittedTurnInProgress) {
500
+ this.view.addSystemMessage("current task cannot be steered; queued messages unchanged", "warn");
501
+ return;
502
+ }
503
+ const text = this.queuedMessageBuffer.flush().join("\n\n");
504
+ this.view.addSystemMessage("queued messages will steer at the next turn boundary", "success");
505
+ this.view.requestRender();
506
+ void this.session
507
+ .steer(text, { historyEntryId: `session-steer-${randomUUID()}` })
508
+ .catch((error) => {
509
+ this.view.addSystemMessage(`steering failed: ${error.message}`, "error");
510
+ });
511
+ }
512
+ popQueuedMessageIntoEditor() {
513
+ this.queuedMessageBuffer.popIntoEditor({
514
+ getEditorText: () => this.view.getEditorText(),
515
+ setEditorText: (text) => this.view.setEditorText(text),
516
+ });
517
+ this.view.requestRender();
518
+ }
519
+ dequeueQueuedMessagesIntoEditor() {
520
+ this.queuedMessageBuffer.dequeueIntoEditor({
521
+ getEditorText: () => this.view.getEditorText(),
522
+ setEditorText: (text) => this.view.setEditorText(text),
523
+ });
524
+ this.view.requestRender();
525
+ }
526
+ collapseQueuedMessages() {
527
+ if (this.queuedMessageBuffer.collapse()) {
528
+ this.view.addSystemMessage("queued messages collapsed", "success", {
529
+ persist: false,
530
+ });
531
+ this.view.requestRender();
532
+ }
533
+ }
534
+ cycleSubagentSelection() {
535
+ this.view.cycleSubagentSelection(1);
536
+ this.view.requestRender();
537
+ }
538
+ startTurnCaffeinate() {
539
+ if (!this.caffeinated || this.disableCaffeinateForSession || this.turnCaffeinate) {
540
+ return;
541
+ }
542
+ if (this.deps.env.platform() !== "darwin") {
543
+ this.disableCaffeinateForSession = true;
544
+ return;
545
+ }
546
+ const abortController = new AbortController();
547
+ const completion = this.deps.spawn(CAFFEINATE_COMMAND, ["-i"], {
548
+ detached: true,
549
+ killProcessGroup: true,
550
+ signal: abortController.signal,
551
+ stdio: ["ignore", "ignore", "ignore"],
552
+ });
553
+ completion.catch(() => { });
554
+ this.turnCaffeinate = { abortController, completion };
555
+ }
556
+ async stopTurnCaffeinate() {
557
+ const session = this.turnCaffeinate;
558
+ if (!session) {
559
+ return;
560
+ }
561
+ this.turnCaffeinate = undefined;
562
+ if (!session.abortController.signal.aborted) {
563
+ session.abortController.abort();
564
+ }
565
+ try {
566
+ await session.completion;
567
+ }
568
+ catch (error) {
569
+ if (this.disableCaffeinateForSession) {
570
+ return;
571
+ }
572
+ this.disableCaffeinateForSession = true;
573
+ this.view.addSystemMessage(`failed to run caffeinate: ${error.message}`, "warn");
574
+ }
575
+ }
576
+ async runSessionTurn(task) {
577
+ if (this.isSessionOperationActive()) {
578
+ return;
579
+ }
580
+ this.isStreaming = true;
581
+ this.submittedTurnInProgress = true;
582
+ this.startTurnTimer();
583
+ this.startTurnCaffeinate();
584
+ this.view.startWorkingIcon();
585
+ this.refreshStatus();
586
+ try {
587
+ const result = await task();
588
+ if (result.turn.blocked) {
589
+ this.view.addSystemMessage(`turn blocked: ${result.turn.blocked.message}`, "error");
590
+ }
591
+ await this.syncFromSessionSnapshot();
592
+ }
593
+ catch (error) {
594
+ this.view.addSystemMessage(`session turn failed: ${error.message}`, "error");
595
+ this.stopVisibleSessionTurn();
596
+ void this.stopTurnCaffeinate();
597
+ await this.syncFromSessionSnapshot();
598
+ }
599
+ finally {
600
+ this.submittedTurnInProgress = false;
601
+ this.stopVisibleSessionTurn();
602
+ await this.stopTurnCaffeinate();
603
+ this.refreshStatus();
604
+ await this.drainQueuedMessages();
605
+ }
606
+ }
607
+ async drainQueuedMessages() {
608
+ await this.queuedMessageBuffer.drain({
609
+ isStreaming: () => this.isStreaming,
610
+ onUserInput: (text) => this.submitUserText(text),
611
+ requestRender: () => this.view.requestRender(),
612
+ sendTerminalNotification: (title) => this.view.sendTerminalNotification(title),
613
+ buildIdleNotificationTitle: () => "tau is idle",
614
+ });
615
+ }
616
+ async interrupt() {
617
+ if (this.interruptLifecycle.interruptActiveTask()) {
618
+ this.view.addSystemMessage("interrupted", "error");
619
+ return;
620
+ }
621
+ if (this.listenRecording) {
622
+ void this.runListenTransition(() => this.stopListenCapture());
623
+ return;
624
+ }
625
+ if (this.speakTask) {
626
+ if (!this.speakTask.abortController.signal.aborted) {
627
+ this.speakTask.abortController.abort();
628
+ }
629
+ this.view.addSystemMessage("interrupted", "error");
630
+ return;
631
+ }
632
+ if (!this.isStreaming) {
633
+ return;
634
+ }
635
+ try {
636
+ const result = await this.session.interrupt();
637
+ this.view.addSystemMessage(result.interrupted ? "interrupted" : "interrupt requested", "error");
638
+ }
639
+ catch (error) {
640
+ this.view.addSystemMessage(`interrupt failed: ${error.message}`, "error");
641
+ }
642
+ }
643
+ async toggleListenCapture() {
644
+ if (this.listenTransition) {
645
+ this.view.addSystemMessage("speech recording state change already in progress", "warn");
646
+ return;
647
+ }
648
+ if (this.listenRecording) {
649
+ await this.runListenTransition(() => this.stopListenCapture());
650
+ return;
651
+ }
652
+ await this.startListenCaptureFromCommand();
653
+ }
654
+ async startListenCaptureFromCommand() {
655
+ if (this.listenTransition) {
656
+ this.view.addSystemMessage("speech recording state change already in progress", "warn");
657
+ return;
658
+ }
659
+ if (this.listenRecording) {
660
+ this.view.addSystemMessage("speech recording already in progress", "warn");
661
+ return;
662
+ }
663
+ if (this.isTranscribingListen) {
664
+ this.view.addSystemMessage("speech transcription already in progress", "warn");
665
+ return;
666
+ }
667
+ await this.runListenTransition(() => this.startListenCapture());
668
+ }
669
+ async runListenTransition(task) {
670
+ if (this.listenTransition) {
671
+ return;
672
+ }
673
+ const transition = task();
674
+ this.listenTransition = transition;
675
+ try {
676
+ await transition;
677
+ }
678
+ finally {
679
+ if (this.listenTransition === transition) {
680
+ this.listenTransition = undefined;
681
+ }
682
+ }
683
+ }
684
+ async startListenCapture() {
685
+ if (this.deps.env.platform() !== "darwin") {
686
+ this.view.addSystemMessage("/listen is currently supported only on macOS.", "warn");
687
+ return;
688
+ }
689
+ const apiKey = getSpeechToTextApiKey(this.config, this.deps);
690
+ if (!apiKey) {
691
+ this.view.addSystemMessage(getSpeechToTextApiKeyErrorMessage(this.config, "use /listen"), "error");
692
+ return;
693
+ }
694
+ let audioPath;
695
+ try {
696
+ audioPath = await createListenTempFilePath(this.deps);
697
+ const abortController = new AbortController();
698
+ const completion = startListenAudioCapture({
699
+ deps: this.deps,
700
+ audioPath,
701
+ signal: abortController.signal,
702
+ });
703
+ const recording = {
704
+ audioPath,
705
+ stopRequested: false,
706
+ abortController,
707
+ completion,
708
+ };
709
+ recording.maxDurationTimeout = setTimeout(() => {
710
+ if (this.listenRecording !== recording || this.listenTransition)
711
+ return;
712
+ void this.runListenTransition(() => this.stopListenCapture());
713
+ }, LISTEN_RECORDING_MAX_DURATION_MS);
714
+ this.listenRecording = recording;
715
+ this.view.setEditorInputEnabled(false);
716
+ this.refreshStatus();
717
+ void this.watchListenRecording(recording);
718
+ }
719
+ catch (err) {
720
+ if (audioPath) {
721
+ await cleanupListenTempFile(audioPath);
722
+ }
723
+ this.view.addSystemMessage(`failed to start recording: ${err.message}`, "error");
724
+ }
725
+ }
726
+ async stopListenCapture() {
727
+ const recording = this.listenRecording;
728
+ if (!recording)
729
+ return;
730
+ recording.stopRequested = true;
731
+ this.clearListenRecordingMaxDurationTimeout(recording);
732
+ this.listenRecording = undefined;
733
+ this.view.setEditorInputEnabled(true);
734
+ this.refreshStatus();
735
+ recording.abortController.abort();
736
+ try {
737
+ await recording.completion;
738
+ }
739
+ catch (err) {
740
+ this.view.addSystemMessage(`recording failed: ${err.message}`, "error");
741
+ await cleanupListenTempFile(recording.audioPath);
742
+ return;
743
+ }
744
+ this.isTranscribingListen = true;
745
+ try {
746
+ const audio = await readListenAudio(recording.audioPath);
747
+ if (audio.byteLength < LISTEN_RECORDING_MIN_BYTES) {
748
+ this.view.addSystemMessage("recording too short, try again", "warn");
749
+ return;
750
+ }
751
+ const transcript = await transcribeListenAudio({
752
+ config: this.config,
753
+ deps: this.deps,
754
+ audio,
755
+ });
756
+ const text = transcript.trim();
757
+ if (!text) {
758
+ return;
759
+ }
760
+ this.view.insertEditorTextAtCursor(text);
761
+ }
762
+ catch (err) {
763
+ this.view.addSystemMessage(`speech transcription failed: ${err.message}`, "error");
764
+ }
765
+ finally {
766
+ this.isTranscribingListen = false;
767
+ await cleanupListenTempFile(recording.audioPath);
768
+ }
769
+ }
770
+ async cancelListenCapture() {
771
+ const recording = this.listenRecording;
772
+ if (!recording)
773
+ return;
774
+ recording.stopRequested = true;
775
+ this.clearListenRecordingMaxDurationTimeout(recording);
776
+ this.listenRecording = undefined;
777
+ this.view.setEditorInputEnabled(true);
778
+ this.refreshStatus();
779
+ recording.abortController.abort();
780
+ try {
781
+ await recording.completion;
782
+ }
783
+ catch {
784
+ // ignore disposal errors
785
+ }
786
+ await cleanupListenTempFile(recording.audioPath);
787
+ }
788
+ async watchListenRecording(recording) {
789
+ try {
790
+ const result = await recording.completion;
791
+ this.clearListenRecordingMaxDurationTimeout(recording);
792
+ if (this.listenRecording !== recording || recording.stopRequested)
793
+ return;
794
+ this.listenRecording = undefined;
795
+ this.view.setEditorInputEnabled(true);
796
+ this.refreshStatus();
797
+ const detail = result.exitCode !== null
798
+ ? `ffmpeg exited with code ${result.exitCode}`
799
+ : result.closeSignal
800
+ ? `ffmpeg terminated by signal ${result.closeSignal}`
801
+ : "ffmpeg exited";
802
+ this.view.addSystemMessage(`recording stopped unexpectedly (${detail})`, "error");
803
+ await cleanupListenTempFile(recording.audioPath);
804
+ }
805
+ catch (err) {
806
+ this.clearListenRecordingMaxDurationTimeout(recording);
807
+ if (this.listenRecording !== recording || recording.stopRequested)
808
+ return;
809
+ this.listenRecording = undefined;
810
+ this.view.setEditorInputEnabled(true);
811
+ this.refreshStatus();
812
+ const error = err;
813
+ if (error.code === "ENOENT") {
814
+ this.view.addSystemMessage("ffmpeg not found. install it with: brew install ffmpeg", "error");
815
+ }
816
+ else {
817
+ this.view.addSystemMessage(`recording failed: ${error.message}`, "error");
818
+ }
819
+ await cleanupListenTempFile(recording.audioPath);
820
+ }
821
+ }
822
+ clearListenRecordingMaxDurationTimeout(recording) {
823
+ if (!recording.maxDurationTimeout)
824
+ return;
825
+ clearTimeout(recording.maxDurationTimeout);
826
+ recording.maxDurationTimeout = undefined;
827
+ }
828
+ async createNewSession() {
829
+ if (this.isSessionOperationActive()) {
830
+ this.view.addSystemMessage("cannot create a new session while a turn is running", "warn");
831
+ return;
832
+ }
833
+ if (!this.createSession) {
834
+ this.view.addSystemMessage("new session creation is unavailable", "error");
835
+ return;
836
+ }
837
+ try {
838
+ const previousUnsubscribe = this.eventUnsubscribe;
839
+ const previousSession = this.session;
840
+ const nextSession = await this.createSession({
841
+ executionEnvironment: this.createExecutionEnvironmentInputFromSnapshot(),
842
+ personaId: this.snapshot.settings.personaId,
843
+ riskLevel: this.snapshot.settings.riskLevel,
844
+ ...(this.snapshot.settings.reasoning !== undefined
845
+ ? { reasoning: this.snapshot.settings.reasoning }
846
+ : {}),
847
+ });
848
+ this.eventUnsubscribe = undefined;
849
+ previousUnsubscribe?.();
850
+ try {
851
+ await previousSession.unobserve();
852
+ }
853
+ catch (detachError) {
854
+ this.view.addSystemMessage(`old session unobserve failed: ${detachError.message}`, "warn");
855
+ }
856
+ this.session = nextSession;
857
+ this.snapshot = await this.session.snapshot();
858
+ this.observedSessionRevision = this.snapshot.revision;
859
+ this.eventUnsubscribe = this.session.onDelta((delta) => this.onSdkDelta(delta));
860
+ this.view.resetToolUiSession();
861
+ this.assistantMessages = [];
862
+ this.startLocalUiSession();
863
+ this.addSessionIdentityMessage();
864
+ this.renderSnapshot(this.snapshot);
865
+ this.refreshStatus();
866
+ }
867
+ catch (error) {
868
+ this.view.addSystemMessage(`new session failed: ${error.message}`, "error");
869
+ }
870
+ }
871
+ createExecutionEnvironmentInputFromSnapshot() {
872
+ const snapshot = this.snapshot.executionEnvironment;
873
+ switch (snapshot.kind) {
874
+ case "local":
875
+ return { kind: "local", cwd: snapshot.cwd };
876
+ case "cloudflare-sandbox":
877
+ return {
878
+ kind: "cloudflare-sandbox",
879
+ bridgeId: snapshot.bridgeId,
880
+ sandboxId: snapshot.sandboxId,
881
+ cwd: snapshot.cwd,
882
+ };
883
+ case "fly-sprite":
884
+ return {
885
+ kind: "fly-sprite",
886
+ apiId: snapshot.apiId,
887
+ spriteName: snapshot.spriteName,
888
+ cwd: snapshot.cwd,
889
+ };
890
+ }
891
+ }
892
+ onSdkDelta(delta) {
893
+ if (this.snapshotRecovery) {
894
+ this.snapshotRecoveryDeltas.push(delta);
895
+ return;
896
+ }
897
+ if (this.tryApplySdkDelta(delta)) {
898
+ return;
899
+ }
900
+ this.snapshotRecoveryDeltas.push(delta);
901
+ void this.recoverFromRevisionGap();
902
+ }
903
+ tryApplySdkDelta(delta) {
904
+ if (delta.delta.type === "snapshot.reset" && delta.toRevision < this.snapshot.revision) {
905
+ return true;
906
+ }
907
+ if (delta.delta.type === "snapshot.patch" && delta.toRevision <= this.snapshot.revision) {
908
+ return true;
909
+ }
910
+ if (delta.delta.type === "snapshot.patch" && delta.fromRevision !== this.snapshot.revision) {
911
+ return false;
912
+ }
913
+ try {
914
+ if (this.tryApplyFastContentAppendDelta(delta)) {
915
+ return true;
916
+ }
917
+ if (this.tryApplyFastToolFacetDelta(delta)) {
918
+ return true;
919
+ }
920
+ if (this.tryApplyFastAgentDelta(delta)) {
921
+ return true;
922
+ }
923
+ const nextSnapshot = applySessionProtocolDelta(this.snapshot, delta);
924
+ if (this.shouldRenderAutoCompactedReset(delta)) {
925
+ this.renderAutoCompactedReset(nextSnapshot);
926
+ }
927
+ else {
928
+ this.syncRenderedHistory(nextSnapshot);
929
+ }
930
+ this.refreshStatus();
931
+ }
932
+ catch (error) {
933
+ this.view.addSystemMessage(`session delta failed: ${error.message}`, "warn");
934
+ return false;
935
+ }
936
+ return true;
937
+ }
938
+ tryApplyFastContentAppendDelta(delta) {
939
+ if (delta.delta.type !== "snapshot.patch" || delta.delta.changes.length !== 1) {
940
+ return false;
941
+ }
942
+ const change = delta.delta.changes[0];
943
+ if (change.type !== "message.content.append") {
944
+ return false;
945
+ }
946
+ const nextSnapshot = applySessionProtocolDelta(this.snapshot, delta);
947
+ this.snapshot = nextSnapshot;
948
+ this.observedSessionRevision = Math.max(this.observedSessionRevision, nextSnapshot.revision);
949
+ const message = nextSnapshot.messages.find((entry) => entry.id === change.messageId);
950
+ if (!message ||
951
+ this.hiddenHistoryEntryIds.has(message.id) ||
952
+ !isMessageInTimeline(nextSnapshot, message.id)) {
953
+ return true;
954
+ }
955
+ const model = this.buildProtocolMessageModel(message);
956
+ if (!model) {
957
+ return true;
958
+ }
959
+ if (this.renderedMessageIds.includes(message.id)) {
960
+ if (model.type === "assistant" || model.type === "assistant_partial") {
961
+ this.view.updateAssistantMessage(message.id, model);
962
+ }
963
+ else {
964
+ this.view.updateMessage(message.id, model);
965
+ }
966
+ }
967
+ else {
968
+ this.renderedMessageIds.push(this.view.addMessage(model, message.id));
969
+ }
970
+ return true;
971
+ }
972
+ tryApplyFastAgentDelta(delta) {
973
+ if (delta.delta.type !== "snapshot.patch" || delta.delta.changes.length !== 1) {
974
+ return false;
975
+ }
976
+ const change = delta.delta.changes[0];
977
+ if (change.type !== "agent.set") {
978
+ return false;
979
+ }
980
+ const previousAgent = this.snapshot.agents[change.agent.id];
981
+ this.snapshot = applySessionProtocolDelta(this.snapshot, delta);
982
+ this.observedSessionRevision = Math.max(this.observedSessionRevision, this.snapshot.revision);
983
+ for (const event of subagentUiEventsForAgentDelta(previousAgent, change.agent)) {
984
+ this.view.handleSubagentEvent(event);
985
+ }
986
+ this.refreshStatus();
987
+ return true;
988
+ }
989
+ tryApplyFastToolFacetDelta(delta) {
990
+ if (delta.delta.type !== "snapshot.patch") {
991
+ return false;
992
+ }
993
+ const facetChanges = delta.delta.changes.filter((change) => change.type === "facet.set");
994
+ if (facetChanges.length !== 1 ||
995
+ delta.delta.changes.some((change) => change.type !== "tool.set" && change.type !== "facet.set")) {
996
+ return false;
997
+ }
998
+ const facetChange = facetChanges[0];
999
+ const nextFacet = facetChange.facet;
1000
+ if (nextFacet.kind !== "tau.tool-ui-events" || nextFacet.subject.type !== "tool") {
1001
+ return false;
1002
+ }
1003
+ const previousEvents = toolUiEventsFromFacet(this.snapshot.facets[nextFacet.id]);
1004
+ const nextEvents = toolUiEventsFromFacet(nextFacet);
1005
+ if (!canAppendToolUiEvents(nextEvents, previousEvents)) {
1006
+ return false;
1007
+ }
1008
+ this.snapshot = applySessionProtocolDelta(this.snapshot, delta);
1009
+ this.observedSessionRevision = Math.max(this.observedSessionRevision, this.snapshot.revision);
1010
+ for (const event of nextEvents.slice(previousEvents.length)) {
1011
+ this.view.handleToolUiEvent(event);
1012
+ }
1013
+ this.refreshStatus();
1014
+ return true;
1015
+ }
1016
+ renderSnapshot(snapshot) {
1017
+ this.snapshot = snapshot;
1018
+ this.observedSessionRevision = Math.max(this.observedSessionRevision, snapshot.revision);
1019
+ this.assistantMessages = [];
1020
+ const messages = getTimelineMessages(snapshot);
1021
+ for (const entry of messages) {
1022
+ this.renderProtocolMessage(entry);
1023
+ }
1024
+ this.syncSnapshotToolAndAgentUi(snapshot);
1025
+ }
1026
+ hydrateRunningSnapshotState() {
1027
+ if (this.snapshot.lifecycle !== "running") {
1028
+ return;
1029
+ }
1030
+ this.isStreaming = true;
1031
+ this.startTurnTimer();
1032
+ this.view.startWorkingIcon();
1033
+ }
1034
+ renderProtocolMessage(message) {
1035
+ const model = this.buildProtocolMessageModel(message);
1036
+ if (!model) {
1037
+ return;
1038
+ }
1039
+ if (isAssistantMessage(message.message)) {
1040
+ this.assistantMessages.push(message.message);
1041
+ }
1042
+ this.renderedMessageIds.push(this.view.addMessage(model, message.id));
1043
+ }
1044
+ syncRenderedHistory(snapshot) {
1045
+ this.snapshot = snapshot;
1046
+ this.observedSessionRevision = Math.max(this.observedSessionRevision, snapshot.revision);
1047
+ const messages = getTimelineMessages(snapshot);
1048
+ const snapshotIds = new Set(messages.map((entry) => entry.id));
1049
+ const staleIds = this.renderedMessageIds.filter((id) => !snapshotIds.has(id));
1050
+ if (staleIds.length > 0) {
1051
+ this.view.removeMessages(staleIds);
1052
+ for (const id of staleIds) {
1053
+ this.clientRenderedUserMessages.delete(id);
1054
+ }
1055
+ }
1056
+ const renderedIds = new Set(this.renderedMessageIds);
1057
+ this.renderedMessageIds.splice(0, this.renderedMessageIds.length, ...this.renderedMessageIds.filter((id) => snapshotIds.has(id)));
1058
+ for (const entry of messages) {
1059
+ if (this.hiddenHistoryEntryIds.has(entry.id)) {
1060
+ if (!renderedIds.has(entry.id)) {
1061
+ this.renderedMessageIds.push(entry.id);
1062
+ }
1063
+ continue;
1064
+ }
1065
+ const model = this.clientRenderedUserMessages.get(entry.id) ?? this.buildProtocolMessageModel(entry);
1066
+ if (!model) {
1067
+ continue;
1068
+ }
1069
+ if (renderedIds.has(entry.id)) {
1070
+ if (model.type === "assistant") {
1071
+ this.view.updateAssistantMessage(entry.id, model);
1072
+ }
1073
+ else {
1074
+ this.view.updateMessage(entry.id, model);
1075
+ }
1076
+ }
1077
+ else {
1078
+ this.renderedMessageIds.push(this.view.addMessage(model, entry.id));
1079
+ }
1080
+ }
1081
+ this.assistantMessages = this.collectAssistantMessages(snapshot);
1082
+ this.updateStreamingStateFromSnapshot(snapshot);
1083
+ this.syncSnapshotToolAndAgentUi(snapshot);
1084
+ }
1085
+ buildProtocolMessageModel(message) {
1086
+ if (message.message.role === "system") {
1087
+ return undefined;
1088
+ }
1089
+ if (message.message.role === "assistant" && !isAssistantMessage(message.message)) {
1090
+ const partial = assistantPartialFromProtocolMessage(message);
1091
+ if (!partial.text && !(this.showThinking && partial.thinking)) {
1092
+ return undefined;
1093
+ }
1094
+ return {
1095
+ type: "assistant_partial",
1096
+ text: partial.text,
1097
+ thinking: partial.thinking,
1098
+ };
1099
+ }
1100
+ if (message.message.role === "toolResult" &&
1101
+ hasToolUiFacetForToolCall(this.snapshot, message.message.toolCallId)) {
1102
+ return undefined;
1103
+ }
1104
+ if (isCoreMessage(message.message)) {
1105
+ return buildHistoryMessageModel(message.message);
1106
+ }
1107
+ return undefined;
1108
+ }
1109
+ updateStreamingStateFromSnapshot(snapshot) {
1110
+ if (snapshot.lifecycle === "running") {
1111
+ this.startObservedSessionTurn();
1112
+ return;
1113
+ }
1114
+ this.finishObservedSessionTurn();
1115
+ }
1116
+ syncSnapshotToolAndAgentUi(snapshot) {
1117
+ this.view.resetToolUiSession();
1118
+ for (const event of getToolUiEventsInModelOrder(snapshot)) {
1119
+ this.view.handleToolUiEvent(event);
1120
+ }
1121
+ for (const agent of Object.values(snapshot.agents)) {
1122
+ for (const event of subagentUiEventsFromAgentRun(agent)) {
1123
+ this.view.handleSubagentEvent(event);
1124
+ }
1125
+ }
1126
+ }
1127
+ collectAssistantMessages(snapshot) {
1128
+ const messages = [];
1129
+ for (const entry of snapshot.messages) {
1130
+ if (isAssistantMessage(entry.message)) {
1131
+ messages.push(entry.message);
1132
+ }
1133
+ }
1134
+ return messages;
1135
+ }
1136
+ async syncFromSessionSnapshot() {
1137
+ try {
1138
+ this.syncRenderedHistory(await this.session.snapshot());
1139
+ this.refreshStatus();
1140
+ return true;
1141
+ }
1142
+ catch (error) {
1143
+ this.view.addSystemMessage(`snapshot refresh failed: ${error.message}`, "warn");
1144
+ return false;
1145
+ }
1146
+ }
1147
+ finishObservedSessionTurn() {
1148
+ if (!this.stopVisibleSessionTurn()) {
1149
+ return;
1150
+ }
1151
+ void this.stopTurnCaffeinate();
1152
+ if (this.submittedTurnInProgress) {
1153
+ return;
1154
+ }
1155
+ void this.syncFromSessionSnapshot().finally(() => {
1156
+ void this.drainQueuedMessages();
1157
+ });
1158
+ }
1159
+ stopVisibleSessionTurn() {
1160
+ if (!this.isStreaming) {
1161
+ return false;
1162
+ }
1163
+ this.isStreaming = false;
1164
+ this.view.stopWorkingIcon();
1165
+ this.stopTurnTimer();
1166
+ this.refreshStatus();
1167
+ return true;
1168
+ }
1169
+ startObservedSessionTurn() {
1170
+ if (this.isStreaming) {
1171
+ return;
1172
+ }
1173
+ this.isStreaming = true;
1174
+ this.startTurnTimer();
1175
+ this.startTurnCaffeinate();
1176
+ this.view.startWorkingIcon();
1177
+ this.refreshStatus();
1178
+ }
1179
+ isSessionOperationActive() {
1180
+ return this.isStreaming || this.submittedTurnInProgress;
1181
+ }
1182
+ async recoverFromRevisionGap() {
1183
+ if (this.snapshotRecovery) {
1184
+ return;
1185
+ }
1186
+ this.snapshotRecovery = this.syncFromSessionSnapshot()
1187
+ .then((synced) => {
1188
+ if (synced) {
1189
+ return this.replaySnapshotRecoveryDeltas();
1190
+ }
1191
+ })
1192
+ .finally(() => {
1193
+ this.snapshotRecovery = undefined;
1194
+ });
1195
+ await this.snapshotRecovery;
1196
+ }
1197
+ async replaySnapshotRecoveryDeltas() {
1198
+ while (this.snapshotRecoveryDeltas.length > 0) {
1199
+ const deltas = this.snapshotRecoveryDeltas.splice(0);
1200
+ for (let index = 0; index < deltas.length; index++) {
1201
+ const delta = deltas[index];
1202
+ if (this.tryApplySdkDelta(delta)) {
1203
+ continue;
1204
+ }
1205
+ if (!(await this.syncFromSessionSnapshot())) {
1206
+ this.snapshotRecoveryDeltas.unshift(delta, ...deltas.slice(index + 1));
1207
+ return;
1208
+ }
1209
+ }
1210
+ }
1211
+ }
1212
+ startLocalUiSession() {
1213
+ this.hiddenHistoryEntryIds.clear();
1214
+ this.clientRenderedUserMessages.clear();
1215
+ this.renderedMessageIds.splice(0);
1216
+ this.view.addMessage({ type: "session_divider", label: "new session" });
1217
+ }
1218
+ addSessionIdentityMessage() {
1219
+ this.view.addSystemMessage(this.formatSessionIdentityText(), "muted");
1220
+ }
1221
+ renderCompactedSnapshot(snapshot) {
1222
+ this.startLocalUiSession();
1223
+ this.renderSnapshot(snapshot);
1224
+ }
1225
+ shouldRenderAutoCompactedReset(delta) {
1226
+ return (delta.reason === "maintenance" &&
1227
+ delta.delta.type === "snapshot.reset" &&
1228
+ this.hasRunningAutoCompactionOperation(this.snapshot));
1229
+ }
1230
+ renderAutoCompactedReset(snapshot) {
1231
+ this.view.resetToolUiSessionPreservingSubagents();
1232
+ this.startLocalUiSession();
1233
+ this.renderSnapshot(snapshot);
1234
+ const metadata = this.getAutoCompactionMetadata(snapshot);
1235
+ if (metadata) {
1236
+ this.view.addMessage({
1237
+ type: "system",
1238
+ text: formatAutoCompactionRetainedText(metadata),
1239
+ kind: "muted",
1240
+ });
1241
+ }
1242
+ }
1243
+ toggleThinkingVisibility() {
1244
+ this.showThinking = !this.showThinking;
1245
+ this.view.setThinkingVisibility(this.showThinking);
1246
+ this.view.addSystemMessage(this.showThinking ? "thoughts visible" : "thoughts hidden", "success");
1247
+ }
1248
+ toggleCompactToolUi() {
1249
+ this.compactToolUi = !this.compactToolUi;
1250
+ this.view.setCompactToolUi(this.compactToolUi);
1251
+ this.view.addSystemMessage(this.compactToolUi ? "compact tool ui enabled" : "compact tool ui disabled", "success");
1252
+ }
1253
+ async cycleRiskLevel() {
1254
+ const next = this.snapshot.settings.riskLevel === "read-only" ? "read-write" : "read-only";
1255
+ await this.setRiskLevel(next);
1256
+ }
1257
+ async cyclePersonality() {
1258
+ const personas = this.snapshot.catalog.personas;
1259
+ if (personas.length === 0) {
1260
+ this.view.addSystemMessage("no personas available.", "warn");
1261
+ return;
1262
+ }
1263
+ const currentIndex = personas.findIndex((persona) => persona.id.toLowerCase() === this.snapshot.settings.personaId.toLowerCase());
1264
+ const next = personas[(currentIndex + 1 + personas.length) % personas.length];
1265
+ await this.setPersona(next.id);
1266
+ }
1267
+ async cycleReasoningLevel() {
1268
+ const allowed = this.getAllowedReasoningLevels(this.getCurrentPersonaSnapshot());
1269
+ const current = (this.snapshot.settings.reasoning ?? allowed[0]);
1270
+ const index = allowed.indexOf(current);
1271
+ const next = allowed[(index + 1) % allowed.length] ?? allowed[0];
1272
+ if (this.isSessionOperationActive()) {
1273
+ this.view.addSystemMessage("cannot change reasoning while a session turn is running", "warn");
1274
+ return;
1275
+ }
1276
+ try {
1277
+ this.snapshot = await this.session.setReasoning(next);
1278
+ this.refreshStatus();
1279
+ }
1280
+ catch (error) {
1281
+ this.view.addSystemMessage(`reasoning change failed: ${error.message}`, "error");
1282
+ }
1283
+ }
1284
+ getAllowedReasoningLevels(persona) {
1285
+ if (!this.snapshot.bootstrap.model.reasoning) {
1286
+ return ["none"];
1287
+ }
1288
+ const raw = persona?.allowedReasoningLevels;
1289
+ if (!raw || raw.length === 0) {
1290
+ return REASONING_LEVELS;
1291
+ }
1292
+ const normalized = raw.filter((level) => REASONING_LEVELS.includes(level));
1293
+ const unique = [...new Set(normalized)];
1294
+ return unique.length ? unique : REASONING_LEVELS;
1295
+ }
1296
+ async setRiskLevel(rawLevel) {
1297
+ const riskLevel = rawLevel.trim();
1298
+ if (riskLevel !== "read-only" && riskLevel !== "read-write") {
1299
+ this.view.addSystemMessage(`invalid risk level '${rawLevel}'. allowed: read-only, read-write`, "error");
1300
+ return;
1301
+ }
1302
+ if (this.isSessionOperationActive()) {
1303
+ this.view.addSystemMessage("cannot change risk while a session turn is running", "warn");
1304
+ return;
1305
+ }
1306
+ try {
1307
+ this.snapshot = await this.session.setRiskLevel(riskLevel);
1308
+ this.syncRenderedHistory(this.snapshot);
1309
+ this.refreshStatus();
1310
+ this.view.addSystemMessage(this.formatRiskLevelNotice(riskLevel), "success");
1311
+ }
1312
+ catch (error) {
1313
+ this.view.addSystemMessage(`risk change failed: ${error.message}`, "error");
1314
+ }
1315
+ }
1316
+ formatRiskLevelNotice(level) {
1317
+ const details = getRiskLevelDescription(level);
1318
+ return details ? `risk level set to ${level} (${details})` : `risk level set to ${level}`;
1319
+ }
1320
+ async setPersona(rawId) {
1321
+ const id = rawId.trim();
1322
+ if (!id) {
1323
+ this.view.addSystemMessage("missing persona id", "error");
1324
+ return;
1325
+ }
1326
+ const persona = this.snapshot.catalog.personas.find((candidate) => candidate.id.toLowerCase() === id.toLowerCase());
1327
+ if (!persona) {
1328
+ this.view.addSystemMessage(`unknown persona '${id}'.`, "error");
1329
+ return;
1330
+ }
1331
+ if (this.isSessionOperationActive()) {
1332
+ this.view.addSystemMessage("cannot switch persona while a session turn is running", "warn");
1333
+ return;
1334
+ }
1335
+ try {
1336
+ this.snapshot = await this.session.setPersona(persona.id);
1337
+ this.syncRenderedHistory(this.snapshot);
1338
+ this.refreshStatus();
1339
+ this.view.addSystemMessage(`switched to ${persona.label} (${this.snapshot.bootstrap.model.id})`, "success");
1340
+ }
1341
+ catch (error) {
1342
+ this.view.addSystemMessage(`persona switch failed: ${error.message}`, "error");
1343
+ }
1344
+ }
1345
+ async insertPrompt(rawId) {
1346
+ const id = rawId.trim();
1347
+ if (!id) {
1348
+ this.view.addSystemMessage("missing prompt id", "error");
1349
+ return;
1350
+ }
1351
+ const prompt = this.snapshot.catalog.prompts.find((candidate) => candidate.id.toLowerCase() === id.toLowerCase());
1352
+ if (!prompt) {
1353
+ this.view.addSystemMessage(`unknown prompt '${id}'.`, "error");
1354
+ return;
1355
+ }
1356
+ try {
1357
+ const resolved = await this.session.resolvePrompt(prompt.id);
1358
+ this.view.setEditorText(resolved.text);
1359
+ }
1360
+ catch (error) {
1361
+ this.view.addSystemMessage(`prompt load failed: ${error.message}`, "error");
1362
+ }
1363
+ }
1364
+ async reloadContent() {
1365
+ if (this.isSessionOperationActive()) {
1366
+ this.view.addSystemMessage("cannot reload while a session turn is running", "warn");
1367
+ return;
1368
+ }
1369
+ try {
1370
+ const result = await this.session.reload();
1371
+ this.snapshot = result.snapshot;
1372
+ this.syncRenderedHistory(this.snapshot);
1373
+ this.refreshStatus();
1374
+ for (const warning of result.warnings) {
1375
+ this.view.addSystemMessage(warning, "warn");
1376
+ }
1377
+ const summary = [
1378
+ `${result.counts.personas} personas`,
1379
+ `${result.counts.prompts} prompts`,
1380
+ `${result.counts.skills} skills`,
1381
+ ...formatAgentsMdReloadSummary(this.getAgentsFilePaths().length),
1382
+ ].join(", ");
1383
+ this.view.addSystemMessage(`reloaded: ${summary}.`, "success");
1384
+ }
1385
+ catch (error) {
1386
+ this.view.addSystemMessage(`reload failed: ${error.message}`, "error");
1387
+ }
1388
+ }
1389
+ async compactSession(mode, guidanceText) {
1390
+ if (this.isSessionOperationActive()) {
1391
+ this.view.addSystemMessage("cannot compact while a session turn is running", "warn");
1392
+ return;
1393
+ }
1394
+ const guidance = guidanceText?.trim() ?? "";
1395
+ this.view.addSystemMessage("summarizing session...", "success");
1396
+ try {
1397
+ const result = await this.session.compact(mode, {
1398
+ ...(guidance ? { guidance } : {}),
1399
+ });
1400
+ this.renderCompactedSnapshot(result.snapshot);
1401
+ this.refreshStatus();
1402
+ const message = mode === "summary-and-last" && result.includedLastAssistant
1403
+ ? "session compacted. previous context and last assistant message have been included."
1404
+ : "session compacted. previous context has been summarized.";
1405
+ this.view.addSystemMessage(message, "success");
1406
+ }
1407
+ catch (error) {
1408
+ const message = error.message || "compaction failed";
1409
+ const kind = message === "no conversation to compact." ? "warn" : "error";
1410
+ this.view.addSystemMessage(kind === "warn" ? message : `compact failed: ${message}`, kind);
1411
+ }
1412
+ }
1413
+ async pruneSession(strategy, extraText) {
1414
+ if (this.isSessionOperationActive()) {
1415
+ this.view.addSystemMessage("cannot prune while a session turn is running", "warn");
1416
+ return;
1417
+ }
1418
+ const extra = extraText?.trim() ?? "";
1419
+ const parsed = strategy === "smart"
1420
+ ? parseSessionPruneFractionAndGuidance(extra)
1421
+ : { fraction: parseSessionPruneFraction(extra) };
1422
+ if (parsed.fraction === null) {
1423
+ this.view.addSystemMessage("invalid prune fraction. use a number between 0 and 1.", "error");
1424
+ return;
1425
+ }
1426
+ if (strategy === "smart" && parsed.fraction !== 0) {
1427
+ this.view.addSystemMessage("sampling prune candidates...", "success");
1428
+ }
1429
+ try {
1430
+ const result = await this.session.pruneToolResults(strategy, {
1431
+ fraction: parsed.fraction,
1432
+ ...("guidance" in parsed && parsed.guidance !== undefined
1433
+ ? { guidance: parsed.guidance }
1434
+ : {}),
1435
+ });
1436
+ this.syncRenderedHistory(result.snapshot);
1437
+ this.refreshStatus();
1438
+ this.view.addSystemMessage(result.message, result.noop ? "warn" : "success");
1439
+ }
1440
+ catch (error) {
1441
+ this.view.addSystemMessage(`prune failed: ${error.message}`, "error");
1442
+ }
1443
+ }
1444
+ async speakLastAssistantMessage() {
1445
+ if (this.speakTask) {
1446
+ this.view.addSystemMessage("speech playback already in progress", "warn");
1447
+ return;
1448
+ }
1449
+ if (this.isSessionOperationActive()) {
1450
+ this.view.addSystemMessage("wait for the assistant to finish before speaking", "warn");
1451
+ return;
1452
+ }
1453
+ if (this.deps.env.platform() !== "darwin") {
1454
+ this.view.addSystemMessage("/speak is currently supported only on macOS.", "warn");
1455
+ return;
1456
+ }
1457
+ const lastAssistant = this.getLastAssistantMessage();
1458
+ if (!lastAssistant) {
1459
+ this.view.addSystemMessage("no assistant message to speak yet.", "warn");
1460
+ return;
1461
+ }
1462
+ const sourceText = extractAssistantText(lastAssistant).trim();
1463
+ if (!sourceText) {
1464
+ this.view.addSystemMessage("last assistant message was empty.", "warn");
1465
+ return;
1466
+ }
1467
+ const apiKey = getGoogleApiKey(this.config, this.deps.env.env());
1468
+ if (!apiKey) {
1469
+ this.view.addSystemMessage("set GEMINI_API_KEY or apiKeys.google to use /speak", "error");
1470
+ return;
1471
+ }
1472
+ this.isStreaming = true;
1473
+ this.speechStatusHint = "rewriting for speech...";
1474
+ this.refreshStatus();
1475
+ this.view.startWorkingIcon();
1476
+ const abortController = new AbortController();
1477
+ const completion = this.runSpeakTask({
1478
+ apiKey,
1479
+ sourceText,
1480
+ signal: abortController.signal,
1481
+ });
1482
+ const task = { abortController, completion };
1483
+ this.speakTask = task;
1484
+ try {
1485
+ await completion;
1486
+ }
1487
+ catch (err) {
1488
+ if (abortController.signal.aborted) {
1489
+ return;
1490
+ }
1491
+ this.view.addSystemMessage(`speech synthesis failed: ${err.message}`, "error");
1492
+ }
1493
+ finally {
1494
+ const wasAborted = abortController.signal.aborted;
1495
+ if (this.speakTask === task) {
1496
+ this.speakTask = undefined;
1497
+ }
1498
+ this.speechStatusHint = undefined;
1499
+ this.view.stopWorkingIcon();
1500
+ this.isStreaming = false;
1501
+ this.refreshStatus();
1502
+ this.view.requestRender();
1503
+ if (wasAborted) {
1504
+ this.dequeueQueuedMessagesIntoEditor();
1505
+ }
1506
+ else {
1507
+ void this.drainQueuedMessages();
1508
+ }
1509
+ }
1510
+ }
1511
+ async runSpeakTask(args) {
1512
+ await runSpeechPlaybackTask({
1513
+ deps: this.deps,
1514
+ apiKey: args.apiKey,
1515
+ sourceText: args.sourceText,
1516
+ signal: args.signal,
1517
+ onStatusHint: (hint) => {
1518
+ this.speechStatusHint = hint;
1519
+ this.refreshStatus();
1520
+ },
1521
+ });
1522
+ }
1523
+ refreshStatus() {
1524
+ this.view.updateStatus({
1525
+ footer: {
1526
+ contextUsage: this.getContextUsageString(),
1527
+ sessionCost: this.getSessionCostString(),
1528
+ duration: this.getTurnDurationString(),
1529
+ riskLevel: this.snapshot.settings.riskLevel,
1530
+ commandHint: this.diffReviewService.getCommandHint(this.getSnapshotOperationStatusHint() ?? this.speechStatusHint ?? this.commandHint),
1531
+ },
1532
+ editor: {
1533
+ mode: this.getInputMode(),
1534
+ cwdLabel: this.getFooterCwdLabel(),
1535
+ personaName: this.getCurrentPersonaSnapshot()?.label ?? this.snapshot.settings.personaId,
1536
+ reasoningLabel: this.snapshot.settings.reasoning ?? "none",
1537
+ reasoning: this.snapshot.settings.reasoning === undefined ||
1538
+ this.snapshot.settings.reasoning === "none"
1539
+ ? undefined
1540
+ : this.snapshot.settings.reasoning,
1541
+ },
1542
+ });
1543
+ }
1544
+ getInputMode() {
1545
+ if (this.listenRecording)
1546
+ return "recording";
1547
+ if (this.isBashIncognito)
1548
+ return "bash_incognito";
1549
+ if (this.isBashMode)
1550
+ return "bash";
1551
+ if (this.isMemoryMode)
1552
+ return "memory";
1553
+ return "normal";
1554
+ }
1555
+ getSnapshotOperationStatusHint() {
1556
+ const hasRunningAutoCompaction = this.hasRunningAutoCompactionOperation(this.snapshot);
1557
+ return hasRunningAutoCompaction ? "compacting context..." : undefined;
1558
+ }
1559
+ hasRunningAutoCompactionOperation(snapshot) {
1560
+ return snapshot.timeline.some((item) => item.type === "operation" &&
1561
+ item.operation.kind === "auto-compaction" &&
1562
+ item.operation.status === "running");
1563
+ }
1564
+ getContextUsageString() {
1565
+ const last = this.getLastAssistantMessage();
1566
+ const windowTokens = last
1567
+ ? this.getContextWindowForLastTurn(last)
1568
+ : this.getBootstrapContextWindow();
1569
+ const { input, read, write, output } = this.getSessionUsageTotals();
1570
+ const stats = `↑${formatTokenWindow(input)} ↓${formatTokenWindow(output)} (r${formatTokenWindow(read)} w${formatTokenWindow(write)})`;
1571
+ const contextWindowUsageTokens = getAssistantContextWindowUsage(last);
1572
+ const percent = windowTokens > 0 ? (contextWindowUsageTokens / windowTokens) * 100 : 0;
1573
+ const percentStr = `${formatAdaptiveNumber(percent, 1, 3)}%`;
1574
+ return `${stats} · ${percentStr}/${formatTokenWindow(windowTokens)}`;
1575
+ }
1576
+ getSessionCostString() {
1577
+ let total = 0;
1578
+ for (const entry of this.snapshot.messages) {
1579
+ if (isAssistantMessage(entry.message)) {
1580
+ total += entry.message.usage?.cost?.total ?? 0;
1581
+ }
1582
+ }
1583
+ return formatSessionCost(total + this.view.getToolUiCostTotal());
1584
+ }
1585
+ getTurnDurationString() {
1586
+ const now = Date.now();
1587
+ const elapsed = this.currentTurnStartedAt !== undefined
1588
+ ? Math.max(0, now - this.currentTurnStartedAt)
1589
+ : Math.max(0, this.lastTurnDurationMs);
1590
+ return formatDurationMs(elapsed);
1591
+ }
1592
+ startTurnTimer() {
1593
+ this.currentTurnStartedAt = Date.now();
1594
+ this.lastTurnDurationMs = 0;
1595
+ if (this.turnTimer) {
1596
+ clearInterval(this.turnTimer);
1597
+ }
1598
+ this.turnTimer = setInterval(() => this.refreshStatus(), 1000);
1599
+ }
1600
+ stopTurnTimer() {
1601
+ if (this.currentTurnStartedAt !== undefined) {
1602
+ this.lastTurnDurationMs = Math.max(0, Date.now() - this.currentTurnStartedAt);
1603
+ }
1604
+ this.currentTurnStartedAt = undefined;
1605
+ if (this.turnTimer) {
1606
+ clearInterval(this.turnTimer);
1607
+ this.turnTimer = undefined;
1608
+ }
1609
+ }
1610
+ getLastAssistantMessage() {
1611
+ if (this.assistantMessages.length > 0) {
1612
+ return this.assistantMessages[this.assistantMessages.length - 1];
1613
+ }
1614
+ for (let i = this.snapshot.messages.length - 1; i >= 0; i -= 1) {
1615
+ const message = this.snapshot.messages[i]?.message;
1616
+ if (isAssistantMessage(message)) {
1617
+ return message;
1618
+ }
1619
+ }
1620
+ return undefined;
1621
+ }
1622
+ getSessionUsageTotals() {
1623
+ let input = 0;
1624
+ let read = 0;
1625
+ let write = 0;
1626
+ let output = 0;
1627
+ for (const entry of this.snapshot.messages) {
1628
+ if (!isAssistantMessage(entry.message)) {
1629
+ continue;
1630
+ }
1631
+ input += entry.message.usage?.input ?? 0;
1632
+ read += entry.message.usage?.cacheRead ?? 0;
1633
+ write += entry.message.usage?.cacheWrite ?? 0;
1634
+ output += entry.message.usage?.output ?? 0;
1635
+ }
1636
+ return { input, read, write, output };
1637
+ }
1638
+ getContextWindowForLastTurn(message) {
1639
+ const usage = message.usage;
1640
+ return typeof usage.contextWindow === "number" && Number.isFinite(usage.contextWindow)
1641
+ ? usage.contextWindow
1642
+ : this.getBootstrapContextWindow();
1643
+ }
1644
+ getBootstrapContextWindow() {
1645
+ return this.snapshot.bootstrap.model.contextWindow ?? 0;
1646
+ }
1647
+ getFooterCwdLabel() {
1648
+ const cwd = formatPathForSessionDisplay(this.snapshot.executionEnvironment.cwd, this.snapshot.executionEnvironment.home);
1649
+ return this.isRemoteSessionTarget() ? `remote · ${cwd}` : cwd;
1650
+ }
1651
+ formatSessionIdentityText() {
1652
+ return `session id: ${this.snapshot.sessionId}`;
1653
+ }
1654
+ isRemoteSessionTarget() {
1655
+ return this.targetLabel !== "in-process" || this.snapshot.executionEnvironment.kind !== "local";
1656
+ }
1657
+ getAgentsFilePaths() {
1658
+ const systemPrompt = this.snapshot.messages.find((entry) => entry.message.role === "system")
1659
+ ?.message.content;
1660
+ if (typeof systemPrompt !== "string") {
1661
+ return [];
1662
+ }
1663
+ const files = new Set();
1664
+ const regex = /<file path="([^"]*AGENTS\.md)">/g;
1665
+ for (const match of systemPrompt.matchAll(regex)) {
1666
+ const path = unescapeXmlAttribute(match[1] ?? "");
1667
+ if (path) {
1668
+ files.add(path);
1669
+ }
1670
+ }
1671
+ return [...files];
1672
+ }
1673
+ getAutoCompactionMetadata(snapshot) {
1674
+ for (const entry of snapshot.messages) {
1675
+ if (!isCoreMessage(entry.message)) {
1676
+ continue;
1677
+ }
1678
+ const metadata = getAutoCompactionMetadataFromMessage(entry.message);
1679
+ if (metadata) {
1680
+ return metadata;
1681
+ }
1682
+ }
1683
+ return undefined;
1684
+ }
1685
+ async copyLastAssistantText() {
1686
+ await copyAssistantTextToClipboard({
1687
+ view: this.view,
1688
+ message: this.getLastAssistantMessage(),
1689
+ });
1690
+ }
1691
+ async copyLastAssistantCode() {
1692
+ await copyAssistantCodeToClipboard({
1693
+ view: this.view,
1694
+ message: this.getLastAssistantMessage(),
1695
+ });
1696
+ }
1697
+ async startDiffReview(argsText) {
1698
+ if (this.diffReviewService.isActive()) {
1699
+ this.view.addSystemMessage("diff review is already active.", "warn");
1700
+ return;
1701
+ }
1702
+ if (!this.isDiffReviewIdle()) {
1703
+ this.view.addSystemMessage("wait for tau to become idle before starting /diff.", "warn");
1704
+ return;
1705
+ }
1706
+ await this.diffReviewService.start(argsText);
1707
+ }
1708
+ isDiffReviewIdle() {
1709
+ return (!this.isStreaming &&
1710
+ !this.submittedTurnInProgress &&
1711
+ !this.listenRecording &&
1712
+ !this.listenTransition &&
1713
+ !this.isTranscribingListen &&
1714
+ !this.speakTask &&
1715
+ !this.diffReviewService.isActive());
1716
+ }
1717
+ resolveDiffToolConfig() {
1718
+ return this.config.diffTool ?? this.defaultDiffTool;
1719
+ }
1720
+ async startDiffReviewBridge(args) {
1721
+ const snapshot = await captureDiffReviewSnapshot({
1722
+ cwd: this.snapshot.executionEnvironment.cwd,
1723
+ source: args.source,
1724
+ signal: args.signal,
1725
+ deps: createSessionDiffReviewSnapshotDeps(this.session),
1726
+ });
1727
+ const ephemeral = await this.session.createEphemeralContext({
1728
+ instructions: buildDiffReviewInstructions(snapshot),
1729
+ tools: ["bash", "view_image"],
1730
+ riskLevel: "read-only",
1731
+ });
1732
+ const bridge = new DiffReviewBridge({
1733
+ snapshot,
1734
+ contextWindow: this.snapshot.bootstrap.model.contextWindow,
1735
+ submitThreadMessage: (options) => this.session.submitEphemeralThread({
1736
+ contextId: ephemeral.contextId,
1737
+ threadId: options.threadId,
1738
+ ...(options.forkFromThreadId ? { forkFromThreadId: options.forkFromThreadId } : {}),
1739
+ message: options.message,
1740
+ }),
1741
+ toolLaunchCwd: this.snapshot.executionEnvironment.cwd,
1742
+ deps: this.deps,
1743
+ ...(this.diffToolLauncher ? { toolLauncher: this.diffToolLauncher } : {}),
1744
+ });
1745
+ const unsubscribeEphemeral = this.session.onEphemeral((message) => {
1746
+ const event = message.event;
1747
+ if (event.type !== "ephemeral-agent.thread-update" ||
1748
+ event.contextId !== ephemeral.contextId) {
1749
+ return;
1750
+ }
1751
+ bridge.applyThreadUpdate(event.threadId, {
1752
+ costTotal: event.update.costTotal,
1753
+ usage: { ...event.update.usage },
1754
+ ...(event.update.lastActivityText
1755
+ ? { lastActivityText: event.update.lastActivityText }
1756
+ : {}),
1757
+ });
1758
+ });
1759
+ try {
1760
+ await bridge.start();
1761
+ if (args.signal.aborted) {
1762
+ throw new Error("diff review start aborted");
1763
+ }
1764
+ await bridge.launchTool(args.diffTool);
1765
+ }
1766
+ catch (error) {
1767
+ unsubscribeEphemeral();
1768
+ await this.session.closeEphemeralContext(ephemeral.contextId).catch(() => undefined);
1769
+ await bridge.cancel("launch_failed").catch(() => undefined);
1770
+ await bridge.close().catch(() => undefined);
1771
+ throw error;
1772
+ }
1773
+ const result = bridge.result.finally(async () => {
1774
+ unsubscribeEphemeral();
1775
+ await this.session.closeEphemeralContext(ephemeral.contextId).catch(() => undefined);
1776
+ });
1777
+ return { bridge, result };
1778
+ }
1779
+ async handleReturnedDiffReview(review) {
1780
+ const model = {
1781
+ type: "user",
1782
+ text: review.review,
1783
+ kind: "review",
1784
+ };
1785
+ this.clientRenderedUserMessages.set(review.historyEntryId, model);
1786
+ if (!this.renderedMessageIds.includes(review.historyEntryId)) {
1787
+ this.renderedMessageIds.push(review.historyEntryId);
1788
+ }
1789
+ try {
1790
+ const result = await this.session.record(formatDiffReviewUserMessage(review), {
1791
+ historyEntryId: review.historyEntryId,
1792
+ });
1793
+ this.syncRenderedHistory(result.snapshot);
1794
+ }
1795
+ catch (error) {
1796
+ this.view.addSystemMessage(`failed to add diff review to session: ${error.message}`, "error");
1797
+ }
1798
+ }
1799
+ startRewindFlow() {
1800
+ if (this.isSessionOperationActive()) {
1801
+ return;
1802
+ }
1803
+ const candidates = this.snapshot.messages.flatMap((entry) => {
1804
+ if (!entry.modelVisible || entry.message.role !== "user") {
1805
+ return [];
1806
+ }
1807
+ const text = extractHistoryUserText(entry.message);
1808
+ return [
1809
+ {
1810
+ id: entry.id,
1811
+ text,
1812
+ label: formatRewindCandidateLabel(text),
1813
+ },
1814
+ ];
1815
+ });
1816
+ if (candidates.length === 0) {
1817
+ this.view.addSystemMessage("no user messages available to rewind.", "warn");
1818
+ return;
1819
+ }
1820
+ this.view.showRewindPicker({
1821
+ items: candidates.map(({ id, label }) => ({ id, label })),
1822
+ onSelect: (id) => {
1823
+ const selected = candidates.find((candidate) => candidate.id === id);
1824
+ if (!selected) {
1825
+ this.view.hideRewindPicker();
1826
+ this.view.addSystemMessage("rewind selection failed.", "error");
1827
+ return;
1828
+ }
1829
+ void this.applyRewindSelection(selected.id);
1830
+ },
1831
+ onCancel: () => {
1832
+ this.view.hideRewindPicker();
1833
+ },
1834
+ });
1835
+ }
1836
+ async applyRewindSelection(historyEntryId) {
1837
+ this.view.hideRewindPicker();
1838
+ try {
1839
+ const rewound = await this.session.rewindToHistoryEntryId(historyEntryId);
1840
+ this.syncRenderedHistory(rewound.snapshot);
1841
+ this.view.removeMessagesFrom(rewound.historyEntryId);
1842
+ this.view.removeMessages(rewound.removedEntryIds);
1843
+ this.view.setEditorText(rewound.text);
1844
+ this.refreshStatus();
1845
+ }
1846
+ catch {
1847
+ this.view.addSystemMessage("rewind failed.", "error");
1848
+ }
1849
+ }
1850
+ terminateSelectedSubagent() {
1851
+ const selectedId = this.view.getSelectedSubagentId();
1852
+ if (!selectedId) {
1853
+ this.view.addSystemMessage("no active subagent selected", "warn");
1854
+ return;
1855
+ }
1856
+ void this.session
1857
+ .terminateSubagent(selectedId)
1858
+ .then((result) => {
1859
+ if (!result.found) {
1860
+ this.view.addSystemMessage(`unknown subagent id: ${selectedId}`, "warn");
1861
+ }
1862
+ })
1863
+ .catch((err) => {
1864
+ this.view.addSystemMessage(`failed to terminate subagent: ${err.message}`, "error");
1865
+ });
1866
+ }
1867
+ async stashEditorToClipboard() {
1868
+ const text = this.view.getExpandedEditorText();
1869
+ if (!text.trim()) {
1870
+ this.view.addSystemMessage("no input to stash yet", "warn");
1871
+ return;
1872
+ }
1873
+ try {
1874
+ await copyTextToClipboard(text);
1875
+ this.view.setEditorText("");
1876
+ this.view.addSystemMessage("stashed input to clipboard", "success");
1877
+ }
1878
+ catch (err) {
1879
+ this.view.addSystemMessage(`clipboard copy failed: ${err.message}`, "error");
1880
+ }
1881
+ }
1882
+ isSingleLineInput(text) {
1883
+ return !/[\r\n]/.test(text);
1884
+ }
1885
+ getCurrentPersonaSnapshot() {
1886
+ return this.snapshot.catalog.personas.find((persona) => persona.id.toLowerCase() === this.snapshot.settings.personaId.toLowerCase());
1887
+ }
1888
+ }
1889
+ function getTimelineMessages(snapshot) {
1890
+ const messagesById = new Map(snapshot.messages.map((message) => [message.id, message]));
1891
+ return snapshot.timeline.flatMap((item) => {
1892
+ if (item.type !== "message") {
1893
+ return [];
1894
+ }
1895
+ const message = messagesById.get(item.messageId);
1896
+ return message ? [message] : [];
1897
+ });
1898
+ }
1899
+ function createSessionDiffReviewSnapshotDeps(session) {
1900
+ return {
1901
+ spawn: async (cmd, args, options = {}) => {
1902
+ void options.env;
1903
+ void options.maxCaptureBytes;
1904
+ if (options.signal?.aborted) {
1905
+ return {
1906
+ stdout: "",
1907
+ stderr: "",
1908
+ output: "",
1909
+ exitCode: null,
1910
+ captureLimitExceeded: false,
1911
+ timedOut: false,
1912
+ aborted: true,
1913
+ closeSignal: null,
1914
+ };
1915
+ }
1916
+ const command = [cmd, ...args].map(shellQuote).join(" ");
1917
+ const result = await session.exec(command, {
1918
+ ...(options.cwd !== undefined ? { cwd: options.cwd } : {}),
1919
+ ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
1920
+ });
1921
+ return {
1922
+ stdout: result.stdout,
1923
+ stderr: result.stderr,
1924
+ output: result.output,
1925
+ exitCode: result.exitCode,
1926
+ captureLimitExceeded: result.truncated,
1927
+ timedOut: false,
1928
+ aborted: options.signal?.aborted ?? false,
1929
+ closeSignal: null,
1930
+ };
1931
+ },
1932
+ env: createDefaultCoreDeps().env,
1933
+ fs: {
1934
+ readFile: async (path) => {
1935
+ const script = "process.stdout.write(require('node:fs').readFileSync(process.argv[1]))";
1936
+ const result = await session.exec(`node -e ${shellQuote(script)} ${shellQuote(path)}`);
1937
+ if (result.exitCode !== 0) {
1938
+ throw new Error(result.stderr || result.output || `failed to read ${path}`);
1939
+ }
1940
+ return result.stdout;
1941
+ },
1942
+ },
1943
+ };
1944
+ }
1945
+ function isMessageInTimeline(snapshot, messageId) {
1946
+ return snapshot.timeline.some((item) => item.type === "message" && item.messageId === messageId);
1947
+ }
1948
+ function hasToolUiFacetForToolCall(snapshot, toolCallId) {
1949
+ return Object.values(snapshot.facets).some((facet) => facet.kind === "tau.tool-ui-events" &&
1950
+ facet.subject.type === "tool" &&
1951
+ facet.subject.id === toolCallId);
1952
+ }
1953
+ function assistantPartialFromProtocolMessage(message) {
1954
+ if (message.message.role !== "assistant") {
1955
+ return { text: "", thinking: "" };
1956
+ }
1957
+ let text = "";
1958
+ const thinking = [];
1959
+ for (const content of message.message.content) {
1960
+ if (content.type === "text") {
1961
+ text += content.text;
1962
+ }
1963
+ if (content.type === "thinking") {
1964
+ thinking.push(content.thinking);
1965
+ }
1966
+ }
1967
+ return { text, thinking: thinking.join("\n\n") };
1968
+ }
1969
+ function isAssistantMessage(message) {
1970
+ return (message?.role === "assistant" &&
1971
+ "usage" in message &&
1972
+ "api" in message &&
1973
+ typeof message.stopReason === "string");
1974
+ }
1975
+ function getAssistantContextWindowUsage(message) {
1976
+ if (!message?.usage) {
1977
+ return 0;
1978
+ }
1979
+ const usage = message.usage;
1980
+ if (typeof usage.contextWindowUsageTokens === "number" &&
1981
+ Number.isFinite(usage.contextWindowUsageTokens)) {
1982
+ return usage.contextWindowUsageTokens;
1983
+ }
1984
+ return ((message.usage.input ?? 0) +
1985
+ (message.usage.cacheRead ?? 0) +
1986
+ (message.usage.cacheWrite ?? 0) +
1987
+ (message.usage.output ?? 0));
1988
+ }
1989
+ function formatPathForSessionDisplay(path, home) {
1990
+ if (!home) {
1991
+ return path;
1992
+ }
1993
+ if (path === home) {
1994
+ return "~";
1995
+ }
1996
+ if (path.startsWith(`${home}/`)) {
1997
+ return `~${path.slice(home.length)}`;
1998
+ }
1999
+ return path;
2000
+ }
2001
+ function formatAgentsMdReloadSummary(count) {
2002
+ if (count <= 0) {
2003
+ return [];
2004
+ }
2005
+ return [`${count} AGENTS.md`];
2006
+ }
2007
+ function formatAutoCompactionRetainedText(result) {
2008
+ const count = result.retainedMessageCount;
2009
+ const messageLabel = count === 1 ? "message" : "messages";
2010
+ if (result.cutType === "split-turn") {
2011
+ return `retained current turn suffix, ${count} ${messageLabel}`;
2012
+ }
2013
+ return `retained ${count} recent ${messageLabel}`;
2014
+ }
2015
+ function unescapeXmlAttribute(value) {
2016
+ return value
2017
+ .replaceAll("&quot;", '"')
2018
+ .replaceAll("&apos;", "'")
2019
+ .replaceAll("&lt;", "<")
2020
+ .replaceAll("&gt;", ">")
2021
+ .replaceAll("&amp;", "&");
2022
+ }
2023
+ function isCoreMessage(message) {
2024
+ switch (message.role) {
2025
+ case "user":
2026
+ return typeof message.content === "string" || Array.isArray(message.content);
2027
+ case "assistant":
2028
+ return isAssistantMessage(message);
2029
+ case "toolResult":
2030
+ return true;
2031
+ default:
2032
+ return false;
2033
+ }
2034
+ }
2035
+ function isToolUiEvent(value) {
2036
+ return (typeof value === "object" &&
2037
+ value !== null &&
2038
+ "type" in value &&
2039
+ typeof value.type === "string" &&
2040
+ "toolCallId" in value &&
2041
+ typeof value.toolCallId === "string");
2042
+ }
2043
+ function getToolUiEventsInModelOrder(snapshot) {
2044
+ const facetsByToolId = new Map();
2045
+ for (const facet of Object.values(snapshot.facets)) {
2046
+ if (facet.kind === "tau.tool-ui-events" && facet.subject.type === "tool") {
2047
+ facetsByToolId.set(facet.subject.id, facet);
2048
+ }
2049
+ }
2050
+ const events = [];
2051
+ const emittedFacetIds = new Set();
2052
+ for (const toolId of getToolIdsInModelOrder(snapshot)) {
2053
+ const facet = facetsByToolId.get(toolId);
2054
+ if (!facet) {
2055
+ continue;
2056
+ }
2057
+ emittedFacetIds.add(facet.id);
2058
+ events.push(...toolUiEventsFromFacet(facet));
2059
+ }
2060
+ for (const facet of Object.values(snapshot.facets)) {
2061
+ if (emittedFacetIds.has(facet.id)) {
2062
+ continue;
2063
+ }
2064
+ events.push(...toolUiEventsFromFacet(facet));
2065
+ }
2066
+ return events;
2067
+ }
2068
+ function getToolIdsInModelOrder(snapshot) {
2069
+ const messageOrder = new Map(snapshot.messages.map((message, index) => [message.id, index]));
2070
+ return Object.values(snapshot.tools)
2071
+ .sort((left, right) => {
2072
+ const leftMessageIndex = messageOrder.get(left.call.messageId) ?? Number.MAX_SAFE_INTEGER;
2073
+ const rightMessageIndex = messageOrder.get(right.call.messageId) ?? Number.MAX_SAFE_INTEGER;
2074
+ return (leftMessageIndex - rightMessageIndex || left.call.contentIndex - right.call.contentIndex);
2075
+ })
2076
+ .map((tool) => tool.id);
2077
+ }
2078
+ function toolUiEventsFromFacet(facet) {
2079
+ if (facet?.kind !== "tau.tool-ui-events" || !Array.isArray(facet.data.events)) {
2080
+ return [];
2081
+ }
2082
+ return facet.data.events.filter(isToolUiEvent);
2083
+ }
2084
+ function canAppendToolUiEvents(events, previousEvents) {
2085
+ if (previousEvents.length > events.length) {
2086
+ return false;
2087
+ }
2088
+ if (previousEvents.length === 0) {
2089
+ return true;
2090
+ }
2091
+ return previousEvents.every((event, index) => JSON.stringify(event) === JSON.stringify(events[index]));
2092
+ }
2093
+ function subagentUiEventsFromAgentRun(agent) {
2094
+ const state = subagentStateFromAgentRun(agent);
2095
+ const events = [{ type: "subagent_spawned", state }];
2096
+ if (agent.progress !== undefined) {
2097
+ events.push(subagentProgressEventFromAgentRun(agent));
2098
+ }
2099
+ if (agent.abortRequested) {
2100
+ events.push({ type: "subagent_abort_requested", id: agent.id });
2101
+ }
2102
+ if (agent.status !== "running") {
2103
+ events.push({ type: "subagent_finished", state });
2104
+ }
2105
+ return events;
2106
+ }
2107
+ function subagentUiEventsForAgentDelta(previous, next) {
2108
+ if (!previous) {
2109
+ return subagentUiEventsFromAgentRun(next);
2110
+ }
2111
+ const events = [];
2112
+ if (!previous.abortRequested && next.abortRequested) {
2113
+ events.push({ type: "subagent_abort_requested", id: next.id });
2114
+ }
2115
+ if (next.status !== "running") {
2116
+ events.push({
2117
+ type: "subagent_finished",
2118
+ state: subagentStateFromAgentRun(next),
2119
+ });
2120
+ return events;
2121
+ }
2122
+ if (agentProgressChanged(previous, next)) {
2123
+ events.push(subagentProgressEventFromAgentRun(next));
2124
+ }
2125
+ return events;
2126
+ }
2127
+ function subagentStateFromAgentRun(agent) {
2128
+ const status = agent.status === "succeeded"
2129
+ ? "success"
2130
+ : agent.status === "failed"
2131
+ ? "error"
2132
+ : agent.status === "cancelled"
2133
+ ? "aborted"
2134
+ : "running";
2135
+ const state = {
2136
+ id: agent.id,
2137
+ name: agent.name,
2138
+ title: agent.title,
2139
+ status,
2140
+ ...(agent.modelLabel !== undefined ? { modelLabel: agent.modelLabel } : {}),
2141
+ costTotal: agent.costTotal,
2142
+ turns: agent.turns,
2143
+ toolCalls: agent.toolCalls,
2144
+ usage: { ...agent.usage },
2145
+ startedAt: agent.startedAt,
2146
+ ...(agent.finishedAt !== undefined ? { finishedAt: agent.finishedAt } : {}),
2147
+ abortRequested: agent.abortRequested,
2148
+ ...(agent.error !== undefined ? { error: agent.error } : {}),
2149
+ ...(agent.finalText !== undefined ? { finalText: agent.finalText } : {}),
2150
+ };
2151
+ return state;
2152
+ }
2153
+ function subagentProgressEventFromAgentRun(agent) {
2154
+ return {
2155
+ type: "subagent_progress",
2156
+ id: agent.id,
2157
+ text: agent.progress ?? "",
2158
+ costTotal: agent.costTotal,
2159
+ turns: agent.turns,
2160
+ toolCalls: agent.toolCalls,
2161
+ usage: { ...agent.usage },
2162
+ };
2163
+ }
2164
+ function agentProgressChanged(previous, next) {
2165
+ return (previous.progress !== next.progress ||
2166
+ previous.costTotal !== next.costTotal ||
2167
+ previous.turns !== next.turns ||
2168
+ previous.toolCalls !== next.toolCalls ||
2169
+ previous.usage.input !== next.usage.input ||
2170
+ previous.usage.output !== next.usage.output ||
2171
+ previous.usage.cacheRead !== next.usage.cacheRead ||
2172
+ previous.usage.cacheWrite !== next.usage.cacheWrite ||
2173
+ previous.usage.contextWindowUsageTokens !== next.usage.contextWindowUsageTokens ||
2174
+ previous.usage.contextWindow !== next.usage.contextWindow);
2175
+ }
2176
+ //# sourceMappingURL=session_chat_controller.js.map