pi-ui-extend 0.1.17 → 0.1.19

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 (79) 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 +1 -0
  4. package/dist/app/input/input-controller.d.ts +1 -0
  5. package/dist/app/input/input-controller.js +29 -0
  6. package/dist/app/input/input-paste-handler.d.ts +1 -1
  7. package/dist/app/input/input-paste-handler.js +6 -5
  8. package/dist/app/input/voice-controller.js +16 -12
  9. package/dist/app/model/model-usage-status.js +4 -27
  10. package/dist/app/popup/popup-menu-controller.d.ts +1 -5
  11. package/dist/app/popup/popup-menu-controller.js +7 -8
  12. package/dist/app/process.js +7 -0
  13. package/dist/app/rendering/conversation-entry-renderer.js +17 -16
  14. package/dist/app/rendering/conversation-viewport.js +4 -35
  15. package/dist/app/rendering/editor-layout-renderer.d.ts +5 -1
  16. package/dist/app/rendering/editor-layout-renderer.js +25 -16
  17. package/dist/app/rendering/popup-menu-renderer.d.ts +1 -5
  18. package/dist/app/rendering/popup-menu-renderer.js +24 -34
  19. package/dist/app/rendering/render-controller.d.ts +2 -0
  20. package/dist/app/rendering/render-controller.js +38 -33
  21. package/dist/app/rendering/render-text.js +2 -2
  22. package/dist/app/rendering/status-line-renderer.js +1 -1
  23. package/dist/app/rendering/tab-line-renderer.js +3 -3
  24. package/dist/app/runtime.js +29 -3
  25. package/dist/app/screen/file-link-opener.d.ts +2 -0
  26. package/dist/app/screen/file-link-opener.js +84 -17
  27. package/dist/app/screen/mouse-controller.d.ts +1 -2
  28. package/dist/app/screen/mouse-controller.js +19 -28
  29. package/dist/app/screen/screen-styler.js +1 -1
  30. package/dist/app/session/lazy-session-manager.d.ts +1 -1
  31. package/dist/app/session/lazy-session-manager.js +64 -52
  32. package/dist/app/session/queued-message-controller.d.ts +6 -0
  33. package/dist/app/session/queued-message-controller.js +9 -1
  34. package/dist/app/session/queued-message-entries.d.ts +8 -0
  35. package/dist/app/session/queued-message-entries.js +41 -0
  36. package/dist/app/session/session-lifecycle-controller.d.ts +9 -1
  37. package/dist/app/session/session-lifecycle-controller.js +45 -11
  38. package/dist/app/session/tabs-controller.d.ts +11 -1
  39. package/dist/app/session/tabs-controller.js +197 -30
  40. package/dist/app/terminal/terminal-controller.d.ts +2 -0
  41. package/dist/app/terminal/terminal-controller.js +7 -5
  42. package/dist/schemas/pi-tools-suite-schema.d.ts +3 -0
  43. package/dist/schemas/pi-tools-suite-schema.js +3 -0
  44. package/dist/theme.d.ts +3 -0
  45. package/dist/theme.js +8 -2
  46. package/extensions/session-title/config.ts +3 -3
  47. package/extensions/session-title/index.ts +60 -5
  48. package/external/pi-tools-suite/README.md +3 -2
  49. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +1 -0
  50. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +1 -0
  51. package/external/pi-tools-suite/src/async-subagents/core/config.ts +0 -3
  52. package/external/pi-tools-suite/src/async-subagents/core/notifications.ts +64 -0
  53. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +1 -0
  54. package/external/pi-tools-suite/src/async-subagents/index.ts +54 -8
  55. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +4 -4
  56. package/external/pi-tools-suite/src/config.ts +56 -0
  57. package/external/pi-tools-suite/src/dcp/commands.ts +1 -1
  58. package/external/pi-tools-suite/src/dcp/index.ts +21 -1
  59. package/external/pi-tools-suite/src/dcp/state.ts +234 -7
  60. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +10 -1
  61. package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +580 -0
  62. package/external/pi-tools-suite/src/index.ts +2 -0
  63. package/external/pi-tools-suite/src/lib/lsp.ts +2 -5
  64. package/external/pi-tools-suite/src/lsp/_shared/config.ts +2 -0
  65. package/external/pi-tools-suite/src/lsp/_shared/types.ts +2 -0
  66. package/external/pi-tools-suite/src/lsp/manager.ts +15 -9
  67. package/external/pi-tools-suite/src/telegram-mirror/README.md +168 -0
  68. package/external/pi-tools-suite/src/telegram-mirror/bot.ts +228 -0
  69. package/external/pi-tools-suite/src/telegram-mirror/events.ts +94 -0
  70. package/external/pi-tools-suite/src/telegram-mirror/format.ts +120 -0
  71. package/external/pi-tools-suite/src/telegram-mirror/index.ts +424 -0
  72. package/external/pi-tools-suite/src/telegram-mirror/ipc.ts +420 -0
  73. package/external/pi-tools-suite/src/telegram-mirror/multiplexer.ts +408 -0
  74. package/external/pi-tools-suite/src/telegram-mirror/renderer.ts +214 -0
  75. package/external/pi-tools-suite/src/todo/index.ts +81 -4
  76. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +5 -0
  77. package/external/pi-tools-suite/src/tool-descriptions.ts +4 -4
  78. package/package.json +1 -1
  79. package/schemas/pi-tools-suite.json +19 -0
@@ -49,41 +49,31 @@ export class PopupMenuRenderer {
49
49
  const rightMargin = Math.max(0, width - margin - menuWidth);
50
50
  return `${" ".repeat(margin)}${padOrTrimPlain(line.text, menuWidth)}${" ".repeat(rightMargin)}`;
51
51
  }
52
- renderInlineUserMessageMenu(options, menu) {
53
- const headerLine = options.userLine(formatPopupMenuHeader("Message actions", options.userContentWidth));
54
- headerLine.target = { kind: "popup-menu-close" };
55
- headerLine.segments = [{
56
- start: options.userContentLeft,
57
- end: options.userContentLeft + options.userContentWidth,
58
- foreground: this.host.theme.colors.accent,
59
- background: this.host.theme.colors.popupHeaderBackground,
60
- bold: true,
61
- }];
62
- const lines = [headerLine];
52
+ renderUserMessageMenu(width, menu) {
53
+ const lines = [this.popupMenuHeader("Message actions", width)];
63
54
  for (const item of menu.visibleItems()) {
64
- const label = item.label.padEnd(18, " ");
65
- const description = item.description ?? "";
66
- const marker = item.selected ? "▶" : " ";
67
- const rawText = `${marker} ${label}${description}`;
68
- const text = ellipsizeDisplay(rawText, options.userContentWidth);
69
- const line = options.userLine(text);
70
- line.target = { kind: "popup-menu", index: item.index };
71
- const contentStart = options.userContentLeft;
72
- const labelStart = contentStart + 2;
73
- const labelEnd = Math.min(contentStart + text.length, labelStart + item.label.length);
74
- const descriptionStart = contentStart + 2 + label.length;
75
- line.segments = [
76
- ...(item.selected ? [{ start: contentStart, end: contentStart + 1, foreground: this.host.theme.colors.accent, bold: true }] : []),
77
- {
78
- start: labelStart,
79
- end: labelEnd,
80
- foreground: this.userMessageActionForeground(item.value),
81
- bold: item.selected,
82
- },
83
- ...(descriptionStart < contentStart + text.length
84
- ? [{ start: descriptionStart, end: contentStart + text.length, foreground: this.host.theme.colors.muted }]
85
- : []),
86
- ];
55
+ const marker = item.selected ? " " : " ";
56
+ const text = `${marker}${this.labelDescriptionText(item.label, item.description, width - 2, 18)}`;
57
+ const labelStart = 2;
58
+ const labelEnd = Math.min(text.length, labelStart + item.label.length);
59
+ const description = item.description ? sanitizeText(item.description).replace(/\s+/gu, " ") : "";
60
+ const descriptionStart = description ? text.indexOf(description, labelEnd) : -1;
61
+ const line = {
62
+ text,
63
+ target: { kind: "popup-menu", index: item.index },
64
+ segments: [
65
+ ...(item.selected ? [{ start: 0, end: 1, foreground: this.host.theme.colors.accent, bold: true }] : []),
66
+ {
67
+ start: labelStart,
68
+ end: labelEnd,
69
+ foreground: this.userMessageActionForeground(item.value),
70
+ bold: item.selected,
71
+ },
72
+ ...(descriptionStart >= 0
73
+ ? [{ start: descriptionStart, end: text.length, foreground: this.host.theme.colors.muted }]
74
+ : []),
75
+ ],
76
+ };
87
77
  lines.push(line);
88
78
  }
89
79
  return lines;
@@ -24,6 +24,7 @@ export type AppRenderControllerDeps = {
24
24
  tabLineRenderer: TabLineRenderer;
25
25
  toastController: AppToastController;
26
26
  outputBuffer?: TerminalOutputBuffer;
27
+ loadingConversationOverlayText?: () => string | undefined;
27
28
  voiceProgressOverlayText(): string | undefined;
28
29
  };
29
30
  export declare class AppRenderController {
@@ -38,4 +39,5 @@ export declare class AppRenderController {
38
39
  private renderClickFlashOverlay;
39
40
  private updateStatusMouseState;
40
41
  private renderVoiceProgressOverlay;
42
+ private renderConversationLoadingOverlay;
41
43
  }
@@ -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);
@@ -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) {
@@ -293,15 +294,19 @@ export class AppRenderController {
293
294
  this.deps.mouseController.statusThinkingTarget = this.deps.statusLineRenderer.thinkingTarget(statusLayout.text, statusRow);
294
295
  this.deps.mouseController.statusContextTarget = this.deps.statusLineRenderer.contextTarget(statusLayout.text, statusRow, statusLayout);
295
296
  this.deps.mouseController.statusModelUsageTarget = this.deps.statusLineRenderer.modelUsageTarget(statusLayout.text, statusRow, statusLayout);
296
- this.deps.mouseController.statusDraftQueueTarget = widgetLayout ? this.deps.statusLineRenderer.draftQueueTarget?.(widgetLayout, widgetRow) : undefined;
297
- this.deps.mouseController.statusUserJumpTarget = widgetLayout ? this.deps.statusLineRenderer.userJumpTarget?.(widgetLayout, widgetRow) : undefined;
298
- this.deps.mouseController.statusThinkingExpandTarget = widgetLayout ? this.deps.statusLineRenderer.thinkingExpandTarget?.(widgetLayout, widgetRow) : undefined;
299
- this.deps.mouseController.statusCompactToolsTarget = widgetLayout ? this.deps.statusLineRenderer.compactToolsTarget?.(widgetLayout, widgetRow) : undefined;
300
- this.deps.mouseController.statusTerminalBellSoundTarget = widgetLayout ? this.deps.statusLineRenderer.terminalBellSoundTarget?.(widgetLayout, widgetRow) : undefined;
297
+ if (widgetLayout) {
298
+ this.deps.mouseController.statusDraftQueueTarget = this.deps.statusLineRenderer.draftQueueTarget?.(widgetLayout, widgetRow);
299
+ this.deps.mouseController.statusUserJumpTarget = this.deps.statusLineRenderer.userJumpTarget?.(widgetLayout, widgetRow);
300
+ this.deps.mouseController.statusThinkingExpandTarget = this.deps.statusLineRenderer.thinkingExpandTarget?.(widgetLayout, widgetRow);
301
+ this.deps.mouseController.statusCompactToolsTarget = this.deps.statusLineRenderer.compactToolsTarget?.(widgetLayout, widgetRow);
302
+ this.deps.mouseController.statusTerminalBellSoundTarget = this.deps.statusLineRenderer.terminalBellSoundTarget?.(widgetLayout, widgetRow);
303
+ }
301
304
  this.deps.mouseController.statusSessionTarget = this.deps.statusLineRenderer.sessionTarget(statusLayout.text, statusRow, statusLayout.sessionLabel, statusLayout.workspaceLabel);
302
- this.deps.mouseController.statusPromptEnhancerTarget = widgetLayout ? this.deps.statusLineRenderer.promptEnhancerTarget(widgetLayout, widgetRow) : undefined;
303
- this.deps.mouseController.statusVoiceMicTarget = widgetLayout ? this.deps.statusLineRenderer.voiceMicTarget(widgetLayout, widgetRow) : undefined;
304
- this.deps.mouseController.statusVoiceLanguageTarget = widgetLayout ? this.deps.statusLineRenderer.voiceLanguageTarget(widgetLayout, widgetRow) : undefined;
305
+ if (widgetLayout) {
306
+ this.deps.mouseController.statusPromptEnhancerTarget = this.deps.statusLineRenderer.promptEnhancerTarget(widgetLayout, widgetRow);
307
+ this.deps.mouseController.statusVoiceMicTarget = this.deps.statusLineRenderer.voiceMicTarget(widgetLayout, widgetRow);
308
+ this.deps.mouseController.statusVoiceLanguageTarget = this.deps.statusLineRenderer.voiceLanguageTarget(widgetLayout, widgetRow);
309
+ }
305
310
  this.deps.mouseController.renderedRowTexts.set(statusRow, statusLayout.text);
306
311
  }
307
312
  renderVoiceProgressOverlay(message, width, rows) {
@@ -323,40 +328,40 @@ export class AppRenderController {
323
328
  ].join("");
324
329
  return { row: Math.min(2, rows - 1), text, output };
325
330
  }
331
+ renderConversationLoadingOverlay(message, width, topReservedRows, bodyHeight) {
332
+ if (!message || width <= 0 || bodyHeight <= 0)
333
+ return undefined;
334
+ const overlayWidth = Math.min(stringDisplayWidth(message), width);
335
+ const leftWidth = Math.max(0, Math.floor((width - overlayWidth) / 2));
336
+ const rightWidth = Math.max(0, width - leftWidth - overlayWidth);
337
+ const text = `${" ".repeat(leftWidth)}${padOrTrimPlain(message, overlayWidth)}${" ".repeat(rightWidth)}`;
338
+ const row = topReservedRows + Math.floor((bodyHeight + 1) / 2);
339
+ const output = this.deps.screenStyler.styleLine(row, text, width, {
340
+ foreground: this.deps.theme.colors.muted,
341
+ });
342
+ return { row, text, output };
343
+ }
326
344
  }
327
345
  function visibleToastStates(toastController) {
328
346
  const candidate = toastController;
329
347
  return typeof candidate.visibleStates === "function" ? candidate.visibleStates() : candidate.toast?.visibleStates ?? [];
330
348
  }
331
349
  function inputFrameLine(width, edge) {
350
+ void edge;
332
351
  if (width <= 0)
333
352
  return "";
334
- if (width === 1)
335
- return edge === "top" ? INPUT_FRAME.topLeft : INPUT_FRAME.bottomLeft;
336
- const left = edge === "top" ? INPUT_FRAME.topLeft : INPUT_FRAME.bottomLeft;
337
- const right = edge === "top" ? INPUT_FRAME.topRight : INPUT_FRAME.bottomRight;
338
- return `${left}${INPUT_FRAME.horizontal.repeat(Math.max(0, width - 2))}${right}`;
353
+ return INPUT_FRAME.horizontal.repeat(width);
339
354
  }
340
355
  function frameRenderedLine(line, width, theme, screenStyler) {
341
356
  if (width <= 0)
342
357
  return { line, text: "", output: () => "" };
343
- if (width === 1) {
344
- const border = colorize("", {
345
- foreground: theme.colors.inputBorder,
346
- });
347
- return { line, text: "│", output: () => border };
348
- }
349
- const innerWidth = Math.max(0, width - 2);
350
- const innerText = padOrTrimPlain(line?.text ?? "", innerWidth);
351
- const innerLine = line ? frameInnerRenderedLine(line, innerText, innerWidth) : undefined;
352
- const leftBorder = colorize("│", {
353
- foreground: theme.colors.inputBorder,
354
- });
355
- const rightBorder = leftBorder;
358
+ void theme;
359
+ const text = padOrTrimPlain(line?.text ?? "", width);
360
+ const outputLine = line ? frameInnerRenderedLine(line, text, width) : undefined;
356
361
  return {
357
362
  line,
358
- text: `│${innerText}│`,
359
- output: (row) => `${leftBorder}${screenStyler.styleBaseLine(row, innerLine, innerWidth)}${rightBorder}`,
363
+ text,
364
+ output: (row) => screenStyler.styleBaseLine(row, outputLine, width),
360
365
  };
361
366
  }
362
367
  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) {
@@ -110,7 +110,7 @@ export class StatusLineRenderer {
110
110
  }
111
111
  inputBorderWidgetSegments(layout, text) {
112
112
  const colors = this.host.theme.colors;
113
- const background = colors.inputBorder;
113
+ const background = colors.inputBorderWidgetBackground;
114
114
  const segments = [];
115
115
  const pushWidgetSegment = (widget, foreground) => {
116
116
  if (!widget)
@@ -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 {
@@ -21,8 +21,6 @@ export type InputFrameCopyRows = {
21
21
  inputEndRow: number;
22
22
  inputSeparatorRow: number;
23
23
  inputBottomSeparatorRow: number;
24
- contentStartColumn: number;
25
- contentEndColumn: number;
26
24
  };
27
25
  export type AppMouseControllerHost = {
28
26
  terminalColumns(): number;
@@ -154,6 +152,7 @@ export declare class AppMouseController {
154
152
  private handleStatusContextClick;
155
153
  private handleStatusModelUsageClick;
156
154
  private handleStatusUserJumpClick;
155
+ private handleInputBorderStatusClick;
157
156
  private openStatusUserJumpMenu;
158
157
  private handleStatusDraftQueueClick;
159
158
  private handleStatusThinkingExpandClick;
@@ -57,6 +57,8 @@ export class AppMouseController {
57
57
  if (this.handleInputScrollBar(event))
58
58
  return;
59
59
  this.showClickFlashOnPress(event);
60
+ if (event.button === 0 && !event.released && this.handleInputBorderStatusClick(event))
61
+ return;
60
62
  if (this.handleMouseSelection(event))
61
63
  return;
62
64
  if (this.withClickFlash(event, () => this.handleImageClick(event)))
@@ -73,24 +75,8 @@ export class AppMouseController {
73
75
  return;
74
76
  if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusModelUsageClick(event)))
75
77
  return;
76
- if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusDraftQueueClick(event)))
77
- return;
78
- if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusUserJumpClick(event)))
79
- return;
80
- if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusThinkingExpandClick(event)))
81
- return;
82
- if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusCompactToolsClick(event)))
83
- return;
84
- if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusTerminalBellSoundClick(event)))
85
- return;
86
78
  if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusSessionClick(event)))
87
79
  return;
88
- if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusPromptEnhancerClick(event)))
89
- return;
90
- if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusVoiceMicClick(event)))
91
- return;
92
- if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusVoiceLanguageClick(event)))
93
- return;
94
80
  if (event.button === 0 && this.withClickFlash(event, () => this.handleExtensionInputClick(event)))
95
81
  return;
96
82
  if (event.button === 0 && this.withClickFlash(event, () => this.handleInputClick(event)))
@@ -290,6 +276,7 @@ export class AppMouseController {
290
276
  this.statusUserJumpTarget,
291
277
  this.statusThinkingExpandTarget,
292
278
  this.statusCompactToolsTarget,
279
+ this.statusTerminalBellSoundTarget,
293
280
  this.statusSessionTarget,
294
281
  this.statusPromptEnhancerTarget,
295
282
  this.statusVoiceMicTarget,
@@ -333,7 +320,7 @@ export class AppMouseController {
333
320
  return false;
334
321
  const opened = this.host.openFileLink?.(link) ?? openDetectedFileLink(link);
335
322
  if (!opened)
336
- this.host.showToast("Could not open file link. Install the Zed CLI or set ZED_CLI.", "warning");
323
+ this.host.showToast("Could not open file link in the detected editor or system viewer.", "warning");
337
324
  return true;
338
325
  }
339
326
  handleInputScrollBar(event) {
@@ -476,6 +463,16 @@ export class AppMouseController {
476
463
  void this.openStatusUserJumpMenu();
477
464
  return true;
478
465
  }
466
+ handleInputBorderStatusClick(event) {
467
+ return this.handleStatusDraftQueueClick(event)
468
+ || this.handleStatusUserJumpClick(event)
469
+ || this.handleStatusThinkingExpandClick(event)
470
+ || this.handleStatusCompactToolsClick(event)
471
+ || this.handleStatusTerminalBellSoundClick(event)
472
+ || this.handleStatusPromptEnhancerClick(event)
473
+ || this.handleStatusVoiceMicClick(event)
474
+ || this.handleStatusVoiceLanguageClick(event);
475
+ }
479
476
  async openStatusUserJumpMenu() {
480
477
  try {
481
478
  const refreshPromise = this.host.refreshUserMessageJumpMenuItems?.();
@@ -792,8 +789,6 @@ export class AppMouseController {
792
789
  inputEndRow: toScreenRowExclusive(layout.inputStartRow + layout.renderedInput.lines.length),
793
790
  inputSeparatorRow: toScreenRow(layout.inputSeparatorRow),
794
791
  inputBottomSeparatorRow: toScreenRow(layout.inputBottomSeparatorRow),
795
- contentStartColumn: 2,
796
- contentEndColumn: columns,
797
792
  };
798
793
  }
799
794
  getSelectedConversationText(anchor, current) {
@@ -803,11 +798,13 @@ export class AppMouseController {
803
798
  const renderedLines = this.host.conversationViewport().slice(width, range.start.line, count);
804
799
  const lines = [];
805
800
  for (let index = 0; index < count; index += 1) {
806
- const text = renderedLines[index]?.text ?? "";
801
+ const rendered = renderedLines[index];
802
+ const text = rendered?.text ?? "";
807
803
  const line = range.start.line + index;
808
804
  const startColumn = line === range.start.line ? range.start.x : 1;
809
805
  const endColumn = line === range.end.line ? range.end.x : text.length + 1;
810
- lines.push(sliceByDisplayColumns(text, startColumn, endColumn).trimEnd());
806
+ const lineText = sliceByDisplayColumns(text, startColumn, endColumn);
807
+ lines.push(lineText.trimEnd());
811
808
  }
812
809
  return lines.join("\n").replace(/\s+$/u, "");
813
810
  }
@@ -941,13 +938,7 @@ export function screenSelectionLineText(row, text, startColumn, endColumn, input
941
938
  if (inputFrame && (row === inputFrame.inputSeparatorRow || row === inputFrame.inputBottomSeparatorRow)) {
942
939
  return undefined;
943
940
  }
944
- let copyStartColumn = startColumn;
945
- let copyEndColumn = endColumn;
946
- if (inputFrame && row >= inputFrame.inputStartRow && row < inputFrame.inputEndRow) {
947
- copyStartColumn = Math.max(copyStartColumn, inputFrame.contentStartColumn);
948
- copyEndColumn = Math.min(copyEndColumn, inputFrame.contentEndColumn);
949
- }
950
- return sliceByDisplayColumns(text, copyStartColumn, copyEndColumn);
941
+ return sliceByDisplayColumns(text, startColumn, endColumn);
951
942
  }
952
943
  function sameConversationPoint(left, right) {
953
944
  return !!left && left.line === right.line && left.x === right.x;
@@ -75,7 +75,7 @@ export class ScreenStyler {
75
75
  }
76
76
  styleInputLine(row, text, tagSpans, suggestionSpans, width, tagColor, suggestionColor, frameColor) {
77
77
  const colors = this.host.theme.colors;
78
- const baseOptions = { foreground: colors.inputForeground };
78
+ const baseOptions = { foreground: colors.warning };
79
79
  if (this.selectionRangeForRow(row, width, text))
80
80
  return this.styleLine(row, text, width, baseOptions);
81
81
  const plain = padOrTrimPlain(text, width);
@@ -8,4 +8,4 @@ export type LazySessionHistoryReader = {
8
8
  hasOlder(): boolean;
9
9
  readOlder(limit: number): Promise<SessionEntry[]>;
10
10
  };
11
- export declare function openLazySessionManager(sessionPath: string, options?: LazySessionManagerOptions): SessionManager;
11
+ export declare function openLazySessionManager(sessionPath: string, options?: LazySessionManagerOptions): Promise<SessionManager>;