pi-ui-extend 0.1.38 → 0.1.41

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 (64) hide show
  1. package/dist/app/app.d.ts +0 -1
  2. package/dist/app/app.js +28 -21
  3. package/dist/app/constants.js +1 -1
  4. package/dist/app/input/input-action-controller.d.ts +1 -0
  5. package/dist/app/input/input-action-controller.js +3 -0
  6. package/dist/app/input/input-controller.d.ts +1 -0
  7. package/dist/app/input/input-controller.js +40 -12
  8. package/dist/app/model/model-usage-status.js +4 -2
  9. package/dist/app/process.js +11 -0
  10. package/dist/app/rendering/conversation-tool-renderer.js +4 -6
  11. package/dist/app/session/request-history.js +2 -0
  12. package/dist/app/session/session-event-controller.d.ts +13 -0
  13. package/dist/app/session/session-event-controller.js +27 -0
  14. package/dist/app/session/tabs-controller.d.ts +8 -0
  15. package/dist/app/session/tabs-controller.js +37 -6
  16. package/dist/app/workspace/workspace-actions-controller.d.ts +1 -0
  17. package/dist/app/workspace/workspace-actions-controller.js +2 -1
  18. package/dist/bundled-extensions/terminal-bell/index.js +55 -1
  19. package/dist/config.js +1 -1
  20. package/dist/default-pix-config.js +1 -1
  21. package/dist/markdown-format.js +14 -25
  22. package/dist/terminal-width.d.ts +14 -0
  23. package/dist/terminal-width.js +31 -2
  24. package/dist/theme.js +2 -2
  25. package/external/pi-tools-suite/README.md +34 -9
  26. package/external/pi-tools-suite/package.json +3 -3
  27. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +35 -21
  28. package/external/pi-tools-suite/src/async-subagents/commands.ts +1 -1
  29. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +2 -2
  30. package/external/pi-tools-suite/src/async-subagents/core/config.ts +70 -12
  31. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +1 -1
  32. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +1 -1
  33. package/external/pi-tools-suite/src/async-subagents/core/types.ts +1 -1
  34. package/external/pi-tools-suite/src/async-subagents/index.ts +6 -6
  35. package/external/pi-tools-suite/src/async-subagents/lib.ts +1 -1
  36. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +4 -2
  37. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +2 -2
  38. package/external/pi-tools-suite/src/{glm-coding-discipline → coding-discipline}/index.ts +17 -8
  39. package/external/pi-tools-suite/src/config.ts +1 -1
  40. package/external/pi-tools-suite/src/dcp/auto-compress.ts +368 -0
  41. package/external/pi-tools-suite/src/dcp/compress-tool.ts +3 -0
  42. package/external/pi-tools-suite/src/dcp/config.ts +23 -0
  43. package/external/pi-tools-suite/src/dcp/index.ts +112 -7
  44. package/external/pi-tools-suite/src/dcp/prompts.ts +8 -0
  45. package/external/pi-tools-suite/src/dcp/state.ts +41 -0
  46. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +30 -22
  47. package/external/pi-tools-suite/src/index.ts +2 -1
  48. package/external/pi-tools-suite/src/session-name/index.ts +37 -0
  49. package/external/pi-tools-suite/src/tool-descriptions.ts +16 -4
  50. package/package.json +4 -4
  51. package/skills/skill-creator/SKILL.md +36 -40
  52. package/skills/skill-creator/eval-viewer/viewer.html +2 -2
  53. package/skills/skill-creator/references/schemas.md +1 -1
  54. package/skills/skill-creator/scripts/__pycache__/__init__.cpython-314.pyc +0 -0
  55. package/skills/skill-creator/scripts/__pycache__/aggregate_benchmark.cpython-314.pyc +0 -0
  56. package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-314.pyc +0 -0
  57. package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-314.pyc +0 -0
  58. package/skills/skill-creator/scripts/__pycache__/package_skill.cpython-314.pyc +0 -0
  59. package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-314.pyc +0 -0
  60. package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-314.pyc +0 -0
  61. package/skills/skill-creator/scripts/__pycache__/utils.cpython-314.pyc +0 -0
  62. package/skills/skill-creator/scripts/generate_report.py +1 -1
  63. package/skills/skill-creator/scripts/improve_description.py +14 -24
  64. package/skills/skill-creator/scripts/run_eval.py +89 -82
package/dist/app/app.d.ts CHANGED
@@ -94,7 +94,6 @@ export declare class PiUiExtendApp {
94
94
  private loadSessionHistory;
95
95
  private openSearchResultInNewTab;
96
96
  private scrollToUserMessageJumpTarget;
97
- private findUserEntryBySessionEntryId;
98
97
  private findUserEntryByJumpText;
99
98
  private loadSessionHistoryAsync;
100
99
  private handleSessionEvent;
package/dist/app/app.js CHANGED
@@ -360,6 +360,7 @@ export class PiUiExtendApp {
360
360
  });
361
361
  this.workspaceActions = new AppWorkspaceActionsController({
362
362
  entries: this.entries,
363
+ allEntries: () => this.sessionEvents.allEntries(),
363
364
  runtime: () => this.runtime,
364
365
  awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
365
366
  findUserEntry: (entryId) => this.findUserEntry(entryId),
@@ -665,6 +666,12 @@ export class PiUiExtendApp {
665
666
  setSessionActivity: (activity) => this.setSessionActivity(activity),
666
667
  addEntry: (entry) => this.addEntry(entry),
667
668
  addSessionAbortedEntry: () => this.sessionEvents.addSessionAbortedEntry(),
669
+ emitSessionAborted: () => {
670
+ const runtime = this.runtime;
671
+ if (!runtime)
672
+ return;
673
+ this.extensionEventBusByRuntime.get(runtime)?.emit("pix:session-aborted", { aborted: true });
674
+ },
668
675
  showToast: (message, kind) => this.showToast(message, kind),
669
676
  dismissActiveDialog: () => this.toastController.dismissActiveDialog(),
670
677
  stopVoiceInput: () => this.voiceController.stopRecording(),
@@ -999,39 +1006,39 @@ export class PiUiExtendApp {
999
1006
  return true;
1000
1007
  this.workspaceActions.syncUserSessionEntryMetadata();
1001
1008
  if (target.sessionEntryId) {
1002
- let entry = this.findUserEntryBySessionEntryId(target.sessionEntryId);
1003
- while (!entry && this.sessionEvents.hasOlderSessionHistory() && !this.sessionEvents.isLoadingOlderSessionHistory()) {
1004
- const loaded = await this.sessionEvents.loadOlderSessionHistory({ render: false });
1005
- if (!loaded)
1006
- break;
1007
- entry = this.findUserEntryBySessionEntryId(target.sessionEntryId);
1008
- }
1009
- if (entry && this.scrollController.scrollToConversationEntry(entry.id))
1009
+ const entryId = this.sessionEvents.revealHistoryEntryForSessionEntryId(target.sessionEntryId);
1010
+ if (entryId && this.scrollController.scrollToConversationEntry(entryId))
1010
1011
  return true;
1011
1012
  }
1012
1013
  const fallbackEntry = this.findUserEntryByJumpText(target);
1013
1014
  return fallbackEntry ? this.scrollController.scrollToConversationEntry(fallbackEntry.id) : false;
1014
1015
  }
1015
- findUserEntryBySessionEntryId(sessionEntryId) {
1016
- return this.entries.find((entry) => entry.kind === "user" && entry.sessionEntryId === sessionEntryId);
1017
- }
1018
1016
  findUserEntryByJumpText(target) {
1019
1017
  if (!target.text)
1020
1018
  return undefined;
1021
- const userEntries = this.entries.filter((entry) => entry.kind === "user");
1019
+ const userEntries = this.sessionEvents.allEntries().filter((entry) => entry.kind === "user");
1020
+ let matched;
1022
1021
  if (target.userIndex !== undefined && target.userCount !== undefined) {
1023
- const visibleIndex = target.userIndex - (target.userCount - userEntries.length);
1024
- const entry = userEntries[visibleIndex];
1022
+ const entry = userEntries[target.userIndex];
1025
1023
  if (entry && normalizeJumpTargetText(entry.text) === normalizeJumpTargetText(target.text))
1026
- return entry;
1024
+ matched = entry;
1025
+ }
1026
+ if (!matched) {
1027
+ const normalizedTargetText = normalizeJumpTargetText(target.text);
1028
+ for (let index = userEntries.length - 1; index >= 0; index -= 1) {
1029
+ const entry = userEntries[index];
1030
+ if (entry && normalizeJumpTargetText(entry.text) === normalizedTargetText) {
1031
+ matched = entry;
1032
+ break;
1033
+ }
1034
+ }
1027
1035
  }
1028
- const normalizedTargetText = normalizeJumpTargetText(target.text);
1029
- for (let index = userEntries.length - 1; index >= 0; index -= 1) {
1030
- const entry = userEntries[index];
1031
- if (entry && normalizeJumpTargetText(entry.text) === normalizedTargetText)
1032
- return entry;
1036
+ if (matched?.sessionEntryId) {
1037
+ // Shift the sliding window onto the matched entry so it is present in the
1038
+ // viewport for the subsequent scrollToConversationEntry call.
1039
+ this.sessionEvents.revealHistoryEntryForSessionEntryId(matched.sessionEntryId);
1033
1040
  }
1034
- return undefined;
1041
+ return matched;
1035
1042
  }
1036
1043
  async loadSessionHistoryAsync(options) {
1037
1044
  return this.sessionEvents.loadSessionHistoryAsync(options);
@@ -78,7 +78,7 @@ export const SUBAGENTS_WIDGET_MAX_ROWS = 8;
78
78
  export const DEFAULT_THINKING_TOOL_RULE = {
79
79
  previewLines: 0,
80
80
  direction: "head",
81
- color: "thinkingForeground",
81
+ color: "assistantForeground",
82
82
  };
83
83
  export const TERMINAL_COMMAND_MODIFIER_FLAG = 8;
84
84
  export const GIT_BRANCH_CACHE_MS = 30_000;
@@ -18,6 +18,7 @@ export type AppInputActionControllerHost = {
18
18
  setSessionActivity(activity: SessionActivity): void;
19
19
  addEntry(entry: Entry): void;
20
20
  addSessionAbortedEntry(): void;
21
+ emitSessionAborted(): void;
21
22
  showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
22
23
  dismissActiveDialog?(): boolean;
23
24
  stopVoiceInput(): Promise<void>;
@@ -88,6 +88,9 @@ export class AppInputActionController {
88
88
  }
89
89
  async abortStreamingSession(runtime, options) {
90
90
  const session = runtime.session;
91
+ // Relay the user-initiated abort to extensions (e.g. the terminal-bell
92
+ // extension) so they can suppress the attention bell for this turn.
93
+ this.host.emitSessionAborted();
91
94
  if (this.abortInFlight) {
92
95
  session.agent.abort();
93
96
  if (options.stopIfAlreadyAborting)
@@ -35,6 +35,7 @@ export declare class AppInputController {
35
35
  private readonly pasteHandler;
36
36
  constructor(host: InputControllerHost);
37
37
  handleChunk(chunk: Buffer): void;
38
+ private consumeBufferedSharedEditorInput;
38
39
  private consumeSharedEditorInput;
39
40
  private drainInputBuffer;
40
41
  private consumeBracketedPastePayload;
@@ -12,13 +12,23 @@ export class AppInputController {
12
12
  }
13
13
  handleChunk(chunk) {
14
14
  let data = chunk.toString("utf8");
15
+ const bufferedSharedEditorInput = this.consumeBufferedSharedEditorInput(data);
16
+ if (bufferedSharedEditorInput.kind === "consumed" || bufferedSharedEditorInput.kind === "pending")
17
+ return;
18
+ if (bufferedSharedEditorInput.kind === "passthrough")
19
+ data = bufferedSharedEditorInput.data;
15
20
  if (this.inputBuffer.startsWith("\x1b[<") || data.startsWith("\x1b[<")) {
16
21
  this.inputBuffer += data;
17
22
  this.drainInputBuffer();
18
23
  return;
19
24
  }
20
- if (this.consumeSharedEditorInput(data))
25
+ const sharedEditorInput = this.consumeSharedEditorInput(data);
26
+ if (sharedEditorInput === "consumed")
27
+ return;
28
+ if (sharedEditorInput === "pending") {
29
+ this.inputBuffer = data;
21
30
  return;
31
+ }
22
32
  const extensionInput = this.host.handleExtensionTerminalInput(data);
23
33
  if (extensionInput.consume)
24
34
  return;
@@ -29,41 +39,59 @@ export class AppInputController {
29
39
  this.inputBuffer += data;
30
40
  this.drainInputBuffer();
31
41
  }
42
+ consumeBufferedSharedEditorInput(data) {
43
+ if (this.host.extensionInputUsesEditor?.() !== true)
44
+ return { kind: "none" };
45
+ if (this.inputBuffer.length === 0)
46
+ return { kind: "none" };
47
+ const buffered = `${this.inputBuffer}${data}`;
48
+ const result = this.consumeSharedEditorInput(buffered);
49
+ if (result === "pending") {
50
+ this.inputBuffer = buffered;
51
+ return { kind: "pending" };
52
+ }
53
+ this.inputBuffer = "";
54
+ if (result === "consumed")
55
+ return { kind: "consumed" };
56
+ return { kind: "passthrough", data: buffered };
57
+ }
32
58
  consumeSharedEditorInput(data) {
33
59
  if (this.host.extensionInputUsesEditor?.() !== true)
34
- return false;
60
+ return "none";
35
61
  if (this.host.inputEditor.isInBracketedPaste)
36
- return false;
62
+ return "none";
37
63
  if (data === "\n") {
38
64
  this.insertInputNewline();
39
- return true;
65
+ return "consumed";
40
66
  }
41
67
  if (data === "\r" && this.isShiftPressed()) {
42
68
  this.insertInputNewline();
43
- return true;
69
+ return "consumed";
44
70
  }
45
71
  if (SHIFT_ENTER_ESCAPE_SEQUENCES.includes(data)) {
46
72
  this.insertInputNewline();
47
- return true;
73
+ return "consumed";
48
74
  }
49
75
  if (data === "\x16") {
50
76
  void this.pasteHandler.handleClipboardImagePaste();
51
- return true;
77
+ return "consumed";
52
78
  }
53
79
  const modifiedKey = parseTerminalModifiedKeySequence(data);
80
+ if (modifiedKey.kind === "pending")
81
+ return "pending";
54
82
  if (modifiedKey.kind !== "key")
55
- return false;
83
+ return "none";
56
84
  if (terminalKeyShouldIgnore(modifiedKey.key))
57
- return true;
85
+ return "consumed";
58
86
  if (terminalKeyIsShiftEnter(modifiedKey.key)) {
59
87
  this.insertInputNewline();
60
- return true;
88
+ return "consumed";
61
89
  }
62
90
  if (terminalKeyIsClipboardImagePaste(modifiedKey.key)) {
63
91
  void this.pasteHandler.handleClipboardImagePaste();
64
- return true;
92
+ return "consumed";
65
93
  }
66
- return false;
94
+ return "none";
67
95
  }
68
96
  drainInputBuffer() {
69
97
  while (this.inputBuffer.length > 0) {
@@ -13,6 +13,8 @@ const GOOGLE_ANTIGRAVITY_USER_AGENT = "antigravity/1.11.9 windows/amd64";
13
13
  const REQUEST_TIMEOUT_MS = 10_000;
14
14
  const DAY_SECONDS = 86_400;
15
15
  const HOUR_SECONDS = 3_600;
16
+ const MODEL_USAGE_WARNING_MIN_USED_PERCENT = 5;
17
+ const MODEL_USAGE_WARNING_MIN_ELAPSED_SECONDS = 6 * HOUR_SECONDS;
16
18
  const PI_AUTH_PATH = join(homedir(), ".pi", "agent", "auth.json");
17
19
  const DEFAULT_ANTIGRAVITY_PROJECT_ID = "rising-fact-p41fc";
18
20
  function getPiAuthPath() {
@@ -929,11 +931,11 @@ function modelUsageWindowWillExhaustBeforeReset(window, now) {
929
931
  return false;
930
932
  const timeUntilResetSeconds = Math.max(0, (window.resetAt - now) / 1000);
931
933
  const elapsedSeconds = Math.max(0, window.windowSeconds - timeUntilResetSeconds);
932
- if (elapsedSeconds <= 0)
934
+ if (elapsedSeconds < MODEL_USAGE_WARNING_MIN_ELAPSED_SECONDS)
933
935
  return false;
934
936
  const total = 100;
935
937
  const used = total - window.remainingPercent;
936
- if (used <= 0)
938
+ if (used < MODEL_USAGE_WARNING_MIN_USED_PERCENT)
937
939
  return false;
938
940
  const remaining = total - used;
939
941
  const averageRate = used / elapsedSeconds;
@@ -37,6 +37,17 @@ export async function runProcess(command, args = [], options = {}) {
37
37
  child.once("error", (err) => {
38
38
  error = err;
39
39
  });
40
+ // Writing to stdin after the child has closed it raises EPIPE. This is
41
+ // common with clipboard helpers (xclip/xsel/wl-copy) that exit once they
42
+ // have read enough, or when a candidate command exits early. The child's
43
+ // exit status is still captured by the "close" handler, so treat EPIPE as
44
+ // benign and never let it surface as an unhandled "error" event.
45
+ child.stdin?.once("error", (err) => {
46
+ if (err?.code === "EPIPE")
47
+ return;
48
+ if (error === undefined)
49
+ error = err;
50
+ });
40
51
  child.once("close", (status, signal) => {
41
52
  if (timer)
42
53
  clearTimeout(timer);
@@ -7,7 +7,6 @@ import { formatStructuredText } from "./message-content.js";
7
7
  import { formatSubagentTimestamp, isSubagentRunRenderDetails, isSubagentsToolName, subagentRunName, subagentStatusIcon, taskPreviewMap, } from "../subagents/subagents-model.js";
8
8
  import { formatTodoTaskLine, isTodoDetails, visibleTodoTasks } from "../todo/todo-model.js";
9
9
  import { renderToolBlock } from "./tool-block-renderer.js";
10
- import { thinkingLevelThemeColor } from "./status-line-renderer.js";
11
10
  export function renderConversationToolEntry(entry, width, options) {
12
11
  const todoLines = renderTodoToolEntry(entry, width, options);
13
12
  if (todoLines)
@@ -53,14 +52,14 @@ export function renderThinkingEntry(entry, width, options) {
53
52
  const forceExpanded = Boolean(options.allThinkingExpanded);
54
53
  const compactExpandedText = options.superCompactTools && forceExpanded ? removeBlankLines(expandedText) : expandedText;
55
54
  const expanded = forceExpanded || (entry.expanded && expandedText.trim().length > 0);
56
- const headerColorOverride = entry.level
57
- ? thinkingLevelThemeColor(entry.level, options.colors, options.availableThinkingLevels)
58
- : undefined;
59
55
  const elapsed = thinkingElapsedText(entry, options.currentTimeMs ?? Date.now());
56
+ const headerArgs = [entry.level ? `(${entry.level})` : undefined, elapsed]
57
+ .filter((part) => part !== undefined)
58
+ .join(" ");
60
59
  return renderToolBlock({
61
60
  id: entry.id,
62
61
  toolName: THINKING_TOOL_NAME,
63
- ...(elapsed === undefined ? {} : { headerArgs: elapsed }),
62
+ ...(headerArgs === "" ? {} : { headerArgs }),
64
63
  expanded,
65
64
  status: entry.status,
66
65
  isError: false,
@@ -73,7 +72,6 @@ export function renderThinkingEntry(entry, width, options) {
73
72
  superCompact: Boolean(options.superCompactTools && !forceExpanded),
74
73
  backgroundOverride: options.colors.thinkingMessageBackground,
75
74
  showGutter: true,
76
- ...(headerColorOverride === undefined ? {} : { headerColorOverride }),
77
75
  });
78
76
  }
79
77
  function thinkingElapsedText(entry, currentTimeMs) {
@@ -61,6 +61,8 @@ export class AppRequestHistory {
61
61
  if (this.cursor === undefined) {
62
62
  if (direction > 0)
63
63
  return false;
64
+ if (this.host.getInput().length > 0)
65
+ return false;
64
66
  this.draft = this.host.getInput();
65
67
  this.cursor = this.entries.length - 1;
66
68
  const entry = this.entries[this.cursor];
@@ -90,6 +90,7 @@ export declare class AppSessionEventController {
90
90
  snapshotState(): AppSessionEventControllerState;
91
91
  restoreState(state: AppSessionEventControllerState): void;
92
92
  reset(): void;
93
+ allEntries(): readonly Entry[];
93
94
  loadSessionHistory(): void;
94
95
  loadSessionHistoryAsync(options: {
95
96
  isCancelled: () => boolean;
@@ -99,6 +100,18 @@ export declare class AppSessionEventController {
99
100
  hasOlderSessionHistory(): boolean;
100
101
  isLoadingOlderSessionHistory(): boolean;
101
102
  loadOlderSessionHistory(options?: LoadOlderSessionHistoryOptions): Promise<boolean>;
103
+ /**
104
+ * Reveal the conversation entry for a session entry id (e.g. a user message
105
+ * selected from the jump menu) by shifting the sliding window directly onto
106
+ * it, then return its local entry id so the caller can scroll to it.
107
+ *
108
+ * Unlike paging older history in fixed increments, this works for any branch
109
+ * position in a single synchronous step and does not race with concurrent
110
+ * window re-anchors (e.g. while the agent is streaming). Returns undefined
111
+ * when the entry is neither in the full history window nor in the live
112
+ * entries (e.g. older than the loaded branch).
113
+ */
114
+ revealHistoryEntryForSessionEntryId(sessionEntryId: string): string | undefined;
102
115
  hasNewerSessionHistory(): boolean;
103
116
  isLoadingNewerSessionHistory(): boolean;
104
117
  loadNewerSessionHistory(options?: {
@@ -109,6 +109,9 @@ export class AppSessionEventController {
109
109
  this.assistantTextBuffer = "";
110
110
  this.olderHistoryLoader = undefined;
111
111
  }
112
+ allEntries() {
113
+ return this.historyEntries.length > 0 ? this.historyEntries : this.host.entries;
114
+ }
112
115
  loadSessionHistory() {
113
116
  const runtime = this.host.runtime();
114
117
  if (!runtime)
@@ -165,6 +168,30 @@ export class AppSessionEventController {
165
168
  return this.shiftHistoryWindow(-HISTORY_WINDOW_SHIFT_ENTRIES, options);
166
169
  return this.olderHistoryLoader?.loadOlder(options) ?? false;
167
170
  }
171
+ /**
172
+ * Reveal the conversation entry for a session entry id (e.g. a user message
173
+ * selected from the jump menu) by shifting the sliding window directly onto
174
+ * it, then return its local entry id so the caller can scroll to it.
175
+ *
176
+ * Unlike paging older history in fixed increments, this works for any branch
177
+ * position in a single synchronous step and does not race with concurrent
178
+ * window re-anchors (e.g. while the agent is streaming). Returns undefined
179
+ * when the entry is neither in the full history window nor in the live
180
+ * entries (e.g. older than the loaded branch).
181
+ */
182
+ revealHistoryEntryForSessionEntryId(sessionEntryId) {
183
+ if (this.historyEntries.length > 0) {
184
+ const index = this.historyEntries.findIndex((entry) => entry.kind === "user" && entry.sessionEntryId === sessionEntryId);
185
+ if (index === -1)
186
+ return undefined;
187
+ const targetStart = Math.max(0, Math.min(this.maxHistoryWindowStart(), index - Math.floor(this.historyWindowSize() / 2)));
188
+ if (targetStart !== this.historyWindowStart)
189
+ this.setHistoryWindowStart(targetStart);
190
+ return this.historyEntries[index]?.id;
191
+ }
192
+ const entry = this.host.entries.find((entry) => entry.kind === "user" && entry.sessionEntryId === sessionEntryId);
193
+ return entry?.id;
194
+ }
168
195
  hasNewerSessionHistory() {
169
196
  return this.historyEntries.length > 0 && this.historyWindowStart < this.maxHistoryWindowStart();
170
197
  }
@@ -155,6 +155,14 @@ export declare class AppTabsController {
155
155
  private scheduleTabPrewarm;
156
156
  private prewarmTabs;
157
157
  private cleanupOldProjectSessions;
158
+ /**
159
+ * Unlink a project session file and, best-effort, its DCP sidecar state.
160
+ * The sidecar path is derived from the session id in the first line of the
161
+ * `.jsonl` (mirrors the DCP module's `safeSessionFileName`), so retention
162
+ * never leaves orphan sidecars behind. Everything here is best-effort:
163
+ * session retention must never interrupt the terminal UI.
164
+ */
165
+ private unlinkSessionAndDcpSidecar;
158
166
  private preservedSessionPaths;
159
167
  private maxProjectSessions;
160
168
  }
@@ -1543,12 +1543,7 @@ export class AppTabsController {
1543
1543
  for (const session of sessions) {
1544
1544
  if (keep.has(session.path))
1545
1545
  continue;
1546
- try {
1547
- await unlink(session.path);
1548
- }
1549
- catch {
1550
- // Session retention must never interrupt the terminal UI.
1551
- }
1546
+ await this.unlinkSessionAndDcpSidecar(session.path);
1552
1547
  }
1553
1548
  }
1554
1549
  catch {
@@ -1558,6 +1553,42 @@ export class AppTabsController {
1558
1553
  this.retentionCleanupRunning = false;
1559
1554
  }
1560
1555
  }
1556
+ /**
1557
+ * Unlink a project session file and, best-effort, its DCP sidecar state.
1558
+ * The sidecar path is derived from the session id in the first line of the
1559
+ * `.jsonl` (mirrors the DCP module's `safeSessionFileName`), so retention
1560
+ * never leaves orphan sidecars behind. Everything here is best-effort:
1561
+ * session retention must never interrupt the terminal UI.
1562
+ */
1563
+ async unlinkSessionAndDcpSidecar(sessionPath) {
1564
+ let sidecarPath;
1565
+ try {
1566
+ const firstLine = (await readFile(sessionPath, "utf8")).split("\n", 1)[0]?.trim();
1567
+ if (firstLine) {
1568
+ const parsed = JSON.parse(firstLine);
1569
+ if (parsed.type === "session" && typeof parsed.id === "string" && parsed.id) {
1570
+ sidecarPath = join(dirname(sessionPath), "dcp-state", parsed.id.replace(/[^a-zA-Z0-9._-]/g, "_") + ".json");
1571
+ }
1572
+ }
1573
+ }
1574
+ catch {
1575
+ // Reading the session id is best-effort; proceed to unlink the file.
1576
+ }
1577
+ try {
1578
+ await unlink(sessionPath);
1579
+ }
1580
+ catch {
1581
+ // Session retention must never interrupt the terminal UI.
1582
+ }
1583
+ if (sidecarPath) {
1584
+ try {
1585
+ await unlink(sidecarPath);
1586
+ }
1587
+ catch {
1588
+ // Sidecar removal is best-effort; never interrupt the terminal UI.
1589
+ }
1590
+ }
1591
+ }
1561
1592
  preservedSessionPaths() {
1562
1593
  const preserved = new Set();
1563
1594
  const add = (sessionPath) => {
@@ -3,6 +3,7 @@ import type { Entry } from "../types.js";
3
3
  import { type WorkspaceMutation, type WorkspaceMutationFromToolInput, type WorkspaceMutationPreparation } from "./workspace-undo.js";
4
4
  export type AppWorkspaceActionsControllerHost = {
5
5
  readonly entries: Entry[];
6
+ allEntries?(): readonly Entry[];
6
7
  runtime(): AgentSessionRuntime | undefined;
7
8
  awaitCurrentSessionExtensions(runtime?: AgentSessionRuntime): Promise<void>;
8
9
  findUserEntry(entryId: string): Extract<Entry, {
@@ -48,8 +48,9 @@ export class AppWorkspaceActionsController {
48
48
  });
49
49
  if (branchUserEntries.length === 0)
50
50
  return;
51
+ const loadedEntries = this.host.allEntries?.() ?? this.host.entries;
51
52
  let branchIndex = 0;
52
- for (const entry of this.host.entries) {
53
+ for (const entry of loadedEntries) {
53
54
  if (entry.kind !== "user")
54
55
  continue;
55
56
  const sessionEntry = branchUserEntries[branchIndex];
@@ -15,7 +15,14 @@ const TERMINAL_BELL_ATTENTION_EVENT = "pix:terminal-bell:attention";
15
15
  * extensions, so the renderer emits this on the extension event bus.
16
16
  */
17
17
  const RETRY_ACTIVE_EVENT = "pix:retry-active";
18
- const DEFAULT_COMPLETION_NOTIFICATION_TITLE = "Pix - completion";
18
+ /**
19
+ * Renderer-relayed signal that the user interrupted the session (Esc/Ctrl-C).
20
+ * Payload: `{ aborted: boolean }`. Aborting the SDK stream during tool
21
+ * execution does not always produce an aborted `message_update`, so the
22
+ * renderer relays the abort here to reliably suppress the attention bell.
23
+ */
24
+ const SESSION_ABORTED_EVENT = "pix:session-aborted";
25
+ const DEFAULT_COMPLETION_NOTIFICATION_TITLE = "Pix - complete";
19
26
  const DEFAULT_ERROR_NOTIFICATION_TITLE = "Pix - error";
20
27
  const DEFAULT_QUESTION_NOTIFICATION_TITLE = "Pix - question";
21
28
  const DEFAULT_NOTIFICATION_MESSAGE = "{sessionName}";
@@ -220,9 +227,16 @@ function notificationTitleTemplate(defaultTitle) {
220
227
  function willRetryAfterAgentEnd(event) {
221
228
  return event.willRetry === true;
222
229
  }
230
+ function isAbortedMessageUpdate(event) {
231
+ return event.type === "error" && event.reason === "aborted";
232
+ }
223
233
  function failureReasonFromMessageUpdate(event) {
224
234
  if (event.type !== "error")
225
235
  return undefined;
236
+ // The SDK reports a user-initiated interrupt as `{ type: "error", reason: "aborted" }`.
237
+ // That is not a failure the bell should announce, so treat it as "no reason".
238
+ if (event.reason === "aborted")
239
+ return undefined;
226
240
  const reason = event.error?.errorMessage;
227
241
  return typeof reason === "string" ? trimmed(reason) : undefined;
228
242
  }
@@ -387,6 +401,10 @@ export default function terminalBell(pi) {
387
401
  let deferredUntilSubagentsFinish = false;
388
402
  let liveSubagentCount = 0;
389
403
  let lastFailureReason;
404
+ // True when the user interrupted the session this turn (Esc/Ctrl-C). The
405
+ // attention bell should never ring for a user-initiated abort, so this flag
406
+ // suppresses any queued/pending bell until the next agent_start resets it.
407
+ let userAborted = false;
390
408
  // True while the session is in an auto-retry cycle (relayed via the
391
409
  // extension event bus). Suppresses the failure bell on intermediate retry
392
410
  // attempts; the final exhausted failure still rings because no retry-start
@@ -426,6 +444,13 @@ export default function terminalBell(pi) {
426
444
  // queued this bell and the timer firing, suppress the bell entirely.
427
445
  if (retryActive)
428
446
  return;
447
+ // Safety net: if the user aborted after the bell was queued (e.g. an
448
+ // aborted agent_end with no aborted message_update), suppress it.
449
+ if (userAborted) {
450
+ pendingBell = undefined;
451
+ deferredUntilSubagentsFinish = false;
452
+ return;
453
+ }
429
454
  try {
430
455
  if (!ctx.isIdle()) {
431
456
  if (attempt < MAX_IDLE_RETRIES)
@@ -514,15 +539,39 @@ export default function terminalBell(pi) {
514
539
  deferredUntilSubagentsFinish = false;
515
540
  }
516
541
  });
542
+ pi.events.on(SESSION_ABORTED_EVENT, (data) => {
543
+ const aborted = data != null && typeof data === "object" && data.aborted === true;
544
+ if (!aborted)
545
+ return;
546
+ // The user interrupted the session. Aborting during tool execution does
547
+ // not always produce an aborted `message_update`, so the renderer relays
548
+ // the interrupt here. Suppress any pending bell until the next agent_start.
549
+ userAborted = true;
550
+ lastFailureReason = undefined;
551
+ clearTimer();
552
+ pendingBell = undefined;
553
+ deferredUntilSubagentsFinish = false;
554
+ });
517
555
  pi.on("agent_start", async () => {
518
556
  clearTimer();
519
557
  deferredUntilSubagentsFinish = false;
520
558
  lastFailureReason = undefined;
559
+ userAborted = false;
521
560
  retryActive = false;
522
561
  activeSubagentWaitToolCallIds.clear();
523
562
  notifiedAskUserToolCallIds.clear();
524
563
  });
525
564
  pi.on("message_update", async (event) => {
565
+ if (isAbortedMessageUpdate(event.assistantMessageEvent)) {
566
+ // The user interrupted the stream. Suppress any pending bell until
567
+ // the next agent_start.
568
+ userAborted = true;
569
+ lastFailureReason = undefined;
570
+ clearTimer();
571
+ pendingBell = undefined;
572
+ deferredUntilSubagentsFinish = false;
573
+ return;
574
+ }
526
575
  const reason = failureReasonFromMessageUpdate(event.assistantMessageEvent);
527
576
  if (reason) {
528
577
  lastFailureReason = reason;
@@ -554,6 +603,10 @@ export default function terminalBell(pi) {
554
603
  clearTimer();
555
604
  return;
556
605
  }
606
+ if (userAborted) {
607
+ clearTimer();
608
+ return;
609
+ }
557
610
  if (lastFailureReason) {
558
611
  scheduleBell(ctx, idleDelayMs, 0, renderNotificationTemplate(retryFailureMessageTemplate(), {
559
612
  ...buildNotificationTemplateValues(ctx, pi),
@@ -569,6 +622,7 @@ export default function terminalBell(pi) {
569
622
  deferredUntilSubagentsFinish = false;
570
623
  liveSubagentCount = 0;
571
624
  lastFailureReason = undefined;
625
+ userAborted = false;
572
626
  retryActive = false;
573
627
  activeSubagentWaitToolCallIds.clear();
574
628
  notifiedAskUserToolCallIds.clear();
package/dist/config.js CHANGED
@@ -19,7 +19,7 @@ const DEFAULT_TOOL_RENDERER = {
19
19
  color: "toolTitle",
20
20
  },
21
21
  tools: {
22
- thinking: { previewLines: 0, direction: "head", color: "thinkingForeground" },
22
+ thinking: { previewLines: 0, direction: "head", color: "assistantForeground" },
23
23
  bash: { previewLines: 6, direction: "tail", color: "warning" },
24
24
  Bash: { previewLines: 6, direction: "tail", color: "warning" },
25
25
  shell: { previewLines: 6, direction: "tail", color: "warning" },
@@ -10,7 +10,7 @@ export const DEFAULT_PIX_CONFIG_JSONC = String.raw `{
10
10
  "toolRenderer": {
11
11
  "default": { "previewLines": 0, "direction": "head", "color": "toolTitle" },
12
12
  "tools": {
13
- "thinking": { "previewLines": 0, "direction": "head", "color": "thinkingForeground" },
13
+ "thinking": { "previewLines": 0, "direction": "head", "color": "assistantForeground" },
14
14
  "bash": { "previewLines": 6, "direction": "tail", "color": "warning" },
15
15
  "Bash": { "previewLines": 6, "direction": "tail", "color": "warning" },
16
16
  "shell": { "previewLines": 6, "direction": "tail", "color": "warning" },