@markusylisiurunen/tau 0.1.54 → 0.1.56

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 (43) hide show
  1. package/README.md +18 -0
  2. package/dist/app.js +19 -240
  3. package/dist/app.js.map +1 -1
  4. package/dist/config.js +1 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/content_loader.js +70 -41
  7. package/dist/content_loader.js.map +1 -1
  8. package/dist/debug.js +22 -7
  9. package/dist/debug.js.map +1 -1
  10. package/dist/main.js +19 -8
  11. package/dist/main.js.map +1 -1
  12. package/dist/session/session_engine.js +8 -6
  13. package/dist/session/session_engine.js.map +1 -1
  14. package/dist/subagents/subagent_engine.js +6 -1
  15. package/dist/subagents/subagent_engine.js.map +1 -1
  16. package/dist/tool_ui_router.js +274 -0
  17. package/dist/tool_ui_router.js.map +1 -0
  18. package/dist/tools/bash.js +32 -104
  19. package/dist/tools/bash.js.map +1 -1
  20. package/dist/tools/grep.js +20 -72
  21. package/dist/tools/grep.js.map +1 -1
  22. package/dist/ui/file_execution.js +12 -45
  23. package/dist/ui/file_execution.js.map +1 -1
  24. package/dist/ui/restricted_execution.js +16 -66
  25. package/dist/ui/restricted_execution.js.map +1 -1
  26. package/dist/ui/tool_output_helpers.js +30 -0
  27. package/dist/ui/tool_output_helpers.js.map +1 -0
  28. package/dist/utils/agents_files.js +156 -0
  29. package/dist/utils/agents_files.js.map +1 -0
  30. package/dist/utils/context.js +2 -275
  31. package/dist/utils/context.js.map +1 -1
  32. package/dist/utils/context_builder.js +123 -0
  33. package/dist/utils/context_builder.js.map +1 -0
  34. package/dist/utils/flex_retry.js +12 -0
  35. package/dist/utils/flex_retry.js.map +1 -0
  36. package/dist/utils/project_files.js +11 -153
  37. package/dist/utils/project_files.js.map +1 -1
  38. package/dist/utils/spawn_capture.js +135 -0
  39. package/dist/utils/spawn_capture.js.map +1 -0
  40. package/dist/utils/truncate.js +131 -101
  41. package/dist/utils/truncate.js.map +1 -1
  42. package/dist/version.js +1 -1
  43. package/package.json +1 -1
package/README.md CHANGED
@@ -224,6 +224,7 @@ store settings in `~/.config/tau/config.json`:
224
224
  "defaultPersona": "gpt-5.2-chat",
225
225
  "defaultRisk": "read-write",
226
226
  "toolDisplayMode": "compact",
227
+ "disableBuiltinPersonas": false,
227
228
  "userPreferences": "prefer concise responses. use TypeScript for examples."
228
229
  }
229
230
  ```
@@ -236,6 +237,8 @@ the `userPreferences` field lets you set guidance that applies to every conversa
236
237
 
237
238
  `toolDisplayMode` controls how tool calls appear: `"compact"` (default) shows one-line summaries, `"full"` shows detailed blocks.
238
239
 
240
+ if `disableBuiltinPersonas` is set to `true`, tau will not load any built-in personas. only personas from `~/.config/tau/personas/` and `.tau/personas/` will be available.
241
+
239
242
  ### project bash commands
240
243
 
241
244
  define shortcuts for common shell commands in `.tau/config.json` at your project root (or `~/.tau/config.json` globally). tau resolves the project root via git, so you can run tau from subdirectories and it will still pick up `.tau/config.json`:
@@ -280,6 +283,7 @@ the frontmatter defines the persona's id, provider, and model. the markdown body
280
283
 
281
284
  you can also set model parameters via optional frontmatter fields:
282
285
 
286
+ - `extends`: inherit settings from a built-in persona id (for example `gpt-5.2-coder`). only optional fields are inherited; `provider` and `model` are still required on the extending persona. if the markdown body is empty, the base persona's system prompt is used.
283
287
  - `reasoning`: one of `none`, `minimal`, `low`, `medium`, `high`, `xhigh`
284
288
  - `allowedReasoningLevels`: list of reasoning levels shown in the ui
285
289
  - `skills`: list of enabled skill names (matched by `name` in skill frontmatter), or `"*"` to enable all discovered skills
@@ -295,6 +299,20 @@ you can also set model parameters via optional frontmatter fields:
295
299
 
296
300
  use it with `--persona my-assistant` or `/persona:my-assistant`. if a project persona id conflicts with a user or built-in persona, the project persona wins.
297
301
 
302
+ to clone a built-in persona but swap the provider/model, use `extends`:
303
+
304
+ ```markdown
305
+ ---
306
+ id: my-haiku-coder
307
+ extends: gpt-5.2-coder
308
+ provider: anthropic
309
+ model: claude-haiku-4-5
310
+ ---
311
+
312
+ ```
313
+
314
+ by default, user-level personas can’t use built-in persona ids. if you set `disableBuiltinPersonas: true`, those ids become available for custom personas.
315
+
298
316
  ### custom prompts
299
317
 
300
318
  save reusable prompt templates in `~/.config/tau/prompts/` (user-level) or `.tau/prompts/` (project-level). project-level `.tau/` directories are discovered by walking up from the current working directory to the git repo root:
package/dist/app.js CHANGED
@@ -13,6 +13,7 @@ import { renderExport } from "./export/index.js";
13
13
  import { SessionEngine } from "./session/session_engine.js";
14
14
  import { formatSubagentsForPrompt } from "./subagents/registry.js";
15
15
  import { createAppTerminal } from "./terminal.js";
16
+ import { ToolUiRouter } from "./tool_ui_router.js";
16
17
  import { BASH_USER_MAX_STDERR_LINES, BASH_USER_MAX_STDERR_TOKENS, BASH_USER_MAX_STDOUT_LINES, BASH_USER_MAX_STDOUT_TOKENS, createBashToolDefinition, executeBashTool, formatBashUserMessageText, prepareBashOutput, } from "./tools/bash.js";
17
18
  import { createEditToolDefinition } from "./tools/edit.js";
18
19
  import { createForkToolDefinition } from "./tools/fork.js";
@@ -23,15 +24,12 @@ import { ToolRegistry } from "./tools/registry.js";
23
24
  import { createTaskToolDefinition } from "./tools/task.js";
24
25
  import { createWriteToolDefinition } from "./tools/write.js";
25
26
  import { REASONING_LEVELS, } from "./types.js";
26
- import { buildBashAbortedView, buildBashBlockedView, buildBashExecutionView, buildBashRunningView, } from "./ui/bash_execution.js";
27
+ import { buildBashExecutionView } from "./ui/bash_execution.js";
27
28
  import { ChatContainerComponent } from "./ui/chat_container.js";
28
29
  import { CustomEditor } from "./ui/custom_editor.js";
29
- import { buildEditBlockedView, buildEditSuccessView, buildWriteBlockedView, buildWriteSuccessView, } from "./ui/file_execution.js";
30
30
  import { FooterComponent } from "./ui/footer.js";
31
31
  import { QueuedMessagesComponent } from "./ui/queued_messages.js";
32
- import { buildGrepBlockedView, buildGrepFinishedView, buildGrepRunningView, buildListBlockedView, buildListSuccessView, buildReadBlockedView, buildReadSuccessView, } from "./ui/restricted_execution.js";
33
32
  import { getFileAutocompleteToken, SlashAutocompleteProvider } from "./ui/slash_autocomplete.js";
34
- import { buildTaskBlockedView, buildTaskFinishedView, buildTaskRunningView, } from "./ui/task_execution.js";
35
33
  import { createUiTheme } from "./ui/theme.js";
36
34
  import { buildThemePreviewMessages } from "./ui/theme_preview.js";
37
35
  import { buildBaseSystemPrompt, buildEnvironmentTag, buildProjectContextBlock, buildSkillsIndexBlock, findAgentsFilesInScopeDetailed, formatRiskLevelChangeNotice, } from "./utils/context.js";
@@ -40,7 +38,7 @@ import { formatAdaptiveNumber, formatCwd, formatTokenWindow } from "./utils/form
40
38
  import { getGitRoot } from "./utils/git.js";
41
39
  import { extractAllFencedCodeBlocks, extractAssistantText } from "./utils/messages.js";
42
40
  import { streamModel } from "./utils/model_stream.js";
43
- import { listProjectFiles, listProjectFilesAsync } from "./utils/project_files.js";
41
+ import { listProjectFilesAsync } from "./utils/project_files.js";
44
42
  import { APP_VERSION } from "./version.js";
45
43
  export class ChatApp {
46
44
  ui;
@@ -49,6 +47,7 @@ export class ChatApp {
49
47
  queuedMessages;
50
48
  editor;
51
49
  uiTheme;
50
+ toolUiRouter;
52
51
  personas;
53
52
  currentPersona;
54
53
  prompts;
@@ -58,10 +57,6 @@ export class ChatApp {
58
57
  initialUserMessage;
59
58
  config;
60
59
  engine;
61
- runningBashComponents = new Map();
62
- runningTaskComponents = new Map();
63
- taskEvents = new Map(); // toolCallId -> accumulated events
64
- subagentCostTotal = 0;
65
60
  isStreaming = false;
66
61
  queuedUserMessages = [];
67
62
  isDrainingQueuedUserMessages = false;
@@ -119,7 +114,8 @@ export class ChatApp {
119
114
  agentsFiles: this.agentsFiles,
120
115
  })
121
116
  : undefined;
122
- this.projectFiles = listProjectFiles(process.cwd());
117
+ this.projectFiles = [];
118
+ this.refreshProjectFilesInBackground();
123
119
  this.currentPersona =
124
120
  (options.initialPersonaId &&
125
121
  this.personas.find((p) => p.id.toLowerCase() === options.initialPersonaId.toLowerCase())) ||
@@ -170,6 +166,12 @@ export class ChatApp {
170
166
  this.footer = new FooterComponent(this.uiTheme, this.ui);
171
167
  this.queuedMessages = new QueuedMessagesComponent(this.uiTheme, this.queuedUserMessages);
172
168
  this.editor = new CustomEditor(this.uiTheme);
169
+ this.toolUiRouter = new ToolUiRouter({
170
+ theme: this.uiTheme,
171
+ chatContainer: this.chatContainer,
172
+ requestRender: () => this.ui.requestRender(),
173
+ onCostUpdated: () => this.updateFooter(),
174
+ });
173
175
  this.setupUI();
174
176
  this.setupEditor();
175
177
  }
@@ -262,6 +264,7 @@ export class ChatApp {
262
264
  void listProjectFilesAsync(process.cwd())
263
265
  .then((files) => {
264
266
  this.projectFiles = files;
267
+ this.ui.requestRender();
265
268
  })
266
269
  .catch(() => {
267
270
  // Ignore refresh errors; autocomplete will keep using the existing cache.
@@ -398,7 +401,7 @@ export class ChatApp {
398
401
  total += m.usage?.cost?.total ?? 0;
399
402
  }
400
403
  }
401
- return `$${formatAdaptiveNumber(total + this.subagentCostTotal, 2, 5)}`;
404
+ return `$${formatAdaptiveNumber(total + this.toolUiRouter.getSubagentCostTotal(), 2, 5)}`;
402
405
  }
403
406
  getTurnDurationString() {
404
407
  const now = Date.now();
@@ -896,10 +899,7 @@ export class ChatApp {
896
899
  }
897
900
  clearSession() {
898
901
  this.engine.reset();
899
- this.runningBashComponents.clear();
900
- this.runningTaskComponents.clear();
901
- this.taskEvents.clear();
902
- this.subagentCostTotal = 0;
902
+ this.toolUiRouter.resetSession();
903
903
  this.expandedFilesInCurrentPrompt.clear();
904
904
  this.expandedSkillsInCurrentPrompt.clear();
905
905
  this.chatContainer.addMessage({ type: "session_divider", label: "new session" });
@@ -1032,10 +1032,7 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1032
1032
  this.previousSessionSummary = previousSessionContext;
1033
1033
  // Reset the session state but preserve history with divider and summary
1034
1034
  this.engine.reset();
1035
- this.runningBashComponents.clear();
1036
- this.runningTaskComponents.clear();
1037
- this.taskEvents.clear();
1038
- this.subagentCostTotal = 0;
1035
+ this.toolUiRouter.resetSession();
1039
1036
  this.expandedFilesInCurrentPrompt.clear();
1040
1037
  this.expandedSkillsInCurrentPrompt.clear();
1041
1038
  this.chatContainer.addMessage({ type: "session_divider", label: "new session" });
@@ -1339,211 +1336,7 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1339
1336
  break;
1340
1337
  }
1341
1338
  case "tool_ui": {
1342
- const uiEvent = event.uiEvent;
1343
- if (uiEvent.type === "bash_started") {
1344
- this.chatContainer.addMessage({
1345
- type: "tool",
1346
- view: buildBashRunningView(this.uiTheme, uiEvent.command),
1347
- }, uiEvent.toolCallId);
1348
- this.runningBashComponents.set(uiEvent.toolCallId, {
1349
- command: uiEvent.command,
1350
- });
1351
- this.ui.requestRender();
1352
- }
1353
- else if (uiEvent.type === "bash_execution") {
1354
- const running = this.runningBashComponents.get(uiEvent.toolCallId);
1355
- this.chatContainer.replaceMessage(uiEvent.toolCallId, {
1356
- type: "tool",
1357
- view: buildBashExecutionView(this.uiTheme, uiEvent.command, uiEvent.exitCode, uiEvent.truncationInfo, uiEvent.durationMs),
1358
- });
1359
- if (running) {
1360
- this.runningBashComponents.delete(uiEvent.toolCallId);
1361
- }
1362
- this.ui.requestRender();
1363
- }
1364
- else if (uiEvent.type === "bash_blocked") {
1365
- if (uiEvent.toolCallId) {
1366
- const running = this.runningBashComponents.get(uiEvent.toolCallId);
1367
- this.chatContainer.replaceMessage(uiEvent.toolCallId, {
1368
- type: "tool",
1369
- view: buildBashBlockedView(this.uiTheme, uiEvent.command, uiEvent.reason),
1370
- });
1371
- if (running) {
1372
- this.runningBashComponents.delete(uiEvent.toolCallId);
1373
- }
1374
- }
1375
- else {
1376
- this.chatContainer.addMessage({
1377
- type: "tool",
1378
- view: buildBashBlockedView(this.uiTheme, uiEvent.command, uiEvent.reason),
1379
- });
1380
- }
1381
- this.ui.requestRender();
1382
- }
1383
- else if (uiEvent.type === "task_started") {
1384
- if (!this.taskEvents.has(uiEvent.toolCallId)) {
1385
- this.taskEvents.set(uiEvent.toolCallId, []);
1386
- }
1387
- const kind = uiEvent.kind ?? "task";
1388
- const subagentName = uiEvent.name.trim() || undefined;
1389
- this.chatContainer.addMessage({
1390
- type: "tool",
1391
- view: buildTaskRunningView(this.uiTheme, uiEvent.title, [], 0, 0, 0, {
1392
- kind,
1393
- subagentName,
1394
- }),
1395
- }, uiEvent.toolCallId);
1396
- this.runningTaskComponents.set(uiEvent.toolCallId, {
1397
- kind,
1398
- name: subagentName,
1399
- title: uiEvent.title,
1400
- costTotal: 0,
1401
- turns: 0,
1402
- toolCalls: 0,
1403
- });
1404
- this.ui.requestRender();
1405
- }
1406
- else if (uiEvent.type === "task_progress") {
1407
- let events = this.taskEvents.get(uiEvent.toolCallId);
1408
- if (!events) {
1409
- events = [];
1410
- this.taskEvents.set(uiEvent.toolCallId, events);
1411
- }
1412
- events.push(uiEvent.event);
1413
- const running = this.runningTaskComponents.get(uiEvent.toolCallId);
1414
- const kind = uiEvent.kind ?? running?.kind ?? "task";
1415
- const subagentName = uiEvent.name.trim() || undefined;
1416
- if (running) {
1417
- running.kind = kind;
1418
- running.name = subagentName;
1419
- running.title = uiEvent.title;
1420
- running.costTotal = uiEvent.costTotal;
1421
- running.turns = uiEvent.turns;
1422
- running.toolCalls = uiEvent.toolCalls;
1423
- }
1424
- this.chatContainer.replaceMessage(uiEvent.toolCallId, {
1425
- type: "tool",
1426
- view: buildTaskRunningView(this.uiTheme, uiEvent.title, events, uiEvent.costTotal, uiEvent.turns, uiEvent.toolCalls, { kind, subagentName }),
1427
- });
1428
- this.ui.requestRender();
1429
- }
1430
- else if (uiEvent.type === "task_finished") {
1431
- const running = this.runningTaskComponents.get(uiEvent.toolCallId);
1432
- const kind = uiEvent.kind ?? running?.kind ?? "task";
1433
- const subagentName = uiEvent.name.trim() || undefined;
1434
- this.chatContainer.replaceMessage(uiEvent.toolCallId, {
1435
- type: "tool",
1436
- view: buildTaskFinishedView(this.uiTheme, uiEvent.title, uiEvent.costTotal, uiEvent.turns, uiEvent.toolCalls, uiEvent.status, uiEvent.finalOutput, { kind, subagentName }),
1437
- });
1438
- this.runningTaskComponents.delete(uiEvent.toolCallId);
1439
- this.taskEvents.delete(uiEvent.toolCallId);
1440
- this.subagentCostTotal += uiEvent.costTotal;
1441
- this.updateFooter();
1442
- this.ui.requestRender();
1443
- }
1444
- else if (uiEvent.type === "task_blocked") {
1445
- const running = this.runningTaskComponents.get(uiEvent.toolCallId);
1446
- const kind = uiEvent.kind ?? running?.kind ?? "task";
1447
- const subagentName = uiEvent.name?.trim() || undefined;
1448
- if (running) {
1449
- this.chatContainer.replaceMessage(uiEvent.toolCallId, {
1450
- type: "tool",
1451
- view: buildTaskBlockedView(this.uiTheme, uiEvent.title, uiEvent.reason, {
1452
- kind,
1453
- subagentName,
1454
- }),
1455
- });
1456
- }
1457
- else {
1458
- this.chatContainer.addMessage({
1459
- type: "tool",
1460
- view: buildTaskBlockedView(this.uiTheme, uiEvent.title, uiEvent.reason, {
1461
- kind,
1462
- subagentName,
1463
- }),
1464
- }, uiEvent.toolCallId);
1465
- }
1466
- this.runningTaskComponents.delete(uiEvent.toolCallId);
1467
- this.taskEvents.delete(uiEvent.toolCallId);
1468
- this.ui.requestRender();
1469
- }
1470
- else if (uiEvent.type === "write_success") {
1471
- this.chatContainer.addMessage({
1472
- type: "tool",
1473
- view: buildWriteSuccessView(this.uiTheme, uiEvent.path, uiEvent.bytes, uiEvent.lines, uiEvent.content),
1474
- });
1475
- this.ui.requestRender();
1476
- }
1477
- else if (uiEvent.type === "write_blocked") {
1478
- this.chatContainer.addMessage({
1479
- type: "tool",
1480
- view: buildWriteBlockedView(this.uiTheme, uiEvent.path, uiEvent.reason),
1481
- });
1482
- this.ui.requestRender();
1483
- }
1484
- else if (uiEvent.type === "edit_success") {
1485
- this.chatContainer.addMessage({
1486
- type: "tool",
1487
- view: buildEditSuccessView(this.uiTheme, uiEvent.path, uiEvent.oldLength, uiEvent.newLength, uiEvent.oldText, uiEvent.newText),
1488
- });
1489
- this.ui.requestRender();
1490
- }
1491
- else if (uiEvent.type === "edit_blocked") {
1492
- this.chatContainer.addMessage({
1493
- type: "tool",
1494
- view: buildEditBlockedView(this.uiTheme, uiEvent.path, uiEvent.reason),
1495
- });
1496
- this.ui.requestRender();
1497
- }
1498
- else if (uiEvent.type === "read_success") {
1499
- this.chatContainer.addMessage({
1500
- type: "tool",
1501
- view: buildReadSuccessView(this.uiTheme, uiEvent.path, uiEvent.startLine, uiEvent.endLine, uiEvent.content, uiEvent.modelTruncation),
1502
- });
1503
- this.ui.requestRender();
1504
- }
1505
- else if (uiEvent.type === "read_blocked") {
1506
- this.chatContainer.addMessage({
1507
- type: "tool",
1508
- view: buildReadBlockedView(this.uiTheme, uiEvent.path, uiEvent.reason),
1509
- });
1510
- this.ui.requestRender();
1511
- }
1512
- else if (uiEvent.type === "list_success") {
1513
- this.chatContainer.addMessage({
1514
- type: "tool",
1515
- view: buildListSuccessView(this.uiTheme, uiEvent.path, uiEvent.offset, uiEvent.limit, uiEvent.total, uiEvent.returned, uiEvent.entries),
1516
- });
1517
- this.ui.requestRender();
1518
- }
1519
- else if (uiEvent.type === "list_blocked") {
1520
- this.chatContainer.addMessage({
1521
- type: "tool",
1522
- view: buildListBlockedView(this.uiTheme, uiEvent.path, uiEvent.reason),
1523
- });
1524
- this.ui.requestRender();
1525
- }
1526
- else if (uiEvent.type === "grep_started") {
1527
- this.chatContainer.addMessage({
1528
- type: "tool",
1529
- view: buildGrepRunningView(this.uiTheme, uiEvent.pattern),
1530
- }, uiEvent.toolCallId);
1531
- this.ui.requestRender();
1532
- }
1533
- else if (uiEvent.type === "grep_finished") {
1534
- this.chatContainer.replaceMessage(uiEvent.toolCallId, {
1535
- type: "tool",
1536
- view: buildGrepFinishedView(this.uiTheme, uiEvent.pattern, uiEvent.status, uiEvent.exitCode, uiEvent.stdout, uiEvent.stderr, uiEvent.captureTruncated),
1537
- });
1538
- this.ui.requestRender();
1539
- }
1540
- else if (uiEvent.type === "grep_blocked") {
1541
- this.chatContainer.addMessage({
1542
- type: "tool",
1543
- view: buildGrepBlockedView(this.uiTheme, uiEvent.pattern, uiEvent.reason),
1544
- }, uiEvent.toolCallId);
1545
- this.ui.requestRender();
1546
- }
1339
+ this.toolUiRouter.handle(event.uiEvent);
1547
1340
  break;
1548
1341
  }
1549
1342
  case "notice": {
@@ -1563,26 +1356,12 @@ Write plain prose, no formatting. Be thorough enough that the reader can resume
1563
1356
  finally {
1564
1357
  const wasAborted = this.currentTurnAbort?.signal.aborted ?? false;
1565
1358
  const reason = wasAborted ? "aborted" : "interrupted";
1566
- for (const [id, running] of this.runningBashComponents.entries()) {
1567
- this.chatContainer.replaceMessage(id, {
1568
- type: "tool",
1569
- view: buildBashAbortedView(this.uiTheme, running.command, reason),
1570
- });
1571
- }
1572
- const taskStatus = wasAborted ? "aborted" : "error";
1573
- for (const [id, running] of this.runningTaskComponents.entries()) {
1574
- this.chatContainer.replaceMessage(id, {
1575
- type: "tool",
1576
- view: buildTaskFinishedView(this.uiTheme, running.title, running.costTotal, running.turns, running.toolCalls, taskStatus, reason, { kind: running.kind, subagentName: running.name }),
1577
- });
1578
- }
1359
+ this.toolUiRouter.finalizePending(reason);
1579
1360
  this.footer.stop();
1580
1361
  this.stopTurnTimer();
1581
1362
  this.isStreaming = false;
1582
1363
  this.currentTurnAbort = undefined;
1583
- this.runningBashComponents.clear();
1584
- this.runningTaskComponents.clear();
1585
- this.taskEvents.clear();
1364
+ this.toolUiRouter.clearTransientState();
1586
1365
  this.pendingIdleNotification = true;
1587
1366
  this.ui.requestRender();
1588
1367
  void this.drainQueuedUserMessages();