@markusylisiurunen/tau 0.1.10 → 0.1.12

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 (63) hide show
  1. package/README.md +44 -21
  2. package/dist/app.js +135 -31
  3. package/dist/app.js.map +1 -1
  4. package/dist/bash_commands.js +37 -16
  5. package/dist/bash_commands.js.map +1 -1
  6. package/dist/cli.js +23 -7
  7. package/dist/cli.js.map +1 -1
  8. package/dist/commands.js +86 -13
  9. package/dist/commands.js.map +1 -1
  10. package/dist/config.js +27 -15
  11. package/dist/config.js.map +1 -1
  12. package/dist/content_loader.js +225 -97
  13. package/dist/content_loader.js.map +1 -1
  14. package/dist/debug.js +149 -0
  15. package/dist/debug.js.map +1 -0
  16. package/dist/main.js +35 -0
  17. package/dist/main.js.map +1 -1
  18. package/dist/personas.js +29 -8
  19. package/dist/personas.js.map +1 -1
  20. package/dist/session/session_engine.js +3 -10
  21. package/dist/session/session_engine.js.map +1 -1
  22. package/dist/subagents/registry.js +4 -1
  23. package/dist/subagents/registry.js.map +1 -1
  24. package/dist/subagents/subagent_engine.js +22 -7
  25. package/dist/subagents/subagent_engine.js.map +1 -1
  26. package/dist/subagents/web.js +58 -0
  27. package/dist/subagents/web.js.map +1 -0
  28. package/dist/tools/bash.js +9 -4
  29. package/dist/tools/bash.js.map +1 -1
  30. package/dist/tools/edit.js +8 -5
  31. package/dist/tools/edit.js.map +1 -1
  32. package/dist/tools/parallel_api.js +30 -0
  33. package/dist/tools/parallel_api.js.map +1 -0
  34. package/dist/tools/registry.js.map +1 -1
  35. package/dist/tools/task.js +8 -5
  36. package/dist/tools/task.js.map +1 -1
  37. package/dist/tools/web_fetch.js +241 -0
  38. package/dist/tools/web_fetch.js.map +1 -0
  39. package/dist/tools/web_search.js +229 -0
  40. package/dist/tools/web_search.js.map +1 -0
  41. package/dist/tools/write.js +7 -4
  42. package/dist/tools/write.js.map +1 -1
  43. package/dist/types.js +5 -1
  44. package/dist/types.js.map +1 -1
  45. package/dist/ui/bash_execution.js +26 -0
  46. package/dist/ui/bash_execution.js.map +1 -1
  47. package/dist/ui/custom_editor.js +13 -0
  48. package/dist/ui/custom_editor.js.map +1 -1
  49. package/dist/ui/session_summary.js +1 -10
  50. package/dist/ui/session_summary.js.map +1 -1
  51. package/dist/ui/slash_autocomplete.js +6 -3
  52. package/dist/ui/slash_autocomplete.js.map +1 -1
  53. package/dist/ui/task_execution.js +15 -0
  54. package/dist/ui/task_execution.js.map +1 -1
  55. package/dist/utils/context.js +31 -0
  56. package/dist/utils/context.js.map +1 -1
  57. package/dist/utils/project_files.js +144 -3
  58. package/dist/utils/project_files.js.map +1 -1
  59. package/dist/utils/streaming_settings.js +15 -0
  60. package/dist/utils/streaming_settings.js.map +1 -0
  61. package/dist/utils/zod.js +9 -0
  62. package/dist/utils/zod.js.map +1 -0
  63. package/package.json +6 -4
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # tau
2
2
 
3
- a terminal-based AI chat client for working with code. tau gives you access to Claude, GPT, and Gemini models, each equipped with tools to explore, write, and edit files in your project.
3
+ a terminal-based AI chat client for working with code. tau gives you access to Claude, GPT, and Gemini models, each equipped with tools to explore, write, and edit files in your project, plus optional sub-agents for deeper codebase investigation and web research.
4
4
 
5
5
  ![tau](./assets/tau.png)
6
6
 
@@ -24,13 +24,16 @@ or store keys in `~/.config/tau/config.json`:
24
24
  "apiKeys": {
25
25
  "anthropic": "sk-ant-...",
26
26
  "openai": "sk-...",
27
- "google": "..."
27
+ "google": "...",
28
+ "parallel": "..."
28
29
  }
29
30
  }
30
31
  ```
31
32
 
32
33
  environment variables take precedence over the config file.
33
34
 
35
+ `parallel` is only needed for the web sub-agent tools (`web_search`/`web_fetch`).
36
+
34
37
  ## security notice
35
38
 
36
39
  **the risk level system is a UX guardrail, not a hard security boundary.** it helps prevent accidental writes and guides model behavior, but it has significant limitations:
@@ -86,7 +89,7 @@ tau comes with several built-in personas across different models:
86
89
  - **GPT-5.2** (OpenAI)
87
90
  - **Gemini 3 Pro** and **Gemini 2.5 Flash** (Google)
88
91
 
89
- each model has three variants: a general-purpose assistant, a coder variant optimized for software engineering, and a raw variant with minimal prompting.
92
+ each model has three variants: a general-purpose assistant, a coder variant optimized for software engineering, and a raw variant with minimal prompting. basic and coder variants include the `web` sub-agent for web research, and coder variants also include the `explore` sub-agent for multi-turn codebase investigation.
90
93
 
91
94
  switch personas at startup with `--persona` or mid-session with `/persona:<id>`:
92
95
 
@@ -94,6 +97,15 @@ switch personas at startup with `--persona` or mid-session with `/persona:<id>`:
94
97
  tau --persona opus-4.5-coder
95
98
  ```
96
99
 
100
+ ## sub-agents
101
+
102
+ some personas can run isolated sub-agents via the internal `task` tool:
103
+
104
+ - `explore`: read-only, multi-turn codebase investigation
105
+ - `web`: high-threshold web research using Parallel Search/Extract (`web_search`/`web_fetch`)
106
+
107
+ to use the web sub-agent, set `apiKeys.parallel` in `~/.config/tau/config.json` (see above). tau will only make web calls when needed or when you explicitly ask for web research.
108
+
97
109
  ## reasoning
98
110
 
99
111
  some models support extended thinking, where they reason through problems before responding. cycle through reasoning levels with `shift+tab`, or set one at startup:
@@ -114,7 +126,9 @@ you can also pipe content directly:
114
126
  cat src/app.ts | tau --persona opus-4.5
115
127
  ```
116
128
 
117
- for project-aware sessions, use `--with-context` to inject your AGENTS.md (or similar project guidelines file) into the system prompt. run `tau --help` to see all available options.
129
+ for project-aware sessions, use `--with-context` to inject your AGENTS.md into the system prompt. tau searches for this file in the current directory and parent directories up to your home folder.
130
+
131
+ run `tau --help` to see all available options, or `tau --debug` to inspect loaded personas, prompts, skills, and the full system prompt for debugging configuration issues.
118
132
 
119
133
  ## memory mode
120
134
 
@@ -130,20 +144,20 @@ tau will create or update AGENTS.md at your project root, integrating the new in
130
144
 
131
145
  tau supports slash commands for common actions:
132
146
 
133
- | command | description |
134
- | ---------------------- | -------------------------------------------- |
135
- | `/help` | show available commands |
136
- | `/new` | clear the session and start fresh |
137
- | `/copy` | copy the last assistant message |
138
- | `/copy:code` | copy just the code blocks |
139
- | `/reload` | reload personas and prompts from disk |
140
- | `/fork:only-summary` | compress history and continue with a summary |
141
- | `/fork:with-last-turn` | compress history but keep the last exchange |
142
- | `/persona:<id>` | switch to a different persona |
143
- | `/prompt:<id>` | insert a saved prompt template |
144
- | `/bash:<id>` | run a saved shell command |
145
- | `/risk:<level>` | change the risk level |
146
- | `!<cmd>` | run a shell command directly |
147
+ | command | description |
148
+ | ---------------------- | ---------------------------------------------- |
149
+ | `/help` | show available commands |
150
+ | `/new` | clear the session and start fresh |
151
+ | `/copy` | copy the last assistant message |
152
+ | `/copy:code` | copy just the code blocks |
153
+ | `/reload` | reload personas, prompts, and skills from disk |
154
+ | `/fork:only-summary` | compress history and continue with a summary |
155
+ | `/fork:with-last-turn` | compress history but keep the last exchange |
156
+ | `/persona:<id>` | switch to a different persona |
157
+ | `/prompt:<id>` | insert a saved prompt template |
158
+ | `/bash:<id>` | run a saved shell command |
159
+ | `/risk:<level>` | change the risk level |
160
+ | `!<cmd>` | run a shell command directly |
147
161
 
148
162
  the fork commands are useful when conversations get long. they compress everything into a summary so the model retains context without the overhead of a full history.
149
163
 
@@ -155,6 +169,7 @@ the fork commands are useful when conversations get long. they compress everythi
155
169
  | `ctrl+t` | toggle thinking visibility |
156
170
  | `ctrl+o` | toggle compact tool display |
157
171
  | `ctrl+f` | expand @file mentions |
172
+ | `alt+up` | pop queued message |
158
173
  | `esc` | interrupt generation |
159
174
  | `ctrl+c` | exit |
160
175
 
@@ -169,7 +184,8 @@ store settings in `~/.config/tau/config.json`:
169
184
  "apiKeys": {
170
185
  "anthropic": "sk-ant-...",
171
186
  "openai": "sk-...",
172
- "google": "..."
187
+ "google": "...",
188
+ "parallel": "..."
173
189
  },
174
190
  "defaultPersona": "gpt-5.2",
175
191
  "defaultRisk": "read-write",
@@ -222,7 +238,8 @@ you can also set model parameters via optional frontmatter fields:
222
238
 
223
239
  - `reasoning`: one of `none`, `minimal`, `low`, `medium`, `high`, `xhigh`
224
240
  - `allowedReasoningLevels`: list of reasoning levels shown in the ui
225
- - `subagents`: enable sub-agents (currently only `explore` is supported) for multi-turn codebase investigation. you can specify as a list (`subagents: [explore]`) to use the main persona's model, or as an object to customize each sub-agent's model and reasoning. example:
241
+ - `skills`: list of enabled skill names (matched by `name` in skill frontmatter), or `"*"` to enable all discovered skills
242
+ - `subagents`: enable sub-agents (`explore` for multi-turn codebase investigation, `web` for web research). you can specify as a list (`subagents: [explore]`, `subagents: [web]`, or `subagents: [explore, web]`) to use the main persona's model, or as an object to customize each sub-agent's model and reasoning. example:
226
243
  ```yaml
227
244
  subagents:
228
245
  explore:
@@ -248,7 +265,13 @@ suggest specific improvements with code examples.
248
265
 
249
266
  insert them with `/prompt:review`. if a project prompt id conflicts with a user or built-in prompt, the project prompt wins.
250
267
 
251
- use `/reload` to pick up changes to personas and prompts without restarting.
268
+ ### skills
269
+
270
+ skills are optional markdown files discovered at `~/.config/tau/skills/<dir>/SKILL.md` (user-level) and `.tau/skills/<dir>/SKILL.md` (project-level). each `SKILL.md` must contain yaml frontmatter with `name` and `description`.
271
+
272
+ enable skills per persona with the `skills` frontmatter field. you can list specific skill names (matched by `name` in skill frontmatter), or use `"*"` to enable all discovered skills. all non-raw built-in personas (basic and coder variants) have `skills: "*"` by default. tau injects an index of enabled skills into the system prompt containing only each skill's `name`, `description`, and absolute file path.
273
+
274
+ use `/reload` to pick up changes to personas, prompts, and skills without restarting.
252
275
 
253
276
  ## how it works
254
277
 
package/dist/app.js CHANGED
@@ -14,9 +14,9 @@ import { createEditToolDefinition } from "./tools/edit.js";
14
14
  import { ToolRegistry } from "./tools/registry.js";
15
15
  import { createTaskToolDefinition } from "./tools/task.js";
16
16
  import { createWriteToolDefinition } from "./tools/write.js";
17
- import { REASONING_LEVELS } from "./types.js";
17
+ import { REASONING_LEVELS, } from "./types.js";
18
18
  import { AssistantMessageComponent } from "./ui/assistant_message.js";
19
- import { renderBashBlocked, renderBashExecution, renderBashRunning } from "./ui/bash_execution.js";
19
+ import { renderBashAborted, renderBashBlocked, renderBashExecution, renderBashRunning, } from "./ui/bash_execution.js";
20
20
  import { ChatContainerComponent } from "./ui/chat_container.js";
21
21
  import { CustomEditor } from "./ui/custom_editor.js";
22
22
  import { renderEditBlocked, renderEditSuccess, renderWriteBlocked, renderWriteSuccess, } from "./ui/file_execution.js";
@@ -24,17 +24,17 @@ import { FooterComponent } from "./ui/footer.js";
24
24
  import { QueuedMessagesComponent } from "./ui/queued_messages.js";
25
25
  import { SessionDividerComponent } from "./ui/session_divider.js";
26
26
  import { SessionSummaryComponent } from "./ui/session_summary.js";
27
- import { SlashAutocompleteProvider } from "./ui/slash_autocomplete.js";
27
+ import { getFileAutocompleteToken, SlashAutocompleteProvider } from "./ui/slash_autocomplete.js";
28
28
  import { SystemMessageComponent } from "./ui/system_message.js";
29
29
  import { renderTaskBlocked, renderTaskFinished, renderTaskRunning } from "./ui/task_execution.js";
30
30
  import { editorBorderForReasoning, theme } from "./ui/theme.js";
31
31
  import { UserMessageComponent } from "./ui/user_message.js";
32
- import { buildBaseSystemPrompt, buildEnvironmentTag, buildProjectContextBlock, findAgentsFilesFromCwdToHome, formatRiskLevelChangeNotice, } from "./utils/context.js";
32
+ import { buildBaseSystemPrompt, buildEnvironmentTag, buildProjectContextBlock, buildSkillsIndexBlock, findAgentsFilesFromCwdToHome, formatRiskLevelChangeNotice, } from "./utils/context.js";
33
33
  import { formatHistoryForCompression } from "./utils/fork.js";
34
34
  import { formatAdaptiveNumber, formatCwd, formatTokenWindow } from "./utils/format.js";
35
35
  import { getGitRoot } from "./utils/git.js";
36
36
  import { extractAllFencedCodeBlocks, extractAssistantText } from "./utils/messages.js";
37
- import { listProjectFiles } from "./utils/project_files.js";
37
+ import { listProjectFiles, listProjectFilesAsync } from "./utils/project_files.js";
38
38
  const { palette } = theme;
39
39
  export class ChatApp {
40
40
  ui;
@@ -45,14 +45,15 @@ export class ChatApp {
45
45
  personas;
46
46
  currentPersona;
47
47
  prompts;
48
+ skills;
48
49
  bashCommands;
49
50
  repoRoot;
50
51
  initialUserMessage;
51
52
  config;
52
53
  assistantComponents = [];
53
54
  engine;
54
- runningBashComponents = new Map(); // toolCallId -> component index
55
- runningTaskComponents = new Map(); // toolCallId -> component index
55
+ runningBashComponents = new Map();
56
+ runningTaskComponents = new Map();
56
57
  taskEvents = new Map(); // toolCallId -> accumulated events
57
58
  subagentCostTotal = 0;
58
59
  isStreaming = false;
@@ -68,7 +69,9 @@ export class ChatApp {
68
69
  initialRiskLevel;
69
70
  environmentTag;
70
71
  projectContextBlock;
71
- projectFiles;
72
+ projectFiles = [];
73
+ isRefreshingProjectFiles = false;
74
+ isInFileAutocomplete = false;
72
75
  agentsFiles;
73
76
  baseSystemPrompt;
74
77
  pendingRiskLevelChange;
@@ -77,6 +80,7 @@ export class ChatApp {
77
80
  constructor(options) {
78
81
  this.personas = options.personas;
79
82
  this.prompts = options.prompts ?? [];
83
+ this.skills = options.skills ?? [];
80
84
  this.bashCommands = options.bashCommands ?? [];
81
85
  this.repoRoot = getGitRoot(process.cwd()) ?? process.cwd();
82
86
  this.initialUserMessage = options.initialUserMessage;
@@ -105,6 +109,7 @@ export class ChatApp {
105
109
  this.clampPersonaReasoning(this.currentPersona);
106
110
  this.baseSystemPrompt = buildBaseSystemPrompt({
107
111
  personaSystemPrompt: this.currentPersona.systemPrompt,
112
+ skillsBlock: this.getSkillsIndexBlockForPersona(this.currentPersona).skillsBlock,
108
113
  projectContextBlock: this.projectContextBlock,
109
114
  environmentTag: this.environmentTag,
110
115
  userPreferences: this.config.userPreferences,
@@ -138,7 +143,7 @@ export class ChatApp {
138
143
  this.ui.addChild(this.editor);
139
144
  this.ui.addChild(this.footer);
140
145
  const headerText = `\n${palette.accent("tau")} ${palette.muted("– terminal chat")}\n\n` +
141
- palette.muted(buildHelpText(this.agentsFiles));
146
+ palette.muted(buildHelpText(this.agentsFiles, this.skills));
142
147
  this.chatContainer.addMessage(new Text(headerText, 1, 0));
143
148
  this.ui.setFocus(this.editor);
144
149
  this.updateFooter();
@@ -168,9 +173,15 @@ export class ChatApp {
168
173
  this.editor.onChange = (text) => {
169
174
  const wasBash = this.isBashMode;
170
175
  const wasMemory = this.isMemoryMode;
176
+ const wasInFileAutocomplete = this.isInFileAutocomplete;
171
177
  const trimmed = text.trimStart();
172
178
  this.isBashMode = trimmed.startsWith("!");
173
179
  this.isMemoryMode = trimmed.startsWith("#");
180
+ const beforeCursor = this.getEditorTextBeforeCursor();
181
+ this.isInFileAutocomplete = Boolean(getFileAutocompleteToken(beforeCursor));
182
+ if (!wasInFileAutocomplete && this.isInFileAutocomplete) {
183
+ this.refreshProjectFilesInBackground();
184
+ }
174
185
  if (wasBash !== this.isBashMode || wasMemory !== this.isMemoryMode) {
175
186
  this.updateEditorBorderColor();
176
187
  }
@@ -181,6 +192,27 @@ export class ChatApp {
181
192
  })), () => this.projectFiles));
182
193
  this.editor.onSubmit = (text) => this.handleSubmit(text);
183
194
  }
195
+ getEditorTextBeforeCursor() {
196
+ const { line, col } = this.editor.getCursor();
197
+ const lines = this.editor.getLines();
198
+ const current = lines[line] ?? "";
199
+ return current.slice(0, col);
200
+ }
201
+ refreshProjectFilesInBackground() {
202
+ if (this.isRefreshingProjectFiles)
203
+ return;
204
+ this.isRefreshingProjectFiles = true;
205
+ void listProjectFilesAsync(process.cwd())
206
+ .then((files) => {
207
+ this.projectFiles = files;
208
+ })
209
+ .catch(() => {
210
+ // Ignore refresh errors; autocomplete will keep using the existing cache.
211
+ })
212
+ .finally(() => {
213
+ this.isRefreshingProjectFiles = false;
214
+ });
215
+ }
184
216
  async start() {
185
217
  this.ui.start();
186
218
  if (this.initialUserMessage) {
@@ -332,6 +364,34 @@ export class ChatApp {
332
364
  persona.settings.reasoning = allowed[0];
333
365
  }
334
366
  }
367
+ getSkillsIndexBlockForPersona(persona) {
368
+ const enabled = persona.skills;
369
+ if (!enabled) {
370
+ return { unknown: [] };
371
+ }
372
+ const skillsByName = new Map();
373
+ for (const skill of this.skills) {
374
+ skillsByName.set(skill.name.toLowerCase(), skill);
375
+ }
376
+ const selected = [];
377
+ const unknown = [];
378
+ const seen = new Set();
379
+ const enabledArray = enabled === "*" ? Array.from(skillsByName.keys()) : enabled;
380
+ for (const name of enabledArray) {
381
+ const key = name.toLowerCase();
382
+ if (seen.has(key))
383
+ continue;
384
+ seen.add(key);
385
+ const skill = skillsByName.get(key);
386
+ if (skill) {
387
+ selected.push(skill);
388
+ }
389
+ else if (enabled !== "*") {
390
+ unknown.push(name);
391
+ }
392
+ }
393
+ return { skillsBlock: buildSkillsIndexBlock(selected), unknown };
394
+ }
335
395
  // User Actions ----------------------------------------------------------------------------------
336
396
  toggleThinkingVisibility() {
337
397
  this.showThinking = !this.showThinking;
@@ -549,7 +609,7 @@ export class ChatApp {
549
609
  }
550
610
  }
551
611
  showHelp() {
552
- this.addSystemMessage(buildHelpText(this.agentsFiles), palette.muted);
612
+ this.addSystemMessage(buildHelpText(this.agentsFiles, this.skills), palette.muted);
553
613
  }
554
614
  async copyLastAssistantMessage() {
555
615
  const lastAssistant = this.getLastAssistantMessage();
@@ -672,6 +732,7 @@ export class ChatApp {
672
732
  });
673
733
  this.baseSystemPrompt = buildBaseSystemPrompt({
674
734
  personaSystemPrompt: this.currentPersona.systemPrompt,
735
+ skillsBlock: this.getSkillsIndexBlockForPersona(this.currentPersona).skillsBlock,
675
736
  projectContextBlock: this.projectContextBlock,
676
737
  environmentTag: this.environmentTag,
677
738
  previousSessionSummary,
@@ -823,8 +884,10 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
823
884
  }
824
885
  this.currentPersona = persona;
825
886
  this.clampPersonaReasoning(this.currentPersona);
887
+ const skillsContext = this.getSkillsIndexBlockForPersona(this.currentPersona);
826
888
  this.baseSystemPrompt = buildBaseSystemPrompt({
827
889
  personaSystemPrompt: this.currentPersona.systemPrompt,
890
+ skillsBlock: skillsContext.skillsBlock,
828
891
  projectContextBlock: this.projectContextBlock,
829
892
  environmentTag: this.environmentTag,
830
893
  previousSessionSummary: this.previousSessionSummary,
@@ -833,6 +896,9 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
833
896
  this.engine.setPersona(this.currentPersona, this.baseSystemPrompt);
834
897
  this.updateFooter();
835
898
  this.updateEditorBorderColor();
899
+ if (skillsContext.unknown.length > 0) {
900
+ this.addSystemMessage(`warning: unknown skills enabled: ${skillsContext.unknown.join(", ")}`, palette.noticeWarn);
901
+ }
836
902
  this.addSystemMessage(`switched to ${persona.label} (${persona.model.id})`, palette.noticeSuccess);
837
903
  }
838
904
  insertPrompt(id) {
@@ -859,12 +925,13 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
859
925
  }
860
926
  try {
861
927
  const result = await loadAllContent();
862
- const { personas, prompts, errors } = result;
928
+ const { personas, prompts, skills, errors } = result;
863
929
  const bashResult = loadBashCommands(process.cwd());
864
930
  this.bashCommands = bashResult.commands;
865
931
  // Update the personas and prompts lists
866
932
  this.personas = personas;
867
933
  this.prompts = prompts;
934
+ this.skills = skills;
868
935
  // Try to preserve the current persona; fall back to first if not found
869
936
  const currentPersonaId = this.currentPersona.id.toLowerCase();
870
937
  const updatedPersona = personas.find((p) => p.id.toLowerCase() === currentPersonaId);
@@ -879,25 +946,31 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
879
946
  this.addSystemMessage(`previous persona no longer available; switched to ${this.currentPersona.label || this.currentPersona.id}.`, palette.noticeWarn);
880
947
  }
881
948
  // Rebuild system prompt and update the engine
949
+ const skillsContext = this.getSkillsIndexBlockForPersona(this.currentPersona);
882
950
  this.baseSystemPrompt = buildBaseSystemPrompt({
883
951
  personaSystemPrompt: this.currentPersona.systemPrompt,
952
+ skillsBlock: skillsContext.skillsBlock,
884
953
  projectContextBlock: this.projectContextBlock,
885
954
  environmentTag: this.environmentTag,
886
955
  previousSessionSummary: this.previousSessionSummary,
887
956
  userPreferences: this.config.userPreferences,
888
957
  });
889
958
  this.engine.setPersona(this.currentPersona, this.baseSystemPrompt);
959
+ if (skillsContext.unknown.length > 0) {
960
+ this.addSystemMessage(`warning: unknown skills enabled: ${skillsContext.unknown.join(", ")}`, palette.noticeWarn);
961
+ }
890
962
  // Update UI
891
963
  this.updateFooter();
892
964
  this.updateEditorBorderColor();
893
965
  // Display summary
894
966
  const personaCount = personas.length;
895
967
  const promptCount = prompts.length;
968
+ const skillCount = skills.length;
896
969
  const bashCount = bashResult.commands.length;
897
970
  const errorCount = errors.length + bashResult.errors.length;
898
971
  const summary = errorCount > 0
899
- ? `reloaded: ${personaCount} personas, ${promptCount} prompts, ${bashCount} bash commands (${errorCount} errors).`
900
- : `reloaded: ${personaCount} personas, ${promptCount} prompts, ${bashCount} bash commands.`;
972
+ ? `reloaded: ${personaCount} personas, ${promptCount} prompts, ${skillCount} skills, ${bashCount} bash commands (${errorCount} errors).`
973
+ : `reloaded: ${personaCount} personas, ${promptCount} prompts, ${skillCount} skills, ${bashCount} bash commands.`;
901
974
  this.addSystemMessage(summary, palette.noticeSuccess);
902
975
  this.ui.requestRender();
903
976
  }
@@ -973,15 +1046,18 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
973
1046
  if (uiEvent.type === "bash_started") {
974
1047
  // Create and add the running component, storing its index
975
1048
  const index = this.chatContainer.addToolMessage((compact) => renderBashRunning(uiEvent.command, compact));
976
- this.runningBashComponents.set(uiEvent.toolCallId, index);
1049
+ this.runningBashComponents.set(uiEvent.toolCallId, {
1050
+ index,
1051
+ command: uiEvent.command,
1052
+ });
977
1053
  this.ui.requestRender();
978
1054
  }
979
1055
  else if (uiEvent.type === "bash_execution") {
980
1056
  // Check if we have a running component for this toolCallId
981
- const runningIndex = this.runningBashComponents.get(uiEvent.toolCallId);
982
- if (runningIndex !== undefined) {
1057
+ const running = this.runningBashComponents.get(uiEvent.toolCallId);
1058
+ if (running) {
983
1059
  // Replace the running component with the finished execution component
984
- this.chatContainer.replaceToolMessageAtIndex(runningIndex, (compact) => renderBashExecution(uiEvent.command, uiEvent.exitCode, uiEvent.truncationInfo, compact));
1060
+ this.chatContainer.replaceToolMessageAtIndex(running.index, (compact) => renderBashExecution(uiEvent.command, uiEvent.exitCode, uiEvent.truncationInfo, compact));
985
1061
  this.runningBashComponents.delete(uiEvent.toolCallId);
986
1062
  }
987
1063
  else {
@@ -993,10 +1069,10 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
993
1069
  else if (uiEvent.type === "bash_blocked") {
994
1070
  // Check if this is a post-acceptance failure that has a running card
995
1071
  if (uiEvent.toolCallId) {
996
- const runningIndex = this.runningBashComponents.get(uiEvent.toolCallId);
997
- if (runningIndex !== undefined) {
1072
+ const running = this.runningBashComponents.get(uiEvent.toolCallId);
1073
+ if (running) {
998
1074
  // Replace the running component with the blocked component
999
- this.chatContainer.replaceToolMessageAtIndex(runningIndex, (compact) => renderBashBlocked(uiEvent.command, uiEvent.reason, compact));
1075
+ this.chatContainer.replaceToolMessageAtIndex(running.index, (compact) => renderBashBlocked(uiEvent.command, uiEvent.reason, compact));
1000
1076
  this.runningBashComponents.delete(uiEvent.toolCallId);
1001
1077
  }
1002
1078
  else {
@@ -1015,7 +1091,14 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1015
1091
  this.taskEvents.set(uiEvent.toolCallId, []);
1016
1092
  }
1017
1093
  const index = this.chatContainer.addToolMessage((compact) => renderTaskRunning(uiEvent.title, [], 0, 0, 0, compact, uiEvent.name));
1018
- this.runningTaskComponents.set(uiEvent.toolCallId, index);
1094
+ this.runningTaskComponents.set(uiEvent.toolCallId, {
1095
+ index,
1096
+ name: uiEvent.name,
1097
+ title: uiEvent.title,
1098
+ costTotal: 0,
1099
+ turns: 0,
1100
+ toolCalls: 0,
1101
+ });
1019
1102
  this.ui.requestRender();
1020
1103
  }
1021
1104
  else if (uiEvent.type === "task_progress") {
@@ -1026,20 +1109,32 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1026
1109
  this.taskEvents.set(uiEvent.toolCallId, events);
1027
1110
  }
1028
1111
  events.push(uiEvent.event);
1029
- const runningIndex = this.runningTaskComponents.get(uiEvent.toolCallId);
1030
- if (runningIndex !== undefined) {
1031
- this.chatContainer.replaceToolMessageAtIndex(runningIndex, (compact) => renderTaskRunning(uiEvent.title, events, uiEvent.costTotal, uiEvent.turns, uiEvent.toolCalls, compact, uiEvent.name));
1112
+ const running = this.runningTaskComponents.get(uiEvent.toolCallId);
1113
+ if (running) {
1114
+ running.name = uiEvent.name;
1115
+ running.title = uiEvent.title;
1116
+ running.costTotal = uiEvent.costTotal;
1117
+ running.turns = uiEvent.turns;
1118
+ running.toolCalls = uiEvent.toolCalls;
1119
+ this.chatContainer.replaceToolMessageAtIndex(running.index, (compact) => renderTaskRunning(uiEvent.title, events, uiEvent.costTotal, uiEvent.turns, uiEvent.toolCalls, compact, uiEvent.name));
1032
1120
  }
1033
1121
  else {
1034
1122
  const index = this.chatContainer.addToolMessage((compact) => renderTaskRunning(uiEvent.title, events, uiEvent.costTotal, uiEvent.turns, uiEvent.toolCalls, compact, uiEvent.name));
1035
- this.runningTaskComponents.set(uiEvent.toolCallId, index);
1123
+ this.runningTaskComponents.set(uiEvent.toolCallId, {
1124
+ index,
1125
+ name: uiEvent.name,
1126
+ title: uiEvent.title,
1127
+ costTotal: uiEvent.costTotal,
1128
+ turns: uiEvent.turns,
1129
+ toolCalls: uiEvent.toolCalls,
1130
+ });
1036
1131
  }
1037
1132
  this.ui.requestRender();
1038
1133
  }
1039
1134
  else if (uiEvent.type === "task_finished") {
1040
- const runningIndex = this.runningTaskComponents.get(uiEvent.toolCallId);
1041
- if (runningIndex !== undefined) {
1042
- this.chatContainer.replaceToolMessageAtIndex(runningIndex, (compact) => renderTaskFinished(uiEvent.title, uiEvent.costTotal, uiEvent.turns, uiEvent.toolCalls, uiEvent.status, uiEvent.finalOutput, compact, uiEvent.name));
1135
+ const running = this.runningTaskComponents.get(uiEvent.toolCallId);
1136
+ if (running) {
1137
+ this.chatContainer.replaceToolMessageAtIndex(running.index, (compact) => renderTaskFinished(uiEvent.title, uiEvent.costTotal, uiEvent.turns, uiEvent.toolCalls, uiEvent.status, uiEvent.finalOutput, compact, uiEvent.name));
1043
1138
  this.runningTaskComponents.delete(uiEvent.toolCallId);
1044
1139
  }
1045
1140
  else {
@@ -1051,9 +1146,9 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1051
1146
  this.ui.requestRender();
1052
1147
  }
1053
1148
  else if (uiEvent.type === "task_blocked") {
1054
- const runningIndex = this.runningTaskComponents.get(uiEvent.toolCallId);
1055
- if (runningIndex !== undefined) {
1056
- this.chatContainer.replaceToolMessageAtIndex(runningIndex, (compact) => renderTaskBlocked(uiEvent.title, uiEvent.reason, compact, uiEvent.name));
1149
+ const running = this.runningTaskComponents.get(uiEvent.toolCallId);
1150
+ if (running) {
1151
+ this.chatContainer.replaceToolMessageAtIndex(running.index, (compact) => renderTaskBlocked(uiEvent.title, uiEvent.reason, compact, uiEvent.name));
1057
1152
  this.runningTaskComponents.delete(uiEvent.toolCallId);
1058
1153
  }
1059
1154
  else {
@@ -1098,6 +1193,15 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1098
1193
  this.addSystemMessage(`error: ${err.message}`, palette.noticeError);
1099
1194
  }
1100
1195
  finally {
1196
+ const wasAborted = this.currentTurnAbort?.signal.aborted ?? false;
1197
+ const reason = wasAborted ? "aborted" : "interrupted";
1198
+ for (const running of this.runningBashComponents.values()) {
1199
+ this.chatContainer.replaceToolMessageAtIndex(running.index, (compact) => renderBashAborted(running.command, reason, compact));
1200
+ }
1201
+ const taskStatus = wasAborted ? "aborted" : "error";
1202
+ for (const running of this.runningTaskComponents.values()) {
1203
+ this.chatContainer.replaceToolMessageAtIndex(running.index, (compact) => renderTaskFinished(running.title, running.costTotal, running.turns, running.toolCalls, taskStatus, reason, compact, running.name));
1204
+ }
1101
1205
  this.footer.stop();
1102
1206
  this.isStreaming = false;
1103
1207
  this.currentTurnAbort = undefined;