@markusylisiurunen/tau 0.1.38 → 0.1.40

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.
package/dist/app.js CHANGED
@@ -20,22 +20,16 @@ import { ToolRegistry } from "./tools/registry.js";
20
20
  import { createTaskToolDefinition } from "./tools/task.js";
21
21
  import { createWriteToolDefinition } from "./tools/write.js";
22
22
  import { REASONING_LEVELS, } from "./types.js";
23
- import { AppIntroComponent } from "./ui/app_intro.js";
24
- import { AssistantMessageComponent } from "./ui/assistant_message.js";
25
- import { renderBashAborted, renderBashBlocked, renderBashExecution, renderBashRunning, } from "./ui/bash_execution.js";
23
+ import { buildBashAbortedView, buildBashBlockedView, buildBashExecutionView, buildBashRunningView, } from "./ui/bash_execution.js";
26
24
  import { ChatContainerComponent } from "./ui/chat_container.js";
27
25
  import { CustomEditor } from "./ui/custom_editor.js";
28
- import { renderEditBlocked, renderEditSuccess, renderWriteBlocked, renderWriteSuccess, } from "./ui/file_execution.js";
26
+ import { buildEditBlockedView, buildEditSuccessView, buildWriteBlockedView, buildWriteSuccessView, } from "./ui/file_execution.js";
29
27
  import { FooterComponent } from "./ui/footer.js";
30
28
  import { QueuedMessagesComponent } from "./ui/queued_messages.js";
31
- import { renderGrepBlocked, renderGrepFinished, renderGrepRunning, renderListBlocked, renderListSuccess, renderReadBlocked, renderReadSuccess, } from "./ui/restricted_execution.js";
32
- import { SessionDividerComponent } from "./ui/session_divider.js";
33
- import { SessionSummaryComponent } from "./ui/session_summary.js";
29
+ import { buildGrepBlockedView, buildGrepFinishedView, buildGrepRunningView, buildListBlockedView, buildListSuccessView, buildReadBlockedView, buildReadSuccessView, } from "./ui/restricted_execution.js";
34
30
  import { getFileAutocompleteToken, SlashAutocompleteProvider } from "./ui/slash_autocomplete.js";
35
- import { SystemMessageComponent } from "./ui/system_message.js";
36
- import { renderTaskBlocked, renderTaskFinished, renderTaskRunning } from "./ui/task_execution.js";
31
+ import { buildTaskBlockedView, buildTaskFinishedView, buildTaskRunningView, } from "./ui/task_execution.js";
37
32
  import { createUiTheme } from "./ui/theme.js";
38
- import { UserMessageComponent } from "./ui/user_message.js";
39
33
  import { buildBaseSystemPrompt, buildEnvironmentTag, buildProjectContextBlock, buildSkillsIndexBlock, findAgentsFilesFromCwdToHome, formatRiskLevelChangeNotice, } from "./utils/context.js";
40
34
  import { formatHistoryForCompression } from "./utils/fork.js";
41
35
  import { formatAdaptiveNumber, formatCwd, formatTokenWindow } from "./utils/format.js";
@@ -58,7 +52,6 @@ export class ChatApp {
58
52
  repoRoot;
59
53
  initialUserMessage;
60
54
  config;
61
- assistantComponents = [];
62
55
  engine;
63
56
  runningBashComponents = new Map();
64
57
  runningTaskComponents = new Map();
@@ -142,11 +135,11 @@ export class ChatApp {
142
135
  });
143
136
  this.uiTheme = createUiTheme("ansi");
144
137
  this.ui = new TUI(createAppTerminal());
145
- this.chatContainer = new ChatContainerComponent();
138
+ this.chatContainer = new ChatContainerComponent(this.uiTheme);
146
139
  this.chatContainer.setCompactToolUi(this.compactToolUi);
147
140
  this.footer = new FooterComponent(this.uiTheme, this.ui);
148
141
  this.queuedMessages = new QueuedMessagesComponent(this.uiTheme, this.queuedUserMessages);
149
- this.editor = new CustomEditor(this.uiTheme.editorTheme);
142
+ this.editor = new CustomEditor(this.uiTheme);
150
143
  this.setupUI();
151
144
  this.setupEditor();
152
145
  }
@@ -156,7 +149,12 @@ export class ChatApp {
156
149
  this.ui.addChild(this.queuedMessages);
157
150
  this.ui.addChild(this.editor);
158
151
  this.ui.addChild(this.footer);
159
- this.chatContainer.addMessage(new AppIntroComponent(this.uiTheme, "tau", APP_VERSION, buildHelpText(this.agentsFiles, this.skills)));
152
+ this.chatContainer.addMessage({
153
+ type: "app_intro",
154
+ appName: "tau",
155
+ version: APP_VERSION,
156
+ helpText: buildHelpText(this.agentsFiles, this.skills),
157
+ });
160
158
  this.ui.setFocus(this.editor);
161
159
  this.updateFooter();
162
160
  this.updateEditorBorderColor();
@@ -244,13 +242,11 @@ export class ChatApp {
244
242
  const cwd = formatCwd(process.cwd());
245
243
  const personaName = this.currentPersona.label || this.currentPersona.id;
246
244
  this.footer.setStatus({
247
- cwd,
248
245
  contextUsage,
249
246
  sessionCost,
250
- personaLabel: personaName,
251
- reasoningLabel,
252
247
  riskLevel: this.riskLevel,
253
248
  });
249
+ this.updateEditorHeader(cwd, personaName, reasoningLabel);
254
250
  this.ui.requestRender();
255
251
  }
256
252
  updateEditorBorderColor() {
@@ -264,19 +260,30 @@ export class ChatApp {
264
260
  else {
265
261
  this.editor.borderColor = this.uiTheme.editorBorderForReasoning(this.currentPersona.settings.reasoning);
266
262
  }
263
+ this.updateEditorHeader();
267
264
  this.ui.requestRender();
268
265
  }
266
+ updateEditorHeader(cwd = formatCwd(process.cwd()), personaName = this.currentPersona.label || this.currentPersona.id, reasoningLabel = this.currentPersona.settings.reasoning ?? "none") {
267
+ if (this.isBashMode) {
268
+ this.editor.setHeader("bash", "", { leftStyle: this.editor.borderColor });
269
+ return;
270
+ }
271
+ if (this.isMemoryMode) {
272
+ this.editor.setHeader("memoize", "", { leftStyle: this.editor.borderColor });
273
+ return;
274
+ }
275
+ this.editor.setHeader(cwd, `${personaName} (${reasoningLabel})`);
276
+ }
269
277
  addSystemMessage(text, kind) {
270
- this.chatContainer.addMessage(new SystemMessageComponent(this.uiTheme, text, kind));
278
+ this.chatContainer.addMessage({ type: "system", text, kind });
271
279
  this.ui.requestRender();
272
280
  }
273
281
  addUserMessage(text, opts) {
274
- this.chatContainer.addMessage(new UserMessageComponent(this.uiTheme, text, opts));
275
- this.ui.requestRender();
276
- }
277
- addAssistantComponent(component) {
278
- this.chatContainer.addMessage(component);
279
- this.assistantComponents.push(component);
282
+ this.chatContainer.addMessage({
283
+ type: "user",
284
+ text,
285
+ isMemoryMode: opts?.isMemoryMode,
286
+ });
280
287
  this.ui.requestRender();
281
288
  }
282
289
  // Context & Cost Tracking -----------------------------------------------------------------------
@@ -286,13 +293,13 @@ export class ChatApp {
286
293
  ? this.getContextWindowForLastTurn(last)
287
294
  : this.currentPersona.model.contextWindow;
288
295
  const { input, read, write, output } = this.getSessionTotals();
289
- const stats = `i${formatTokenWindow(input)} r${formatTokenWindow(read)} w${formatTokenWindow(write)} o${formatTokenWindow(output)}`;
296
+ const stats = `↑${formatTokenWindow(input)} ↓${formatTokenWindow(output)} cache ${formatTokenWindow(read)}/${formatTokenWindow(write)}`;
290
297
  const promptTokensSent = last
291
298
  ? (last.usage?.input ?? 0) + (last.usage?.cacheRead ?? 0) + (last.usage?.cacheWrite ?? 0)
292
299
  : 0;
293
300
  const percent = windowTokens > 0 ? (promptTokensSent / windowTokens) * 100 : 0;
294
301
  const percentStr = `${formatAdaptiveNumber(percent, 1, 3)}%`;
295
- return `${stats} ${percentStr}/${formatTokenWindow(windowTokens)}`;
302
+ return `${stats} · ${percentStr}/${formatTokenWindow(windowTokens)}`;
296
303
  }
297
304
  getSessionCostString() {
298
305
  let total = 0;
@@ -442,9 +449,6 @@ export class ChatApp {
442
449
  // User Actions ----------------------------------------------------------------------------------
443
450
  toggleThinkingVisibility() {
444
451
  this.showThinking = !this.showThinking;
445
- this.assistantComponents.forEach((c) => {
446
- c.setThinkingVisibility(this.showThinking);
447
- });
448
452
  this.chatContainer.setThinkingVisibility(this.showThinking);
449
453
  const message = this.showThinking
450
454
  ? "thoughts visible (ctrl+t to hide)"
@@ -699,13 +703,12 @@ export class ChatApp {
699
703
  }
700
704
  clearSession() {
701
705
  this.engine.reset();
702
- this.assistantComponents = [];
703
706
  this.runningBashComponents.clear();
704
707
  this.runningTaskComponents.clear();
705
708
  this.taskEvents.clear();
706
709
  this.subagentCostTotal = 0;
707
710
  this.expandedFilesInCurrentPrompt.clear();
708
- this.chatContainer.addMessage(new SessionDividerComponent(this.uiTheme, "new session"));
711
+ this.chatContainer.addMessage({ type: "session_divider", label: "new session" });
709
712
  this.isBashMode = false;
710
713
  this.isMemoryMode = false;
711
714
  this.previousSessionSummary = undefined;
@@ -835,14 +838,16 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
835
838
  this.previousSessionSummary = previousSessionContext;
836
839
  // Reset the session state but preserve history with divider and summary
837
840
  this.engine.reset();
838
- this.assistantComponents = [];
839
841
  this.runningBashComponents.clear();
840
842
  this.runningTaskComponents.clear();
841
843
  this.taskEvents.clear();
842
844
  this.subagentCostTotal = 0;
843
845
  this.expandedFilesInCurrentPrompt.clear();
844
- this.chatContainer.addMessage(new SessionDividerComponent(this.uiTheme, "new session"));
845
- this.chatContainer.addMessage(new SessionSummaryComponent(this.uiTheme, this.previousSessionSummary));
846
+ this.chatContainer.addMessage({ type: "session_divider", label: "new session" });
847
+ this.chatContainer.addMessage({
848
+ type: "session_summary",
849
+ summary: this.previousSessionSummary,
850
+ });
846
851
  this.isBashMode = false;
847
852
  this.isMemoryMode = false;
848
853
  // Rebuild environment tag and system prompt with the new summary and current risk level
@@ -1045,18 +1050,16 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1045
1050
  if (currentAssistant)
1046
1051
  return currentAssistant;
1047
1052
  currentAssistant = {
1048
- component: new AssistantMessageComponent(this.uiTheme, undefined, this.showThinking),
1049
1053
  inserted: false,
1054
+ model: { type: "assistant_partial", text: "", thinking: "" },
1050
1055
  };
1051
1056
  return currentAssistant;
1052
1057
  };
1053
- const ensureAssistantInserted = () => {
1054
- const state = ensureCurrentAssistant();
1058
+ const ensureAssistantInserted = (state) => {
1055
1059
  if (state.inserted)
1056
1060
  return;
1057
1061
  state.inserted = true;
1058
- this.addAssistantComponent(state.component);
1059
- state.component.setThinkingVisibility(this.showThinking);
1062
+ state.id = this.chatContainer.addMessage(state.model);
1060
1063
  };
1061
1064
  for await (const event of this.engine.processTurn(this.currentTurnAbort.signal)) {
1062
1065
  if (this.currentTurnAbort.signal.aborted)
@@ -1064,34 +1067,39 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1064
1067
  switch (event.type) {
1065
1068
  case "assistant_start":
1066
1069
  currentAssistant = {
1067
- component: new AssistantMessageComponent(this.uiTheme, undefined, this.showThinking),
1068
1070
  inserted: false,
1071
+ model: { type: "assistant_partial", text: "", thinking: "" },
1069
1072
  };
1070
1073
  break;
1071
1074
  case "assistant_partial": {
1072
1075
  const state = ensureCurrentAssistant();
1073
1076
  const { snapshot } = event;
1077
+ const model = {
1078
+ type: "assistant_partial",
1079
+ text: snapshot.hasTextStarted ? snapshot.text : "",
1080
+ thinking: snapshot.thinking,
1081
+ };
1082
+ state.model = model;
1074
1083
  const shouldInsert = snapshot.hasTextStarted || (this.showThinking && snapshot.hasAnyThinking);
1075
1084
  if (shouldInsert && !state.inserted) {
1076
- ensureAssistantInserted();
1085
+ ensureAssistantInserted(state);
1077
1086
  }
1078
- if (state.inserted) {
1079
- // Capture visibility state before update
1080
- const wasVisible = state.component.hasVisibleText;
1081
- state.component.updatePartial(snapshot.hasTextStarted ? snapshot.text : "", snapshot.thinking);
1082
- // If component became visible (e.g. text started after thoughts were hidden),
1083
- // rebuild the container to show it
1084
- if (!wasVisible && state.component.hasVisibleText) {
1085
- this.chatContainer.rebuild();
1086
- }
1087
+ if (state.inserted && state.id) {
1088
+ this.chatContainer.updateMessage(state.id, model);
1087
1089
  this.ui.requestRender();
1088
1090
  }
1089
1091
  break;
1090
1092
  }
1091
1093
  case "assistant_final": {
1092
- ensureAssistantInserted();
1093
- ensureCurrentAssistant().component.updateFromMessage(event.message);
1094
- this.chatContainer.rebuild();
1094
+ const state = ensureCurrentAssistant();
1095
+ const model = { type: "assistant", message: event.message };
1096
+ state.model = model;
1097
+ if (!state.inserted) {
1098
+ ensureAssistantInserted(state);
1099
+ }
1100
+ if (state.id) {
1101
+ this.chatContainer.updateMessage(state.id, model);
1102
+ }
1095
1103
  this.updateFooter();
1096
1104
  this.ui.requestRender();
1097
1105
  currentAssistant = undefined;
@@ -1100,7 +1108,10 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1100
1108
  case "tool_ui": {
1101
1109
  const uiEvent = event.uiEvent;
1102
1110
  if (uiEvent.type === "bash_started") {
1103
- this.chatContainer.addToolMessage((compact) => renderBashRunning(this.uiTheme, uiEvent.command, compact), uiEvent.toolCallId);
1111
+ this.chatContainer.addMessage({
1112
+ type: "tool",
1113
+ view: buildBashRunningView(this.uiTheme, uiEvent.command),
1114
+ }, uiEvent.toolCallId);
1104
1115
  this.runningBashComponents.set(uiEvent.toolCallId, {
1105
1116
  command: uiEvent.command,
1106
1117
  });
@@ -1108,7 +1119,10 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1108
1119
  }
1109
1120
  else if (uiEvent.type === "bash_execution") {
1110
1121
  const running = this.runningBashComponents.get(uiEvent.toolCallId);
1111
- this.chatContainer.replaceToolMessage(uiEvent.toolCallId, (compact) => renderBashExecution(this.uiTheme, uiEvent.command, uiEvent.exitCode, uiEvent.truncationInfo, compact));
1122
+ this.chatContainer.replaceMessage(uiEvent.toolCallId, {
1123
+ type: "tool",
1124
+ view: buildBashExecutionView(this.uiTheme, uiEvent.command, uiEvent.exitCode, uiEvent.truncationInfo, uiEvent.durationMs),
1125
+ });
1112
1126
  if (running) {
1113
1127
  this.runningBashComponents.delete(uiEvent.toolCallId);
1114
1128
  }
@@ -1117,13 +1131,19 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1117
1131
  else if (uiEvent.type === "bash_blocked") {
1118
1132
  if (uiEvent.toolCallId) {
1119
1133
  const running = this.runningBashComponents.get(uiEvent.toolCallId);
1120
- this.chatContainer.replaceToolMessage(uiEvent.toolCallId, (compact) => renderBashBlocked(this.uiTheme, uiEvent.command, uiEvent.reason, compact));
1134
+ this.chatContainer.replaceMessage(uiEvent.toolCallId, {
1135
+ type: "tool",
1136
+ view: buildBashBlockedView(this.uiTheme, uiEvent.command, uiEvent.reason),
1137
+ });
1121
1138
  if (running) {
1122
1139
  this.runningBashComponents.delete(uiEvent.toolCallId);
1123
1140
  }
1124
1141
  }
1125
1142
  else {
1126
- this.chatContainer.addToolMessage((compact) => renderBashBlocked(this.uiTheme, uiEvent.command, uiEvent.reason, compact));
1143
+ this.chatContainer.addMessage({
1144
+ type: "tool",
1145
+ view: buildBashBlockedView(this.uiTheme, uiEvent.command, uiEvent.reason),
1146
+ });
1127
1147
  }
1128
1148
  this.ui.requestRender();
1129
1149
  }
@@ -1133,10 +1153,13 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1133
1153
  }
1134
1154
  const kind = uiEvent.kind ?? "task";
1135
1155
  const subagentName = uiEvent.name.trim() || undefined;
1136
- this.chatContainer.addToolMessage((compact) => renderTaskRunning(this.uiTheme, uiEvent.title, [], 0, 0, 0, compact, {
1137
- kind,
1138
- subagentName,
1139
- }), uiEvent.toolCallId);
1156
+ this.chatContainer.addMessage({
1157
+ type: "tool",
1158
+ view: buildTaskRunningView(this.uiTheme, uiEvent.title, [], 0, 0, 0, {
1159
+ kind,
1160
+ subagentName,
1161
+ }),
1162
+ }, uiEvent.toolCallId);
1140
1163
  this.runningTaskComponents.set(uiEvent.toolCallId, {
1141
1164
  kind,
1142
1165
  name: subagentName,
@@ -1165,14 +1188,20 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1165
1188
  running.turns = uiEvent.turns;
1166
1189
  running.toolCalls = uiEvent.toolCalls;
1167
1190
  }
1168
- this.chatContainer.replaceToolMessage(uiEvent.toolCallId, (compact) => renderTaskRunning(this.uiTheme, uiEvent.title, events, uiEvent.costTotal, uiEvent.turns, uiEvent.toolCalls, compact, { kind, subagentName }));
1191
+ this.chatContainer.replaceMessage(uiEvent.toolCallId, {
1192
+ type: "tool",
1193
+ view: buildTaskRunningView(this.uiTheme, uiEvent.title, events, uiEvent.costTotal, uiEvent.turns, uiEvent.toolCalls, { kind, subagentName }),
1194
+ });
1169
1195
  this.ui.requestRender();
1170
1196
  }
1171
1197
  else if (uiEvent.type === "task_finished") {
1172
1198
  const running = this.runningTaskComponents.get(uiEvent.toolCallId);
1173
1199
  const kind = uiEvent.kind ?? running?.kind ?? "task";
1174
1200
  const subagentName = uiEvent.name.trim() || undefined;
1175
- this.chatContainer.replaceToolMessage(uiEvent.toolCallId, (compact) => renderTaskFinished(this.uiTheme, uiEvent.title, uiEvent.costTotal, uiEvent.turns, uiEvent.toolCalls, uiEvent.status, uiEvent.finalOutput, compact, { kind, subagentName }));
1201
+ this.chatContainer.replaceMessage(uiEvent.toolCallId, {
1202
+ type: "tool",
1203
+ view: buildTaskFinishedView(this.uiTheme, uiEvent.title, uiEvent.costTotal, uiEvent.turns, uiEvent.toolCalls, uiEvent.status, uiEvent.finalOutput, { kind, subagentName }),
1204
+ });
1176
1205
  this.runningTaskComponents.delete(uiEvent.toolCallId);
1177
1206
  this.taskEvents.delete(uiEvent.toolCallId);
1178
1207
  this.subagentCostTotal += uiEvent.costTotal;
@@ -1184,63 +1213,102 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1184
1213
  const kind = uiEvent.kind ?? running?.kind ?? "task";
1185
1214
  const subagentName = uiEvent.name?.trim() || undefined;
1186
1215
  if (running) {
1187
- this.chatContainer.replaceToolMessage(uiEvent.toolCallId, (compact) => renderTaskBlocked(this.uiTheme, uiEvent.title, uiEvent.reason, compact, {
1188
- kind,
1189
- subagentName,
1190
- }));
1216
+ this.chatContainer.replaceMessage(uiEvent.toolCallId, {
1217
+ type: "tool",
1218
+ view: buildTaskBlockedView(this.uiTheme, uiEvent.title, uiEvent.reason, {
1219
+ kind,
1220
+ subagentName,
1221
+ }),
1222
+ });
1191
1223
  }
1192
1224
  else {
1193
- this.chatContainer.addToolMessage((compact) => renderTaskBlocked(this.uiTheme, uiEvent.title, uiEvent.reason, compact, {
1194
- kind,
1195
- subagentName,
1196
- }), uiEvent.toolCallId);
1225
+ this.chatContainer.addMessage({
1226
+ type: "tool",
1227
+ view: buildTaskBlockedView(this.uiTheme, uiEvent.title, uiEvent.reason, {
1228
+ kind,
1229
+ subagentName,
1230
+ }),
1231
+ }, uiEvent.toolCallId);
1197
1232
  }
1198
1233
  this.runningTaskComponents.delete(uiEvent.toolCallId);
1199
1234
  this.taskEvents.delete(uiEvent.toolCallId);
1200
1235
  this.ui.requestRender();
1201
1236
  }
1202
1237
  else if (uiEvent.type === "write_success") {
1203
- this.chatContainer.addToolMessage((compact) => renderWriteSuccess(this.uiTheme, uiEvent.path, uiEvent.bytes, uiEvent.lines, uiEvent.content, compact));
1238
+ this.chatContainer.addMessage({
1239
+ type: "tool",
1240
+ view: buildWriteSuccessView(this.uiTheme, uiEvent.path, uiEvent.bytes, uiEvent.lines, uiEvent.content),
1241
+ });
1204
1242
  this.ui.requestRender();
1205
1243
  }
1206
1244
  else if (uiEvent.type === "write_blocked") {
1207
- this.chatContainer.addToolMessage((compact) => renderWriteBlocked(this.uiTheme, uiEvent.path, uiEvent.reason, compact));
1245
+ this.chatContainer.addMessage({
1246
+ type: "tool",
1247
+ view: buildWriteBlockedView(this.uiTheme, uiEvent.path, uiEvent.reason),
1248
+ });
1208
1249
  this.ui.requestRender();
1209
1250
  }
1210
1251
  else if (uiEvent.type === "edit_success") {
1211
- this.chatContainer.addToolMessage((compact) => renderEditSuccess(this.uiTheme, uiEvent.path, uiEvent.oldLength, uiEvent.newLength, uiEvent.oldText, uiEvent.newText, compact));
1252
+ this.chatContainer.addMessage({
1253
+ type: "tool",
1254
+ view: buildEditSuccessView(this.uiTheme, uiEvent.path, uiEvent.oldLength, uiEvent.newLength, uiEvent.oldText, uiEvent.newText),
1255
+ });
1212
1256
  this.ui.requestRender();
1213
1257
  }
1214
1258
  else if (uiEvent.type === "edit_blocked") {
1215
- this.chatContainer.addToolMessage((compact) => renderEditBlocked(this.uiTheme, uiEvent.path, uiEvent.reason, compact));
1259
+ this.chatContainer.addMessage({
1260
+ type: "tool",
1261
+ view: buildEditBlockedView(this.uiTheme, uiEvent.path, uiEvent.reason),
1262
+ });
1216
1263
  this.ui.requestRender();
1217
1264
  }
1218
1265
  else if (uiEvent.type === "read_success") {
1219
- this.chatContainer.addToolMessage((compact) => renderReadSuccess(this.uiTheme, uiEvent.path, uiEvent.startLine, uiEvent.endLine, uiEvent.content, uiEvent.modelTruncation, compact));
1266
+ this.chatContainer.addMessage({
1267
+ type: "tool",
1268
+ view: buildReadSuccessView(this.uiTheme, uiEvent.path, uiEvent.startLine, uiEvent.endLine, uiEvent.content, uiEvent.modelTruncation),
1269
+ });
1220
1270
  this.ui.requestRender();
1221
1271
  }
1222
1272
  else if (uiEvent.type === "read_blocked") {
1223
- this.chatContainer.addToolMessage((compact) => renderReadBlocked(this.uiTheme, uiEvent.path, uiEvent.reason, compact));
1273
+ this.chatContainer.addMessage({
1274
+ type: "tool",
1275
+ view: buildReadBlockedView(this.uiTheme, uiEvent.path, uiEvent.reason),
1276
+ });
1224
1277
  this.ui.requestRender();
1225
1278
  }
1226
1279
  else if (uiEvent.type === "list_success") {
1227
- this.chatContainer.addToolMessage((compact) => renderListSuccess(this.uiTheme, uiEvent.path, uiEvent.offset, uiEvent.limit, uiEvent.total, uiEvent.returned, uiEvent.entries, compact));
1280
+ this.chatContainer.addMessage({
1281
+ type: "tool",
1282
+ view: buildListSuccessView(this.uiTheme, uiEvent.path, uiEvent.offset, uiEvent.limit, uiEvent.total, uiEvent.returned, uiEvent.entries),
1283
+ });
1228
1284
  this.ui.requestRender();
1229
1285
  }
1230
1286
  else if (uiEvent.type === "list_blocked") {
1231
- this.chatContainer.addToolMessage((compact) => renderListBlocked(this.uiTheme, uiEvent.path, uiEvent.reason, compact));
1287
+ this.chatContainer.addMessage({
1288
+ type: "tool",
1289
+ view: buildListBlockedView(this.uiTheme, uiEvent.path, uiEvent.reason),
1290
+ });
1232
1291
  this.ui.requestRender();
1233
1292
  }
1234
1293
  else if (uiEvent.type === "grep_started") {
1235
- this.chatContainer.addToolMessage((compact) => renderGrepRunning(this.uiTheme, uiEvent.pattern, compact), uiEvent.toolCallId);
1294
+ this.chatContainer.addMessage({
1295
+ type: "tool",
1296
+ view: buildGrepRunningView(this.uiTheme, uiEvent.pattern),
1297
+ }, uiEvent.toolCallId);
1236
1298
  this.ui.requestRender();
1237
1299
  }
1238
1300
  else if (uiEvent.type === "grep_finished") {
1239
- this.chatContainer.replaceToolMessage(uiEvent.toolCallId, (compact) => renderGrepFinished(this.uiTheme, uiEvent.pattern, uiEvent.status, uiEvent.exitCode, uiEvent.stdout, uiEvent.stderr, uiEvent.captureTruncated, compact));
1301
+ this.chatContainer.replaceMessage(uiEvent.toolCallId, {
1302
+ type: "tool",
1303
+ view: buildGrepFinishedView(this.uiTheme, uiEvent.pattern, uiEvent.status, uiEvent.exitCode, uiEvent.stdout, uiEvent.stderr, uiEvent.captureTruncated),
1304
+ });
1240
1305
  this.ui.requestRender();
1241
1306
  }
1242
1307
  else if (uiEvent.type === "grep_blocked") {
1243
- this.chatContainer.addToolMessage((compact) => renderGrepBlocked(this.uiTheme, uiEvent.pattern, uiEvent.reason, compact), uiEvent.toolCallId);
1308
+ this.chatContainer.addMessage({
1309
+ type: "tool",
1310
+ view: buildGrepBlockedView(this.uiTheme, uiEvent.pattern, uiEvent.reason),
1311
+ }, uiEvent.toolCallId);
1244
1312
  this.ui.requestRender();
1245
1313
  }
1246
1314
  break;
@@ -1262,11 +1330,17 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1262
1330
  const wasAborted = this.currentTurnAbort?.signal.aborted ?? false;
1263
1331
  const reason = wasAborted ? "aborted" : "interrupted";
1264
1332
  for (const [id, running] of this.runningBashComponents.entries()) {
1265
- this.chatContainer.replaceToolMessage(id, (compact) => renderBashAborted(this.uiTheme, running.command, reason, compact));
1333
+ this.chatContainer.replaceMessage(id, {
1334
+ type: "tool",
1335
+ view: buildBashAbortedView(this.uiTheme, running.command, reason),
1336
+ });
1266
1337
  }
1267
1338
  const taskStatus = wasAborted ? "aborted" : "error";
1268
1339
  for (const [id, running] of this.runningTaskComponents.entries()) {
1269
- this.chatContainer.replaceToolMessage(id, (compact) => renderTaskFinished(this.uiTheme, running.title, running.costTotal, running.turns, running.toolCalls, taskStatus, reason, compact, { kind: running.kind, subagentName: running.name }));
1340
+ this.chatContainer.replaceMessage(id, {
1341
+ type: "tool",
1342
+ view: buildTaskFinishedView(this.uiTheme, running.title, running.costTotal, running.turns, running.toolCalls, taskStatus, reason, { kind: running.kind, subagentName: running.name }),
1343
+ });
1270
1344
  }
1271
1345
  this.footer.stop();
1272
1346
  this.isStreaming = false;
@@ -1283,12 +1357,17 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1283
1357
  async runBashCommand(command, opts) {
1284
1358
  this.isStreaming = true;
1285
1359
  try {
1360
+ const startedAt = Date.now();
1286
1361
  const { stdout, stderr, exitCode, truncated: captureTruncated, } = await executeBashTool(command, { cwd: opts?.cwd });
1362
+ const durationMs = Math.max(0, Date.now() - startedAt);
1287
1363
  const truncationInfo = prepareBashOutput(stdout, stderr, captureTruncated, {
1288
1364
  stdout: { maxLines: BASH_USER_MAX_STDOUT_LINES, maxTokens: BASH_USER_MAX_STDOUT_TOKENS },
1289
1365
  stderr: { maxLines: BASH_USER_MAX_STDERR_LINES, maxTokens: BASH_USER_MAX_STDERR_TOKENS },
1290
1366
  });
1291
- this.chatContainer.addMessage(renderBashExecution(this.uiTheme, command, exitCode, truncationInfo, false));
1367
+ this.chatContainer.addMessage({
1368
+ type: "tool",
1369
+ view: buildBashExecutionView(this.uiTheme, command, exitCode, truncationInfo, durationMs, "you ran", 12, 12),
1370
+ });
1292
1371
  this.engine.addUserText(formatBashUserMessageText({ command, truncationInfo }));
1293
1372
  this.ui.requestRender();
1294
1373
  }