pi-ui-extend 0.1.18 → 0.1.20

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 (70) hide show
  1. package/dist/app/app.js +8 -6
  2. package/dist/app/constants.d.ts +1 -0
  3. package/dist/app/constants.js +2 -1
  4. package/dist/app/input/voice-controller.js +16 -12
  5. package/dist/app/popup/popup-menu-controller.d.ts +1 -5
  6. package/dist/app/popup/popup-menu-controller.js +7 -8
  7. package/dist/app/process.js +7 -0
  8. package/dist/app/rendering/conversation-entry-renderer.js +17 -16
  9. package/dist/app/rendering/conversation-viewport.js +4 -35
  10. package/dist/app/rendering/editor-layout-renderer.d.ts +5 -1
  11. package/dist/app/rendering/editor-layout-renderer.js +29 -20
  12. package/dist/app/rendering/popup-menu-renderer.d.ts +1 -5
  13. package/dist/app/rendering/popup-menu-renderer.js +24 -34
  14. package/dist/app/rendering/render-controller.d.ts +2 -0
  15. package/dist/app/rendering/render-controller.js +40 -49
  16. package/dist/app/rendering/render-text.js +2 -2
  17. package/dist/app/rendering/status-line-renderer.d.ts +2 -0
  18. package/dist/app/rendering/status-line-renderer.js +75 -29
  19. package/dist/app/rendering/tab-line-renderer.js +3 -3
  20. package/dist/app/runtime.js +29 -3
  21. package/dist/app/screen/file-link-opener.d.ts +2 -0
  22. package/dist/app/screen/file-link-opener.js +84 -17
  23. package/dist/app/screen/mouse-controller.d.ts +1 -3
  24. package/dist/app/screen/mouse-controller.js +14 -14
  25. package/dist/app/screen/screen-styler.js +1 -1
  26. package/dist/app/session/lazy-session-manager.d.ts +1 -1
  27. package/dist/app/session/lazy-session-manager.js +64 -52
  28. package/dist/app/session/queued-message-controller.d.ts +6 -0
  29. package/dist/app/session/queued-message-controller.js +9 -1
  30. package/dist/app/session/queued-message-entries.d.ts +8 -0
  31. package/dist/app/session/queued-message-entries.js +41 -0
  32. package/dist/app/session/session-lifecycle-controller.d.ts +9 -1
  33. package/dist/app/session/session-lifecycle-controller.js +45 -11
  34. package/dist/app/session/tabs-controller.d.ts +11 -1
  35. package/dist/app/session/tabs-controller.js +197 -30
  36. package/dist/app/terminal/terminal-controller.d.ts +2 -0
  37. package/dist/app/terminal/terminal-controller.js +7 -5
  38. package/dist/app/types.d.ts +1 -1
  39. package/dist/schemas/pi-tools-suite-schema.d.ts +3 -0
  40. package/dist/schemas/pi-tools-suite-schema.js +3 -0
  41. package/dist/theme.d.ts +11 -0
  42. package/dist/theme.js +26 -4
  43. package/extensions/question/tui.ts +1 -1
  44. package/extensions/session-title/config.ts +3 -3
  45. package/extensions/session-title/index.ts +60 -5
  46. package/external/pi-tools-suite/README.md +3 -2
  47. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +1 -0
  48. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +1 -0
  49. package/external/pi-tools-suite/src/async-subagents/core/config.ts +0 -3
  50. package/external/pi-tools-suite/src/async-subagents/core/notifications.ts +64 -0
  51. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +1 -0
  52. package/external/pi-tools-suite/src/async-subagents/index.ts +54 -8
  53. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +4 -4
  54. package/external/pi-tools-suite/src/config.ts +13 -0
  55. package/external/pi-tools-suite/src/dcp/state.ts +9 -4
  56. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +5 -1
  57. package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +683 -0
  58. package/external/pi-tools-suite/src/index.ts +1 -0
  59. package/external/pi-tools-suite/src/lib/lsp.ts +2 -5
  60. package/external/pi-tools-suite/src/lsp/_shared/config.ts +2 -0
  61. package/external/pi-tools-suite/src/lsp/_shared/types.ts +2 -0
  62. package/external/pi-tools-suite/src/lsp/manager.ts +15 -9
  63. package/external/pi-tools-suite/src/telegram-mirror/ipc.ts +1 -0
  64. package/external/pi-tools-suite/src/todo/index.ts +81 -4
  65. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +5 -0
  66. package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
  67. package/package.json +3 -14
  68. package/schemas/pi-tools-suite.json +19 -0
  69. package/apps/desktop-tauri/README.md +0 -103
  70. package/apps/desktop-tauri/bin/pix-desktop.mjs +0 -89
@@ -6,10 +6,6 @@ import { ANSI_RESET, colorLine, colorize } from "../../theme.js";
6
6
  import { stringDisplayWidth } from "../../terminal-width.js";
7
7
  import { padOrTrimPlain } from "./render-text.js";
8
8
  const INPUT_FRAME = {
9
- topLeft: "╭",
10
- topRight: "╮",
11
- bottomLeft: "╰",
12
- bottomRight: "╯",
13
9
  horizontal: "─",
14
10
  };
15
11
  export class AppRenderController {
@@ -129,6 +125,11 @@ export class AppRenderController {
129
125
  setRenderedBackground(row, rendered?.backgroundOverride);
130
126
  appendFrameOutput("conversation", row, this.renderFrameRow(row, this.deps.screenStyler.styleBaseLine(row, rendered, conversationColumns)));
131
127
  }
128
+ const loadingConversationOverlay = this.renderConversationLoadingOverlay(this.deps.loadingConversationOverlayText?.(), conversationColumns, topReservedRows, bodyHeight);
129
+ if (loadingConversationOverlay) {
130
+ this.deps.mouseController.renderedRowTexts.set(loadingConversationOverlay.row, loadingConversationOverlay.text);
131
+ appendFrameOutput("conversation", loadingConversationOverlay.row, this.renderFrameRow(loadingConversationOverlay.row, loadingConversationOverlay.output));
132
+ }
132
133
  const aboveEditorStartRow = inputSeparatorRow + 1;
133
134
  for (let index = 0; index < aboveEditorLines.length; index += 1) {
134
135
  const rendered = frameRenderedLine(aboveEditorLines[index], columns, this.deps.theme, this.deps.screenStyler);
@@ -149,7 +150,7 @@ export class AppRenderController {
149
150
  if (row < statusRow) {
150
151
  this.deps.mouseController.renderedRowTexts.set(row, separatorText);
151
152
  appendFrameOutput("inputStatus", row, this.renderFrameRow(row, this.deps.screenStyler.styleLine(row, separatorText, columns, {
152
- foreground: this.deps.theme.colors.inputBorder,
153
+ foreground: this.deps.theme.colors.tabBorder,
153
154
  })));
154
155
  }
155
156
  }
@@ -160,7 +161,7 @@ export class AppRenderController {
160
161
  const row = toScreenRow(inputStartRow + index);
161
162
  this.deps.mouseController.renderedRowTexts.set(row, inputLine);
162
163
  const tagColor = this.deps.theme.colors.accent;
163
- const styledLine = this.deps.screenStyler.styleInputLine(row, inputLine, tagSpans, suggestionSpans, columns, tagColor, this.deps.theme.colors.muted, this.deps.theme.colors.inputBorder);
164
+ const styledLine = this.deps.screenStyler.styleInputLine(row, inputLine, tagSpans, suggestionSpans, columns, tagColor, this.deps.theme.colors.muted);
164
165
  appendFrameOutput("inputStatus", row, this.renderFrameRow(row, styledLine));
165
166
  }
166
167
  if (renderedInput.scrollBar && columns > 0) {
@@ -175,7 +176,15 @@ export class AppRenderController {
175
176
  })}`);
176
177
  }
177
178
  }
178
- const belowEditorStartRow = inputStartRow + renderedInput.lines.length;
179
+ if (inputBottomSeparatorRow && inputBottomSeparatorRow > inputSeparatorRow && inputBottomSeparatorRow < statusRow) {
180
+ const separatorText = inputFrameLine(columns, "bottom");
181
+ const row = toScreenRow(inputBottomSeparatorRow);
182
+ this.deps.mouseController.renderedRowTexts.set(row, separatorText);
183
+ appendFrameOutput("inputStatus", row, this.renderFrameRow(row, this.deps.screenStyler.styleLine(row, separatorText, columns, {
184
+ foreground: this.deps.theme.colors.tabBorder,
185
+ })));
186
+ }
187
+ const belowEditorStartRow = (inputBottomSeparatorRow ?? (inputStartRow + renderedInput.lines.length - 1)) + 1;
179
188
  for (let index = 0; index < belowEditorLines.length; index += 1) {
180
189
  const rendered = frameRenderedLine(belowEditorLines[index], columns, this.deps.theme, this.deps.screenStyler);
181
190
  const row = toScreenRow(belowEditorStartRow + index);
@@ -190,25 +199,7 @@ export class AppRenderController {
190
199
  appendFrameOutput("inputStatus", row, this.renderFrameRow(row, rendered.output(row)));
191
200
  }
192
201
  const statusLayout = this.deps.statusLineRenderer.layout(columns);
193
- const statusLineRenderer = this.deps.statusLineRenderer;
194
- const inputBorderWidgetsLayout = statusLineRenderer.inputBorderWidgetsLayout?.(columns);
195
- if (inputBottomSeparatorRow > 1) {
196
- const separatorText = inputFrameLine(columns, "bottom");
197
- const row = toScreenRow(inputBottomSeparatorRow);
198
- if (row < statusRow) {
199
- const text = inputBorderWidgetsLayout
200
- ? overlayText(separatorText, inputBorderWidgetsLayout.inputBorderWidgetStartColumn ?? 1, inputBorderWidgetsLayout.text)
201
- : separatorText;
202
- this.deps.mouseController.renderedRowTexts.set(row, text);
203
- const output = inputBorderWidgetsLayout && statusLineRenderer.renderInputBorderWidgets
204
- ? statusLineRenderer.renderInputBorderWidgets(row, inputBorderWidgetsLayout, separatorText, columns)
205
- : this.deps.screenStyler.styleLine(row, separatorText, columns, {
206
- foreground: this.deps.theme.colors.inputBorder,
207
- });
208
- appendFrameOutput("inputStatus", row, this.renderFrameRow(row, output));
209
- }
210
- }
211
- this.updateStatusMouseState(statusLayout, statusRow, inputBorderWidgetsLayout, toScreenRow(inputBottomSeparatorRow));
202
+ this.updateStatusMouseState(statusLayout, statusRow);
212
203
  appendFrameOutput("inputStatus", statusRow, this.renderFrameRow(statusRow, this.deps.statusLineRenderer.render(statusRow, statusLayout, columns)));
213
204
  const voiceProgressOverlay = this.renderVoiceProgressOverlay(this.deps.voiceProgressOverlayText(), columns, statusRow);
214
205
  if (voiceProgressOverlay) {
@@ -286,9 +277,9 @@ export class AppRenderController {
286
277
  const text = fixedCellText(flash.text, width);
287
278
  return `\x1b[${flash.y};${flash.startColumn}H\x1b[7m${text}${ANSI_RESET}`;
288
279
  }
289
- updateStatusMouseState(statusLayout, statusRow, inputBorderWidgetsLayout, inputBorderWidgetsRow) {
290
- const widgetLayout = inputBorderWidgetsLayout;
291
- const widgetRow = inputBorderWidgetsRow ?? statusRow;
280
+ updateStatusMouseState(statusLayout, statusRow) {
281
+ const widgetLayout = statusLayout;
282
+ const widgetRow = statusRow;
292
283
  this.deps.mouseController.statusModelTarget = this.deps.statusLineRenderer.modelTarget(statusLayout.text, statusRow);
293
284
  this.deps.mouseController.statusThinkingTarget = this.deps.statusLineRenderer.thinkingTarget(statusLayout.text, statusRow);
294
285
  this.deps.mouseController.statusContextTarget = this.deps.statusLineRenderer.contextTarget(statusLayout.text, statusRow, statusLayout);
@@ -327,40 +318,40 @@ export class AppRenderController {
327
318
  ].join("");
328
319
  return { row: Math.min(2, rows - 1), text, output };
329
320
  }
321
+ renderConversationLoadingOverlay(message, width, topReservedRows, bodyHeight) {
322
+ if (!message || width <= 0 || bodyHeight <= 0)
323
+ return undefined;
324
+ const overlayWidth = Math.min(stringDisplayWidth(message), width);
325
+ const leftWidth = Math.max(0, Math.floor((width - overlayWidth) / 2));
326
+ const rightWidth = Math.max(0, width - leftWidth - overlayWidth);
327
+ const text = `${" ".repeat(leftWidth)}${padOrTrimPlain(message, overlayWidth)}${" ".repeat(rightWidth)}`;
328
+ const row = topReservedRows + Math.floor((bodyHeight + 1) / 2);
329
+ const output = this.deps.screenStyler.styleLine(row, text, width, {
330
+ foreground: this.deps.theme.colors.muted,
331
+ });
332
+ return { row, text, output };
333
+ }
330
334
  }
331
335
  function visibleToastStates(toastController) {
332
336
  const candidate = toastController;
333
337
  return typeof candidate.visibleStates === "function" ? candidate.visibleStates() : candidate.toast?.visibleStates ?? [];
334
338
  }
335
339
  function inputFrameLine(width, edge) {
340
+ void edge;
336
341
  if (width <= 0)
337
342
  return "";
338
- if (width === 1)
339
- return edge === "top" ? INPUT_FRAME.topLeft : INPUT_FRAME.bottomLeft;
340
- const left = edge === "top" ? INPUT_FRAME.topLeft : INPUT_FRAME.bottomLeft;
341
- const right = edge === "top" ? INPUT_FRAME.topRight : INPUT_FRAME.bottomRight;
342
- return `${left}${INPUT_FRAME.horizontal.repeat(Math.max(0, width - 2))}${right}`;
343
+ return INPUT_FRAME.horizontal.repeat(width);
343
344
  }
344
345
  function frameRenderedLine(line, width, theme, screenStyler) {
345
346
  if (width <= 0)
346
347
  return { line, text: "", output: () => "" };
347
- if (width === 1) {
348
- const border = colorize("", {
349
- foreground: theme.colors.inputBorder,
350
- });
351
- return { line, text: "│", output: () => border };
352
- }
353
- const innerWidth = Math.max(0, width - 2);
354
- const innerText = padOrTrimPlain(line?.text ?? "", innerWidth);
355
- const innerLine = line ? frameInnerRenderedLine(line, innerText, innerWidth) : undefined;
356
- const leftBorder = colorize("│", {
357
- foreground: theme.colors.inputBorder,
358
- });
359
- const rightBorder = leftBorder;
348
+ void theme;
349
+ const text = padOrTrimPlain(line?.text ?? "", width);
350
+ const outputLine = line ? frameInnerRenderedLine(line, text, width) : undefined;
360
351
  return {
361
352
  line,
362
- text: `│${innerText}│`,
363
- output: (row) => `${leftBorder}${screenStyler.styleBaseLine(row, innerLine, innerWidth)}${rightBorder}`,
353
+ text,
354
+ output: (row) => screenStyler.styleBaseLine(row, outputLine, width),
364
355
  };
365
356
  }
366
357
  function frameInnerRenderedLine(line, text, width) {
@@ -98,8 +98,8 @@ export function padOrTrimPlain(text, width) {
98
98
  }
99
99
  export function horizontalPaddingLayout(width) {
100
100
  const safeWidth = Math.max(1, width);
101
- const left = safeWidth > 1 ? 1 : 0;
102
- const right = safeWidth > 2 ? 1 : 0;
101
+ const left = 0;
102
+ const right = 0;
103
103
  return { left, right, contentWidth: Math.max(1, safeWidth - left - right) };
104
104
  }
105
105
  export function padHorizontalText(text, width) {
@@ -53,6 +53,8 @@ export declare class StatusLineRenderer {
53
53
  terminalBellSoundTarget(layout: StatusLineLayout, row: number): StatusTerminalBellSoundTarget | undefined;
54
54
  sessionTarget(statusText: string, row: number, label: string, workspaceLabel: string): StatusSessionTarget | undefined;
55
55
  private segments;
56
+ private fitWorkspaceLabel;
57
+ private pushStatusWidgetSegments;
56
58
  private pushPromptEnhancerWidgetSegment;
57
59
  private pushUserJumpWidgetSegment;
58
60
  private pushDraftQueueWidgetSegment;
@@ -1,12 +1,10 @@
1
1
  import { compactProgressBarSegments, formatCompactProgressBar } from "../../context-progress-bar.js";
2
- import { padOrTrimPlain } from "./render-text.js";
2
+ import { ellipsizeDisplay, padOrTrimPlain } from "./render-text.js";
3
3
  import { displayIndexForColumn, stringDisplayWidth } from "../../terminal-width.js";
4
4
  import { APP_ICONS } from "../icons.js";
5
5
  import { resolveColor, resolveModelColor } from "../../config.js";
6
6
  const MODEL_USAGE_PROGRESS_BAR_WIDTH = stringDisplayWidth(formatCompactProgressBar(100));
7
- const INPUT_BORDER_WIDGET_INSET = 2;
8
- const INPUT_BORDER_WIDGET_SEPARATOR = "─";
9
- const STATUS_ICON_BUTTON_WIDTH = 3;
7
+ const STATUS_WIDGET_GAP = " ";
10
8
  export class StatusLineRenderer {
11
9
  host;
12
10
  constructor(host) {
@@ -18,17 +16,29 @@ export class StatusLineRenderer {
18
16
  const baseStatus = this.host.currentStatus();
19
17
  const workspaceLabel = this.host.statusWorkspaceLabel();
20
18
  const modelUsageLabel = this.host.modelUsageStatusLabel();
21
- const workspaceDetailsLabel = modelUsageLabel ? `${workspaceLabel} ${modelUsageLabel}` : workspaceLabel;
22
- const contextBarLabel = this.contextBarLabel(baseStatus, contentWidth, workspaceDetailsLabel);
19
+ const widgetsLayout = this.inputBorderWidgetsLayout(contentWidth);
20
+ const leftWidth = widgetsLayout?.inputBorderWidgetStartColumn
21
+ ? Math.max(0, widgetsLayout.inputBorderWidgetStartColumn - 2)
22
+ : contentWidth;
23
+ const fullWorkspaceDetailsLabel = modelUsageLabel ? `${workspaceLabel} ${modelUsageLabel}` : workspaceLabel;
24
+ const contextBarLabel = this.contextBarLabel(baseStatus, leftWidth, fullWorkspaceDetailsLabel);
23
25
  const status = contextBarLabel ? `${baseStatus} ${contextBarLabel}` : baseStatus;
24
26
  const sessionLabel = "";
25
- const details = `${status} ${workspaceDetailsLabel}`;
26
- const text = padOrTrimPlain(`${statusDot} ${details}`, width);
27
+ const fittedWorkspaceLabel = this.fitWorkspaceLabel(statusDot, status, workspaceLabel, modelUsageLabel, leftWidth);
28
+ const workspaceDetailsLabel = modelUsageLabel
29
+ ? `${fittedWorkspaceLabel ? `${fittedWorkspaceLabel} ` : ""}${modelUsageLabel}`
30
+ : fittedWorkspaceLabel;
31
+ const details = workspaceDetailsLabel ? `${status} ${workspaceDetailsLabel}` : status;
32
+ const leftText = padOrTrimPlain(`${statusDot} ${details}`, leftWidth);
33
+ const text = widgetsLayout?.inputBorderWidgetStartColumn
34
+ ? overlayText(padOrTrimPlain(leftText, contentWidth), widgetsLayout.inputBorderWidgetStartColumn, widgetsLayout.text)
35
+ : padOrTrimPlain(leftText, contentWidth);
27
36
  return {
37
+ ...widgetsLayout,
28
38
  details,
29
39
  text,
30
40
  sessionLabel,
31
- workspaceLabel,
41
+ workspaceLabel: fittedWorkspaceLabel,
32
42
  ...(modelUsageLabel ? { modelUsageLabel } : {}),
33
43
  ...(contextBarLabel ? { contextBarLabel } : {}),
34
44
  };
@@ -76,17 +86,18 @@ export class StatusLineRenderer {
76
86
  if (!hasParts)
77
87
  return undefined;
78
88
  const parts = [];
79
- const totalWidth = widgets.reduce((total, widget, index) => total + (index > 0 ? 1 : 0) + stringDisplayWidth(widget.text), 0);
80
- const endColumn = Math.max(1, width - INPUT_BORDER_WIDGET_INSET);
89
+ const gapWidth = stringDisplayWidth(STATUS_WIDGET_GAP);
90
+ const totalWidth = widgets.reduce((total, widget, index) => total + stringDisplayWidth(widget.text) + (index > 0 ? gapWidth : 0), 0);
91
+ const endColumn = Math.max(1, width + 1);
81
92
  const startColumn = endColumn - totalWidth;
82
- if (startColumn < 2)
93
+ if (startColumn < 1)
83
94
  return undefined;
84
95
  layout.inputBorderWidgetStartColumn = startColumn;
85
96
  let nextColumn = startColumn;
86
97
  for (const [index, widget] of widgets.entries()) {
87
98
  if (index > 0) {
88
- parts.push(INPUT_BORDER_WIDGET_SEPARATOR);
89
- nextColumn += 1;
99
+ parts.push(STATUS_WIDGET_GAP);
100
+ nextColumn += gapWidth;
90
101
  }
91
102
  parts.push(widget.text);
92
103
  widget.assign(nextColumn, widget.text);
@@ -110,7 +121,6 @@ export class StatusLineRenderer {
110
121
  }
111
122
  inputBorderWidgetSegments(layout, text) {
112
123
  const colors = this.host.theme.colors;
113
- const background = colors.inputBorder;
114
124
  const segments = [];
115
125
  const pushWidgetSegment = (widget, foreground) => {
116
126
  if (!widget)
@@ -119,7 +129,6 @@ export class StatusLineRenderer {
119
129
  start: displayIndexForColumn(text, widget.startColumn),
120
130
  end: displayIndexForColumn(text, widget.endColumn),
121
131
  foreground,
122
- background,
123
132
  });
124
133
  };
125
134
  pushWidgetSegment(layout.draftQueueWidget, colors.info);
@@ -138,14 +147,12 @@ export class StatusLineRenderer {
138
147
  start: displayIndexForColumn(text, voiceWidget.startColumn),
139
148
  end: displayIndexForColumn(text, voiceWidget.micEndColumn),
140
149
  foreground: this.host.voiceStatusWidgetActive() ? colors.error : colors.muted,
141
- background,
142
150
  });
143
151
  if (voiceWidget.languageEndColumn > voiceWidget.languageStartColumn) {
144
152
  segments.push({
145
153
  start: displayIndexForColumn(text, voiceWidget.languageStartColumn),
146
154
  end: displayIndexForColumn(text, voiceWidget.languageEndColumn),
147
155
  foreground: colors.statusForeground,
148
- background,
149
156
  });
150
157
  }
151
158
  }
@@ -271,16 +278,10 @@ export class StatusLineRenderer {
271
278
  end: statusDotStart + APP_ICONS.record.length,
272
279
  foreground: this.statusDotColor(),
273
280
  }] : [];
274
- this.pushDraftQueueWidgetSegment(segments, statusText);
275
- this.pushUserJumpWidgetSegment(segments, statusText);
276
- this.pushThinkingExpandWidgetSegment(segments, statusText, layout);
277
- this.pushCompactToolsWidgetSegment(segments, statusText);
278
- this.pushTerminalBellSoundWidgetSegment(segments, statusText);
281
+ this.pushStatusWidgetSegments(segments, statusText, layout);
279
282
  this.pushWorkspaceSegments(segments, statusText, layout.workspaceLabel);
280
283
  if (layout.modelUsageLabel)
281
284
  this.pushModelUsageSegments(segments, statusText, layout.modelUsageLabel);
282
- this.pushPromptEnhancerWidgetSegment(segments, statusText);
283
- this.pushVoiceWidgetSegment(segments, statusText);
284
285
  const session = this.host.session;
285
286
  if (!session)
286
287
  return segments;
@@ -304,6 +305,50 @@ export class StatusLineRenderer {
304
305
  this.pushSegment(segments, statusText.lastIndexOf(layout.sessionLabel), layout.sessionLabel.length, this.host.theme.colors.selectionForeground);
305
306
  return segments;
306
307
  }
308
+ fitWorkspaceLabel(statusDot, status, workspaceLabel, modelUsageLabel, width) {
309
+ const modelUsageSuffix = modelUsageLabel ? ` ${modelUsageLabel}` : "";
310
+ const available = width - stringDisplayWidth(`${statusDot} ${status} `) - stringDisplayWidth(modelUsageSuffix);
311
+ if (available <= 0)
312
+ return "";
313
+ return ellipsizeDisplay(workspaceLabel, available);
314
+ }
315
+ pushStatusWidgetSegments(segments, statusText, layout) {
316
+ const colors = this.host.theme.colors;
317
+ const widgets = [
318
+ { widget: layout.draftQueueWidget, foreground: colors.info },
319
+ { widget: layout.promptEnhancerWidget, foreground: this.host.promptEnhancerStatusWidgetActive()
320
+ ? colors.warning
321
+ : this.host.promptEnhancerStatusWidgetEnabled()
322
+ ? colors.info
323
+ : colors.muted },
324
+ { widget: layout.userJumpWidget, foreground: this.host.userMessageJumpMenuActive?.() ? colors.info : colors.muted },
325
+ { widget: layout.terminalBellSoundWidget, foreground: this.host.terminalBellSoundStatusWidgetEnabled() ? colors.info : colors.muted },
326
+ { widget: layout.thinkingExpandWidget, foreground: this.host.allThinkingExpandedActive?.() ? colors.info : colors.muted },
327
+ { widget: layout.compactToolsWidget, foreground: this.host.superCompactToolsActive?.() ? colors.info : colors.muted },
328
+ ].filter((entry) => Boolean(entry.widget));
329
+ for (const { widget, foreground } of widgets) {
330
+ segments.push({
331
+ start: displayIndexForColumn(statusText, widget.startColumn),
332
+ end: displayIndexForColumn(statusText, widget.endColumn),
333
+ foreground,
334
+ });
335
+ }
336
+ const voiceWidget = layout.voiceWidget;
337
+ if (voiceWidget) {
338
+ segments.push({
339
+ start: displayIndexForColumn(statusText, voiceWidget.startColumn),
340
+ end: displayIndexForColumn(statusText, voiceWidget.micEndColumn),
341
+ foreground: this.host.voiceStatusWidgetActive() ? colors.error : colors.muted,
342
+ });
343
+ if (voiceWidget.languageEndColumn > voiceWidget.languageStartColumn) {
344
+ segments.push({
345
+ start: displayIndexForColumn(statusText, voiceWidget.languageStartColumn),
346
+ end: displayIndexForColumn(statusText, voiceWidget.languageEndColumn),
347
+ foreground: colors.statusForeground,
348
+ });
349
+ }
350
+ }
351
+ }
307
352
  pushPromptEnhancerWidgetSegment(segments, statusText) {
308
353
  const widgetText = this.host.promptEnhancerStatusWidgetText();
309
354
  const start = statusText.lastIndexOf(widgetText);
@@ -497,7 +542,7 @@ export class StatusLineRenderer {
497
542
  };
498
543
  }
499
544
  iconButtonText(icon) {
500
- return padOrTrimPlain(` ${icon} `, STATUS_ICON_BUTTON_WIDTH);
545
+ return icon;
501
546
  }
502
547
  voiceBorderWidgetText(widgetText) {
503
548
  const parts = this.voiceBorderWidgetParts(widgetText);
@@ -505,21 +550,22 @@ export class StatusLineRenderer {
505
550
  return "";
506
551
  const micButton = this.iconButtonText(parts.buttonIconText);
507
552
  if (parts.languageText) {
508
- return `${micButton}${INPUT_BORDER_WIDGET_SEPARATOR}${parts.languageText}`;
553
+ return `${micButton}${STATUS_WIDGET_GAP}${parts.languageText}`;
509
554
  }
510
555
  return micButton;
511
556
  }
512
557
  voiceWidgetLayout(startColumn, sourceText, widgetText) {
513
558
  const parts = this.voiceBorderWidgetParts(sourceText);
559
+ const micWidth = parts?.buttonIconText ? stringDisplayWidth(parts.buttonIconText) : stringDisplayWidth(widgetText);
514
560
  const languageStartOffset = parts?.languageText
515
- ? STATUS_ICON_BUTTON_WIDTH + stringDisplayWidth(INPUT_BORDER_WIDGET_SEPARATOR)
561
+ ? micWidth + stringDisplayWidth(STATUS_WIDGET_GAP)
516
562
  : stringDisplayWidth(widgetText);
517
563
  const languageEndOffset = parts?.languageText
518
564
  ? languageStartOffset + stringDisplayWidth(parts.languageText)
519
565
  : languageStartOffset;
520
566
  return {
521
567
  startColumn,
522
- micEndColumn: startColumn + STATUS_ICON_BUTTON_WIDTH,
568
+ micEndColumn: startColumn + micWidth,
523
569
  languageStartColumn: startColumn + languageStartOffset,
524
570
  languageEndColumn: startColumn + languageEndOffset,
525
571
  endColumn: startColumn + stringDisplayWidth(widgetText),
@@ -50,7 +50,7 @@ export class TabLineRenderer {
50
50
  segments.push({
51
51
  start: separatorOffset + 1,
52
52
  end: separatorOffset + 2,
53
- foreground: this.host.theme.colors.inputBorder,
53
+ foreground: this.host.theme.colors.tabBorder,
54
54
  });
55
55
  displayColumn += separatorWidth;
56
56
  }
@@ -89,7 +89,7 @@ export class TabLineRenderer {
89
89
  segments.push({
90
90
  start: newTabDividerOffset,
91
91
  end: newTabDividerOffset + 1,
92
- foreground: this.host.theme.colors.inputBorder,
92
+ foreground: this.host.theme.colors.tabBorder,
93
93
  });
94
94
  segments.push({
95
95
  start: lineText.length - APP_ICONS.plus.length,
@@ -119,7 +119,7 @@ export class TabLineRenderer {
119
119
  }
120
120
  renderBottom(row, layout, width) {
121
121
  return this.host.screenStyler.styleLine(row, this.bottomText(layout, width), width, {
122
- foreground: this.host.theme.colors.inputBorder,
122
+ foreground: this.host.theme.colors.tabBorder,
123
123
  });
124
124
  }
125
125
  bottomText(layout, width) {
@@ -133,6 +133,32 @@ function isBundledQuestionConflict(error, bundledExtensionPaths) {
133
133
  }
134
134
  return false;
135
135
  }
136
+ const bundledSkillsInstallPromises = new Map();
137
+ const piToolsSuiteInstallPromises = new Map();
138
+ async function ensureBundledSkillsInstalledOnce(options = {}) {
139
+ const targetPath = resolve(options.targetPath ?? bundledSkillsInstallPath(options.homeDir));
140
+ const existing = bundledSkillsInstallPromises.get(targetPath);
141
+ if (existing)
142
+ return await existing;
143
+ const pending = ensureBundledSkillsInstalled(options).catch((error) => {
144
+ bundledSkillsInstallPromises.delete(targetPath);
145
+ throw error;
146
+ });
147
+ bundledSkillsInstallPromises.set(targetPath, pending);
148
+ return await pending;
149
+ }
150
+ async function ensurePiToolsSuiteExtensionInstalledOnce(options = {}) {
151
+ const targetPath = resolve(options.targetPath ?? piToolsSuiteExtensionInstallPath(options.agentDir));
152
+ const existing = piToolsSuiteInstallPromises.get(targetPath);
153
+ if (existing)
154
+ return await existing;
155
+ const pending = ensurePiToolsSuiteExtensionInstalled(options).catch((error) => {
156
+ piToolsSuiteInstallPromises.delete(targetPath);
157
+ throw error;
158
+ });
159
+ piToolsSuiteInstallPromises.set(targetPath, pending);
160
+ return await pending;
161
+ }
136
162
  export function resolvePixRuntimeModelRef(options, sessionManager, config = loadPixConfig()) {
137
163
  if (options.modelRef)
138
164
  return options.modelRef;
@@ -176,8 +202,8 @@ export async function createPixRuntime(options, runtimeOptions = {}) {
176
202
  const effectiveModelRef = resolvePixRuntimeModelRef(options, sessionManager, config);
177
203
  const parsedModel = effectiveModelRef ? parseModelRef(effectiveModelRef) : undefined;
178
204
  const initialThinkingLevel = resolvePixRuntimeInitialThinkingLevel(options, sessionManager, config);
179
- await ensureBundledSkillsInstalled();
180
- await ensurePiToolsSuiteExtensionInstalled({ agentDir });
205
+ await ensureBundledSkillsInstalledOnce();
206
+ await ensurePiToolsSuiteExtensionInstalledOnce({ agentDir });
181
207
  const bundledExtensionPaths = getBundledExtensionPaths();
182
208
  const services = await createAgentSessionServices({
183
209
  cwd,
@@ -232,7 +258,7 @@ export async function createPixRuntime(options, runtimeOptions = {}) {
232
258
  sessionManager: options.noSession
233
259
  ? SessionManager.inMemory(options.cwd)
234
260
  : options.sessionPath
235
- ? openLazySessionManager(options.sessionPath, { cwdOverride: options.cwd })
261
+ ? await openLazySessionManager(options.sessionPath, { cwdOverride: options.cwd })
236
262
  : SessionManager.create(options.cwd),
237
263
  });
238
264
  }
@@ -3,6 +3,8 @@ import { spawn } from "node:child_process";
3
3
  import type { RenderedLink } from "./file-links.js";
4
4
  type FileLinkOpenerDeps = {
5
5
  existsSync: typeof existsSync;
6
+ env: NodeJS.ProcessEnv;
7
+ platform: NodeJS.Platform;
6
8
  spawn: typeof spawn;
7
9
  };
8
10
  export declare function setFileLinkOpenerTestDeps(overrides: Partial<FileLinkOpenerDeps>): () => void;
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { spawn } from "node:child_process";
3
- import { delimiter, join } from "node:path";
3
+ import { isAbsolute, posix, win32 } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- let deps = { existsSync, spawn };
5
+ let deps = { existsSync, env: process.env, platform: process.platform, spawn };
6
6
  export function setFileLinkOpenerTestDeps(overrides) {
7
7
  const previous = deps;
8
8
  deps = { ...deps, ...overrides };
@@ -14,13 +14,10 @@ export function openFileLink(link) {
14
14
  const filePath = link.filePath ?? filePathFromUrl(link.url);
15
15
  if (!filePath)
16
16
  return false;
17
- const target = zedTarget(filePath, link.line, link.column);
18
- const candidates = zedCommandCandidates();
19
- if (trySpawnCandidates(candidates, [target]))
17
+ const editorLaunch = preferredEditorLaunch(filePath, link.line, link.column);
18
+ if (editorLaunch && trySpawnCandidates(editorLaunch.candidates, editorLaunch.args))
20
19
  return true;
21
- if (process.platform === "darwin")
22
- return spawnDetached("open", ["-a", "Zed", filePath]);
23
- return false;
20
+ return openPathWithSystemViewer(filePath);
24
21
  }
25
22
  function filePathFromUrl(url) {
26
23
  if (!url.startsWith("file://"))
@@ -37,29 +34,99 @@ function zedTarget(filePath, line, column) {
37
34
  return filePath;
38
35
  return column === undefined ? `${filePath}:${line}` : `${filePath}:${line}:${column}`;
39
36
  }
37
+ function gotoTarget(filePath, line, column) {
38
+ if (line === undefined)
39
+ return filePath;
40
+ return column === undefined ? `${filePath}:${line}` : `${filePath}:${line}:${column}`;
41
+ }
42
+ function preferredEditorLaunch(filePath, line, column) {
43
+ switch (detectEditor(deps.env)) {
44
+ case "cursor":
45
+ return { args: ["--goto", gotoTarget(filePath, line, column)], candidates: commandCandidates(deps.env.CURSOR_CLI, "cursor") };
46
+ case "jetbrains":
47
+ return {
48
+ args: jetbrainsTargetArgs(filePath, line),
49
+ candidates: commandCandidates(deps.env.JETBRAINS_IDE_CLI, "idea", "idea64", "webstorm", "webstorm64", "pycharm", "pycharm64", "goland", "goland64", "clion", "clion64", "phpstorm", "phpstorm64", "rubymine", "rubymine64", "rider", "rider64"),
50
+ };
51
+ case "vscode":
52
+ return { args: ["--goto", gotoTarget(filePath, line, column)], candidates: commandCandidates(deps.env.VSCODE_CLI, "code", "code-insiders") };
53
+ case "windsurf":
54
+ return { args: ["--goto", gotoTarget(filePath, line, column)], candidates: commandCandidates(deps.env.WINDSURF_CLI, "windsurf") };
55
+ case "zed":
56
+ return { args: [zedTarget(filePath, line, column)], candidates: zedCommandCandidates() };
57
+ default:
58
+ return undefined;
59
+ }
60
+ }
61
+ function detectEditor(env) {
62
+ const termProgram = env.TERM_PROGRAM?.trim().toLowerCase();
63
+ const terminalEmulator = env.TERMINAL_EMULATOR?.trim().toLowerCase();
64
+ const terminalProvider = env.TERMINAL_PROVIDER?.trim().toLowerCase();
65
+ if (termProgram === "cursor" || env.CURSOR_TRACE_ID || env.CURSOR_TRACE)
66
+ return "cursor";
67
+ if (termProgram === "windsurf")
68
+ return "windsurf";
69
+ if (termProgram === "zed" || env.ZED_CLI)
70
+ return "zed";
71
+ if (termProgram === "vscode" || env.VSCODE_IPC_HOOK_CLI || env.VSCODE_GIT_IPC_HANDLE)
72
+ return "vscode";
73
+ if (terminalEmulator?.includes("jetbrains") || terminalProvider === "jetbrains")
74
+ return "jetbrains";
75
+ return undefined;
76
+ }
40
77
  function zedCommandCandidates() {
41
- const candidates = [process.env.ZED_CLI, "zed", "zeditor"];
42
- if (process.platform === "darwin")
78
+ const candidates = [deps.env.ZED_CLI, "zed", "zeditor"];
79
+ if (deps.platform === "darwin")
43
80
  candidates.push("/opt/homebrew/bin/zed", "/usr/local/bin/zed");
44
81
  return candidates.filter((candidate) => Boolean(candidate));
45
82
  }
83
+ function commandCandidates(primary, ...rest) {
84
+ return [primary, ...rest].filter((candidate) => Boolean(candidate));
85
+ }
86
+ function jetbrainsTargetArgs(filePath, line) {
87
+ if (line === undefined)
88
+ return [filePath];
89
+ return ["--line", `${line}`, filePath];
90
+ }
46
91
  function trySpawnCandidates(candidates, args) {
47
92
  for (const command of candidates) {
48
- if (command.includes("/") && !deps.existsSync(command))
49
- continue;
50
- if (!command.includes("/") && !commandOnPath(command))
93
+ if (!canRunCommand(command))
51
94
  continue;
52
95
  if (spawnDetached(command, args))
53
96
  return true;
54
97
  }
55
98
  return false;
56
99
  }
100
+ function canRunCommand(command) {
101
+ if (hasPathSeparator(command) || isAbsolute(command))
102
+ return deps.existsSync(command);
103
+ return commandOnPath(command);
104
+ }
105
+ function hasPathSeparator(command) {
106
+ return command.includes("/") || command.includes("\\");
107
+ }
57
108
  function commandOnPath(command) {
58
- const pathEntries = process.env.PATH?.split(delimiter) ?? [];
59
- const extensions = process.platform === "win32"
60
- ? (process.env.PATHEXT?.split(";") ?? [".EXE", ".CMD", ".BAT", ".COM"])
109
+ const pathEntries = deps.env.PATH?.split(pathDelimiter()) ?? [];
110
+ const extensions = deps.platform === "win32"
111
+ ? (deps.env.PATHEXT?.split(";") ?? [".EXE", ".CMD", ".BAT", ".COM"])
61
112
  : [""];
62
- return pathEntries.some((entry) => extensions.some((extension) => deps.existsSync(join(entry, `${command}${extension}`))));
113
+ return pathEntries.some((entry) => pathCommandCandidates(entry, command, extensions).some((candidate) => deps.existsSync(candidate)));
114
+ }
115
+ function pathDelimiter() {
116
+ return deps.platform === "win32" ? ";" : ":";
117
+ }
118
+ function pathCommandCandidates(entry, command, extensions) {
119
+ const pathApi = deps.platform === "win32" ? win32 : posix;
120
+ if (deps.platform !== "win32" || pathApi.extname(command))
121
+ return [pathApi.join(entry, command)];
122
+ return [pathApi.join(entry, command), ...extensions.map((extension) => pathApi.join(entry, `${command}${extension}`))];
123
+ }
124
+ function openPathWithSystemViewer(filePath) {
125
+ if (deps.platform === "darwin")
126
+ return spawnDetached("open", [filePath]);
127
+ if (deps.platform === "win32")
128
+ return spawnDetached("cmd", ["/c", "start", "", filePath]);
129
+ return spawnDetached("xdg-open", [filePath]);
63
130
  }
64
131
  function spawnDetached(command, args) {
65
132
  try {
@@ -20,9 +20,7 @@ export type InputFrameCopyRows = {
20
20
  inputStartRow: number;
21
21
  inputEndRow: number;
22
22
  inputSeparatorRow: number;
23
- inputBottomSeparatorRow: number;
24
- contentStartColumn: number;
25
- contentEndColumn: number;
23
+ inputBottomSeparatorRow?: number;
26
24
  };
27
25
  export type AppMouseControllerHost = {
28
26
  terminalColumns(): number;