@markusylisiurunen/tau 0.1.2

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 (106) hide show
  1. package/README.md +228 -0
  2. package/dist/app.js +1140 -0
  3. package/dist/app.js.map +1 -0
  4. package/dist/bash_commands.js +81 -0
  5. package/dist/bash_commands.js.map +1 -0
  6. package/dist/cli.js +147 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/clipboard.js +16 -0
  9. package/dist/clipboard.js.map +1 -0
  10. package/dist/commands.js +76 -0
  11. package/dist/commands.js.map +1 -0
  12. package/dist/config.js +39 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/content_loader.js +241 -0
  15. package/dist/content_loader.js.map +1 -0
  16. package/dist/debug-ui.js +134 -0
  17. package/dist/debug-ui.js.map +1 -0
  18. package/dist/main.js +85 -0
  19. package/dist/main.js.map +1 -0
  20. package/dist/personas.js +227 -0
  21. package/dist/personas.js.map +1 -0
  22. package/dist/prompts.js +261 -0
  23. package/dist/prompts.js.map +1 -0
  24. package/dist/session/message_accumulator.js +56 -0
  25. package/dist/session/message_accumulator.js.map +1 -0
  26. package/dist/session/session_engine.js +290 -0
  27. package/dist/session/session_engine.js.map +1 -0
  28. package/dist/subagents/explore.js +42 -0
  29. package/dist/subagents/explore.js.map +1 -0
  30. package/dist/subagents/registry.js +30 -0
  31. package/dist/subagents/registry.js.map +1 -0
  32. package/dist/subagents/subagent_engine.js +147 -0
  33. package/dist/subagents/subagent_engine.js.map +1 -0
  34. package/dist/subagents/types.js +2 -0
  35. package/dist/subagents/types.js.map +1 -0
  36. package/dist/terminal.js +115 -0
  37. package/dist/terminal.js.map +1 -0
  38. package/dist/tools/bash.js +334 -0
  39. package/dist/tools/bash.js.map +1 -0
  40. package/dist/tools/edit.js +199 -0
  41. package/dist/tools/edit.js.map +1 -0
  42. package/dist/tools/registry.js +15 -0
  43. package/dist/tools/registry.js.map +1 -0
  44. package/dist/tools/task.js +202 -0
  45. package/dist/tools/task.js.map +1 -0
  46. package/dist/tools/write.js +89 -0
  47. package/dist/tools/write.js.map +1 -0
  48. package/dist/types.js +9 -0
  49. package/dist/types.js.map +1 -0
  50. package/dist/ui/assistant_message.js +87 -0
  51. package/dist/ui/assistant_message.js.map +1 -0
  52. package/dist/ui/bash_execution.js +127 -0
  53. package/dist/ui/bash_execution.js.map +1 -0
  54. package/dist/ui/chat_container.js +90 -0
  55. package/dist/ui/chat_container.js.map +1 -0
  56. package/dist/ui/components/dynamic_border.js +11 -0
  57. package/dist/ui/components/dynamic_border.js.map +1 -0
  58. package/dist/ui/components/one_line_segments.js +79 -0
  59. package/dist/ui/components/one_line_segments.js.map +1 -0
  60. package/dist/ui/components/padded_container.js +24 -0
  61. package/dist/ui/components/padded_container.js.map +1 -0
  62. package/dist/ui/custom_editor.js +53 -0
  63. package/dist/ui/custom_editor.js.map +1 -0
  64. package/dist/ui/file_execution.js +189 -0
  65. package/dist/ui/file_execution.js.map +1 -0
  66. package/dist/ui/footer.js +56 -0
  67. package/dist/ui/footer.js.map +1 -0
  68. package/dist/ui/queued_messages.js +31 -0
  69. package/dist/ui/queued_messages.js.map +1 -0
  70. package/dist/ui/session_divider.js +17 -0
  71. package/dist/ui/session_divider.js.map +1 -0
  72. package/dist/ui/session_summary.js +30 -0
  73. package/dist/ui/session_summary.js.map +1 -0
  74. package/dist/ui/slash_autocomplete.js +177 -0
  75. package/dist/ui/slash_autocomplete.js.map +1 -0
  76. package/dist/ui/system_message.js +8 -0
  77. package/dist/ui/system_message.js.map +1 -0
  78. package/dist/ui/task_execution.js +190 -0
  79. package/dist/ui/task_execution.js.map +1 -0
  80. package/dist/ui/theme.js +108 -0
  81. package/dist/ui/theme.js.map +1 -0
  82. package/dist/ui/tool_output.js +24 -0
  83. package/dist/ui/tool_output.js.map +1 -0
  84. package/dist/ui/user_message.js +14 -0
  85. package/dist/ui/user_message.js.map +1 -0
  86. package/dist/utils/color.js +13 -0
  87. package/dist/utils/color.js.map +1 -0
  88. package/dist/utils/context.js +108 -0
  89. package/dist/utils/context.js.map +1 -0
  90. package/dist/utils/fork.js +41 -0
  91. package/dist/utils/fork.js.map +1 -0
  92. package/dist/utils/format.js +35 -0
  93. package/dist/utils/format.js.map +1 -0
  94. package/dist/utils/fuzzy.js +48 -0
  95. package/dist/utils/fuzzy.js.map +1 -0
  96. package/dist/utils/git.js +21 -0
  97. package/dist/utils/git.js.map +1 -0
  98. package/dist/utils/messages.js +41 -0
  99. package/dist/utils/messages.js.map +1 -0
  100. package/dist/utils/never.js +4 -0
  101. package/dist/utils/never.js.map +1 -0
  102. package/dist/utils/project_files.js +96 -0
  103. package/dist/utils/project_files.js.map +1 -0
  104. package/dist/utils/truncate.js +299 -0
  105. package/dist/utils/truncate.js.map +1 -0
  106. package/package.json +37 -0
package/dist/app.js ADDED
@@ -0,0 +1,1140 @@
1
+ import { homedir } from "node:os";
2
+ import { join, resolve } from "node:path";
3
+ import { streamSimple } from "@mariozechner/pi-ai";
4
+ import { Spacer, Text, TUI } from "@mariozechner/pi-tui";
5
+ import { loadBashCommands } from "./bash_commands.js";
6
+ import { copyTextToClipboard } from "./clipboard.js";
7
+ import { buildHelpText, getRiskLevelDescription, parseCommand } from "./commands.js";
8
+ import { getApiKeyForProvider } from "./config.js";
9
+ import { loadAllContent } from "./content_loader.js";
10
+ import { SessionEngine } from "./session/session_engine.js";
11
+ import { createAppTerminal } from "./terminal.js";
12
+ import { createBashToolDefinition, executeBashTool, formatBashUserMessageText, prepareBashOutput, } from "./tools/bash.js";
13
+ import { createEditToolDefinition } from "./tools/edit.js";
14
+ import { ToolRegistry } from "./tools/registry.js";
15
+ import { createTaskToolDefinition } from "./tools/task.js";
16
+ import { createWriteToolDefinition } from "./tools/write.js";
17
+ import { REASONING_LEVELS } from "./types.js";
18
+ import { AssistantMessageComponent } from "./ui/assistant_message.js";
19
+ import { renderBashBlocked, renderBashExecution, renderBashRunning } from "./ui/bash_execution.js";
20
+ import { ChatContainerComponent } from "./ui/chat_container.js";
21
+ import { CustomEditor } from "./ui/custom_editor.js";
22
+ import { renderEditBlocked, renderEditSuccess, renderWriteBlocked, renderWriteSuccess, } from "./ui/file_execution.js";
23
+ import { FooterComponent } from "./ui/footer.js";
24
+ import { QueuedMessagesComponent } from "./ui/queued_messages.js";
25
+ import { SessionDividerComponent } from "./ui/session_divider.js";
26
+ import { SessionSummaryComponent } from "./ui/session_summary.js";
27
+ import { SlashAutocompleteProvider } from "./ui/slash_autocomplete.js";
28
+ import { SystemMessageComponent } from "./ui/system_message.js";
29
+ import { renderTaskBlocked, renderTaskFinished, renderTaskRunning } from "./ui/task_execution.js";
30
+ import { editorBorderForReasoning, theme } from "./ui/theme.js";
31
+ import { UserMessageComponent } from "./ui/user_message.js";
32
+ import { buildBaseSystemPrompt, buildEnvironmentTag, buildProjectContextBlock, findAgentsFilesFromCwdToHome, formatRiskLevelChangeNotice, } from "./utils/context.js";
33
+ import { formatHistoryForCompression } from "./utils/fork.js";
34
+ import { formatAdaptiveNumber, formatCwd, formatTokenWindow } from "./utils/format.js";
35
+ import { getGitRoot } from "./utils/git.js";
36
+ import { extractAllFencedCodeBlocks, extractAssistantText } from "./utils/messages.js";
37
+ import { listProjectFiles } from "./utils/project_files.js";
38
+ const { palette } = theme;
39
+ export class ChatApp {
40
+ ui;
41
+ chatContainer;
42
+ footer;
43
+ queuedMessages;
44
+ editor;
45
+ personas;
46
+ currentPersona;
47
+ prompts;
48
+ bashCommands;
49
+ repoRoot;
50
+ initialUserMessage;
51
+ config;
52
+ assistantComponents = [];
53
+ engine;
54
+ runningBashComponents = new Map(); // toolCallId -> component index
55
+ runningTaskComponents = new Map(); // toolCallId -> component index
56
+ taskEvents = new Map(); // toolCallId -> accumulated events
57
+ subagentCostTotal = 0;
58
+ isStreaming = false;
59
+ queuedUserMessages = [];
60
+ isDrainingQueuedUserMessages = false;
61
+ isBashMode = false;
62
+ isMemoryMode = false;
63
+ showThinking = false;
64
+ compactToolUi = true;
65
+ currentTurnAbort;
66
+ riskLevel = "read-only";
67
+ initialRiskLevel;
68
+ environmentTag;
69
+ projectContextBlock;
70
+ projectFiles;
71
+ agentsFiles;
72
+ baseSystemPrompt;
73
+ pendingRiskLevelChange;
74
+ previousSessionSummary;
75
+ expandedFilesInCurrentPrompt = new Set();
76
+ constructor(options) {
77
+ this.personas = options.personas;
78
+ this.prompts = options.prompts ?? [];
79
+ this.bashCommands = options.bashCommands ?? [];
80
+ this.repoRoot = getGitRoot(process.cwd()) ?? process.cwd();
81
+ this.initialUserMessage = options.initialUserMessage;
82
+ this.config = options.config ?? {};
83
+ this.compactToolUi = this.config.toolDisplayMode !== "full";
84
+ if (options.initialRiskLevel) {
85
+ this.riskLevel = options.initialRiskLevel;
86
+ }
87
+ this.initialRiskLevel = this.riskLevel;
88
+ this.environmentTag = buildEnvironmentTag({
89
+ riskLevel: this.initialRiskLevel,
90
+ cwd: process.cwd(),
91
+ datetime: new Date().toISOString(),
92
+ });
93
+ this.agentsFiles = options.withContext
94
+ ? findAgentsFilesFromCwdToHome(process.cwd(), homedir())
95
+ : [];
96
+ this.projectContextBlock = options.withContext
97
+ ? buildProjectContextBlock({ cwd: process.cwd(), home: homedir() })
98
+ : undefined;
99
+ this.projectFiles = listProjectFiles(process.cwd());
100
+ this.currentPersona =
101
+ (options.initialPersonaId &&
102
+ this.personas.find((p) => p.id.toLowerCase() === options.initialPersonaId.toLowerCase())) ||
103
+ this.personas[0];
104
+ this.clampPersonaReasoning(this.currentPersona);
105
+ this.baseSystemPrompt = buildBaseSystemPrompt({
106
+ personaSystemPrompt: this.currentPersona.systemPrompt,
107
+ projectContextBlock: this.projectContextBlock,
108
+ environmentTag: this.environmentTag,
109
+ userPreferences: this.config.userPreferences,
110
+ });
111
+ const toolRegistry = new ToolRegistry([
112
+ createBashToolDefinition(),
113
+ createWriteToolDefinition(),
114
+ createEditToolDefinition(),
115
+ createTaskToolDefinition(),
116
+ ]);
117
+ this.engine = new SessionEngine({
118
+ persona: this.currentPersona,
119
+ baseSystemPrompt: this.baseSystemPrompt,
120
+ riskLevel: this.riskLevel,
121
+ toolRegistry,
122
+ config: this.config,
123
+ });
124
+ this.ui = new TUI(createAppTerminal(Boolean(this.initialUserMessage)));
125
+ this.chatContainer = new ChatContainerComponent();
126
+ this.chatContainer.setCompactToolUi(this.compactToolUi);
127
+ this.footer = new FooterComponent(this.ui);
128
+ this.queuedMessages = new QueuedMessagesComponent(() => this.queuedUserMessages);
129
+ this.editor = new CustomEditor(theme.editorTheme);
130
+ this.setupUI();
131
+ this.setupEditor();
132
+ }
133
+ setupUI() {
134
+ this.ui.addChild(this.chatContainer);
135
+ this.ui.addChild(new Spacer(1));
136
+ this.ui.addChild(this.queuedMessages);
137
+ this.ui.addChild(this.editor);
138
+ this.ui.addChild(this.footer);
139
+ const headerText = `\n${palette.accent("tau")} ${palette.muted("– terminal chat")}\n\n` +
140
+ palette.muted(buildHelpText(this.agentsFiles));
141
+ this.chatContainer.addMessage(new Text(headerText, 1, 0));
142
+ this.ui.setFocus(this.editor);
143
+ this.updateFooter();
144
+ this.updateEditorBorderColor();
145
+ }
146
+ setupEditor() {
147
+ this.editor.onCtrlC = () => {
148
+ this.stop();
149
+ process.exit(0);
150
+ };
151
+ this.editor.onCtrlT = () => this.toggleThinkingVisibility();
152
+ this.editor.onCtrlO = () => this.toggleCompactToolUi();
153
+ this.editor.onShiftTab = () => this.cycleReasoningLevel();
154
+ this.editor.onEscape = () => this.interruptAssistantTurn();
155
+ this.editor.onCtrlF = () => {
156
+ this.expandFileMentions().catch((err) => {
157
+ this.addSystemMessage(`file expansion failed: ${err.message}`, palette.noticeError);
158
+ });
159
+ };
160
+ this.editor.onAltUp = () => this.popQueuedUserMessageIntoEditor();
161
+ this.editor.beforeSubmit = (text) => {
162
+ if (!this.isStreaming)
163
+ return true;
164
+ const trimmed = text.trimStart();
165
+ return !trimmed.startsWith("/") && !trimmed.startsWith("!");
166
+ };
167
+ this.editor.onChange = (text) => {
168
+ const wasBash = this.isBashMode;
169
+ const wasMemory = this.isMemoryMode;
170
+ const trimmed = text.trimStart();
171
+ this.isBashMode = trimmed.startsWith("!");
172
+ this.isMemoryMode = trimmed.startsWith("#");
173
+ if (wasBash !== this.isBashMode || wasMemory !== this.isMemoryMode) {
174
+ this.updateEditorBorderColor();
175
+ }
176
+ };
177
+ this.editor.setAutocompleteProvider(new SlashAutocompleteProvider(() => this.personas.map((p) => ({ id: p.id, label: p.label })), () => this.prompts.map((t) => ({ id: t.id, label: t.label })), () => this.bashCommands.map((b) => ({
178
+ id: b.id,
179
+ description: b.description,
180
+ })), () => this.projectFiles));
181
+ this.editor.onSubmit = (text) => this.handleSubmit(text);
182
+ }
183
+ async start() {
184
+ this.ui.start();
185
+ if (this.initialUserMessage) {
186
+ await this.sendInitialUserMessage(this.initialUserMessage);
187
+ }
188
+ }
189
+ stop() {
190
+ this.ui.stop();
191
+ }
192
+ // UI Updates ------------------------------------------------------------------------------------
193
+ updateFooter() {
194
+ const reasoningLabel = this.currentPersona.settings.reasoning || "default";
195
+ const toolLabel = this.formatRiskLevelLabel();
196
+ const contextUsage = this.getContextUsageString();
197
+ const sessionCost = this.getSessionCostString();
198
+ const cwd = formatCwd(process.cwd());
199
+ const left = palette.dim(`${cwd} · ${contextUsage} · ${sessionCost}`);
200
+ const personaName = this.currentPersona.label || this.currentPersona.id;
201
+ const statusPart = palette.dim(`${personaName} · ${reasoningLabel} · `);
202
+ const right = `${statusPart}${toolLabel}`;
203
+ this.footer.setLeftRight(left, right);
204
+ this.ui.requestRender();
205
+ }
206
+ formatRiskLevelLabel() {
207
+ switch (this.riskLevel) {
208
+ case "none":
209
+ return palette.riskNone("none");
210
+ case "read-only":
211
+ return palette.riskReadOnly("read-only");
212
+ case "read-write":
213
+ return palette.riskReadWrite("read-write");
214
+ }
215
+ }
216
+ updateEditorBorderColor() {
217
+ if (this.isBashMode) {
218
+ this.editor.borderColor = (s) => palette.bashRan(s);
219
+ }
220
+ else if (this.isMemoryMode) {
221
+ this.editor.borderColor = (s) => palette.memoryMode(s);
222
+ }
223
+ else {
224
+ this.editor.borderColor = editorBorderForReasoning(this.currentPersona.settings.reasoning);
225
+ }
226
+ this.ui.requestRender();
227
+ }
228
+ addSystemMessage(text, styleFn) {
229
+ this.chatContainer.addMessage(new SystemMessageComponent(text, styleFn));
230
+ this.ui.requestRender();
231
+ }
232
+ addUserMessage(text, opts) {
233
+ this.chatContainer.addMessage(new UserMessageComponent(text, opts));
234
+ this.ui.requestRender();
235
+ }
236
+ addAssistantComponent(component) {
237
+ this.chatContainer.addMessage(component);
238
+ this.assistantComponents.push(component);
239
+ this.ui.requestRender();
240
+ }
241
+ // Context & Cost Tracking -----------------------------------------------------------------------
242
+ getContextUsageString() {
243
+ const last = this.getLastAssistantMessage();
244
+ const windowTokens = last
245
+ ? this.getContextWindowForLastTurn(last)
246
+ : this.currentPersona.model.contextWindow;
247
+ const { read, write } = this.getCacheTotals();
248
+ let stats = `R${formatTokenWindow(read)} W${formatTokenWindow(write)}`;
249
+ if (!last) {
250
+ return `${stats} 0%/${formatTokenWindow(windowTokens)}`;
251
+ }
252
+ // Sum output tokens from assistant messages in the current turn (after last user message)
253
+ let totalOutputTokens = 0;
254
+ for (let i = this.engine.history.length - 1; i >= 0; i--) {
255
+ const m = this.engine.history[i];
256
+ if (m.role === "user") {
257
+ break;
258
+ }
259
+ if (m.role === "assistant") {
260
+ totalOutputTokens += m.usage?.output ?? 0;
261
+ }
262
+ }
263
+ stats += ` O${formatTokenWindow(totalOutputTokens)}`;
264
+ const promptTokensSent = (last.usage?.input ?? 0) + (last.usage?.cacheRead ?? 0) + (last.usage?.cacheWrite ?? 0);
265
+ const percent = windowTokens > 0 ? (promptTokensSent / windowTokens) * 100 : 0;
266
+ const percentStr = `${formatAdaptiveNumber(percent, 1, 3)}%`;
267
+ return `${stats} ${percentStr}/${formatTokenWindow(windowTokens)}`;
268
+ }
269
+ getSessionCostString() {
270
+ let total = 0;
271
+ for (const m of this.engine.history) {
272
+ if (m.role === "assistant") {
273
+ total += m.usage?.cost?.total ?? 0;
274
+ }
275
+ }
276
+ return `$${formatAdaptiveNumber(total + this.subagentCostTotal, 2, 5)}`;
277
+ }
278
+ getCacheTotals() {
279
+ let read = 0;
280
+ let write = 0;
281
+ for (const m of this.engine.history) {
282
+ if (m.role === "assistant") {
283
+ const usage = m.usage;
284
+ read += usage?.cacheRead ?? 0;
285
+ write += usage?.cacheWrite ?? 0;
286
+ }
287
+ }
288
+ return { read, write };
289
+ }
290
+ getLastAssistantMessage() {
291
+ const history = this.engine.history;
292
+ for (let i = history.length - 1; i >= 0; i--) {
293
+ const m = history[i];
294
+ if (m?.role === "assistant")
295
+ return m;
296
+ }
297
+ return undefined;
298
+ }
299
+ getContextWindowForLastTurn(last) {
300
+ const exactPersona = this.personas.find((p) => p.model.provider === last.provider && p.model.id === last.model);
301
+ return exactPersona?.model.contextWindow ?? this.currentPersona.model.contextWindow;
302
+ }
303
+ // Reasoning Level Management --------------------------------------------------------------------
304
+ cycleReasoningLevel() {
305
+ const allowed = this.getAllowedReasoningLevels(this.currentPersona);
306
+ const current = (this.currentPersona.settings.reasoning ?? allowed[0]);
307
+ const index = allowed.indexOf(current);
308
+ const next = allowed[(index + 1) % allowed.length];
309
+ this.currentPersona.settings.reasoning = next;
310
+ this.updateFooter();
311
+ this.updateEditorBorderColor();
312
+ }
313
+ isReasoningEffort(value) {
314
+ return typeof value === "string" && REASONING_LEVELS.includes(value);
315
+ }
316
+ getAllowedReasoningLevels(persona) {
317
+ if (!persona.model.reasoning) {
318
+ return ["none"];
319
+ }
320
+ const raw = persona.allowedReasoningLevels;
321
+ if (!raw || raw.length === 0) {
322
+ return REASONING_LEVELS;
323
+ }
324
+ const normalized = raw.filter((level) => this.isReasoningEffort(level));
325
+ const unique = [...new Set(normalized)];
326
+ return unique.length ? unique : REASONING_LEVELS;
327
+ }
328
+ clampPersonaReasoning(persona) {
329
+ const allowed = this.getAllowedReasoningLevels(persona);
330
+ if (!allowed.includes(persona.settings.reasoning)) {
331
+ persona.settings.reasoning = allowed[0];
332
+ }
333
+ }
334
+ // User Actions ----------------------------------------------------------------------------------
335
+ toggleThinkingVisibility() {
336
+ this.showThinking = !this.showThinking;
337
+ this.assistantComponents.forEach((c) => {
338
+ c.setThinkingVisibility(this.showThinking);
339
+ });
340
+ this.chatContainer.setThinkingVisibility(this.showThinking);
341
+ const message = this.showThinking
342
+ ? "thoughts visible (ctrl+t to hide)"
343
+ : "thoughts hidden (ctrl+t to show)";
344
+ this.addSystemMessage(message, palette.noticeSuccess);
345
+ this.ui.requestRender();
346
+ }
347
+ toggleCompactToolUi() {
348
+ this.compactToolUi = !this.compactToolUi;
349
+ this.chatContainer.setCompactToolUi(this.compactToolUi);
350
+ const message = this.compactToolUi
351
+ ? "compact tool UI enabled (ctrl+o to disable)"
352
+ : "compact tool UI disabled (ctrl+o to enable)";
353
+ this.addSystemMessage(message, palette.noticeSuccess);
354
+ this.ui.requestRender();
355
+ }
356
+ interruptAssistantTurn() {
357
+ if (!this.isStreaming || this.currentTurnAbort?.signal.aborted)
358
+ return;
359
+ this.currentTurnAbort?.abort();
360
+ this.addSystemMessage("interrupted.", palette.noticeError);
361
+ this.ui.requestRender();
362
+ }
363
+ // Input Handling --------------------------------------------------------------------------------
364
+ queueUserMessage(text) {
365
+ this.queuedUserMessages.push(text);
366
+ this.ui.requestRender();
367
+ }
368
+ popQueuedUserMessageIntoEditor() {
369
+ if (this.editor.getText() !== "")
370
+ return;
371
+ const last = this.queuedUserMessages.pop();
372
+ if (!last)
373
+ return;
374
+ this.editor.setText(last);
375
+ this.ui.requestRender();
376
+ }
377
+ async drainQueuedUserMessages() {
378
+ if (this.isDrainingQueuedUserMessages)
379
+ return;
380
+ this.isDrainingQueuedUserMessages = true;
381
+ try {
382
+ while (!this.isStreaming && this.queuedUserMessages.length > 0) {
383
+ const next = this.queuedUserMessages.shift();
384
+ if (!next)
385
+ return;
386
+ this.ui.requestRender();
387
+ await this.handleSubmit(next);
388
+ }
389
+ }
390
+ finally {
391
+ this.isDrainingQueuedUserMessages = false;
392
+ }
393
+ }
394
+ async handleSubmit(text) {
395
+ const trimmed = text.trim();
396
+ if (!trimmed)
397
+ return;
398
+ if (this.isStreaming) {
399
+ if (trimmed.startsWith("/") || trimmed.startsWith("!")) {
400
+ return;
401
+ }
402
+ this.queueUserMessage(trimmed);
403
+ return;
404
+ }
405
+ if (trimmed.startsWith("/")) {
406
+ await this.handleCommand(trimmed);
407
+ return;
408
+ }
409
+ if (trimmed.startsWith("!")) {
410
+ const command = trimmed.slice(1).trim();
411
+ if (command)
412
+ await this.runBashCommand(command);
413
+ return;
414
+ }
415
+ if (trimmed.startsWith("#")) {
416
+ const request = trimmed.slice(1).trim();
417
+ if (!request) {
418
+ this.addSystemMessage("memory mode request was empty.", palette.noticeWarn);
419
+ return;
420
+ }
421
+ const agentsFilePath = this.getMemoryModeFilePath();
422
+ const textForModel = this.formatMemoryModeUserMessage(agentsFilePath, request);
423
+ await this.sendUserMessage(request, { textForModel, isMemoryMode: true });
424
+ return;
425
+ }
426
+ await this.sendUserMessage(trimmed);
427
+ }
428
+ async sendUserMessage(text, opts) {
429
+ this.addUserMessage(text, { isMemoryMode: opts?.isMemoryMode });
430
+ this.expandedFilesInCurrentPrompt.clear();
431
+ const systemNotice = this.pendingRiskLevelChange
432
+ ? formatRiskLevelChangeNotice(this.pendingRiskLevelChange)
433
+ : undefined;
434
+ this.pendingRiskLevelChange = undefined;
435
+ const baseTextForModel = opts?.textForModel ?? text;
436
+ const textForModel = systemNotice ? `${systemNotice}\n\n${baseTextForModel}` : baseTextForModel;
437
+ this.engine.addUserText(textForModel);
438
+ await this.runAssistantTurn();
439
+ }
440
+ async sendInitialUserMessage(text) {
441
+ const trimmed = text.trim();
442
+ if (!trimmed || this.isStreaming)
443
+ return;
444
+ this.addUserMessage(trimmed);
445
+ this.engine.addUserText(trimmed);
446
+ await this.runAssistantTurn();
447
+ }
448
+ getMemoryModeFilePath() {
449
+ const cwd = process.cwd();
450
+ const gitRoot = getGitRoot(cwd);
451
+ if (gitRoot) {
452
+ return resolve(join(gitRoot, "AGENTS.md"));
453
+ }
454
+ return resolve(join(cwd, "AGENTS.md"));
455
+ }
456
+ formatMemoryModeUserMessage(agentsFilePath, request) {
457
+ const system = [
458
+ "Memory mode: update the project guidelines file at:",
459
+ agentsFilePath,
460
+ "",
461
+ "If the file exists, use the edit tool to update it. If it does not exist, use the write tool to create it.",
462
+ "Preserve all unrelated content and match the existing formatting style.",
463
+ "Integrate the user's request thoughtfully. Don't just append it verbatim.",
464
+ "Place new content in the most appropriate existing section, or create a new section if needed.",
465
+ "Always prefer an existing section over creating a new one. Sometimes changes are required in more than one place.",
466
+ "",
467
+ "Do not mention this surrounding instruction in your response.",
468
+ ].join("\n");
469
+ return ["<system>", system, "</system>", "", request].join("\n");
470
+ }
471
+ // Command Handling ------------------------------------------------------------------------------
472
+ async handleCommand(raw) {
473
+ const cmd = parseCommand(raw);
474
+ switch (cmd.type) {
475
+ case "help":
476
+ this.showHelp();
477
+ break;
478
+ case "copy":
479
+ await this.copyLastAssistantMessage();
480
+ break;
481
+ case "copyCode":
482
+ await this.copyLastAssistantCodeBlock();
483
+ break;
484
+ case "new":
485
+ this.clearSession();
486
+ break;
487
+ case "forkOnlySummary":
488
+ await this.forkSessionOnlySummary();
489
+ break;
490
+ case "forkSummaryAndLastTurn":
491
+ await this.forkSessionSummaryAndLastTurn();
492
+ break;
493
+ case "risk":
494
+ this.setRiskLevel(cmd.level);
495
+ break;
496
+ case "persona":
497
+ this.switchPersona(cmd.id);
498
+ break;
499
+ case "prompt":
500
+ this.insertPrompt(cmd.id);
501
+ break;
502
+ case "bash":
503
+ await this.runSavedBashCommand(cmd.id);
504
+ break;
505
+ case "reload":
506
+ await this.reloadContent();
507
+ break;
508
+ case "unknown":
509
+ this.addSystemMessage("unknown command. type /help.", palette.noticeError);
510
+ break;
511
+ }
512
+ }
513
+ showHelp() {
514
+ this.addSystemMessage(buildHelpText(this.agentsFiles), palette.muted);
515
+ }
516
+ async copyLastAssistantMessage() {
517
+ const lastAssistant = this.getLastAssistantMessage();
518
+ if (!lastAssistant) {
519
+ this.addSystemMessage("no assistant message to copy yet.", palette.noticeWarn);
520
+ return;
521
+ }
522
+ const text = extractAssistantText(lastAssistant);
523
+ if (!text.trim()) {
524
+ this.addSystemMessage("last assistant message was empty.", palette.noticeWarn);
525
+ return;
526
+ }
527
+ try {
528
+ await copyTextToClipboard(text);
529
+ this.addSystemMessage("copied last assistant message to clipboard.", palette.noticeSuccess);
530
+ }
531
+ catch (err) {
532
+ this.addSystemMessage(`clipboard copy failed: ${err.message}`, palette.noticeError);
533
+ }
534
+ }
535
+ async copyLastAssistantCodeBlock() {
536
+ const lastAssistant = this.getLastAssistantMessage();
537
+ if (!lastAssistant) {
538
+ this.addSystemMessage("no assistant message to copy yet.", palette.noticeWarn);
539
+ return;
540
+ }
541
+ const text = extractAssistantText(lastAssistant);
542
+ const code = extractAllFencedCodeBlocks(text);
543
+ if (!code) {
544
+ this.addSystemMessage("no code block to copy yet.", palette.noticeWarn);
545
+ return;
546
+ }
547
+ try {
548
+ await copyTextToClipboard(code);
549
+ this.addSystemMessage("copied all code blocks to clipboard.", palette.noticeSuccess);
550
+ }
551
+ catch (err) {
552
+ this.addSystemMessage(`clipboard copy failed: ${err.message}`, palette.noticeError);
553
+ }
554
+ }
555
+ clearSession() {
556
+ this.engine.reset();
557
+ this.assistantComponents = [];
558
+ this.runningBashComponents.clear();
559
+ this.runningTaskComponents.clear();
560
+ this.taskEvents.clear();
561
+ this.subagentCostTotal = 0;
562
+ this.expandedFilesInCurrentPrompt.clear();
563
+ this.chatContainer.addMessage(new SessionDividerComponent("new session"));
564
+ this.isBashMode = false;
565
+ this.isMemoryMode = false;
566
+ this.previousSessionSummary = undefined;
567
+ this.rebuildSystemPrompt();
568
+ this.updateEditorBorderColor();
569
+ this.updateFooter();
570
+ this.ui.requestRender();
571
+ }
572
+ escapeXml(text) {
573
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
574
+ }
575
+ extractLastTurn(history) {
576
+ if (history.length === 0) {
577
+ return {};
578
+ }
579
+ let lastUserIndex = -1;
580
+ let lastUserText;
581
+ // Find the last user message and extract its text
582
+ for (let i = history.length - 1; i >= 0; i--) {
583
+ if (history[i].role === "user") {
584
+ lastUserIndex = i;
585
+ const userMessage = history[i];
586
+ const textParts = [];
587
+ for (const block of userMessage.content) {
588
+ if (typeof block === "string") {
589
+ textParts.push(block);
590
+ }
591
+ else if (block.type === "text") {
592
+ textParts.push(block.text);
593
+ }
594
+ }
595
+ const combined = textParts.join("\n").trim();
596
+ if (combined) {
597
+ lastUserText = combined;
598
+ }
599
+ break;
600
+ }
601
+ }
602
+ let lastAssistantText;
603
+ if (lastUserIndex >= 0) {
604
+ // Find the last assistant message after the last user message
605
+ for (let i = history.length - 1; i > lastUserIndex; i--) {
606
+ if (history[i].role === "assistant") {
607
+ const text = extractAssistantText(history[i]).trim();
608
+ if (text) {
609
+ lastAssistantText = text;
610
+ }
611
+ break;
612
+ }
613
+ }
614
+ }
615
+ else {
616
+ // No user message found; look for the last assistant message overall
617
+ for (let i = history.length - 1; i >= 0; i--) {
618
+ if (history[i].role === "assistant") {
619
+ const text = extractAssistantText(history[i]).trim();
620
+ if (text) {
621
+ lastAssistantText = text;
622
+ }
623
+ break;
624
+ }
625
+ }
626
+ }
627
+ return { lastUserText, lastAssistantText };
628
+ }
629
+ rebuildSystemPrompt(previousSessionSummary) {
630
+ this.environmentTag = buildEnvironmentTag({
631
+ riskLevel: this.riskLevel,
632
+ cwd: process.cwd(),
633
+ datetime: new Date().toISOString(),
634
+ });
635
+ this.baseSystemPrompt = buildBaseSystemPrompt({
636
+ personaSystemPrompt: this.currentPersona.systemPrompt,
637
+ projectContextBlock: this.projectContextBlock,
638
+ environmentTag: this.environmentTag,
639
+ previousSessionSummary,
640
+ userPreferences: this.config.userPreferences,
641
+ });
642
+ this.engine.setPersona(this.currentPersona, this.baseSystemPrompt);
643
+ }
644
+ async generateSummary(history) {
645
+ const formattedHistory = formatHistoryForCompression(history);
646
+ const summaryPrompt = `
647
+ Summarize this conversation so another assistant can continue without losing context. Be specific and factual. Aim for extreme compression; at least 90% reduction from the original conversation length, preferably more. Every word should earn its place.
648
+
649
+ <conversation>
650
+ ${formattedHistory.trim()}
651
+ </conversation>
652
+
653
+ The conversation format uses \`--- USER ---\` and \`--- ASSISTANT ---\` markers. Tool calls appear as \`[Tool call: name(arguments)]\` and outputs as \`[Tool output: name (truncated)]\`. Outputs are truncated, so when tools were used, describe what was attempted rather than assuming outcomes.
654
+
655
+ Capture only what matters for continuity:
656
+
657
+ - The goal or topic. What did the user want to accomplish or discuss? Note how this evolved if it changed during the conversation.
658
+ - Key substance. For discussions: important facts, explanations, or ideas that were shared. For coding tasks: files created or modified, commands run, with concrete paths and names. Distinguish between "attempted" and "confirmed working" when tools were involved.
659
+ - Decisions and preferences. Conclusions reached, options chosen, or constraints the user specified. These should carry forward.
660
+ - Open threads. What's unresolved? For discussions: unanswered questions, topics to revisit. For tasks: what's incomplete, broken, or in progress when the conversation ended.
661
+ - Skip the back-and-forth. Collapse tangents and false starts into what ultimately mattered. The reader has no context beyond what you provide, so name things concretely and include enough detail to resume without guessing.
662
+
663
+ Ruthlessly compress: collapse tangents, skip back-and-forth, omit pleasantries. Name things concretely (paths, functions, errors) but use minimal words.
664
+
665
+ Write plain prose, no formatting. Be thorough enough that the reader can resume without guessing, but don't narrate every exchange. When relevant, name things concretely: file paths, function names, error messages. The reader has no context beyond what you provide as the summary.
666
+ `.trim();
667
+ const apiKey = getApiKeyForProvider(this.config, this.currentPersona.model.provider);
668
+ const stream = streamSimple(this.currentPersona.model, {
669
+ systemPrompt: [
670
+ "You are a precise and thorough conversation summarizer.",
671
+ "Your task is to distill conversations into clear, actionable summaries that preserve all context needed for seamless continuation.",
672
+ "Focus on facts, decisions, and concrete details rather than narrative flow.",
673
+ "Be specific about file paths, function names, and technical details when present.",
674
+ "Distinguish between what was attempted versus what was confirmed to work.",
675
+ ].join(" "),
676
+ messages: [
677
+ {
678
+ role: "user",
679
+ content: [{ type: "text", text: summaryPrompt }],
680
+ timestamp: Date.now(),
681
+ },
682
+ ],
683
+ }, { reasoning: "medium", ...(apiKey && { apiKey }) });
684
+ const final = await stream.result();
685
+ return extractAssistantText(final).trim();
686
+ }
687
+ applySessionContext(previousSessionContext) {
688
+ this.previousSessionSummary = previousSessionContext;
689
+ // Reset the session state but preserve history with divider and summary
690
+ this.engine.reset();
691
+ this.assistantComponents = [];
692
+ this.runningBashComponents.clear();
693
+ this.runningTaskComponents.clear();
694
+ this.taskEvents.clear();
695
+ this.subagentCostTotal = 0;
696
+ this.expandedFilesInCurrentPrompt.clear();
697
+ this.chatContainer.addMessage(new SessionDividerComponent("new session"));
698
+ this.chatContainer.addMessage(new SessionSummaryComponent(this.previousSessionSummary));
699
+ this.isBashMode = false;
700
+ this.isMemoryMode = false;
701
+ // Rebuild environment tag and system prompt with the new summary and current risk level
702
+ this.rebuildSystemPrompt(this.previousSessionSummary);
703
+ this.updateEditorBorderColor();
704
+ this.updateFooter();
705
+ }
706
+ async forkSessionOnlySummary() {
707
+ const history = this.engine.history;
708
+ if (history.length === 0) {
709
+ this.addSystemMessage("no conversation to fork.", palette.noticeWarn);
710
+ return;
711
+ }
712
+ this.addSystemMessage("summarizing session...", palette.noticeSuccess);
713
+ this.isStreaming = true;
714
+ this.footer.startWorkingIcon();
715
+ try {
716
+ const summary = await this.generateSummary(history);
717
+ this.applySessionContext(summary);
718
+ this.addSystemMessage("session forked. previous context has been summarized.", palette.noticeSuccess);
719
+ }
720
+ catch (err) {
721
+ this.addSystemMessage(`fork failed: ${err.message}`, palette.noticeError);
722
+ }
723
+ finally {
724
+ this.footer.stop();
725
+ this.isStreaming = false;
726
+ this.ui.requestRender();
727
+ void this.drainQueuedUserMessages();
728
+ }
729
+ }
730
+ async forkSessionSummaryAndLastTurn() {
731
+ const history = this.engine.history;
732
+ if (history.length === 0) {
733
+ this.addSystemMessage("no conversation to fork.", palette.noticeWarn);
734
+ return;
735
+ }
736
+ this.addSystemMessage("summarizing session...", palette.noticeSuccess);
737
+ this.isStreaming = true;
738
+ this.footer.startWorkingIcon();
739
+ try {
740
+ const summary = await this.generateSummary(history);
741
+ // Extract the last turn from history
742
+ const lastTurn = this.extractLastTurn(history);
743
+ // Build the combined context with summary and last turn
744
+ let sessionContext = summary;
745
+ if (lastTurn.lastUserText || lastTurn.lastAssistantText) {
746
+ sessionContext += "\n\nLast turn from previous session (verbatim):\n";
747
+ sessionContext += "<last_turn>";
748
+ if (lastTurn.lastUserText) {
749
+ sessionContext += `\n<last_user_message>${this.escapeXml(lastTurn.lastUserText)}</last_user_message>`;
750
+ }
751
+ if (lastTurn.lastAssistantText) {
752
+ sessionContext += `\n<last_assistant_message>${this.escapeXml(lastTurn.lastAssistantText)}</last_assistant_message>`;
753
+ }
754
+ sessionContext += "\n</last_turn>";
755
+ }
756
+ this.applySessionContext(sessionContext);
757
+ this.addSystemMessage("session forked. previous context and last turn have been included.", palette.noticeSuccess);
758
+ }
759
+ catch (err) {
760
+ this.addSystemMessage(`fork failed: ${err.message}`, palette.noticeError);
761
+ }
762
+ finally {
763
+ this.footer.stop();
764
+ this.isStreaming = false;
765
+ this.ui.requestRender();
766
+ void this.drainQueuedUserMessages();
767
+ }
768
+ }
769
+ setRiskLevel(level) {
770
+ const previous = this.riskLevel;
771
+ this.riskLevel = level;
772
+ this.engine.setRiskLevel(level);
773
+ this.updateFooter();
774
+ if (previous !== level) {
775
+ this.pendingRiskLevelChange = { from: previous, to: level };
776
+ }
777
+ const details = getRiskLevelDescription(level);
778
+ this.addSystemMessage(`risk level set to '${level}': ${details}`, palette.noticeSuccess);
779
+ }
780
+ switchPersona(id) {
781
+ const persona = this.personas.find((p) => p.id.toLowerCase() === id.toLowerCase());
782
+ if (!persona) {
783
+ this.addSystemMessage(`unknown persona '${id}'.`, palette.noticeError);
784
+ return;
785
+ }
786
+ this.currentPersona = persona;
787
+ this.clampPersonaReasoning(this.currentPersona);
788
+ this.baseSystemPrompt = buildBaseSystemPrompt({
789
+ personaSystemPrompt: this.currentPersona.systemPrompt,
790
+ projectContextBlock: this.projectContextBlock,
791
+ environmentTag: this.environmentTag,
792
+ previousSessionSummary: this.previousSessionSummary,
793
+ userPreferences: this.config.userPreferences,
794
+ });
795
+ this.engine.setPersona(this.currentPersona, this.baseSystemPrompt);
796
+ this.updateFooter();
797
+ this.updateEditorBorderColor();
798
+ this.addSystemMessage(`switched to ${persona.label} (${persona.model.id})`, palette.noticeSuccess);
799
+ }
800
+ insertPrompt(id) {
801
+ const prompt = this.prompts.find((p) => p.id.toLowerCase() === id.toLowerCase());
802
+ if (!prompt) {
803
+ this.addSystemMessage(`unknown prompt '${id}'.`, palette.noticeError);
804
+ return;
805
+ }
806
+ this.editor.setText(prompt.template);
807
+ this.ui.requestRender();
808
+ }
809
+ async runSavedBashCommand(id) {
810
+ const saved = this.bashCommands.find((b) => b.id.toLowerCase() === id.toLowerCase());
811
+ if (!saved) {
812
+ this.addSystemMessage(`unknown bash command '${id}'.`, palette.noticeError);
813
+ return;
814
+ }
815
+ await this.runBashCommand(saved.cmd, { cwd: this.repoRoot });
816
+ }
817
+ async reloadContent() {
818
+ if (this.isStreaming) {
819
+ this.addSystemMessage("cannot reload while streaming. try again after the response.", palette.noticeWarn);
820
+ return;
821
+ }
822
+ try {
823
+ const result = await loadAllContent();
824
+ const { personas, prompts, errors } = result;
825
+ const bashResult = loadBashCommands(process.cwd());
826
+ this.bashCommands = bashResult.commands;
827
+ // Update the personas and prompts lists
828
+ this.personas = personas;
829
+ this.prompts = prompts;
830
+ // Try to preserve the current persona; fall back to first if not found
831
+ const currentPersonaId = this.currentPersona.id.toLowerCase();
832
+ const updatedPersona = personas.find((p) => p.id.toLowerCase() === currentPersonaId);
833
+ if (updatedPersona) {
834
+ this.currentPersona = updatedPersona;
835
+ this.clampPersonaReasoning(this.currentPersona);
836
+ }
837
+ else {
838
+ // Persona no longer exists; switch to the first one
839
+ this.currentPersona = personas[0];
840
+ this.clampPersonaReasoning(this.currentPersona);
841
+ this.addSystemMessage(`previous persona no longer available; switched to ${this.currentPersona.label || this.currentPersona.id}.`, palette.noticeWarn);
842
+ }
843
+ // Rebuild system prompt and update the engine
844
+ this.baseSystemPrompt = buildBaseSystemPrompt({
845
+ personaSystemPrompt: this.currentPersona.systemPrompt,
846
+ projectContextBlock: this.projectContextBlock,
847
+ environmentTag: this.environmentTag,
848
+ previousSessionSummary: this.previousSessionSummary,
849
+ userPreferences: this.config.userPreferences,
850
+ });
851
+ this.engine.setPersona(this.currentPersona, this.baseSystemPrompt);
852
+ // Update UI
853
+ this.updateFooter();
854
+ this.updateEditorBorderColor();
855
+ // Display summary
856
+ const personaCount = personas.length;
857
+ const promptCount = prompts.length;
858
+ const bashCount = bashResult.commands.length;
859
+ const errorCount = errors.length + bashResult.errors.length;
860
+ const summary = errorCount > 0
861
+ ? `reloaded: ${personaCount} personas, ${promptCount} prompts, ${bashCount} bash commands (${errorCount} errors).`
862
+ : `reloaded: ${personaCount} personas, ${promptCount} prompts, ${bashCount} bash commands.`;
863
+ this.addSystemMessage(summary, palette.noticeSuccess);
864
+ this.ui.requestRender();
865
+ }
866
+ catch (err) {
867
+ this.addSystemMessage(`reload failed: ${err.message}`, palette.noticeError);
868
+ }
869
+ }
870
+ // Assistant Turn --------------------------------------------------------------------------------
871
+ async runAssistantTurn() {
872
+ this.isStreaming = true;
873
+ this.currentTurnAbort = new AbortController();
874
+ this.footer.startWorkingIcon();
875
+ try {
876
+ let currentAssistant;
877
+ const ensureCurrentAssistant = () => {
878
+ if (currentAssistant)
879
+ return currentAssistant;
880
+ currentAssistant = {
881
+ component: new AssistantMessageComponent(undefined, this.showThinking),
882
+ inserted: false,
883
+ };
884
+ return currentAssistant;
885
+ };
886
+ const ensureAssistantInserted = () => {
887
+ const state = ensureCurrentAssistant();
888
+ if (state.inserted)
889
+ return;
890
+ state.inserted = true;
891
+ this.addAssistantComponent(state.component);
892
+ state.component.setThinkingVisibility(this.showThinking);
893
+ };
894
+ for await (const event of this.engine.processTurn(this.currentTurnAbort.signal)) {
895
+ if (this.currentTurnAbort.signal.aborted)
896
+ break;
897
+ switch (event.type) {
898
+ case "assistant_start":
899
+ currentAssistant = {
900
+ component: new AssistantMessageComponent(undefined, this.showThinking),
901
+ inserted: false,
902
+ };
903
+ break;
904
+ case "assistant_partial": {
905
+ const state = ensureCurrentAssistant();
906
+ const { snapshot } = event;
907
+ const shouldInsert = snapshot.hasTextStarted || (this.showThinking && snapshot.hasAnyThinking);
908
+ if (shouldInsert && !state.inserted) {
909
+ ensureAssistantInserted();
910
+ }
911
+ if (state.inserted) {
912
+ // Capture visibility state before update
913
+ const wasVisible = state.component.hasVisibleText;
914
+ state.component.updatePartial(snapshot.hasTextStarted ? snapshot.text : "", snapshot.thinking);
915
+ // If component became visible (e.g. text started after thoughts were hidden),
916
+ // rebuild the container to show it
917
+ if (!wasVisible && state.component.hasVisibleText) {
918
+ this.chatContainer.rebuild();
919
+ }
920
+ this.ui.requestRender();
921
+ }
922
+ break;
923
+ }
924
+ case "assistant_final": {
925
+ ensureAssistantInserted();
926
+ ensureCurrentAssistant().component.updateFromMessage(event.message);
927
+ this.chatContainer.rebuild();
928
+ this.updateFooter();
929
+ this.ui.requestRender();
930
+ currentAssistant = undefined;
931
+ break;
932
+ }
933
+ case "tool_ui": {
934
+ const uiEvent = event.uiEvent;
935
+ if (uiEvent.type === "bash_started") {
936
+ // Create and add the running component, storing its index
937
+ const index = this.chatContainer.addToolMessage((compact) => renderBashRunning(uiEvent.command, compact));
938
+ this.runningBashComponents.set(uiEvent.toolCallId, index);
939
+ this.ui.requestRender();
940
+ }
941
+ else if (uiEvent.type === "bash_execution") {
942
+ // Check if we have a running component for this toolCallId
943
+ const runningIndex = this.runningBashComponents.get(uiEvent.toolCallId);
944
+ if (runningIndex !== undefined) {
945
+ // Replace the running component with the finished execution component
946
+ this.chatContainer.replaceToolMessageAtIndex(runningIndex, (compact) => renderBashExecution(uiEvent.command, uiEvent.exitCode, uiEvent.truncationInfo, compact));
947
+ this.runningBashComponents.delete(uiEvent.toolCallId);
948
+ }
949
+ else {
950
+ // Fallback: add as new component if no running component found
951
+ this.chatContainer.addToolMessage((compact) => renderBashExecution(uiEvent.command, uiEvent.exitCode, uiEvent.truncationInfo, compact));
952
+ }
953
+ this.ui.requestRender();
954
+ }
955
+ else if (uiEvent.type === "bash_blocked") {
956
+ // Check if this is a post-acceptance failure that has a running card
957
+ if (uiEvent.toolCallId) {
958
+ const runningIndex = this.runningBashComponents.get(uiEvent.toolCallId);
959
+ if (runningIndex !== undefined) {
960
+ // Replace the running component with the blocked component
961
+ this.chatContainer.replaceToolMessageAtIndex(runningIndex, (compact) => renderBashBlocked(uiEvent.command, uiEvent.reason, compact));
962
+ this.runningBashComponents.delete(uiEvent.toolCallId);
963
+ }
964
+ else {
965
+ // Fallback: add as new component if no running component found
966
+ this.chatContainer.addToolMessage((compact) => renderBashBlocked(uiEvent.command, uiEvent.reason, compact));
967
+ }
968
+ }
969
+ else {
970
+ // Pre-acceptance blocked event; just append as before
971
+ this.chatContainer.addToolMessage((compact) => renderBashBlocked(uiEvent.command, uiEvent.reason, compact));
972
+ }
973
+ this.ui.requestRender();
974
+ }
975
+ else if (uiEvent.type === "task_started") {
976
+ if (!this.taskEvents.has(uiEvent.toolCallId)) {
977
+ this.taskEvents.set(uiEvent.toolCallId, []);
978
+ }
979
+ const index = this.chatContainer.addToolMessage((compact) => renderTaskRunning(uiEvent.title, [], 0, 0, 0, compact, uiEvent.name));
980
+ this.runningTaskComponents.set(uiEvent.toolCallId, index);
981
+ this.ui.requestRender();
982
+ }
983
+ else if (uiEvent.type === "task_progress") {
984
+ // Accumulate events for this task
985
+ let events = this.taskEvents.get(uiEvent.toolCallId);
986
+ if (!events) {
987
+ events = [];
988
+ this.taskEvents.set(uiEvent.toolCallId, events);
989
+ }
990
+ events.push(uiEvent.event);
991
+ const runningIndex = this.runningTaskComponents.get(uiEvent.toolCallId);
992
+ if (runningIndex !== undefined) {
993
+ this.chatContainer.replaceToolMessageAtIndex(runningIndex, (compact) => renderTaskRunning(uiEvent.title, events, uiEvent.costTotal, uiEvent.turns, uiEvent.toolCalls, compact, uiEvent.name));
994
+ }
995
+ else {
996
+ const index = this.chatContainer.addToolMessage((compact) => renderTaskRunning(uiEvent.title, events, uiEvent.costTotal, uiEvent.turns, uiEvent.toolCalls, compact, uiEvent.name));
997
+ this.runningTaskComponents.set(uiEvent.toolCallId, index);
998
+ }
999
+ this.ui.requestRender();
1000
+ }
1001
+ else if (uiEvent.type === "task_finished") {
1002
+ const runningIndex = this.runningTaskComponents.get(uiEvent.toolCallId);
1003
+ if (runningIndex !== undefined) {
1004
+ this.chatContainer.replaceToolMessageAtIndex(runningIndex, (compact) => renderTaskFinished(uiEvent.title, uiEvent.costTotal, uiEvent.turns, uiEvent.toolCalls, uiEvent.status, uiEvent.finalOutput, compact, uiEvent.name));
1005
+ this.runningTaskComponents.delete(uiEvent.toolCallId);
1006
+ }
1007
+ else {
1008
+ this.chatContainer.addToolMessage((compact) => renderTaskFinished(uiEvent.title, uiEvent.costTotal, uiEvent.turns, uiEvent.toolCalls, uiEvent.status, uiEvent.finalOutput, compact, uiEvent.name));
1009
+ }
1010
+ this.taskEvents.delete(uiEvent.toolCallId);
1011
+ this.subagentCostTotal += uiEvent.costTotal;
1012
+ this.updateFooter();
1013
+ this.ui.requestRender();
1014
+ }
1015
+ else if (uiEvent.type === "task_blocked") {
1016
+ const runningIndex = this.runningTaskComponents.get(uiEvent.toolCallId);
1017
+ if (runningIndex !== undefined) {
1018
+ this.chatContainer.replaceToolMessageAtIndex(runningIndex, (compact) => renderTaskBlocked(uiEvent.title, uiEvent.reason, compact, uiEvent.name));
1019
+ this.runningTaskComponents.delete(uiEvent.toolCallId);
1020
+ }
1021
+ else {
1022
+ this.chatContainer.addToolMessage((compact) => renderTaskBlocked(uiEvent.title, uiEvent.reason, compact, uiEvent.name));
1023
+ }
1024
+ this.taskEvents.delete(uiEvent.toolCallId);
1025
+ this.ui.requestRender();
1026
+ }
1027
+ else if (uiEvent.type === "write_success") {
1028
+ this.chatContainer.addToolMessage((compact) => renderWriteSuccess(uiEvent.path, uiEvent.bytes, uiEvent.lines, uiEvent.preview, uiEvent.previewTruncation, compact));
1029
+ this.ui.requestRender();
1030
+ }
1031
+ else if (uiEvent.type === "write_blocked") {
1032
+ this.chatContainer.addToolMessage((compact) => renderWriteBlocked(uiEvent.path, uiEvent.reason, compact));
1033
+ this.ui.requestRender();
1034
+ }
1035
+ else if (uiEvent.type === "edit_success") {
1036
+ this.chatContainer.addToolMessage((compact) => renderEditSuccess(uiEvent.path, uiEvent.oldLength, uiEvent.newLength, uiEvent.diff, uiEvent.diffTruncation, compact));
1037
+ this.ui.requestRender();
1038
+ }
1039
+ else if (uiEvent.type === "edit_blocked") {
1040
+ this.chatContainer.addToolMessage((compact) => renderEditBlocked(uiEvent.path, uiEvent.reason, compact));
1041
+ this.ui.requestRender();
1042
+ }
1043
+ break;
1044
+ }
1045
+ case "notice": {
1046
+ const style = event.severity === "error"
1047
+ ? palette.noticeError
1048
+ : event.severity === "warn"
1049
+ ? palette.noticeWarn
1050
+ : palette.noticeSuccess;
1051
+ this.addSystemMessage(event.text, style);
1052
+ break;
1053
+ }
1054
+ case "tool_result":
1055
+ break;
1056
+ }
1057
+ }
1058
+ }
1059
+ catch (err) {
1060
+ this.addSystemMessage(`error: ${err.message}`, palette.noticeError);
1061
+ }
1062
+ finally {
1063
+ this.footer.stop();
1064
+ this.isStreaming = false;
1065
+ this.currentTurnAbort = undefined;
1066
+ this.runningBashComponents.clear();
1067
+ this.runningTaskComponents.clear();
1068
+ this.taskEvents.clear();
1069
+ this.ui.requestRender();
1070
+ void this.drainQueuedUserMessages();
1071
+ }
1072
+ }
1073
+ // Direct Bash Execution (user ! commands) -------------------------------------------------------
1074
+ async runBashCommand(command, opts) {
1075
+ this.isStreaming = true;
1076
+ try {
1077
+ const { stdout, stderr, exitCode, truncated: captureTruncated, } = await executeBashTool(command, { cwd: opts?.cwd });
1078
+ const truncationInfo = prepareBashOutput(stdout, stderr, captureTruncated);
1079
+ this.chatContainer.addMessage(renderBashExecution(command, exitCode, truncationInfo, false));
1080
+ this.engine.addUserText(formatBashUserMessageText({ command, truncationInfo }));
1081
+ this.ui.requestRender();
1082
+ }
1083
+ catch (err) {
1084
+ this.addSystemMessage(`bash error: ${err.message}`, palette.noticeError);
1085
+ }
1086
+ finally {
1087
+ this.isStreaming = false;
1088
+ this.ui.requestRender();
1089
+ void this.drainQueuedUserMessages();
1090
+ }
1091
+ }
1092
+ // File Expansion (ctrl+f) -----------------------------------------------------------------------
1093
+ shellQuote(path) {
1094
+ // Wrap in single quotes and escape any single quotes within the path
1095
+ return `'${path.replace(/'/g, "'\\''")}'`;
1096
+ }
1097
+ async expandFileMentions() {
1098
+ if (this.isStreaming) {
1099
+ this.addSystemMessage("cannot expand files while streaming. try again after the response.", palette.noticeWarn);
1100
+ return;
1101
+ }
1102
+ const editorText = this.editor.getText();
1103
+ // Extract @path tokens
1104
+ const tokenRegex = /@([^\s]+)/g;
1105
+ const tokens = [];
1106
+ let match = null;
1107
+ // biome-ignore lint/suspicious/noAssignInExpressions: regex iteration pattern
1108
+ while ((match = tokenRegex.exec(editorText)) !== null) {
1109
+ tokens.push(match[1]);
1110
+ }
1111
+ if (tokens.length === 0) {
1112
+ return;
1113
+ }
1114
+ // Filter to only valid project files and de-duplicate
1115
+ const projectFilesSet = new Set(this.projectFiles);
1116
+ const filesToExpand = [];
1117
+ for (const token of tokens) {
1118
+ // Strip trailing punctuation to handle cases like "@src/app.ts," or "(see @README.md)"
1119
+ const cleanToken = token.replace(/[.,;:)}\]]+$/, "");
1120
+ if (projectFilesSet.has(cleanToken) && !this.expandedFilesInCurrentPrompt.has(cleanToken)) {
1121
+ filesToExpand.push(cleanToken);
1122
+ }
1123
+ }
1124
+ if (filesToExpand.length === 0) {
1125
+ return;
1126
+ }
1127
+ // Run bash commands sequentially for each file
1128
+ for (const filePath of filesToExpand) {
1129
+ const quotedPath = this.shellQuote(filePath);
1130
+ // Format: blank line before header, header, content, blank line after
1131
+ // Ensure trailing newline so multiple files don't run together
1132
+ // Use -- to prevent cat from interpreting filenames starting with - as options
1133
+ const command = `printf '\\n===== %s =====\\n' ${quotedPath}; cat -- ${quotedPath}; printf '\\n'`;
1134
+ await this.runBashCommand(command);
1135
+ // Track this file as expanded in the current prompt
1136
+ this.expandedFilesInCurrentPrompt.add(filePath);
1137
+ }
1138
+ }
1139
+ }
1140
+ //# sourceMappingURL=app.js.map