@markusylisiurunen/tau 0.1.33 → 0.1.36

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 (59) hide show
  1. package/README.md +17 -16
  2. package/dist/app.js +101 -105
  3. package/dist/app.js.map +1 -1
  4. package/dist/commands.js +43 -10
  5. package/dist/commands.js.map +1 -1
  6. package/dist/terminal.js +13 -0
  7. package/dist/terminal.js.map +1 -1
  8. package/dist/tools/bash.js +6 -13
  9. package/dist/tools/bash.js.map +1 -1
  10. package/dist/tools/edit.js +2 -47
  11. package/dist/tools/edit.js.map +1 -1
  12. package/dist/tools/grep.js +3 -23
  13. package/dist/tools/grep.js.map +1 -1
  14. package/dist/tools/read.js +7 -14
  15. package/dist/tools/read.js.map +1 -1
  16. package/dist/tools/registry.js.map +1 -1
  17. package/dist/tools/write.js +1 -19
  18. package/dist/tools/write.js.map +1 -1
  19. package/dist/types.js.map +1 -1
  20. package/dist/ui/app_intro.js +12 -0
  21. package/dist/ui/app_intro.js.map +1 -0
  22. package/dist/ui/assistant_message.js +4 -3
  23. package/dist/ui/assistant_message.js.map +1 -1
  24. package/dist/ui/bash_execution.js +46 -36
  25. package/dist/ui/bash_execution.js.map +1 -1
  26. package/dist/ui/components/one_line_segments.js +69 -0
  27. package/dist/ui/components/one_line_segments.js.map +1 -1
  28. package/dist/ui/custom_editor.js +11 -18
  29. package/dist/ui/custom_editor.js.map +1 -1
  30. package/dist/ui/file_execution.js +70 -27
  31. package/dist/ui/file_execution.js.map +1 -1
  32. package/dist/ui/footer.js +64 -14
  33. package/dist/ui/footer.js.map +1 -1
  34. package/dist/ui/inline.js +4 -0
  35. package/dist/ui/inline.js.map +1 -0
  36. package/dist/ui/queued_messages.js +9 -8
  37. package/dist/ui/queued_messages.js.map +1 -1
  38. package/dist/ui/restricted_execution.js +60 -47
  39. package/dist/ui/restricted_execution.js.map +1 -1
  40. package/dist/ui/session_divider.js +4 -3
  41. package/dist/ui/session_divider.js.map +1 -1
  42. package/dist/ui/session_summary.js +3 -4
  43. package/dist/ui/session_summary.js.map +1 -1
  44. package/dist/ui/slash_autocomplete.js +2 -2
  45. package/dist/ui/slash_autocomplete.js.map +1 -1
  46. package/dist/ui/system_message.js +10 -2
  47. package/dist/ui/system_message.js.map +1 -1
  48. package/dist/ui/task_execution.js +14 -15
  49. package/dist/ui/task_execution.js.map +1 -1
  50. package/dist/ui/theme.js +179 -89
  51. package/dist/ui/theme.js.map +1 -1
  52. package/dist/ui/tool_output.js +7 -2
  53. package/dist/ui/tool_output.js.map +1 -1
  54. package/dist/ui/tool_truncation.js +21 -0
  55. package/dist/ui/tool_truncation.js.map +1 -0
  56. package/dist/ui/user_message.js +1 -2
  57. package/dist/ui/user_message.js.map +1 -1
  58. package/dist/version.js +1 -1
  59. package/package.json +11 -9
package/dist/app.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { homedir } from "node:os";
2
2
  import { join, resolve } from "node:path";
3
3
  import { streamSimple } from "@mariozechner/pi-ai";
4
- import { Spacer, Text, TUI } from "@mariozechner/pi-tui";
4
+ import { Spacer, TUI } from "@mariozechner/pi-tui";
5
5
  import { loadBashCommands } from "./bash_commands.js";
6
6
  import { copyTextToClipboard } from "./clipboard.js";
7
7
  import { buildHelpText, getRiskLevelDescription, parseCommand } from "./commands.js";
@@ -20,6 +20,7 @@ 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";
23
24
  import { AssistantMessageComponent } from "./ui/assistant_message.js";
24
25
  import { renderBashAborted, renderBashBlocked, renderBashExecution, renderBashRunning, } from "./ui/bash_execution.js";
25
26
  import { ChatContainerComponent } from "./ui/chat_container.js";
@@ -33,7 +34,7 @@ import { SessionSummaryComponent } from "./ui/session_summary.js";
33
34
  import { getFileAutocompleteToken, SlashAutocompleteProvider } from "./ui/slash_autocomplete.js";
34
35
  import { SystemMessageComponent } from "./ui/system_message.js";
35
36
  import { renderTaskBlocked, renderTaskFinished, renderTaskRunning } from "./ui/task_execution.js";
36
- import { editorBorderForReasoning, theme } from "./ui/theme.js";
37
+ import { createUiTheme } from "./ui/theme.js";
37
38
  import { UserMessageComponent } from "./ui/user_message.js";
38
39
  import { buildBaseSystemPrompt, buildEnvironmentTag, buildProjectContextBlock, buildSkillsIndexBlock, findAgentsFilesFromCwdToHome, formatRiskLevelChangeNotice, } from "./utils/context.js";
39
40
  import { formatHistoryForCompression } from "./utils/fork.js";
@@ -42,13 +43,13 @@ import { getGitRoot } from "./utils/git.js";
42
43
  import { extractAllFencedCodeBlocks, extractAssistantText } from "./utils/messages.js";
43
44
  import { listProjectFiles, listProjectFilesAsync } from "./utils/project_files.js";
44
45
  import { APP_VERSION } from "./version.js";
45
- const { palette } = theme;
46
46
  export class ChatApp {
47
47
  ui;
48
48
  chatContainer;
49
49
  footer;
50
50
  queuedMessages;
51
51
  editor;
52
+ uiTheme;
52
53
  personas;
53
54
  currentPersona;
54
55
  prompts;
@@ -139,12 +140,13 @@ export class ChatApp {
139
140
  toolRegistry,
140
141
  config: this.config,
141
142
  });
143
+ this.uiTheme = createUiTheme("ansi");
142
144
  this.ui = new TUI(createAppTerminal());
143
145
  this.chatContainer = new ChatContainerComponent();
144
146
  this.chatContainer.setCompactToolUi(this.compactToolUi);
145
- this.footer = new FooterComponent(this.ui);
146
- this.queuedMessages = new QueuedMessagesComponent(() => this.queuedUserMessages);
147
- this.editor = new CustomEditor(theme.editorTheme);
147
+ this.footer = new FooterComponent(this.uiTheme, this.ui);
148
+ this.queuedMessages = new QueuedMessagesComponent(this.uiTheme, this.queuedUserMessages);
149
+ this.editor = new CustomEditor(this.uiTheme.editorTheme);
148
150
  this.setupUI();
149
151
  this.setupEditor();
150
152
  }
@@ -154,9 +156,7 @@ export class ChatApp {
154
156
  this.ui.addChild(this.queuedMessages);
155
157
  this.ui.addChild(this.editor);
156
158
  this.ui.addChild(this.footer);
157
- const headerText = `\n${palette.accent("tau")} ${palette.muted(`– terminal chat (v${APP_VERSION})`)}\n\n` +
158
- palette.muted(buildHelpText(this.agentsFiles, this.skills));
159
- this.chatContainer.addMessage(new Text(headerText, 1, 0));
159
+ this.chatContainer.addMessage(new AppIntroComponent(this.uiTheme, "tau", APP_VERSION, buildHelpText(this.agentsFiles, this.skills)));
160
160
  this.ui.setFocus(this.editor);
161
161
  this.updateFooter();
162
162
  this.updateEditorBorderColor();
@@ -174,7 +174,7 @@ export class ChatApp {
174
174
  this.editor.onEscape = () => this.interruptAssistantTurn();
175
175
  this.editor.onCtrlF = () => {
176
176
  this.expandFileMentions().catch((err) => {
177
- this.addSystemMessage(`file expansion failed: ${err.message}`, palette.noticeError);
177
+ this.addSystemMessage(`file expansion failed: ${err.message}`, "error");
178
178
  });
179
179
  };
180
180
  this.editor.onAltUp = () => this.popQueuedUserMessageIntoEditor();
@@ -238,29 +238,23 @@ export class ChatApp {
238
238
  }
239
239
  // UI Updates ------------------------------------------------------------------------------------
240
240
  updateFooter() {
241
- const reasoningLabel = this.currentPersona.settings.reasoning || "default";
242
- const toolLabel = this.formatRiskLevelLabel();
241
+ const reasoningLabel = this.currentPersona.settings.reasoning ?? "none";
243
242
  const contextUsage = this.getContextUsageString();
244
243
  const sessionCost = this.getSessionCostString();
245
244
  const cwd = formatCwd(process.cwd());
246
- const left = palette.dim(`${cwd} · ${contextUsage} · ${sessionCost}`);
247
245
  const personaName = this.currentPersona.label || this.currentPersona.id;
248
- const statusPart = palette.dim(`${personaName} · ${reasoningLabel} · `);
249
- const right = `${statusPart}${toolLabel}`;
250
- this.footer.setLeftRight(left, right);
246
+ this.footer.setStatus({
247
+ cwd,
248
+ contextUsage,
249
+ sessionCost,
250
+ personaLabel: personaName,
251
+ reasoningLabel,
252
+ riskLevel: this.riskLevel,
253
+ });
251
254
  this.ui.requestRender();
252
255
  }
253
- formatRiskLevelLabel() {
254
- switch (this.riskLevel) {
255
- case "restricted":
256
- return palette.riskRestricted("restricted");
257
- case "read-only":
258
- return palette.riskReadOnly("read-only");
259
- case "read-write":
260
- return palette.riskReadWrite("read-write");
261
- }
262
- }
263
256
  updateEditorBorderColor() {
257
+ const { palette } = this.uiTheme;
264
258
  if (this.isBashMode) {
265
259
  this.editor.borderColor = (s) => palette.bashRan(s);
266
260
  }
@@ -268,16 +262,16 @@ export class ChatApp {
268
262
  this.editor.borderColor = (s) => palette.memoryMode(s);
269
263
  }
270
264
  else {
271
- this.editor.borderColor = editorBorderForReasoning(this.currentPersona.settings.reasoning);
265
+ this.editor.borderColor = this.uiTheme.editorBorderForReasoning(this.currentPersona.settings.reasoning);
272
266
  }
273
267
  this.ui.requestRender();
274
268
  }
275
- addSystemMessage(text, styleFn) {
276
- this.chatContainer.addMessage(new SystemMessageComponent(text, styleFn));
269
+ addSystemMessage(text, kind) {
270
+ this.chatContainer.addMessage(new SystemMessageComponent(this.uiTheme, text, kind));
277
271
  this.ui.requestRender();
278
272
  }
279
273
  addUserMessage(text, opts) {
280
- this.chatContainer.addMessage(new UserMessageComponent(text, opts));
274
+ this.chatContainer.addMessage(new UserMessageComponent(this.uiTheme, text, opts));
281
275
  this.ui.requestRender();
282
276
  }
283
277
  addAssistantComponent(component) {
@@ -388,7 +382,7 @@ export class ChatApp {
388
382
  }
389
383
  }
390
384
  const description = getRiskLevelDescription(next);
391
- this.addSystemMessage(`risk level: ${description} (ctrl+r to cycle)`, palette.noticeSuccess);
385
+ this.addSystemMessage(`risk level: ${description} (ctrl+r to cycle)`, "success");
392
386
  this.ui.requestRender();
393
387
  }
394
388
  // Personality Management ---------------------------------------------------------------
@@ -411,10 +405,10 @@ export class ChatApp {
411
405
  this.updateFooter();
412
406
  this.updateEditorBorderColor();
413
407
  if (skillsContext.unknown.length > 0) {
414
- this.addSystemMessage(`warning: unknown skills enabled: ${skillsContext.unknown.join(", ")}`, palette.noticeWarn);
408
+ this.addSystemMessage(`warning: unknown skills enabled: ${skillsContext.unknown.join(", ")}`, "warn");
415
409
  }
416
410
  const label = this.currentPersona.label || this.currentPersona.id;
417
- this.addSystemMessage(`switched to ${label} (ctrl+p to cycle)`, palette.noticeSuccess);
411
+ this.addSystemMessage(`switched to ${label} (ctrl+p to cycle)`, "success");
418
412
  this.ui.requestRender();
419
413
  }
420
414
  getSkillsIndexBlockForPersona(persona) {
@@ -455,7 +449,7 @@ export class ChatApp {
455
449
  const message = this.showThinking
456
450
  ? "thoughts visible (ctrl+t to hide)"
457
451
  : "thoughts hidden (ctrl+t to show)";
458
- this.addSystemMessage(message, palette.noticeSuccess);
452
+ this.addSystemMessage(message, "success");
459
453
  this.ui.requestRender();
460
454
  }
461
455
  toggleCompactToolUi() {
@@ -464,14 +458,14 @@ export class ChatApp {
464
458
  const message = this.compactToolUi
465
459
  ? "compact tool UI enabled (ctrl+o to disable)"
466
460
  : "compact tool UI disabled (ctrl+o to enable)";
467
- this.addSystemMessage(message, palette.noticeSuccess);
461
+ this.addSystemMessage(message, "success");
468
462
  this.ui.requestRender();
469
463
  }
470
464
  interruptAssistantTurn() {
471
465
  if (!this.isStreaming || this.currentTurnAbort?.signal.aborted)
472
466
  return;
473
467
  this.currentTurnAbort?.abort();
474
- this.addSystemMessage("interrupted.", palette.noticeError);
468
+ this.addSystemMessage("interrupted.", "error");
475
469
  this.ui.requestRender();
476
470
  }
477
471
  // Input Handling --------------------------------------------------------------------------------
@@ -566,7 +560,7 @@ export class ChatApp {
566
560
  if (trimmed.startsWith("#")) {
567
561
  const request = trimmed.slice(1).trim();
568
562
  if (!request) {
569
- this.addSystemMessage("memory mode request was empty.", palette.noticeWarn);
563
+ this.addSystemMessage("memory mode request was empty.", "warn");
570
564
  return;
571
565
  }
572
566
  const agentsFilePath = this.getMemoryModeFilePath();
@@ -635,10 +629,10 @@ export class ChatApp {
635
629
  case "new":
636
630
  this.clearSession();
637
631
  break;
638
- case "forkOnlySummary":
632
+ case "compactOnlySummary":
639
633
  await this.forkSessionOnlySummary();
640
634
  break;
641
- case "forkSummaryAndLastTurn":
635
+ case "compactSummaryAndLastTurn":
642
636
  await this.forkSessionSummaryAndLastTurn();
643
637
  break;
644
638
  case "risk":
@@ -657,50 +651,50 @@ export class ChatApp {
657
651
  await this.reloadContent();
658
652
  break;
659
653
  case "unknown":
660
- this.addSystemMessage("unknown command. type /help.", palette.noticeError);
654
+ this.addSystemMessage("unknown command. type /help.", "error");
661
655
  break;
662
656
  }
663
657
  }
664
658
  showHelp() {
665
- this.addSystemMessage(buildHelpText(this.agentsFiles, this.skills), palette.muted);
659
+ this.addSystemMessage(buildHelpText(this.agentsFiles, this.skills), "muted");
666
660
  }
667
661
  async copyLastAssistantMessage() {
668
662
  const lastAssistant = this.getLastAssistantMessage();
669
663
  if (!lastAssistant) {
670
- this.addSystemMessage("no assistant message to copy yet.", palette.noticeWarn);
664
+ this.addSystemMessage("no assistant message to copy yet.", "warn");
671
665
  return;
672
666
  }
673
667
  const text = extractAssistantText(lastAssistant);
674
668
  if (!text.trim()) {
675
- this.addSystemMessage("last assistant message was empty.", palette.noticeWarn);
669
+ this.addSystemMessage("last assistant message was empty.", "warn");
676
670
  return;
677
671
  }
678
672
  try {
679
673
  await copyTextToClipboard(text);
680
- this.addSystemMessage("copied last assistant message to clipboard.", palette.noticeSuccess);
674
+ this.addSystemMessage("copied last assistant message to clipboard.", "success");
681
675
  }
682
676
  catch (err) {
683
- this.addSystemMessage(`clipboard copy failed: ${err.message}`, palette.noticeError);
677
+ this.addSystemMessage(`clipboard copy failed: ${err.message}`, "error");
684
678
  }
685
679
  }
686
680
  async copyLastAssistantCodeBlock() {
687
681
  const lastAssistant = this.getLastAssistantMessage();
688
682
  if (!lastAssistant) {
689
- this.addSystemMessage("no assistant message to copy yet.", palette.noticeWarn);
683
+ this.addSystemMessage("no assistant message to copy yet.", "warn");
690
684
  return;
691
685
  }
692
686
  const text = extractAssistantText(lastAssistant);
693
687
  const code = extractAllFencedCodeBlocks(text);
694
688
  if (!code) {
695
- this.addSystemMessage("no code block to copy yet.", palette.noticeWarn);
689
+ this.addSystemMessage("no code block to copy yet.", "warn");
696
690
  return;
697
691
  }
698
692
  try {
699
693
  await copyTextToClipboard(code);
700
- this.addSystemMessage("copied all code blocks to clipboard.", palette.noticeSuccess);
694
+ this.addSystemMessage("copied all code blocks to clipboard.", "success");
701
695
  }
702
696
  catch (err) {
703
- this.addSystemMessage(`clipboard copy failed: ${err.message}`, palette.noticeError);
697
+ this.addSystemMessage(`clipboard copy failed: ${err.message}`, "error");
704
698
  }
705
699
  }
706
700
  clearSession() {
@@ -711,7 +705,7 @@ export class ChatApp {
711
705
  this.taskEvents.clear();
712
706
  this.subagentCostTotal = 0;
713
707
  this.expandedFilesInCurrentPrompt.clear();
714
- this.chatContainer.addMessage(new SessionDividerComponent("new session"));
708
+ this.chatContainer.addMessage(new SessionDividerComponent(this.uiTheme, "new session"));
715
709
  this.isBashMode = false;
716
710
  this.isMemoryMode = false;
717
711
  this.previousSessionSummary = undefined;
@@ -847,8 +841,8 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
847
841
  this.taskEvents.clear();
848
842
  this.subagentCostTotal = 0;
849
843
  this.expandedFilesInCurrentPrompt.clear();
850
- this.chatContainer.addMessage(new SessionDividerComponent("new session"));
851
- this.chatContainer.addMessage(new SessionSummaryComponent(this.previousSessionSummary));
844
+ this.chatContainer.addMessage(new SessionDividerComponent(this.uiTheme, "new session"));
845
+ this.chatContainer.addMessage(new SessionSummaryComponent(this.uiTheme, this.previousSessionSummary));
852
846
  this.isBashMode = false;
853
847
  this.isMemoryMode = false;
854
848
  // Rebuild environment tag and system prompt with the new summary and current risk level
@@ -859,19 +853,19 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
859
853
  async forkSessionOnlySummary() {
860
854
  const history = this.engine.history;
861
855
  if (history.length === 0) {
862
- this.addSystemMessage("no conversation to fork.", palette.noticeWarn);
856
+ this.addSystemMessage("no conversation to fork.", "warn");
863
857
  return;
864
858
  }
865
- this.addSystemMessage("summarizing session...", palette.noticeSuccess);
859
+ this.addSystemMessage("summarizing session...", "success");
866
860
  this.isStreaming = true;
867
861
  this.footer.startWorkingIcon();
868
862
  try {
869
863
  const summary = await this.generateSummary(history);
870
864
  this.applySessionContext(summary);
871
- this.addSystemMessage("session forked. previous context has been summarized.", palette.noticeSuccess);
865
+ this.addSystemMessage("session forked. previous context has been summarized.", "success");
872
866
  }
873
867
  catch (err) {
874
- this.addSystemMessage(`fork failed: ${err.message}`, palette.noticeError);
868
+ this.addSystemMessage(`fork failed: ${err.message}`, "error");
875
869
  }
876
870
  finally {
877
871
  this.footer.stop();
@@ -883,10 +877,10 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
883
877
  async forkSessionSummaryAndLastTurn() {
884
878
  const history = this.engine.history;
885
879
  if (history.length === 0) {
886
- this.addSystemMessage("no conversation to fork.", palette.noticeWarn);
880
+ this.addSystemMessage("no conversation to fork.", "warn");
887
881
  return;
888
882
  }
889
- this.addSystemMessage("summarizing session...", palette.noticeSuccess);
883
+ this.addSystemMessage("summarizing session...", "success");
890
884
  this.isStreaming = true;
891
885
  this.footer.startWorkingIcon();
892
886
  try {
@@ -907,10 +901,10 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
907
901
  sessionContext += "\n</last_turn>";
908
902
  }
909
903
  this.applySessionContext(sessionContext);
910
- this.addSystemMessage("session forked. previous context and last turn have been included.", palette.noticeSuccess);
904
+ this.addSystemMessage("session forked. previous context and last turn have been included.", "success");
911
905
  }
912
906
  catch (err) {
913
- this.addSystemMessage(`fork failed: ${err.message}`, palette.noticeError);
907
+ this.addSystemMessage(`fork failed: ${err.message}`, "error");
914
908
  }
915
909
  finally {
916
910
  this.footer.stop();
@@ -934,12 +928,12 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
934
928
  }
935
929
  }
936
930
  const details = getRiskLevelDescription(level);
937
- this.addSystemMessage(`risk level set to '${level}': ${details}`, palette.noticeSuccess);
931
+ this.addSystemMessage(`risk level set to '${level}': ${details}`, "success");
938
932
  }
939
933
  switchPersona(id) {
940
934
  const persona = this.personas.find((p) => p.id.toLowerCase() === id.toLowerCase());
941
935
  if (!persona) {
942
- this.addSystemMessage(`unknown persona '${id}'.`, palette.noticeError);
936
+ this.addSystemMessage(`unknown persona '${id}'.`, "error");
943
937
  return;
944
938
  }
945
939
  this.currentPersona = persona;
@@ -958,14 +952,14 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
958
952
  this.updateFooter();
959
953
  this.updateEditorBorderColor();
960
954
  if (skillsContext.unknown.length > 0) {
961
- this.addSystemMessage(`warning: unknown skills enabled: ${skillsContext.unknown.join(", ")}`, palette.noticeWarn);
955
+ this.addSystemMessage(`warning: unknown skills enabled: ${skillsContext.unknown.join(", ")}`, "warn");
962
956
  }
963
- this.addSystemMessage(`switched to ${persona.label} (${persona.model.id})`, palette.noticeSuccess);
957
+ this.addSystemMessage(`switched to ${persona.label} (${persona.model.id})`, "success");
964
958
  }
965
959
  insertPrompt(id) {
966
960
  const prompt = this.prompts.find((p) => p.id.toLowerCase() === id.toLowerCase());
967
961
  if (!prompt) {
968
- this.addSystemMessage(`unknown prompt '${id}'.`, palette.noticeError);
962
+ this.addSystemMessage(`unknown prompt '${id}'.`, "error");
969
963
  return;
970
964
  }
971
965
  this.editor.setText(prompt.template);
@@ -974,14 +968,14 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
974
968
  async runSavedBashCommand(id) {
975
969
  const saved = this.bashCommands.find((b) => b.id.toLowerCase() === id.toLowerCase());
976
970
  if (!saved) {
977
- this.addSystemMessage(`unknown bash command '${id}'.`, palette.noticeError);
971
+ this.addSystemMessage(`unknown bash command '${id}'.`, "error");
978
972
  return;
979
973
  }
980
974
  await this.runBashCommand(saved.cmd, { cwd: this.repoRoot });
981
975
  }
982
976
  async reloadContent() {
983
977
  if (this.isStreaming) {
984
- this.addSystemMessage("cannot reload while streaming. try again after the response.", palette.noticeWarn);
978
+ this.addSystemMessage("cannot reload while streaming. try again after the response.", "warn");
985
979
  return;
986
980
  }
987
981
  try {
@@ -1004,7 +998,7 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1004
998
  // Persona no longer exists; switch to the first one
1005
999
  this.currentPersona = personas[0];
1006
1000
  this.clampPersonaReasoning(this.currentPersona);
1007
- this.addSystemMessage(`previous persona no longer available; switched to ${this.currentPersona.label || this.currentPersona.id}.`, palette.noticeWarn);
1001
+ this.addSystemMessage(`previous persona no longer available; switched to ${this.currentPersona.label || this.currentPersona.id}.`, "warn");
1008
1002
  }
1009
1003
  // Rebuild system prompt and update the engine
1010
1004
  const skillsContext = this.getSkillsIndexBlockForPersona(this.currentPersona);
@@ -1019,7 +1013,7 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1019
1013
  });
1020
1014
  this.engine.setPersona(this.currentPersona, this.baseSystemPrompt);
1021
1015
  if (skillsContext.unknown.length > 0) {
1022
- this.addSystemMessage(`warning: unknown skills enabled: ${skillsContext.unknown.join(", ")}`, palette.noticeWarn);
1016
+ this.addSystemMessage(`warning: unknown skills enabled: ${skillsContext.unknown.join(", ")}`, "warn");
1023
1017
  }
1024
1018
  // Update UI
1025
1019
  this.updateFooter();
@@ -1033,11 +1027,11 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1033
1027
  const summary = errorCount > 0
1034
1028
  ? `reloaded: ${personaCount} personas, ${promptCount} prompts, ${skillCount} skills, ${bashCount} bash commands (${errorCount} errors).`
1035
1029
  : `reloaded: ${personaCount} personas, ${promptCount} prompts, ${skillCount} skills, ${bashCount} bash commands.`;
1036
- this.addSystemMessage(summary, palette.noticeSuccess);
1030
+ this.addSystemMessage(summary, "success");
1037
1031
  this.ui.requestRender();
1038
1032
  }
1039
1033
  catch (err) {
1040
- this.addSystemMessage(`reload failed: ${err.message}`, palette.noticeError);
1034
+ this.addSystemMessage(`reload failed: ${err.message}`, "error");
1041
1035
  }
1042
1036
  }
1043
1037
  // Assistant Turn --------------------------------------------------------------------------------
@@ -1051,7 +1045,7 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1051
1045
  if (currentAssistant)
1052
1046
  return currentAssistant;
1053
1047
  currentAssistant = {
1054
- component: new AssistantMessageComponent(undefined, this.showThinking),
1048
+ component: new AssistantMessageComponent(this.uiTheme, undefined, this.showThinking),
1055
1049
  inserted: false,
1056
1050
  };
1057
1051
  return currentAssistant;
@@ -1070,7 +1064,7 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1070
1064
  switch (event.type) {
1071
1065
  case "assistant_start":
1072
1066
  currentAssistant = {
1073
- component: new AssistantMessageComponent(undefined, this.showThinking),
1067
+ component: new AssistantMessageComponent(this.uiTheme, undefined, this.showThinking),
1074
1068
  inserted: false,
1075
1069
  };
1076
1070
  break;
@@ -1106,7 +1100,7 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1106
1100
  case "tool_ui": {
1107
1101
  const uiEvent = event.uiEvent;
1108
1102
  if (uiEvent.type === "bash_started") {
1109
- this.chatContainer.addToolMessage((compact) => renderBashRunning(uiEvent.command, compact), uiEvent.toolCallId);
1103
+ this.chatContainer.addToolMessage((compact) => renderBashRunning(this.uiTheme, uiEvent.command, compact), uiEvent.toolCallId);
1110
1104
  this.runningBashComponents.set(uiEvent.toolCallId, {
1111
1105
  command: uiEvent.command,
1112
1106
  });
@@ -1114,7 +1108,7 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1114
1108
  }
1115
1109
  else if (uiEvent.type === "bash_execution") {
1116
1110
  const running = this.runningBashComponents.get(uiEvent.toolCallId);
1117
- this.chatContainer.replaceToolMessage(uiEvent.toolCallId, (compact) => renderBashExecution(uiEvent.command, uiEvent.exitCode, uiEvent.truncationInfo, compact));
1111
+ this.chatContainer.replaceToolMessage(uiEvent.toolCallId, (compact) => renderBashExecution(this.uiTheme, uiEvent.command, uiEvent.exitCode, uiEvent.truncationInfo, compact));
1118
1112
  if (running) {
1119
1113
  this.runningBashComponents.delete(uiEvent.toolCallId);
1120
1114
  }
@@ -1123,13 +1117,13 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1123
1117
  else if (uiEvent.type === "bash_blocked") {
1124
1118
  if (uiEvent.toolCallId) {
1125
1119
  const running = this.runningBashComponents.get(uiEvent.toolCallId);
1126
- this.chatContainer.replaceToolMessage(uiEvent.toolCallId, (compact) => renderBashBlocked(uiEvent.command, uiEvent.reason, compact));
1120
+ this.chatContainer.replaceToolMessage(uiEvent.toolCallId, (compact) => renderBashBlocked(this.uiTheme, uiEvent.command, uiEvent.reason, compact));
1127
1121
  if (running) {
1128
1122
  this.runningBashComponents.delete(uiEvent.toolCallId);
1129
1123
  }
1130
1124
  }
1131
1125
  else {
1132
- this.chatContainer.addToolMessage((compact) => renderBashBlocked(uiEvent.command, uiEvent.reason, compact));
1126
+ this.chatContainer.addToolMessage((compact) => renderBashBlocked(this.uiTheme, uiEvent.command, uiEvent.reason, compact));
1133
1127
  }
1134
1128
  this.ui.requestRender();
1135
1129
  }
@@ -1139,7 +1133,10 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1139
1133
  }
1140
1134
  const kind = uiEvent.kind ?? "task";
1141
1135
  const subagentName = uiEvent.name.trim() || undefined;
1142
- this.chatContainer.addToolMessage((compact) => renderTaskRunning(uiEvent.title, [], 0, 0, 0, compact, { kind, subagentName }), uiEvent.toolCallId);
1136
+ this.chatContainer.addToolMessage((compact) => renderTaskRunning(this.uiTheme, uiEvent.title, [], 0, 0, 0, compact, {
1137
+ kind,
1138
+ subagentName,
1139
+ }), uiEvent.toolCallId);
1143
1140
  this.runningTaskComponents.set(uiEvent.toolCallId, {
1144
1141
  kind,
1145
1142
  name: subagentName,
@@ -1168,14 +1165,14 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1168
1165
  running.turns = uiEvent.turns;
1169
1166
  running.toolCalls = uiEvent.toolCalls;
1170
1167
  }
1171
- this.chatContainer.replaceToolMessage(uiEvent.toolCallId, (compact) => renderTaskRunning(uiEvent.title, events, uiEvent.costTotal, uiEvent.turns, uiEvent.toolCalls, compact, { kind, subagentName }));
1168
+ this.chatContainer.replaceToolMessage(uiEvent.toolCallId, (compact) => renderTaskRunning(this.uiTheme, uiEvent.title, events, uiEvent.costTotal, uiEvent.turns, uiEvent.toolCalls, compact, { kind, subagentName }));
1172
1169
  this.ui.requestRender();
1173
1170
  }
1174
1171
  else if (uiEvent.type === "task_finished") {
1175
1172
  const running = this.runningTaskComponents.get(uiEvent.toolCallId);
1176
1173
  const kind = uiEvent.kind ?? running?.kind ?? "task";
1177
1174
  const subagentName = uiEvent.name.trim() || undefined;
1178
- this.chatContainer.replaceToolMessage(uiEvent.toolCallId, (compact) => renderTaskFinished(uiEvent.title, uiEvent.costTotal, uiEvent.turns, uiEvent.toolCalls, uiEvent.status, uiEvent.finalOutput, compact, { kind, subagentName }));
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 }));
1179
1176
  this.runningTaskComponents.delete(uiEvent.toolCallId);
1180
1177
  this.taskEvents.delete(uiEvent.toolCallId);
1181
1178
  this.subagentCostTotal += uiEvent.costTotal;
@@ -1187,10 +1184,13 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1187
1184
  const kind = uiEvent.kind ?? running?.kind ?? "task";
1188
1185
  const subagentName = uiEvent.name?.trim() || undefined;
1189
1186
  if (running) {
1190
- this.chatContainer.replaceToolMessage(uiEvent.toolCallId, (compact) => renderTaskBlocked(uiEvent.title, uiEvent.reason, compact, { kind, subagentName }));
1187
+ this.chatContainer.replaceToolMessage(uiEvent.toolCallId, (compact) => renderTaskBlocked(this.uiTheme, uiEvent.title, uiEvent.reason, compact, {
1188
+ kind,
1189
+ subagentName,
1190
+ }));
1191
1191
  }
1192
1192
  else {
1193
- this.chatContainer.addToolMessage((compact) => renderTaskBlocked(uiEvent.title, uiEvent.reason, compact, {
1193
+ this.chatContainer.addToolMessage((compact) => renderTaskBlocked(this.uiTheme, uiEvent.title, uiEvent.reason, compact, {
1194
1194
  kind,
1195
1195
  subagentName,
1196
1196
  }), uiEvent.toolCallId);
@@ -1200,58 +1200,54 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1200
1200
  this.ui.requestRender();
1201
1201
  }
1202
1202
  else if (uiEvent.type === "write_success") {
1203
- this.chatContainer.addToolMessage((compact) => renderWriteSuccess(uiEvent.path, uiEvent.bytes, uiEvent.lines, uiEvent.preview, uiEvent.previewTruncation, compact));
1203
+ this.chatContainer.addToolMessage((compact) => renderWriteSuccess(this.uiTheme, uiEvent.path, uiEvent.bytes, uiEvent.lines, uiEvent.content, compact));
1204
1204
  this.ui.requestRender();
1205
1205
  }
1206
1206
  else if (uiEvent.type === "write_blocked") {
1207
- this.chatContainer.addToolMessage((compact) => renderWriteBlocked(uiEvent.path, uiEvent.reason, compact));
1207
+ this.chatContainer.addToolMessage((compact) => renderWriteBlocked(this.uiTheme, uiEvent.path, uiEvent.reason, compact));
1208
1208
  this.ui.requestRender();
1209
1209
  }
1210
1210
  else if (uiEvent.type === "edit_success") {
1211
- this.chatContainer.addToolMessage((compact) => renderEditSuccess(uiEvent.path, uiEvent.oldLength, uiEvent.newLength, uiEvent.diff, uiEvent.diffTruncation, compact));
1211
+ this.chatContainer.addToolMessage((compact) => renderEditSuccess(this.uiTheme, uiEvent.path, uiEvent.oldLength, uiEvent.newLength, uiEvent.oldText, uiEvent.newText, compact));
1212
1212
  this.ui.requestRender();
1213
1213
  }
1214
1214
  else if (uiEvent.type === "edit_blocked") {
1215
- this.chatContainer.addToolMessage((compact) => renderEditBlocked(uiEvent.path, uiEvent.reason, compact));
1215
+ this.chatContainer.addToolMessage((compact) => renderEditBlocked(this.uiTheme, uiEvent.path, uiEvent.reason, compact));
1216
1216
  this.ui.requestRender();
1217
1217
  }
1218
1218
  else if (uiEvent.type === "read_success") {
1219
- this.chatContainer.addToolMessage((compact) => renderReadSuccess(uiEvent.path, uiEvent.startLine, uiEvent.endLine, uiEvent.preview, uiEvent.previewTruncation, uiEvent.modelTruncation, compact));
1219
+ this.chatContainer.addToolMessage((compact) => renderReadSuccess(this.uiTheme, uiEvent.path, uiEvent.startLine, uiEvent.endLine, uiEvent.content, uiEvent.modelTruncation, compact));
1220
1220
  this.ui.requestRender();
1221
1221
  }
1222
1222
  else if (uiEvent.type === "read_blocked") {
1223
- this.chatContainer.addToolMessage((compact) => renderReadBlocked(uiEvent.path, uiEvent.reason, compact));
1223
+ this.chatContainer.addToolMessage((compact) => renderReadBlocked(this.uiTheme, uiEvent.path, uiEvent.reason, compact));
1224
1224
  this.ui.requestRender();
1225
1225
  }
1226
1226
  else if (uiEvent.type === "list_success") {
1227
- this.chatContainer.addToolMessage((compact) => renderListSuccess(uiEvent.path, uiEvent.offset, uiEvent.limit, uiEvent.total, uiEvent.returned, uiEvent.entries, compact));
1227
+ this.chatContainer.addToolMessage((compact) => renderListSuccess(this.uiTheme, uiEvent.path, uiEvent.offset, uiEvent.limit, uiEvent.total, uiEvent.returned, uiEvent.entries, compact));
1228
1228
  this.ui.requestRender();
1229
1229
  }
1230
1230
  else if (uiEvent.type === "list_blocked") {
1231
- this.chatContainer.addToolMessage((compact) => renderListBlocked(uiEvent.path, uiEvent.reason, compact));
1231
+ this.chatContainer.addToolMessage((compact) => renderListBlocked(this.uiTheme, uiEvent.path, uiEvent.reason, compact));
1232
1232
  this.ui.requestRender();
1233
1233
  }
1234
1234
  else if (uiEvent.type === "grep_started") {
1235
- this.chatContainer.addToolMessage((compact) => renderGrepRunning(uiEvent.pattern, compact), uiEvent.toolCallId);
1235
+ this.chatContainer.addToolMessage((compact) => renderGrepRunning(this.uiTheme, uiEvent.pattern, compact), uiEvent.toolCallId);
1236
1236
  this.ui.requestRender();
1237
1237
  }
1238
1238
  else if (uiEvent.type === "grep_finished") {
1239
- this.chatContainer.replaceToolMessage(uiEvent.toolCallId, (compact) => renderGrepFinished(uiEvent.pattern, uiEvent.status, uiEvent.exitCode, uiEvent.stdoutPreview, uiEvent.stdoutPreviewTruncation, uiEvent.stderrPreview, uiEvent.stderrPreviewTruncation, uiEvent.captureTruncated, compact));
1239
+ this.chatContainer.replaceToolMessage(uiEvent.toolCallId, (compact) => renderGrepFinished(this.uiTheme, uiEvent.pattern, uiEvent.status, uiEvent.exitCode, uiEvent.stdout, uiEvent.stderr, uiEvent.captureTruncated, compact));
1240
1240
  this.ui.requestRender();
1241
1241
  }
1242
1242
  else if (uiEvent.type === "grep_blocked") {
1243
- this.chatContainer.addToolMessage((compact) => renderGrepBlocked(uiEvent.pattern, uiEvent.reason, compact), uiEvent.toolCallId);
1243
+ this.chatContainer.addToolMessage((compact) => renderGrepBlocked(this.uiTheme, uiEvent.pattern, uiEvent.reason, compact), uiEvent.toolCallId);
1244
1244
  this.ui.requestRender();
1245
1245
  }
1246
1246
  break;
1247
1247
  }
1248
1248
  case "notice": {
1249
- const style = event.severity === "error"
1250
- ? palette.noticeError
1251
- : event.severity === "warn"
1252
- ? palette.noticeWarn
1253
- : palette.noticeSuccess;
1254
- this.addSystemMessage(event.text, style);
1249
+ const kind = event.severity === "error" ? "error" : event.severity === "warn" ? "warn" : "success";
1250
+ this.addSystemMessage(event.text, kind);
1255
1251
  break;
1256
1252
  }
1257
1253
  case "tool_result":
@@ -1260,17 +1256,17 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1260
1256
  }
1261
1257
  }
1262
1258
  catch (err) {
1263
- this.addSystemMessage(`error: ${err.message}`, palette.noticeError);
1259
+ this.addSystemMessage(`error: ${err.message}`, "error");
1264
1260
  }
1265
1261
  finally {
1266
1262
  const wasAborted = this.currentTurnAbort?.signal.aborted ?? false;
1267
1263
  const reason = wasAborted ? "aborted" : "interrupted";
1268
1264
  for (const [id, running] of this.runningBashComponents.entries()) {
1269
- this.chatContainer.replaceToolMessage(id, (compact) => renderBashAborted(running.command, reason, compact));
1265
+ this.chatContainer.replaceToolMessage(id, (compact) => renderBashAborted(this.uiTheme, running.command, reason, compact));
1270
1266
  }
1271
1267
  const taskStatus = wasAborted ? "aborted" : "error";
1272
1268
  for (const [id, running] of this.runningTaskComponents.entries()) {
1273
- this.chatContainer.replaceToolMessage(id, (compact) => renderTaskFinished(running.title, running.costTotal, running.turns, running.toolCalls, taskStatus, reason, compact, { kind: running.kind, subagentName: running.name }));
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 }));
1274
1270
  }
1275
1271
  this.footer.stop();
1276
1272
  this.isStreaming = false;
@@ -1292,12 +1288,12 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1292
1288
  stdout: { maxLines: BASH_USER_MAX_STDOUT_LINES, maxTokens: BASH_USER_MAX_STDOUT_TOKENS },
1293
1289
  stderr: { maxLines: BASH_USER_MAX_STDERR_LINES, maxTokens: BASH_USER_MAX_STDERR_TOKENS },
1294
1290
  });
1295
- this.chatContainer.addMessage(renderBashExecution(command, exitCode, truncationInfo, false));
1291
+ this.chatContainer.addMessage(renderBashExecution(this.uiTheme, command, exitCode, truncationInfo, false));
1296
1292
  this.engine.addUserText(formatBashUserMessageText({ command, truncationInfo }));
1297
1293
  this.ui.requestRender();
1298
1294
  }
1299
1295
  catch (err) {
1300
- this.addSystemMessage(`bash error: ${err.message}`, palette.noticeError);
1296
+ this.addSystemMessage(`bash error: ${err.message}`, "error");
1301
1297
  }
1302
1298
  finally {
1303
1299
  this.isStreaming = false;
@@ -1312,7 +1308,7 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1312
1308
  }
1313
1309
  async expandFileMentions() {
1314
1310
  if (this.isStreaming) {
1315
- this.addSystemMessage("cannot expand files while streaming. try again after the response.", palette.noticeWarn);
1311
+ this.addSystemMessage("cannot expand files while streaming. try again after the response.", "warn");
1316
1312
  return;
1317
1313
  }
1318
1314
  const editorText = this.editor.getText();