@markusylisiurunen/tau 0.2.57 → 0.2.59

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.
@@ -19,7 +19,7 @@ import { TOOL_NAME_BASH, TOOL_NAME_EDIT } from "../core/tools/tool_names.js";
19
19
  import { REASONING_LEVELS, } from "../core/types.js";
20
20
  import { resolveAgentCwd, resolveSandboxPath } from "../core/utils/agent_environment.js";
21
21
  import { findAgentsFilesFromCwdToHome } from "../core/utils/agents_files.js";
22
- import { buildProjectContextBlock, buildSkillsIndexBlock, findAgentsFilesInScopeDetailed, formatCwdChangeNotice, formatRiskLevelChangeNotice, } from "../core/utils/context.js";
22
+ import { buildProjectContextBlock, buildSkillsIndexBlock, findAgentsFilesInScopeDetailed, formatCwdChangeNotice, formatProjectContextChangeNotice, formatRiskLevelChangeNotice, } from "../core/utils/context.js";
23
23
  import { formatAdaptiveNumber, formatCwd, formatTokenWindow } from "../core/utils/format.js";
24
24
  import { getGitRoot } from "../core/utils/git.js";
25
25
  import { buildLineDiff, collapseLongUnchangedDiffRuns } from "../core/utils/line_diff.js";
@@ -99,8 +99,7 @@ export class ChatController {
99
99
  showThinking = false;
100
100
  compactToolUi = true;
101
101
  commandHint;
102
- assistantTurnInterruptRequested = false;
103
- currentTurnAbort;
102
+ activeBusyTask;
104
103
  riskLevel = "read-only";
105
104
  projectContextBlock;
106
105
  projectFiles = [];
@@ -111,6 +110,7 @@ export class ChatController {
111
110
  agentsConfigErrors;
112
111
  pendingRiskLevelChange;
113
112
  pendingCwdChange;
113
+ pendingProjectContextChange;
114
114
  expandedFilesInCurrentPrompt = new Set();
115
115
  expandedSkillsInCurrentPrompt = new Set();
116
116
  assistantState;
@@ -327,7 +327,7 @@ export class ChatController {
327
327
  void this.runSpeakTransition(() => this.stopSpeakCapture());
328
328
  return;
329
329
  }
330
- this.interruptAssistantTurn();
330
+ this.interruptActiveTask();
331
331
  }
332
332
  onEvent(event) {
333
333
  switch (event.type) {
@@ -710,6 +710,7 @@ export class ChatController {
710
710
  }
711
711
  const nextCwd = this.deps.env.cwd();
712
712
  const previousAgentCwd = this.agentCwd;
713
+ const previousProjectContextBlock = this.projectContextBlock;
713
714
  this.agentCwd =
714
715
  this.sandboxEnabled && this.sandboxRootReal
715
716
  ? resolveSandboxPath({
@@ -723,6 +724,7 @@ export class ChatController {
723
724
  sandboxConfig: this.config.sandbox,
724
725
  });
725
726
  this.refreshProjectContext(nextCwd);
727
+ this.updatePendingProjectContextChange(previousProjectContextBlock, this.projectContextBlock);
726
728
  this.projectFiles = [];
727
729
  this.refreshProjectFilesInBackground();
728
730
  this.expandedFilesInCurrentPrompt.clear();
@@ -795,20 +797,47 @@ export class ChatController {
795
797
  readFile: this.deps.fs.readFile,
796
798
  });
797
799
  }
798
- interruptAssistantTurn() {
799
- if (!this.isStreaming)
800
+ updatePendingProjectContextChange(previous, next) {
801
+ if (previous === next) {
800
802
  return;
801
- if (this.runtime.isTurnRunning) {
802
- if (this.assistantTurnInterruptRequested)
803
- return;
804
- this.assistantTurnInterruptRequested = true;
805
- this.runtime.interruptTurn();
806
- this.view.addSystemMessage("interrupted", "error");
803
+ }
804
+ const from = this.pendingProjectContextChange?.from ?? previous;
805
+ if (from === next) {
806
+ this.pendingProjectContextChange = undefined;
807
807
  return;
808
808
  }
809
- if (this.currentTurnAbort?.signal.aborted)
809
+ this.pendingProjectContextChange = { from, to: next };
810
+ }
811
+ createAbortBusyTask() {
812
+ const abortController = new AbortController();
813
+ return {
814
+ signal: abortController.signal,
815
+ busyTask: {
816
+ requestInterrupt: () => {
817
+ if (abortController.signal.aborted) {
818
+ return false;
819
+ }
820
+ abortController.abort();
821
+ return true;
822
+ },
823
+ },
824
+ };
825
+ }
826
+ beginBusyTask(task) {
827
+ this.activeBusyTask = task;
828
+ }
829
+ endBusyTask(task) {
830
+ if (this.activeBusyTask === task) {
831
+ this.activeBusyTask = undefined;
832
+ }
833
+ }
834
+ interruptActiveTask() {
835
+ if (!this.isStreaming)
836
+ return;
837
+ if (!this.activeBusyTask)
838
+ return;
839
+ if (!this.activeBusyTask.requestInterrupt())
810
840
  return;
811
- this.currentTurnAbort?.abort();
812
841
  this.view.addSystemMessage("interrupted", "error");
813
842
  }
814
843
  // Input Handling --------------------------------------------------------------------------------
@@ -1116,8 +1145,14 @@ export class ChatController {
1116
1145
  if (this.pendingCwdChange) {
1117
1146
  notices.push(formatCwdChangeNotice(this.pendingCwdChange));
1118
1147
  }
1148
+ if (this.pendingProjectContextChange) {
1149
+ notices.push(formatProjectContextChangeNotice({
1150
+ projectContextBlock: this.pendingProjectContextChange.to,
1151
+ }));
1152
+ }
1119
1153
  this.pendingRiskLevelChange = undefined;
1120
1154
  this.pendingCwdChange = undefined;
1155
+ this.pendingProjectContextChange = undefined;
1121
1156
  const systemNotice = notices.length > 0 ? notices.join("\n") : undefined;
1122
1157
  const baseTextForModel = opts?.textForModel ?? text;
1123
1158
  const textForModel = systemNotice ? `${systemNotice}\n\n${baseTextForModel}` : baseTextForModel;
@@ -1541,6 +1576,9 @@ export class ChatController {
1541
1576
  this.isBashMode = false;
1542
1577
  this.isBashIncognito = false;
1543
1578
  this.isMemoryMode = false;
1579
+ this.pendingRiskLevelChange = undefined;
1580
+ this.pendingCwdChange = undefined;
1581
+ this.pendingProjectContextChange = undefined;
1544
1582
  try {
1545
1583
  const report = await this.refreshReloadableContent("new-session");
1546
1584
  this.applyReloadMessages(report.messages);
@@ -1603,32 +1641,46 @@ export class ChatController {
1603
1641
  this.view.addSystemMessage("summarizing session...", "success");
1604
1642
  this.isStreaming = true;
1605
1643
  this.view.startWorkingIcon();
1644
+ const { busyTask, signal } = this.createAbortBusyTask();
1645
+ this.beginBusyTask(busyTask);
1606
1646
  try {
1607
1647
  const result = await this.engine.compact({
1608
1648
  mode: "only-summary",
1609
1649
  guidance,
1650
+ signal,
1610
1651
  });
1611
1652
  this.applyCompactedHistoryUi(result.compactionMessage);
1612
1653
  this.view.addSystemMessage("session compacted. previous context has been summarized.", "success");
1613
1654
  }
1614
1655
  catch (err) {
1615
- this.handleCompactionError(err);
1656
+ if (!signal.aborted) {
1657
+ this.handleCompactionError(err);
1658
+ }
1616
1659
  }
1617
1660
  finally {
1661
+ this.endBusyTask(busyTask);
1618
1662
  this.view.stopWorkingIcon();
1619
1663
  this.isStreaming = false;
1620
1664
  this.view.requestRender();
1621
- void this.drainQueuedUserMessages();
1665
+ if (signal.aborted) {
1666
+ this.dequeueQueuedUserMessagesIntoEditor();
1667
+ }
1668
+ else {
1669
+ void this.drainQueuedUserMessages();
1670
+ }
1622
1671
  }
1623
1672
  }
1624
1673
  async compactSessionSummaryAndLast(guidance) {
1625
1674
  this.view.addSystemMessage("summarizing session...", "success");
1626
1675
  this.isStreaming = true;
1627
1676
  this.view.startWorkingIcon();
1677
+ const { busyTask, signal } = this.createAbortBusyTask();
1678
+ this.beginBusyTask(busyTask);
1628
1679
  try {
1629
1680
  const result = await this.engine.compact({
1630
1681
  mode: "with-last-assistant",
1631
1682
  guidance,
1683
+ signal,
1632
1684
  });
1633
1685
  this.applyCompactedHistoryUi(result.compactionMessage);
1634
1686
  const successText = result.includedLastAssistant
@@ -1637,13 +1689,21 @@ export class ChatController {
1637
1689
  this.view.addSystemMessage(successText, "success");
1638
1690
  }
1639
1691
  catch (err) {
1640
- this.handleCompactionError(err);
1692
+ if (!signal.aborted) {
1693
+ this.handleCompactionError(err);
1694
+ }
1641
1695
  }
1642
1696
  finally {
1697
+ this.endBusyTask(busyTask);
1643
1698
  this.view.stopWorkingIcon();
1644
1699
  this.isStreaming = false;
1645
1700
  this.view.requestRender();
1646
- void this.drainQueuedUserMessages();
1701
+ if (signal.aborted) {
1702
+ this.dequeueQueuedUserMessagesIntoEditor();
1703
+ }
1704
+ else {
1705
+ void this.drainQueuedUserMessages();
1706
+ }
1647
1707
  }
1648
1708
  }
1649
1709
  pruneToolResults(strategy, extra) {
@@ -1815,13 +1875,15 @@ export class ChatController {
1815
1875
  this.view.addSystemMessage("sampling prune candidates...", "success");
1816
1876
  this.isStreaming = true;
1817
1877
  this.view.startWorkingIcon();
1878
+ const { busyTask, signal } = this.createAbortBusyTask();
1879
+ this.beginBusyTask(busyTask);
1818
1880
  try {
1819
1881
  const prompt = this.buildSmartPrunePrompt({
1820
1882
  history,
1821
1883
  targetTokens,
1822
1884
  guidance: parsed.guidance,
1823
1885
  });
1824
- const selection = await this.requestSmartPruneSelection(prompt);
1886
+ const selection = await this.requestSmartPruneSelection(prompt, signal);
1825
1887
  if (selection.length === 0) {
1826
1888
  const summary = getEditSummary();
1827
1889
  if (this.hasAnyPrunedEditChanges(summary)) {
@@ -1858,13 +1920,21 @@ export class ChatController {
1858
1920
  this.view.addSystemMessage(this.buildPruneSummaryMessage(summary, toPrune.length, prunedBytes), "success");
1859
1921
  }
1860
1922
  catch (err) {
1861
- this.view.addSystemMessage(`prune failed: ${err.message}`, "error");
1923
+ if (!signal.aborted) {
1924
+ this.view.addSystemMessage(`prune failed: ${err.message}`, "error");
1925
+ }
1862
1926
  }
1863
1927
  finally {
1928
+ this.endBusyTask(busyTask);
1864
1929
  this.view.stopWorkingIcon();
1865
1930
  this.isStreaming = false;
1866
1931
  this.view.requestRender();
1867
- void this.drainQueuedUserMessages();
1932
+ if (signal.aborted) {
1933
+ this.dequeueQueuedUserMessagesIntoEditor();
1934
+ }
1935
+ else {
1936
+ void this.drainQueuedUserMessages();
1937
+ }
1868
1938
  }
1869
1939
  }
1870
1940
  parsePruneFraction(extra) {
@@ -2301,7 +2371,7 @@ export class ChatController {
2301
2371
  }
2302
2372
  return selected;
2303
2373
  }
2304
- async requestSmartPruneSelection(prompt) {
2374
+ async requestSmartPruneSelection(prompt, signal) {
2305
2375
  let apiKey;
2306
2376
  try {
2307
2377
  apiKey = await this.credentialResolver.getApiKey(this.currentPersona.model.provider, { sessionId: this.engine.sessionId });
@@ -2335,6 +2405,7 @@ export class ChatController {
2335
2405
  }, {
2336
2406
  ...(reasoning ? { reasoning } : {}),
2337
2407
  sessionId: `tau-prune-${randomUUID()}`,
2408
+ ...(signal ? { signal } : {}),
2338
2409
  ...(apiKey && { apiKey }),
2339
2410
  });
2340
2411
  const final = await stream.result();
@@ -2655,9 +2726,23 @@ export class ChatController {
2655
2726
  this.view.startWorkingIcon();
2656
2727
  this.startTurnTimer();
2657
2728
  this.assistantState = undefined;
2658
- this.assistantTurnInterruptRequested = false;
2659
2729
  this.startTurnCaffeinate();
2660
2730
  let runResult = { aborted: false };
2731
+ let interruptRequested = false;
2732
+ const busyTask = {
2733
+ requestInterrupt: () => {
2734
+ if (interruptRequested) {
2735
+ return false;
2736
+ }
2737
+ const interrupted = this.runtime.interruptTurn();
2738
+ if (!interrupted) {
2739
+ return false;
2740
+ }
2741
+ interruptRequested = true;
2742
+ return true;
2743
+ },
2744
+ };
2745
+ this.beginBusyTask(busyTask);
2661
2746
  try {
2662
2747
  runResult = await this.runtime.runTurn((event) => this.onEvent(event));
2663
2748
  }
@@ -2666,13 +2751,13 @@ export class ChatController {
2666
2751
  this.view.addSystemMessage(message, "error");
2667
2752
  }
2668
2753
  finally {
2754
+ this.endBusyTask(busyTask);
2669
2755
  await this.stopTurnCaffeinate();
2670
2756
  const reason = runResult.aborted ? "aborted" : "interrupted";
2671
2757
  this.view.finalizeToolUiPending(reason);
2672
2758
  this.view.stopWorkingIcon();
2673
2759
  this.stopTurnTimer();
2674
2760
  this.isStreaming = false;
2675
- this.assistantTurnInterruptRequested = false;
2676
2761
  this.view.clearToolUiTransientState();
2677
2762
  this.pendingIdleNotification = true;
2678
2763
  this.view.requestRender();
@@ -2687,8 +2772,8 @@ export class ChatController {
2687
2772
  // Direct Bash Execution (user ! commands) -------------------------------------------------------
2688
2773
  async runBashCommand(command, opts) {
2689
2774
  this.isStreaming = true;
2690
- const abortController = new AbortController();
2691
- this.currentTurnAbort = abortController;
2775
+ const { busyTask, signal } = this.createAbortBusyTask();
2776
+ this.beginBusyTask(busyTask);
2692
2777
  let wasAborted = false;
2693
2778
  this.startTurnTimer();
2694
2779
  const toolCallId = `bash-user-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
@@ -2705,7 +2790,7 @@ export class ChatController {
2705
2790
  const startedAt = Date.now();
2706
2791
  const { output, exitCode, truncated: captureTruncated, } = await this.toolBackend.runBash(command, {
2707
2792
  cwd: effectiveWorkingDirectory,
2708
- signal: abortController.signal,
2793
+ signal,
2709
2794
  });
2710
2795
  const durationMs = Math.max(0, Date.now() - startedAt);
2711
2796
  const truncationInfo = await prepareBashOutput(output, captureTruncated, getBashOutputPolicy({ mode: "user" }), this.toolBackend);
@@ -2744,11 +2829,9 @@ export class ChatController {
2744
2829
  this.refreshStatus();
2745
2830
  }
2746
2831
  finally {
2747
- wasAborted = abortController.signal.aborted;
2832
+ wasAborted = signal.aborted;
2833
+ this.endBusyTask(busyTask);
2748
2834
  this.isStreaming = false;
2749
- if (this.currentTurnAbort === abortController) {
2750
- this.currentTurnAbort = undefined;
2751
- }
2752
2835
  this.stopTurnTimer();
2753
2836
  this.view.requestRender();
2754
2837
  if (wasAborted) {