pi-ui-extend 0.1.9 → 0.1.11

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 (92) hide show
  1. package/README.md +23 -2
  2. package/dist/app/app.d.ts +4 -0
  3. package/dist/app/app.js +74 -7
  4. package/dist/app/cli/install.d.ts +2 -0
  5. package/dist/app/cli/install.js +16 -1
  6. package/dist/app/commands/command-controller.js +4 -0
  7. package/dist/app/commands/command-host.d.ts +4 -0
  8. package/dist/app/commands/command-model-actions.d.ts +5 -0
  9. package/dist/app/commands/command-model-actions.js +104 -0
  10. package/dist/app/commands/command-navigation-actions.d.ts +6 -1
  11. package/dist/app/commands/command-navigation-actions.js +37 -14
  12. package/dist/app/commands/command-registry.d.ts +4 -0
  13. package/dist/app/commands/command-registry.js +32 -0
  14. package/dist/app/commands/command-session-actions.d.ts +1 -0
  15. package/dist/app/commands/command-session-actions.js +15 -5
  16. package/dist/app/commands/shell-controller.d.ts +1 -0
  17. package/dist/app/commands/shell-controller.js +1 -1
  18. package/dist/app/constants.d.ts +1 -1
  19. package/dist/app/constants.js +1 -1
  20. package/dist/app/icons.js +1 -1
  21. package/dist/app/input/autocomplete-controller.d.ts +52 -0
  22. package/dist/app/input/autocomplete-controller.js +352 -0
  23. package/dist/app/input/input-action-controller.d.ts +1 -0
  24. package/dist/app/input/input-action-controller.js +21 -0
  25. package/dist/app/input/input-controller.d.ts +1 -0
  26. package/dist/app/input/input-controller.js +2 -0
  27. package/dist/app/input/input-paste-handler.d.ts +1 -0
  28. package/dist/app/input/input-paste-handler.js +22 -18
  29. package/dist/app/input/voice-controller.d.ts +2 -0
  30. package/dist/app/input/voice-controller.js +27 -15
  31. package/dist/app/model/model-usage-status.d.ts +9 -0
  32. package/dist/app/model/model-usage-status.js +124 -34
  33. package/dist/app/popup/popup-action-controller.js +1 -1
  34. package/dist/app/process.d.ts +17 -0
  35. package/dist/app/process.js +68 -0
  36. package/dist/app/rendering/conversation-entry-renderer.js +17 -6
  37. package/dist/app/rendering/conversation-tool-renderer.js +3 -2
  38. package/dist/app/rendering/editor-layout-renderer.d.ts +1 -0
  39. package/dist/app/rendering/editor-layout-renderer.js +11 -1
  40. package/dist/app/rendering/message-content.js +65 -7
  41. package/dist/app/rendering/render-controller.js +6 -1
  42. package/dist/app/rendering/render-text.d.ts +3 -0
  43. package/dist/app/rendering/render-text.js +51 -3
  44. package/dist/app/rendering/status-line-renderer.d.ts +5 -1
  45. package/dist/app/rendering/status-line-renderer.js +69 -25
  46. package/dist/app/rendering/tool-block-renderer.js +13 -31
  47. package/dist/app/runtime.d.ts +6 -1
  48. package/dist/app/runtime.js +35 -2
  49. package/dist/app/screen/clipboard.d.ts +2 -2
  50. package/dist/app/screen/clipboard.js +13 -18
  51. package/dist/app/screen/mouse-controller.d.ts +5 -2
  52. package/dist/app/screen/mouse-controller.js +16 -1
  53. package/dist/app/screen/screen-styler.d.ts +4 -1
  54. package/dist/app/screen/screen-styler.js +3 -2
  55. package/dist/app/screen/status-controller.d.ts +3 -0
  56. package/dist/app/screen/status-controller.js +23 -8
  57. package/dist/app/session/queued-message-controller.d.ts +7 -1
  58. package/dist/app/session/queued-message-controller.js +32 -21
  59. package/dist/app/session/resume-session-loader.d.ts +15 -0
  60. package/dist/app/session/resume-session-loader.js +204 -0
  61. package/dist/app/session/session-event-controller.d.ts +5 -1
  62. package/dist/app/session/session-event-controller.js +72 -5
  63. package/dist/app/session/session-history.js +4 -3
  64. package/dist/app/session/session-lifecycle-controller.d.ts +5 -0
  65. package/dist/app/session/session-lifecycle-controller.js +9 -1
  66. package/dist/app/session/tabs-controller.d.ts +10 -1
  67. package/dist/app/session/tabs-controller.js +101 -5
  68. package/dist/app/terminal/nerd-font-controller.js +16 -17
  69. package/dist/app/terminal/terminal-controller.d.ts +1 -0
  70. package/dist/app/terminal/terminal-controller.js +1 -0
  71. package/dist/app/types.d.ts +14 -0
  72. package/dist/app/workspace/workspace-actions-controller.d.ts +1 -1
  73. package/dist/app/workspace/workspace-actions-controller.js +3 -3
  74. package/dist/app/workspace/workspace-undo.d.ts +1 -1
  75. package/dist/app/workspace/workspace-undo.js +22 -20
  76. package/dist/config.d.ts +27 -0
  77. package/dist/config.js +174 -1
  78. package/dist/default-pix-config.js +38 -353
  79. package/dist/input-editor.d.ts +7 -1
  80. package/dist/input-editor.js +47 -6
  81. package/dist/markdown-format.d.ts +1 -0
  82. package/dist/markdown-format.js +26 -1
  83. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +1 -0
  84. package/external/pi-tools-suite/src/dcp/prompts.ts +1 -0
  85. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +45 -195
  86. package/external/pi-tools-suite/src/lib/lsp.ts +2 -1
  87. package/external/pi-tools-suite/src/lsp/_shared/output.ts +8 -7
  88. package/external/pi-tools-suite/src/lsp/manager.ts +4 -4
  89. package/external/pi-tools-suite/src/repo-discovery/index.ts +49 -2
  90. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +9 -1
  91. package/external/pi-tools-suite/src/tool-descriptions.ts +1 -1
  92. package/package.json +1 -1
@@ -1,8 +1,16 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { expandTabs, padOrTrimDisplay, sliceByDisplayWidth, stringDisplayWidth, wrapDisplayLine } from "../../terminal-width.js";
3
3
  import { APP_ICONS } from "../icons.js";
4
+ const LSP_DIAGNOSTIC_ICON = "\u{f0026}";
4
5
  export function sanitizeText(text) {
5
- return expandTabs(text.replace(/⚠️?/gu, APP_ICONS.alert).replace(/\x1b/g, "␛").replace(/\r/g, ""));
6
+ return expandTabs(text.replace(/⚠️?|\u{f0026}/gu, APP_ICONS.alert).replace(/\x1b/g, "␛").replace(/\r/g, ""));
7
+ }
8
+ export function alertIconPrefixLength(text) {
9
+ if (text.startsWith(APP_ICONS.alert))
10
+ return APP_ICONS.alert.length;
11
+ if (text.startsWith(LSP_DIAGNOSTIC_ICON))
12
+ return LSP_DIAGNOSTIC_ICON.length;
13
+ return /^⚠️?/u.exec(text)?.[0].length;
6
14
  }
7
15
  export function normalizePastedTextForDuplicateKey(text) {
8
16
  return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
@@ -17,12 +25,49 @@ const LSP_DIAGNOSTIC_MUTATION_TOOLS = new Set(["apply_patch", "ast_apply"]);
17
25
  export function hasToolLspDiagnosticsAfterMutation(entry) {
18
26
  return LSP_DIAGNOSTIC_MUTATION_TOOLS.has(entry.toolName.toLowerCase()) && hasLspDiagnosticsAfterMutation(entry.output);
19
27
  }
28
+ export function lspDiagnosticSeverityForLine(line) {
29
+ const counts = lspDiagnosticCounts(line);
30
+ const countSeverity = lspDiagnosticCountSeverity(counts);
31
+ if (countSeverity)
32
+ return countSeverity;
33
+ if (counts.length > 0)
34
+ return undefined;
35
+ const severityMatch = /(?:^|[^\p{L}\p{N}_])(?:diagnosticseverity\.)?(errors?|warnings?|warn|hints?)(?=$|[^\p{L}\p{N}_])/iu.exec(line);
36
+ const severity = severityMatch?.[1]?.toLowerCase();
37
+ if (!severity)
38
+ return undefined;
39
+ if (severity.startsWith("error"))
40
+ return "error";
41
+ if (severity.startsWith("warn"))
42
+ return "warning";
43
+ return "hint";
44
+ }
45
+ function lspDiagnosticCounts(line) {
46
+ return [...line.matchAll(/\b(\d+)\s+(errors?|warnings?|hints?)\b/giu)];
47
+ }
48
+ function lspDiagnosticCountSeverity(counts) {
49
+ for (const severity of ["error", "warning", "hint"]) {
50
+ if (counts.some((match) => Number(match[1]) > 0 && match[2]?.toLowerCase().startsWith(severity)))
51
+ return severity;
52
+ }
53
+ return undefined;
54
+ }
55
+ export function toolLspDiagnosticsAfterMutationSeverity(entry) {
56
+ if (!hasToolLspDiagnosticsAfterMutation(entry))
57
+ return undefined;
58
+ if (/\blsp\s+errors?\s+after\s+mutation\b/i.test(entry.output))
59
+ return "error";
60
+ const diagnosticLines = entry.output.split("\n").map((line) => line.trim());
61
+ if (diagnosticLines.some((line) => lspDiagnosticSeverityForLine(line) === "error"))
62
+ return "error";
63
+ return "warning";
64
+ }
20
65
  export function toolStatusIcon(entry) {
21
66
  if (entry.status === "running")
22
67
  return APP_ICONS.timerSand;
23
68
  if (entry.isError)
24
69
  return APP_ICONS.closeCircle;
25
- if (hasToolLspDiagnosticsAfterMutation(entry))
70
+ if (toolLspDiagnosticsAfterMutationSeverity(entry))
26
71
  return APP_ICONS.alert;
27
72
  return APP_ICONS.checkCircle;
28
73
  }
@@ -31,7 +76,10 @@ export function toolStatusIconColor(entry, colors) {
31
76
  return colors.muted;
32
77
  if (entry.isError)
33
78
  return colors.error;
34
- if (hasToolLspDiagnosticsAfterMutation(entry))
79
+ const lspSeverity = toolLspDiagnosticsAfterMutationSeverity(entry);
80
+ if (lspSeverity === "error")
81
+ return colors.error;
82
+ if (lspSeverity === "warning")
35
83
  return colors.warning;
36
84
  return colors.success;
37
85
  }
@@ -1,6 +1,6 @@
1
1
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
2
2
  import type { Theme } from "../../theme.js";
3
- import type { SessionActivity, StatusCompactToolsTarget, StatusContextTarget, StatusLineLayout, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "../types.js";
3
+ import type { SessionActivity, StatusCompactToolsTarget, StatusContextTarget, StatusDraftQueueTarget, StatusLineLayout, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "../types.js";
4
4
  import type { ScreenStyler } from "../screen/screen-styler.js";
5
5
  import { type ModelColorsConfig } from "../../config.js";
6
6
  export type StatusLineRendererHost = {
@@ -26,6 +26,7 @@ export type StatusLineRendererHost = {
26
26
  terminalBellSoundStatusWidgetEnabled(): boolean;
27
27
  voiceStatusWidgetText(): string;
28
28
  voiceStatusWidgetActive(): boolean;
29
+ queueableInputActive?(): boolean;
29
30
  userMessageJumpMenuActive?(): boolean;
30
31
  allThinkingExpandedActive?(): boolean;
31
32
  superCompactToolsActive?(): boolean;
@@ -43,6 +44,7 @@ export declare class StatusLineRenderer {
43
44
  voiceMicTarget(layout: StatusLineLayout, row: number): StatusVoiceMicTarget | undefined;
44
45
  voiceLanguageTarget(layout: StatusLineLayout, row: number): StatusVoiceLanguageTarget | undefined;
45
46
  userJumpTarget(layout: StatusLineLayout, row: number): StatusUserJumpTarget | undefined;
47
+ draftQueueTarget(layout: StatusLineLayout, row: number): StatusDraftQueueTarget | undefined;
46
48
  thinkingExpandTarget(layout: StatusLineLayout, row: number): StatusThinkingExpandTarget | undefined;
47
49
  compactToolsTarget(layout: StatusLineLayout, row: number): StatusCompactToolsTarget | undefined;
48
50
  terminalBellSoundTarget(layout: StatusLineLayout, row: number): StatusTerminalBellSoundTarget | undefined;
@@ -50,6 +52,8 @@ export declare class StatusLineRenderer {
50
52
  private segments;
51
53
  private pushPromptEnhancerWidgetSegment;
52
54
  private pushUserJumpWidgetSegment;
55
+ private pushDraftQueueWidgetSegment;
56
+ private draftQueueWidgetText;
53
57
  private pushThinkingExpandWidgetSegment;
54
58
  private pushCompactToolsWidgetSegment;
55
59
  private pushTerminalBellSoundWidgetSegment;
@@ -13,13 +13,17 @@ export class StatusLineRenderer {
13
13
  const contentWidth = Math.max(1, width);
14
14
  const left = 0;
15
15
  const statusDot = APP_ICONS.record;
16
+ const draftQueueButton = this.draftQueueWidgetText();
16
17
  const userJumpButton = APP_ICONS.user;
17
18
  const thinkingExpandButton = APP_ICONS.thinkingExpanded;
18
19
  const compactToolsButton = APP_ICONS.compactTools;
19
20
  const terminalBellSoundWidgetText = this.host.terminalBellSoundStatusWidgetText();
20
21
  const promptEnhancerWidgetText = this.host.promptEnhancerStatusWidgetText();
21
22
  const voiceWidgetText = this.host.voiceStatusWidgetText();
22
- const rightWidgetText = [userJumpButton, terminalBellSoundWidgetText, thinkingExpandButton, compactToolsButton, promptEnhancerWidgetText, voiceWidgetText].filter((text) => text.length > 0).join(" ");
23
+ const rightWidgetParts = draftQueueButton.length > 0
24
+ ? [draftQueueButton, promptEnhancerWidgetText, userJumpButton, terminalBellSoundWidgetText, thinkingExpandButton, compactToolsButton, voiceWidgetText]
25
+ : [userJumpButton, terminalBellSoundWidgetText, thinkingExpandButton, compactToolsButton, promptEnhancerWidgetText, voiceWidgetText];
26
+ const rightWidgetText = rightWidgetParts.filter((text) => text.length > 0).join(" ");
23
27
  const rightWidgetWidth = stringDisplayWidth(rightWidgetText);
24
28
  const leftWidth = rightWidgetWidth > 0 && contentWidth > rightWidgetWidth + 1 ? contentWidth - rightWidgetWidth - 1 : contentWidth;
25
29
  const baseStatus = this.host.currentStatus();
@@ -34,37 +38,60 @@ export class StatusLineRenderer {
34
38
  const innerText = leftWidth < contentWidth ? `${leftText} ${rightWidgetText}` : padOrTrimPlain(leftText, contentWidth);
35
39
  const text = padOrTrimPlain(innerText, width);
36
40
  let nextWidgetStartColumn = left + leftWidth + 2;
37
- const userJumpWidget = leftWidth < contentWidth
38
- ? this.widgetLayout(nextWidgetStartColumn, userJumpButton)
41
+ let draftQueueWidget = leftWidth < contentWidth && draftQueueButton.length > 0
42
+ ? this.widgetLayout(nextWidgetStartColumn, draftQueueButton)
39
43
  : undefined;
40
- if (userJumpWidget)
41
- nextWidgetStartColumn = userJumpWidget.endColumn + 1;
42
- const terminalBellSoundWidget = leftWidth < contentWidth && terminalBellSoundWidgetText.length > 0
43
- ? this.widgetLayout(nextWidgetStartColumn, terminalBellSoundWidgetText)
44
- : undefined;
45
- if (terminalBellSoundWidget)
46
- nextWidgetStartColumn = terminalBellSoundWidget.endColumn + 1;
47
- const thinkingExpandWidget = leftWidth < contentWidth
48
- ? this.widgetLayout(nextWidgetStartColumn, thinkingExpandButton)
49
- : undefined;
50
- if (thinkingExpandWidget)
51
- nextWidgetStartColumn = thinkingExpandWidget.endColumn + 1;
52
- const compactToolsWidget = leftWidth < contentWidth
53
- ? this.widgetLayout(nextWidgetStartColumn, compactToolsButton)
54
- : undefined;
55
- if (compactToolsWidget)
56
- nextWidgetStartColumn = compactToolsWidget.endColumn + 1;
57
- const promptEnhancerWidget = leftWidth < contentWidth && promptEnhancerWidgetText.length > 0
58
- ? this.widgetLayout(nextWidgetStartColumn, promptEnhancerWidgetText)
59
- : undefined;
60
- if (promptEnhancerWidget)
61
- nextWidgetStartColumn = promptEnhancerWidget.endColumn + 1;
44
+ if (draftQueueWidget)
45
+ nextWidgetStartColumn = draftQueueWidget.endColumn + 1;
46
+ let promptEnhancerWidget;
47
+ let userJumpWidget;
48
+ let terminalBellSoundWidget;
49
+ let thinkingExpandWidget;
50
+ let compactToolsWidget;
51
+ const appendPromptEnhancerWidget = () => {
52
+ promptEnhancerWidget = leftWidth < contentWidth && promptEnhancerWidgetText.length > 0
53
+ ? this.widgetLayout(nextWidgetStartColumn, promptEnhancerWidgetText)
54
+ : undefined;
55
+ if (promptEnhancerWidget)
56
+ nextWidgetStartColumn = promptEnhancerWidget.endColumn + 1;
57
+ };
58
+ const appendCoreStatusWidgets = () => {
59
+ userJumpWidget = leftWidth < contentWidth
60
+ ? this.widgetLayout(nextWidgetStartColumn, userJumpButton)
61
+ : undefined;
62
+ if (userJumpWidget)
63
+ nextWidgetStartColumn = userJumpWidget.endColumn + 1;
64
+ terminalBellSoundWidget = leftWidth < contentWidth && terminalBellSoundWidgetText.length > 0
65
+ ? this.widgetLayout(nextWidgetStartColumn, terminalBellSoundWidgetText)
66
+ : undefined;
67
+ if (terminalBellSoundWidget)
68
+ nextWidgetStartColumn = terminalBellSoundWidget.endColumn + 1;
69
+ thinkingExpandWidget = leftWidth < contentWidth
70
+ ? this.widgetLayout(nextWidgetStartColumn, thinkingExpandButton)
71
+ : undefined;
72
+ if (thinkingExpandWidget)
73
+ nextWidgetStartColumn = thinkingExpandWidget.endColumn + 1;
74
+ compactToolsWidget = leftWidth < contentWidth
75
+ ? this.widgetLayout(nextWidgetStartColumn, compactToolsButton)
76
+ : undefined;
77
+ if (compactToolsWidget)
78
+ nextWidgetStartColumn = compactToolsWidget.endColumn + 1;
79
+ };
80
+ if (draftQueueWidget) {
81
+ appendPromptEnhancerWidget();
82
+ appendCoreStatusWidgets();
83
+ }
84
+ else {
85
+ appendCoreStatusWidgets();
86
+ appendPromptEnhancerWidget();
87
+ }
62
88
  const voiceWidget = leftWidth < contentWidth && voiceWidgetText.length > 0 ? this.voiceWidgetLayout(nextWidgetStartColumn, voiceWidgetText) : undefined;
63
89
  return {
64
90
  details,
65
91
  text,
66
92
  sessionLabel,
67
93
  workspaceLabel,
94
+ ...(draftQueueWidget ? { draftQueueWidget } : {}),
68
95
  ...(userJumpWidget ? { userJumpWidget } : {}),
69
96
  ...(thinkingExpandWidget ? { thinkingExpandWidget } : {}),
70
97
  ...(compactToolsWidget ? { compactToolsWidget } : {}),
@@ -157,6 +184,12 @@ export class StatusLineRenderer {
157
184
  return undefined;
158
185
  return { row, startColumn: widget.startColumn, endColumn: widget.endColumn };
159
186
  }
187
+ draftQueueTarget(layout, row) {
188
+ const widget = layout.draftQueueWidget;
189
+ if (!widget)
190
+ return undefined;
191
+ return { row, startColumn: widget.startColumn, endColumn: widget.endColumn };
192
+ }
160
193
  thinkingExpandTarget(layout, row) {
161
194
  const widget = layout.thinkingExpandWidget;
162
195
  if (!widget)
@@ -195,6 +228,7 @@ export class StatusLineRenderer {
195
228
  end: statusDotStart + APP_ICONS.record.length,
196
229
  foreground: this.statusDotColor(),
197
230
  }] : [];
231
+ this.pushDraftQueueWidgetSegment(segments, statusText);
198
232
  this.pushUserJumpWidgetSegment(segments, statusText);
199
233
  this.pushThinkingExpandWidgetSegment(segments, statusText);
200
234
  this.pushCompactToolsWidgetSegment(segments, statusText);
@@ -248,6 +282,16 @@ export class StatusLineRenderer {
248
282
  : this.host.theme.colors.muted;
249
283
  this.pushSegment(segments, start, buttonText.length, foreground);
250
284
  }
285
+ pushDraftQueueWidgetSegment(segments, statusText) {
286
+ const buttonText = this.draftQueueWidgetText();
287
+ const start = statusText.indexOf(buttonText);
288
+ if (start < 0 || buttonText.length <= 0)
289
+ return;
290
+ this.pushSegment(segments, start, buttonText.length, this.host.theme.colors.info);
291
+ }
292
+ draftQueueWidgetText() {
293
+ return this.host.queueableInputActive?.() ? APP_ICONS.timerSand : "";
294
+ }
251
295
  pushThinkingExpandWidgetSegment(segments, statusText) {
252
296
  const buttonText = APP_ICONS.thinkingExpanded;
253
297
  const start = statusText.indexOf(buttonText);
@@ -1,12 +1,12 @@
1
1
  import { resolveColor } from "../../config.js";
2
2
  import { expandTabs, sliceByDisplayWidth, stringDisplayWidth, wrapDisplayLineByWords } from "../../terminal-width.js";
3
- import { hasToolLspDiagnosticsAfterMutation, sanitizeText, toolStatusIcon, toolStatusIconColor, wrapLine } from "./render-text.js";
3
+ import { alertIconPrefixLength, hasToolLspDiagnosticsAfterMutation, lspDiagnosticSeverityForLine, sanitizeText, toolStatusIcon, toolStatusIconColor, wrapLine } from "./render-text.js";
4
4
  const TRUNCATED_PREVIEW_MARKER = "… ";
5
5
  export function renderToolBlock(entry, rule, width, colors, options = {}) {
6
6
  if (rule.hidden)
7
7
  return [];
8
8
  const hasLspDiagnostics = hasToolLspDiagnosticsAfterMutation(entry);
9
- const expanded = entry.expanded && !options.superCompact;
9
+ const expanded = entry.expanded;
10
10
  const stateIcon = toolStatusIcon(entry);
11
11
  const toolColor = resolveColor(rule.color, colors);
12
12
  const toolOutputColor = colors.statusForeground;
@@ -31,7 +31,7 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
31
31
  headerLines.push(...renderToolBodyLines(entry.expandedText, width, target, toolOutputColor, entry.bodyStyle, colors, entry.syntaxHighlight, entry.bodyWrap, hasLspDiagnostics, entry.bodyLineStyles, entry.preserveAnsi));
32
32
  return headerLines;
33
33
  }
34
- if (rule.compactHidden || rule.defaultExpanded === true)
34
+ if (rule.compactHidden || (rule.defaultExpanded === true && !options.superCompact))
35
35
  return headerLines;
36
36
  const body = entry.collapsedBody.trimEnd();
37
37
  if (!body || rule.previewLines === 0)
@@ -111,7 +111,10 @@ function renderToolBodyLines(text, width, target, color, style, colors, syntaxHi
111
111
  segment.bold = diffStyle.bold;
112
112
  line.segments = [segment];
113
113
  }
114
- else if (lspDiagnosticStyle) {
114
+ else if (lspDiagnosticStyle?.kind === "alert" && wrapIndex === 0) {
115
+ line.segments = [{ start: 2, end: 2 + lspDiagnosticStyle.length, foreground: colors.warning, bold: true }];
116
+ }
117
+ else if (lspDiagnosticStyle?.kind === "severity") {
115
118
  line.segments = [{ start: 2, end: line.text.length, foreground: lspDiagnosticStyle.foreground }];
116
119
  }
117
120
  else if (bodyLineStyle && line.text.length > 2) {
@@ -348,37 +351,16 @@ function bodyLineStyleForLine(styles, lineIndex, colors) {
348
351
  return resolvedForeground ? { ...segment, foreground: resolvedForeground } : segment;
349
352
  }
350
353
  function lspDiagnosticLineStyle(line, colors) {
354
+ const alertLength = alertIconPrefixLength(line);
355
+ if (alertLength != null)
356
+ return { kind: "alert", length: alertLength };
351
357
  const severity = lspDiagnosticSeverityForLine(line);
352
358
  if (severity === "error")
353
- return { foreground: colors.error };
359
+ return { kind: "severity", foreground: colors.error };
354
360
  if (severity === "warning")
355
- return { foreground: colors.warning };
361
+ return { kind: "severity", foreground: colors.warning };
356
362
  if (severity === "hint")
357
- return { foreground: colors.muted };
358
- return undefined;
359
- }
360
- function lspDiagnosticSeverityForLine(line) {
361
- const countSeverity = lspDiagnosticCountSeverity(line);
362
- if (countSeverity)
363
- return countSeverity;
364
- const severityMatch = /(?:^|[^\p{L}\p{N}_])(?:diagnosticseverity\.)?(errors?|warnings?|warn|hints?)(?=$|[^\p{L}\p{N}_])/iu.exec(line);
365
- const severity = severityMatch?.[1]?.toLowerCase();
366
- if (!severity)
367
- return undefined;
368
- if (severity.startsWith("error"))
369
- return "error";
370
- if (severity.startsWith("warn"))
371
- return "warning";
372
- return "hint";
373
- }
374
- function lspDiagnosticCountSeverity(line) {
375
- const counts = [...line.matchAll(/\b(\d+)\s+(errors?|warnings?|hints?)\b/giu)];
376
- if (counts.length === 0)
377
- return undefined;
378
- for (const severity of ["error", "warning", "hint"]) {
379
- if (counts.some((match) => Number(match[1]) > 0 && match[2]?.toLowerCase().startsWith(severity)))
380
- return severity;
381
- }
363
+ return { kind: "severity", foreground: colors.muted };
382
364
  return undefined;
383
365
  }
384
366
  function syntaxHighlightForLine(highlights, lineIndex) {
@@ -1,4 +1,5 @@
1
- import { type EventBus, type AgentSessionRuntime, type LoadExtensionsResult } from "@earendil-works/pi-coding-agent";
1
+ import { SessionManager, type EventBus, type AgentSessionRuntime, type LoadExtensionsResult, type SessionEntry } from "@earendil-works/pi-coding-agent";
2
+ import { type PixConfig } from "../config.js";
2
3
  import type { AppOptions } from "./types.js";
3
4
  export type PiToolsSuiteInstallAction = "installed" | "already-installed" | "existing-kept" | "missing-source";
4
5
  export type PiToolsSuiteInstallResult = {
@@ -36,4 +37,8 @@ export declare function prioritizeBundledQuestionExtension(base: LoadExtensionsR
36
37
  export type CreatePixRuntimeOptions = {
37
38
  eventBus?: EventBus;
38
39
  };
40
+ type RuntimeSessionManagerModelState = Pick<SessionManager, "getEntries" | "getBranch">;
41
+ export declare function resolvePixRuntimeModelRef(options: Pick<AppOptions, "modelRef">, sessionManager: RuntimeSessionManagerModelState, config?: PixConfig): string | undefined;
42
+ export declare function resolveSessionModelRefFromTail(entries: readonly SessionEntry[]): string | undefined;
39
43
  export declare function createPixRuntime(options: AppOptions, runtimeOptions?: CreatePixRuntimeOptions): Promise<AgentSessionRuntime>;
44
+ export {};
@@ -4,8 +4,9 @@ import { homedir } from "node:os";
4
4
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, SessionManager, } from "@earendil-works/pi-coding-agent";
7
+ import { loadPixConfig, resolveDefaultModelRef } from "../config.js";
7
8
  import { PI_FAVORITE_MODEL_REFS } from "./constants.js";
8
- import { parseModelRef, parseScopedModelRef } from "./model/model-ref.js";
9
+ import { isThinkingLevel, parseModelRef, parseScopedModelRef } from "./model/model-ref.js";
9
10
  const BUNDLED_QUESTION_EXTENSION_NAME = "question";
10
11
  const PI_TOOLS_SUITE_EXTENSION_NAME = "pi-tools-suite";
11
12
  const BUNDLED_EXTENSIONS_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "../..", "extensions");
@@ -131,10 +132,42 @@ function isBundledQuestionConflict(error, bundledExtensionPaths) {
131
132
  }
132
133
  return false;
133
134
  }
135
+ export function resolvePixRuntimeModelRef(options, sessionManager, config = loadPixConfig()) {
136
+ if (options.modelRef)
137
+ return options.modelRef;
138
+ const existingEntryCount = sessionManager.getEntries().length;
139
+ if (existingEntryCount > 0)
140
+ return resolveSessionModelRefFromTail(sessionManager.getBranch());
141
+ return resolveDefaultModelRef(config);
142
+ }
143
+ export function resolveSessionModelRefFromTail(entries) {
144
+ let modelRef;
145
+ let thinkingLevel;
146
+ for (let index = entries.length - 1; index >= 0 && (modelRef === undefined || thinkingLevel === undefined); index--) {
147
+ const entry = entries[index];
148
+ if (!entry)
149
+ continue;
150
+ if (thinkingLevel === undefined && entry.type === "thinking_level_change" && isThinkingLevel(entry.thinkingLevel)) {
151
+ thinkingLevel = entry.thinkingLevel;
152
+ }
153
+ if (modelRef !== undefined)
154
+ continue;
155
+ if (entry.type === "model_change") {
156
+ modelRef = `${entry.provider}/${entry.modelId}`;
157
+ }
158
+ else if (entry.type === "message" && entry.message.role === "assistant") {
159
+ modelRef = `${entry.message.provider}/${entry.message.model}`;
160
+ }
161
+ }
162
+ if (!modelRef)
163
+ return undefined;
164
+ return thinkingLevel ? `${modelRef}:${thinkingLevel}` : modelRef;
165
+ }
134
166
  export async function createPixRuntime(options, runtimeOptions = {}) {
135
- const parsedModel = options.modelRef ? parseModelRef(options.modelRef) : undefined;
136
167
  const agentDir = getAgentDir();
137
168
  const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
169
+ const effectiveModelRef = resolvePixRuntimeModelRef(options, sessionManager);
170
+ const parsedModel = effectiveModelRef ? parseModelRef(effectiveModelRef) : undefined;
138
171
  await ensureBundledSkillsInstalled();
139
172
  await ensurePiToolsSuiteExtensionInstalled({ agentDir });
140
173
  const bundledExtensionPaths = getBundledExtensionPaths();
@@ -1,4 +1,4 @@
1
- export declare function copyTextToClipboard(text: string): void;
2
- export declare function clipboardSupportAvailable(env?: NodeJS.ProcessEnv): boolean;
1
+ export declare function copyTextToClipboard(text: string): Promise<void>;
2
+ export declare function clipboardSupportAvailable(env?: NodeJS.ProcessEnv): Promise<boolean>;
3
3
  export declare function clipboardInstallHint(): string;
4
4
  export declare function osc52ClipboardSequence(text: string, env?: NodeJS.ProcessEnv): string;
@@ -1,22 +1,24 @@
1
- import { spawnSync } from "node:child_process";
2
1
  import { createRequire } from "node:module";
2
+ import { commandExists, runProcess } from "../process.js";
3
3
  const require = createRequire(import.meta.url);
4
- export function copyTextToClipboard(text) {
4
+ export async function copyTextToClipboard(text) {
5
5
  const commands = clipboardCommands();
6
6
  for (const [command, args] of commands) {
7
- const result = spawnSync(command, args, { input: text, stdio: ["pipe", "ignore", "ignore"] });
7
+ const result = await runProcess(command, args, { input: text, maxBufferBytes: 1024 });
8
8
  if (!result.error && result.status === 0)
9
9
  return;
10
10
  }
11
- if (copyWithNativeClipboard(text))
11
+ if (await copyWithNativeClipboard(text))
12
12
  return;
13
13
  if (copyWithOsc52(text))
14
14
  return;
15
15
  throw new Error(`No clipboard command found. ${clipboardInstallHint()}`);
16
16
  }
17
- export function clipboardSupportAvailable(env = process.env) {
18
- if (clipboardCommands().some(([command]) => commandExists(command, env)))
19
- return true;
17
+ export async function clipboardSupportAvailable(env = process.env) {
18
+ for (const [command] of clipboardCommands()) {
19
+ if (await commandExists(command, env))
20
+ return true;
21
+ }
20
22
  return resolveNativeClipboardEntrypoint() !== undefined;
21
23
  }
22
24
  export function clipboardInstallHint() {
@@ -44,7 +46,7 @@ function clipboardCommands() {
44
46
  ];
45
47
  }
46
48
  }
47
- function copyWithNativeClipboard(text) {
49
+ async function copyWithNativeClipboard(text) {
48
50
  const entrypoint = resolveNativeClipboardEntrypoint();
49
51
  if (!entrypoint)
50
52
  return false;
@@ -55,10 +57,10 @@ function copyWithNativeClipboard(text) {
55
57
  const clipboard = require(${JSON.stringify(entrypoint)});
56
58
  await clipboard.setText(readFileSync(0, "utf8"));
57
59
  `;
58
- const result = spawnSync(process.execPath, ["--input-type=module", "-e", script], {
60
+ const result = await runProcess(process.execPath, ["--input-type=module", "-e", script], {
59
61
  input: text,
60
- stdio: ["pipe", "ignore", "ignore"],
61
- timeout: 3_000,
62
+ timeoutMs: 3_000,
63
+ maxBufferBytes: 1024,
62
64
  });
63
65
  return !result.error && result.status === 0;
64
66
  }
@@ -84,10 +86,3 @@ function resolveNativeClipboardEntrypoint() {
84
86
  return undefined;
85
87
  }
86
88
  }
87
- function commandExists(command, env) {
88
- const names = process.platform === "win32" ? [command, command.replace(/\.exe$/iu, ".cmd"), command.replace(/\.exe$/iu, ".bat")] : [command];
89
- return names.some((name) => spawnSync(process.platform === "win32" ? "where" : "sh", process.platform === "win32" ? [name] : ["-lc", `command -v ${shellQuote(name)}`], { env, stdio: "ignore" }).status === 0);
90
- }
91
- function shellQuote(value) {
92
- return `'${value.replaceAll("'", `'\\''`)}'`;
93
- }
@@ -6,7 +6,7 @@ import type { ToastEntry, ToastVariant } from "../../ui.js";
6
6
  import type { AppPopupActionController } from "../popup/popup-action-controller.js";
7
7
  import type { AppPopupMenuController } from "../popup/popup-menu-controller.js";
8
8
  import type { AppScrollController } from "./scroll-controller.js";
9
- import type { Entry, ImageClickTarget, MouseEvent, MouseSelection, StatusContextTarget, StatusCompactToolsTarget, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, TabLineMouseTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "../types.js";
9
+ import type { Entry, ImageClickTarget, MouseEvent, MouseSelection, StatusContextTarget, StatusCompactToolsTarget, StatusDraftQueueTarget, StatusModelTarget, StatusModelUsageTarget, StatusPromptEnhancerTarget, StatusSessionTarget, StatusTerminalBellSoundTarget, TabLineMouseTarget, StatusThinkingExpandTarget, StatusThinkingTarget, StatusUserJumpTarget, StatusVoiceLanguageTarget, StatusVoiceMicTarget } from "../types.js";
10
10
  import { type RenderedLink } from "./file-links.js";
11
11
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
12
12
  type ClickFlash = {
@@ -56,10 +56,11 @@ export type AppMouseControllerHost = {
56
56
  }): void;
57
57
  dismissToast(toastId: number): void;
58
58
  refreshModelUsageStatus(): void | Promise<void>;
59
+ queueInputFromStatus?(): void | Promise<void>;
59
60
  toggleAllThinkingExpanded?(): void;
60
61
  toggleSuperCompactTools?(): void;
61
62
  toggleTerminalBellSound?(): void;
62
- copyTextToClipboard?(text: string): void;
63
+ copyTextToClipboard?(text: string): void | Promise<void>;
63
64
  handleExtensionInputMouse(event: MouseEvent & {
64
65
  localRow: number;
65
66
  localColumn: number;
@@ -100,6 +101,7 @@ export declare class AppMouseController {
100
101
  statusContextTarget: StatusContextTarget | undefined;
101
102
  statusModelUsageTarget: StatusModelUsageTarget | undefined;
102
103
  statusUserJumpTarget: StatusUserJumpTarget | undefined;
104
+ statusDraftQueueTarget: StatusDraftQueueTarget | undefined;
103
105
  statusThinkingExpandTarget: StatusThinkingExpandTarget | undefined;
104
106
  statusCompactToolsTarget: StatusCompactToolsTarget | undefined;
105
107
  statusTerminalBellSoundTarget: StatusTerminalBellSoundTarget | undefined;
@@ -153,6 +155,7 @@ export declare class AppMouseController {
153
155
  private handleStatusContextClick;
154
156
  private handleStatusModelUsageClick;
155
157
  private handleStatusUserJumpClick;
158
+ private handleStatusDraftQueueClick;
156
159
  private handleStatusThinkingExpandClick;
157
160
  private handleStatusCompactToolsClick;
158
161
  private handleStatusTerminalBellSoundClick;
@@ -24,6 +24,7 @@ export class AppMouseController {
24
24
  statusContextTarget;
25
25
  statusModelUsageTarget;
26
26
  statusUserJumpTarget;
27
+ statusDraftQueueTarget;
27
28
  statusThinkingExpandTarget;
28
29
  statusCompactToolsTarget;
29
30
  statusTerminalBellSoundTarget;
@@ -75,6 +76,8 @@ export class AppMouseController {
75
76
  return;
76
77
  if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusModelUsageClick(event)))
77
78
  return;
79
+ if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusDraftQueueClick(event)))
80
+ return;
78
81
  if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusUserJumpClick(event)))
79
82
  return;
80
83
  if (event.button === 0 && this.withClickFlash(event, () => this.handleStatusThinkingExpandClick(event)))
@@ -286,6 +289,7 @@ export class AppMouseController {
286
289
  this.statusThinkingTarget,
287
290
  this.statusContextTarget,
288
291
  this.statusModelUsageTarget,
292
+ this.statusDraftQueueTarget,
289
293
  this.statusUserJumpTarget,
290
294
  this.statusThinkingExpandTarget,
291
295
  this.statusCompactToolsTarget,
@@ -509,6 +513,15 @@ export class AppMouseController {
509
513
  this.host.render();
510
514
  return true;
511
515
  }
516
+ handleStatusDraftQueueClick(event) {
517
+ const target = this.statusDraftQueueTarget;
518
+ if (!target)
519
+ return false;
520
+ if (event.y !== target.row || event.x < target.startColumn || event.x >= target.endColumn)
521
+ return false;
522
+ void this.host.queueInputFromStatus?.();
523
+ return true;
524
+ }
512
525
  handleStatusThinkingExpandClick(event) {
513
526
  const target = this.statusThinkingExpandTarget;
514
527
  if (!target)
@@ -769,7 +782,9 @@ export class AppMouseController {
769
782
  return this.getSelectedScreenText(selection.anchor, selection.current);
770
783
  }
771
784
  copyTextToClipboard(text) {
772
- (this.host.copyTextToClipboard ?? copyTextToClipboard)(text);
785
+ void Promise.resolve((this.host.copyTextToClipboard ?? copyTextToClipboard)(text)).catch((error) => {
786
+ this.host.showToast(error instanceof Error ? error.message : String(error), "error");
787
+ });
773
788
  }
774
789
  getSelectedScreenText(anchor, current) {
775
790
  const range = orderedSelection(anchor, current);
@@ -24,7 +24,10 @@ export declare class ScreenStyler {
24
24
  styleInputLine(row: number, text: string, tagSpans: readonly {
25
25
  start: number;
26
26
  end: number;
27
- }[] | undefined, width: number, tagColor: string, frameColor?: string): string;
27
+ }[] | undefined, suggestionSpans: readonly {
28
+ start: number;
29
+ end: number;
30
+ }[] | undefined, width: number, tagColor: string, suggestionColor: string, frameColor?: string): string;
28
31
  private styleAnsiLine;
29
32
  selectionRangeForRow(row: number, width: number): {
30
33
  startIndex: number;
@@ -72,14 +72,14 @@ export class ScreenStyler {
72
72
  colorize(after, options),
73
73
  ].join("");
74
74
  }
75
- styleInputLine(row, text, tagSpans, width, tagColor, frameColor) {
75
+ styleInputLine(row, text, tagSpans, suggestionSpans, width, tagColor, suggestionColor, frameColor) {
76
76
  const colors = this.host.theme.colors;
77
77
  const baseOptions = { foreground: colors.inputForeground };
78
78
  if (this.selectionRangeForRow(row, width))
79
79
  return this.styleLine(row, text, width, baseOptions);
80
80
  const plain = padOrTrimPlain(text, width);
81
81
  const frameSpans = inputFrameSpans(plain, width, frameColor);
82
- if ((!tagSpans || tagSpans.length === 0) && frameSpans.length === 0) {
82
+ if ((!tagSpans || tagSpans.length === 0) && (!suggestionSpans || suggestionSpans.length === 0) && frameSpans.length === 0) {
83
83
  return hasAnsi(plain) ? this.styleAnsiLine(plain, baseOptions) : colorize(plain, baseOptions);
84
84
  }
85
85
  const chunks = [];
@@ -88,6 +88,7 @@ export class ScreenStyler {
88
88
  const spans = [
89
89
  ...frameSpans,
90
90
  ...(tagSpans ?? []).map((span) => ({ ...span, foreground: tagColor, bold: true })),
91
+ ...(suggestionSpans ?? []).map((span) => ({ ...span, foreground: suggestionColor })),
91
92
  ].sort((a, b) => a.start - b.start || a.end - b.end);
92
93
  for (const span of spans) {
93
94
  const start = Math.max(offset, Math.min(endOffset, span.start));
@@ -7,12 +7,14 @@ export type AppStatusControllerHost = {
7
7
  readonly theme: Theme;
8
8
  readonly blinkController: AppBlinkController;
9
9
  runtimeSession(): AgentSession | undefined;
10
+ render(): void;
10
11
  };
11
12
  export declare class AppStatusController {
12
13
  private readonly host;
13
14
  private status;
14
15
  private statusFollowsSession;
15
16
  private gitBranchCache;
17
+ private gitBranchLookupInFlight;
16
18
  sessionActivity: SessionActivity;
17
19
  get statusDotBright(): boolean;
18
20
  constructor(host: AppStatusControllerHost);
@@ -31,5 +33,6 @@ export declare class AppStatusController {
31
33
  roundedContextUsagePercent(session: AgentSession): number | undefined;
32
34
  contextUsagePercentColor(percent: number): string;
33
35
  private currentGitBranchName;
36
+ private refreshGitBranchName;
34
37
  private startStatusBlink;
35
38
  }