pi-ui-extend 0.1.9 → 0.1.13

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 (121) hide show
  1. package/README.md +23 -2
  2. package/dist/app/app.d.ts +4 -0
  3. package/dist/app/app.js +76 -7
  4. package/dist/app/cli/install.d.ts +16 -0
  5. package/dist/app/cli/install.js +34 -7
  6. package/dist/app/cli/startup-info.js +5 -2
  7. package/dist/app/cli/update.d.ts +7 -0
  8. package/dist/app/cli/update.js +11 -3
  9. package/dist/app/commands/command-controller.js +4 -0
  10. package/dist/app/commands/command-host.d.ts +4 -0
  11. package/dist/app/commands/command-model-actions.d.ts +5 -0
  12. package/dist/app/commands/command-model-actions.js +104 -0
  13. package/dist/app/commands/command-navigation-actions.d.ts +6 -1
  14. package/dist/app/commands/command-navigation-actions.js +37 -14
  15. package/dist/app/commands/command-registry.d.ts +4 -0
  16. package/dist/app/commands/command-registry.js +32 -0
  17. package/dist/app/commands/command-session-actions.d.ts +1 -0
  18. package/dist/app/commands/command-session-actions.js +15 -5
  19. package/dist/app/commands/shell-command.d.ts +7 -0
  20. package/dist/app/commands/shell-command.js +12 -4
  21. package/dist/app/commands/shell-controller.d.ts +1 -0
  22. package/dist/app/commands/shell-controller.js +1 -1
  23. package/dist/app/constants.d.ts +1 -1
  24. package/dist/app/constants.js +1 -1
  25. package/dist/app/icons.d.ts +1 -0
  26. package/dist/app/icons.js +3 -1
  27. package/dist/app/input/autocomplete-controller.d.ts +52 -0
  28. package/dist/app/input/autocomplete-controller.js +352 -0
  29. package/dist/app/input/input-action-controller.d.ts +1 -0
  30. package/dist/app/input/input-action-controller.js +21 -0
  31. package/dist/app/input/input-controller.d.ts +1 -0
  32. package/dist/app/input/input-controller.js +2 -0
  33. package/dist/app/input/input-paste-handler.d.ts +1 -0
  34. package/dist/app/input/input-paste-handler.js +22 -18
  35. package/dist/app/input/prompt-enhancer-controller.d.ts +7 -1
  36. package/dist/app/input/prompt-enhancer-controller.js +12 -3
  37. package/dist/app/input/voice-controller.d.ts +51 -1
  38. package/dist/app/input/voice-controller.js +42 -19
  39. package/dist/app/model/model-usage-status.d.ts +9 -0
  40. package/dist/app/model/model-usage-status.js +124 -34
  41. package/dist/app/popup/popup-action-controller.js +1 -1
  42. package/dist/app/process.d.ts +17 -0
  43. package/dist/app/process.js +68 -0
  44. package/dist/app/rendering/conversation-entry-renderer.js +8 -6
  45. package/dist/app/rendering/conversation-tool-renderer.js +3 -2
  46. package/dist/app/rendering/editor-layout-renderer.d.ts +1 -0
  47. package/dist/app/rendering/editor-layout-renderer.js +11 -1
  48. package/dist/app/rendering/message-content.js +65 -7
  49. package/dist/app/rendering/render-controller.js +6 -1
  50. package/dist/app/rendering/render-text.d.ts +3 -0
  51. package/dist/app/rendering/render-text.js +51 -3
  52. package/dist/app/rendering/status-line-renderer.d.ts +5 -1
  53. package/dist/app/rendering/status-line-renderer.js +61 -25
  54. package/dist/app/rendering/toast-renderer.js +10 -13
  55. package/dist/app/rendering/tool-block-renderer.d.ts +1 -0
  56. package/dist/app/rendering/tool-block-renderer.js +16 -33
  57. package/dist/app/runtime.d.ts +6 -1
  58. package/dist/app/runtime.js +35 -2
  59. package/dist/app/screen/clipboard.d.ts +11 -2
  60. package/dist/app/screen/clipboard.js +29 -21
  61. package/dist/app/screen/file-link-opener.d.ts +8 -0
  62. package/dist/app/screen/file-link-opener.js +11 -3
  63. package/dist/app/screen/file-links.js +3 -3
  64. package/dist/app/screen/image-opener.d.ts +12 -0
  65. package/dist/app/screen/image-opener.js +13 -5
  66. package/dist/app/screen/mouse-controller.d.ts +5 -2
  67. package/dist/app/screen/mouse-controller.js +16 -1
  68. package/dist/app/screen/screen-styler.d.ts +4 -1
  69. package/dist/app/screen/screen-styler.js +3 -2
  70. package/dist/app/screen/status-controller.d.ts +3 -0
  71. package/dist/app/screen/status-controller.js +23 -8
  72. package/dist/app/session/queued-message-controller.d.ts +7 -1
  73. package/dist/app/session/queued-message-controller.js +36 -21
  74. package/dist/app/session/resume-session-loader.d.ts +15 -0
  75. package/dist/app/session/resume-session-loader.js +204 -0
  76. package/dist/app/session/session-event-controller.d.ts +5 -1
  77. package/dist/app/session/session-event-controller.js +72 -5
  78. package/dist/app/session/session-history.js +4 -3
  79. package/dist/app/session/session-lifecycle-controller.d.ts +5 -0
  80. package/dist/app/session/session-lifecycle-controller.js +9 -1
  81. package/dist/app/session/tabs-controller.d.ts +10 -1
  82. package/dist/app/session/tabs-controller.js +101 -5
  83. package/dist/app/terminal/nerd-font-controller.d.ts +16 -0
  84. package/dist/app/terminal/nerd-font-controller.js +30 -23
  85. package/dist/app/terminal/terminal-controller.d.ts +1 -0
  86. package/dist/app/terminal/terminal-controller.js +1 -0
  87. package/dist/app/types.d.ts +14 -0
  88. package/dist/app/workspace/workspace-actions-controller.d.ts +1 -1
  89. package/dist/app/workspace/workspace-actions-controller.js +3 -3
  90. package/dist/app/workspace/workspace-undo.d.ts +1 -1
  91. package/dist/app/workspace/workspace-undo.js +22 -20
  92. package/dist/config.d.ts +27 -0
  93. package/dist/config.js +174 -1
  94. package/dist/default-pix-config.js +39 -353
  95. package/dist/input-editor.d.ts +7 -1
  96. package/dist/input-editor.js +47 -6
  97. package/dist/markdown-format.d.ts +1 -0
  98. package/dist/markdown-format.js +26 -1
  99. package/dist/schemas/index.d.ts +5 -0
  100. package/dist/schemas/index.js +5 -0
  101. package/dist/schemas/pi-tools-suite-schema.d.ts +177 -0
  102. package/dist/schemas/pi-tools-suite-schema.js +218 -0
  103. package/dist/schemas/pix-schema.d.ts +65 -0
  104. package/dist/schemas/pix-schema.js +91 -0
  105. package/dist/terminal-width.js +73 -56
  106. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +3 -0
  107. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +1 -0
  108. package/external/pi-tools-suite/src/dcp/prompts.ts +1 -0
  109. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +46 -195
  110. package/external/pi-tools-suite/src/lib/lsp.ts +2 -1
  111. package/external/pi-tools-suite/src/lsp/_shared/output.ts +8 -7
  112. package/external/pi-tools-suite/src/lsp/manager.ts +4 -4
  113. package/external/pi-tools-suite/src/repo-discovery/index.ts +49 -2
  114. package/external/pi-tools-suite/src/todo/index.ts +4 -2
  115. package/external/pi-tools-suite/src/todo/state/selectors.ts +4 -0
  116. package/external/pi-tools-suite/src/todo/todo.ts +2 -6
  117. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +9 -1
  118. package/external/pi-tools-suite/src/tool-descriptions.ts +1 -1
  119. package/package.json +12 -3
  120. package/schemas/pi-tools-suite.json +881 -0
  121. package/schemas/pix.json +298 -0
@@ -1,4 +1,10 @@
1
1
  import { isRecord } from "../guards.js";
2
+ const MAX_FORMAT_STRING_CHARS = 256 * 1024;
3
+ const MAX_RENDERED_CONTENT_CHARS = 512 * 1024;
4
+ const MAX_STRUCTURED_DEPTH = 8;
5
+ const MAX_STRUCTURED_ARRAY_ITEMS = 200;
6
+ const MAX_STRUCTURED_OBJECT_KEYS = 200;
7
+ const TRUNCATED_MARKER = "\n[… truncated …]";
2
8
  export function stringifyUnknown(value) {
3
9
  if (typeof value === "string")
4
10
  return value;
@@ -13,7 +19,7 @@ export function stringifyUnknown(value) {
13
19
  return name;
14
20
  }
15
21
  try {
16
- return JSON.stringify(value, null, 2);
22
+ return JSON.stringify(normalizeStructuredValue(value), null, 2);
17
23
  }
18
24
  catch {
19
25
  return String(value);
@@ -24,8 +30,10 @@ export function formatStructuredText(value) {
24
30
  const trimmed = value.trim();
25
31
  if (!trimmed)
26
32
  return "(empty)";
33
+ if (trimmed.length > MAX_FORMAT_STRING_CHARS)
34
+ return truncateText(value, MAX_FORMAT_STRING_CHARS);
27
35
  try {
28
- return JSON.stringify(JSON.parse(trimmed), null, 2);
36
+ return JSON.stringify(normalizeStructuredValue(JSON.parse(trimmed)), null, 2);
29
37
  }
30
38
  catch {
31
39
  return value;
@@ -36,25 +44,40 @@ export function formatStructuredText(value) {
36
44
  export function renderContent(content) {
37
45
  const parts = [];
38
46
  let imageCount = 0;
47
+ let renderedChars = 0;
48
+ const pushPart = (part) => {
49
+ const remaining = MAX_RENDERED_CONTENT_CHARS - renderedChars;
50
+ if (remaining <= 0)
51
+ return false;
52
+ const next = part.length > remaining ? truncateText(part, remaining) : part;
53
+ parts.push(next);
54
+ renderedChars += next.length;
55
+ return part.length <= remaining;
56
+ };
39
57
  for (const item of content) {
40
58
  if (!isRecord(item)) {
41
- parts.push(stringifyUnknown(item));
59
+ if (!pushPart(stringifyUnknown(item)))
60
+ break;
42
61
  continue;
43
62
  }
44
63
  if (isImageContent(item)) {
45
64
  imageCount += 1;
46
- parts.push(imageContentLabel(item, imageCount));
65
+ if (!pushPart(imageContentLabel(item, imageCount)))
66
+ break;
47
67
  continue;
48
68
  }
49
69
  if (typeof item.text === "string") {
50
- parts.push(item.text);
70
+ if (!pushPart(item.text))
71
+ break;
51
72
  continue;
52
73
  }
53
74
  if (typeof item.thinking === "string") {
54
- parts.push(item.thinking);
75
+ if (!pushPart(item.thinking))
76
+ break;
55
77
  continue;
56
78
  }
57
- parts.push(stringifyUnknown(item));
79
+ if (!pushPart(stringifyUnknown(item)))
80
+ break;
58
81
  }
59
82
  return parts.join("\n");
60
83
  }
@@ -113,3 +136,38 @@ export function submittedUserDisplayText(displayText, promptText, images) {
113
136
  return userImageLabels(images.length);
114
137
  return promptText.trimEnd();
115
138
  }
139
+ function truncateText(text, maxChars) {
140
+ if (text.length <= maxChars)
141
+ return text;
142
+ return `${text.slice(0, Math.max(0, maxChars))}${TRUNCATED_MARKER}`;
143
+ }
144
+ function normalizeStructuredValue(value, depth = 0, seen = new WeakSet()) {
145
+ if (typeof value === "string")
146
+ return truncateText(value, MAX_FORMAT_STRING_CHARS);
147
+ if (!value || typeof value !== "object")
148
+ return value;
149
+ if (depth >= MAX_STRUCTURED_DEPTH)
150
+ return "[… truncated: depth limit …]";
151
+ if (seen.has(value))
152
+ return "[… circular …]";
153
+ seen.add(value);
154
+ if (Array.isArray(value)) {
155
+ const items = value.slice(0, MAX_STRUCTURED_ARRAY_ITEMS).map((item) => normalizeStructuredValue(item, depth + 1, seen));
156
+ if (value.length > MAX_STRUCTURED_ARRAY_ITEMS)
157
+ items.push(`[… ${value.length - MAX_STRUCTURED_ARRAY_ITEMS} more items …]`);
158
+ return items;
159
+ }
160
+ if (value instanceof Error)
161
+ return value.message || value.name;
162
+ const output = {};
163
+ let count = 0;
164
+ for (const [key, child] of Object.entries(value)) {
165
+ if (count >= MAX_STRUCTURED_OBJECT_KEYS) {
166
+ output["…"] = "truncated: object key limit";
167
+ break;
168
+ }
169
+ output[key] = normalizeStructuredValue(child, depth + 1, seen);
170
+ count += 1;
171
+ }
172
+ return output;
173
+ }
@@ -68,8 +68,11 @@ export class AppRenderController {
68
68
  this.deps.mouseController.statusThinkingTarget = undefined;
69
69
  this.deps.mouseController.statusContextTarget = undefined;
70
70
  this.deps.mouseController.statusModelUsageTarget = undefined;
71
+ this.deps.mouseController.statusDraftQueueTarget = undefined;
72
+ this.deps.mouseController.statusUserJumpTarget = undefined;
71
73
  this.deps.mouseController.statusThinkingExpandTarget = undefined;
72
74
  this.deps.mouseController.statusCompactToolsTarget = undefined;
75
+ this.deps.mouseController.statusTerminalBellSoundTarget = undefined;
73
76
  this.deps.mouseController.statusSessionTarget = undefined;
74
77
  this.deps.mouseController.statusPromptEnhancerTarget = undefined;
75
78
  this.deps.mouseController.statusVoiceMicTarget = undefined;
@@ -165,10 +168,11 @@ export class AppRenderController {
165
168
  for (let index = 0; index < renderedInput.lines.length; index += 1) {
166
169
  const inputLine = renderedInput.lines[index] ?? "";
167
170
  const tagSpans = renderedInput.tagSpans[index];
171
+ const suggestionSpans = renderedInput.suggestionSpans?.[index] ?? [];
168
172
  const row = toScreenRow(inputStartRow + index);
169
173
  this.deps.mouseController.renderedRowTexts.set(row, inputLine);
170
174
  const tagColor = this.deps.theme.colors.accent;
171
- const styledLine = this.deps.screenStyler.styleInputLine(row, inputLine, tagSpans, columns, tagColor, this.deps.theme.colors.inputBorder);
175
+ const styledLine = this.deps.screenStyler.styleInputLine(row, inputLine, tagSpans, suggestionSpans, columns, tagColor, this.deps.theme.colors.muted, this.deps.theme.colors.inputBorder);
172
176
  appendFrameOutput("inputStatus", row, this.renderFrameRow(row, styledLine));
173
177
  }
174
178
  if (renderedInput.scrollBar && columns > 0) {
@@ -291,6 +295,7 @@ export class AppRenderController {
291
295
  this.deps.mouseController.statusThinkingTarget = this.deps.statusLineRenderer.thinkingTarget(statusLayout.text, statusRow);
292
296
  this.deps.mouseController.statusContextTarget = this.deps.statusLineRenderer.contextTarget(statusLayout.text, statusRow, statusLayout);
293
297
  this.deps.mouseController.statusModelUsageTarget = this.deps.statusLineRenderer.modelUsageTarget(statusLayout.text, statusRow, statusLayout);
298
+ this.deps.mouseController.statusDraftQueueTarget = this.deps.statusLineRenderer.draftQueueTarget?.(statusLayout, statusRow);
294
299
  this.deps.mouseController.statusUserJumpTarget = this.deps.statusLineRenderer.userJumpTarget?.(statusLayout, statusRow);
295
300
  this.deps.mouseController.statusThinkingExpandTarget = this.deps.statusLineRenderer.thinkingExpandTarget?.(statusLayout, statusRow);
296
301
  this.deps.mouseController.statusCompactToolsTarget = this.deps.statusLineRenderer.compactToolsTarget?.(statusLayout, statusRow);
@@ -1,10 +1,13 @@
1
1
  import type { Theme } from "../../theme.js";
2
2
  import type { ToolStatusEntry } from "../types.js";
3
3
  export declare function sanitizeText(text: string): string;
4
+ export declare function alertIconPrefixLength(text: string): number | undefined;
4
5
  export declare function normalizePastedTextForDuplicateKey(text: string): string;
5
6
  export declare function shortHash(text: string): string;
6
7
  export declare function hasLspDiagnosticsAfterMutation(output: string): boolean;
7
8
  export declare function hasToolLspDiagnosticsAfterMutation(entry: ToolStatusEntry): boolean;
9
+ export declare function lspDiagnosticSeverityForLine(line: string): "error" | "warning" | "hint" | undefined;
10
+ export declare function toolLspDiagnosticsAfterMutationSeverity(entry: ToolStatusEntry): "error" | "warning" | undefined;
8
11
  export declare function toolStatusIcon(entry: ToolStatusEntry): string;
9
12
  export declare function toolStatusIconColor(entry: ToolStatusEntry, colors: Theme["colors"]): string;
10
13
  export declare function wrapLine(text: string, width: number): string[];
@@ -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,15 @@ 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, promptEnhancerWidgetText, userJumpButton, terminalBellSoundWidgetText, thinkingExpandButton, compactToolsButton, voiceWidgetText];
24
+ const rightWidgetText = rightWidgetParts.filter((text) => text.length > 0).join(" ");
23
25
  const rightWidgetWidth = stringDisplayWidth(rightWidgetText);
24
26
  const leftWidth = rightWidgetWidth > 0 && contentWidth > rightWidgetWidth + 1 ? contentWidth - rightWidgetWidth - 1 : contentWidth;
25
27
  const baseStatus = this.host.currentStatus();
@@ -34,37 +36,54 @@ export class StatusLineRenderer {
34
36
  const innerText = leftWidth < contentWidth ? `${leftText} ${rightWidgetText}` : padOrTrimPlain(leftText, contentWidth);
35
37
  const text = padOrTrimPlain(innerText, width);
36
38
  let nextWidgetStartColumn = left + leftWidth + 2;
37
- const userJumpWidget = leftWidth < contentWidth
38
- ? this.widgetLayout(nextWidgetStartColumn, userJumpButton)
39
+ let draftQueueWidget = leftWidth < contentWidth && draftQueueButton.length > 0
40
+ ? this.widgetLayout(nextWidgetStartColumn, draftQueueButton)
39
41
  : 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;
42
+ if (draftQueueWidget)
43
+ nextWidgetStartColumn = draftQueueWidget.endColumn + 1;
44
+ let promptEnhancerWidget;
45
+ let userJumpWidget;
46
+ let terminalBellSoundWidget;
47
+ let thinkingExpandWidget;
48
+ let compactToolsWidget;
49
+ const appendPromptEnhancerWidget = () => {
50
+ promptEnhancerWidget = leftWidth < contentWidth && promptEnhancerWidgetText.length > 0
51
+ ? this.widgetLayout(nextWidgetStartColumn, promptEnhancerWidgetText)
52
+ : undefined;
53
+ if (promptEnhancerWidget)
54
+ nextWidgetStartColumn = promptEnhancerWidget.endColumn + 1;
55
+ };
56
+ const appendCoreStatusWidgets = () => {
57
+ userJumpWidget = leftWidth < contentWidth
58
+ ? this.widgetLayout(nextWidgetStartColumn, userJumpButton)
59
+ : undefined;
60
+ if (userJumpWidget)
61
+ nextWidgetStartColumn = userJumpWidget.endColumn + 1;
62
+ terminalBellSoundWidget = leftWidth < contentWidth && terminalBellSoundWidgetText.length > 0
63
+ ? this.widgetLayout(nextWidgetStartColumn, terminalBellSoundWidgetText)
64
+ : undefined;
65
+ if (terminalBellSoundWidget)
66
+ nextWidgetStartColumn = terminalBellSoundWidget.endColumn + 1;
67
+ thinkingExpandWidget = leftWidth < contentWidth
68
+ ? this.widgetLayout(nextWidgetStartColumn, thinkingExpandButton)
69
+ : undefined;
70
+ if (thinkingExpandWidget)
71
+ nextWidgetStartColumn = thinkingExpandWidget.endColumn + 1;
72
+ compactToolsWidget = leftWidth < contentWidth
73
+ ? this.widgetLayout(nextWidgetStartColumn, compactToolsButton)
74
+ : undefined;
75
+ if (compactToolsWidget)
76
+ nextWidgetStartColumn = compactToolsWidget.endColumn + 1;
77
+ };
78
+ appendPromptEnhancerWidget();
79
+ appendCoreStatusWidgets();
62
80
  const voiceWidget = leftWidth < contentWidth && voiceWidgetText.length > 0 ? this.voiceWidgetLayout(nextWidgetStartColumn, voiceWidgetText) : undefined;
63
81
  return {
64
82
  details,
65
83
  text,
66
84
  sessionLabel,
67
85
  workspaceLabel,
86
+ ...(draftQueueWidget ? { draftQueueWidget } : {}),
68
87
  ...(userJumpWidget ? { userJumpWidget } : {}),
69
88
  ...(thinkingExpandWidget ? { thinkingExpandWidget } : {}),
70
89
  ...(compactToolsWidget ? { compactToolsWidget } : {}),
@@ -157,6 +176,12 @@ export class StatusLineRenderer {
157
176
  return undefined;
158
177
  return { row, startColumn: widget.startColumn, endColumn: widget.endColumn };
159
178
  }
179
+ draftQueueTarget(layout, row) {
180
+ const widget = layout.draftQueueWidget;
181
+ if (!widget)
182
+ return undefined;
183
+ return { row, startColumn: widget.startColumn, endColumn: widget.endColumn };
184
+ }
160
185
  thinkingExpandTarget(layout, row) {
161
186
  const widget = layout.thinkingExpandWidget;
162
187
  if (!widget)
@@ -195,6 +220,7 @@ export class StatusLineRenderer {
195
220
  end: statusDotStart + APP_ICONS.record.length,
196
221
  foreground: this.statusDotColor(),
197
222
  }] : [];
223
+ this.pushDraftQueueWidgetSegment(segments, statusText);
198
224
  this.pushUserJumpWidgetSegment(segments, statusText);
199
225
  this.pushThinkingExpandWidgetSegment(segments, statusText);
200
226
  this.pushCompactToolsWidgetSegment(segments, statusText);
@@ -248,6 +274,16 @@ export class StatusLineRenderer {
248
274
  : this.host.theme.colors.muted;
249
275
  this.pushSegment(segments, start, buttonText.length, foreground);
250
276
  }
277
+ pushDraftQueueWidgetSegment(segments, statusText) {
278
+ const buttonText = this.draftQueueWidgetText();
279
+ const start = statusText.indexOf(buttonText);
280
+ if (start < 0 || buttonText.length <= 0)
281
+ return;
282
+ this.pushSegment(segments, start, buttonText.length, this.host.theme.colors.info);
283
+ }
284
+ draftQueueWidgetText() {
285
+ return this.host.queueableInputActive?.() ? APP_ICONS.timerSand : "";
286
+ }
251
287
  pushThinkingExpandWidgetSegment(segments, statusText) {
252
288
  const buttonText = APP_ICONS.thinkingExpanded;
253
289
  const start = statusText.indexOf(buttonText);
@@ -64,11 +64,9 @@ function renderDialogToastOverlay(state, width, maxRows, theme, rowOffset) {
64
64
  if (maxRows <= 0 || width <= 0)
65
65
  return [];
66
66
  const maxDialogWidth = Math.max(1, Math.min(width - 4, 72));
67
- const icon = toastKindIcon(state.kind);
68
67
  const closeLabel = `[${APP_ICONS.close}]`;
69
68
  const wrappedLines = dialogMessageLines(state.message, Math.max(1, maxDialogWidth - 4));
70
- const title = `${icon} Dialog`;
71
- const requiredWidth = Math.max(16, stringDisplayWidth(` ${title} ${closeLabel} `) + 2, ...wrappedLines.map((line) => stringDisplayWidth(line) + 4));
69
+ const requiredWidth = Math.max(16, stringDisplayWidth(closeLabel) + 4, ...wrappedLines.map((line) => stringDisplayWidth(line) + 4));
72
70
  const dialogWidth = Math.min(maxDialogWidth, Math.max(16, requiredWidth));
73
71
  const bodyWidth = Math.max(1, dialogWidth - 4);
74
72
  const bodyLines = dialogMessageLines(state.message, bodyWidth);
@@ -76,14 +74,14 @@ function renderDialogToastOverlay(state, width, maxRows, theme, rowOffset) {
76
74
  const visibleBodyLines = bodyLines.slice(0, bodyRows);
77
75
  const includeBottom = maxRows > 1;
78
76
  const dialogRows = [
79
- dialogTopLine(title, closeLabel, dialogWidth),
77
+ dialogTopLine(closeLabel, dialogWidth),
80
78
  ...visibleBodyLines.map((line) => `│ ${padOrTrimPlain(line, bodyWidth)} │`),
81
79
  ...(includeBottom ? [`╰${"─".repeat(Math.max(0, dialogWidth - 2))}╯`] : []),
82
80
  ].slice(0, maxRows);
83
81
  const leftWidth = Math.max(0, width - dialogWidth - 2);
84
82
  const column = leftWidth + 1;
85
83
  const style = toastKindStyle(state.kind, theme);
86
- const closeStartColumn = column + 1 + dialogTopCloseOffset(title, closeLabel, dialogWidth);
84
+ const closeStartColumn = column + 1 + dialogTopCloseOffset(closeLabel, dialogWidth);
87
85
  const closeEndColumn = closeStartColumn + stringDisplayWidth(closeLabel);
88
86
  return dialogRows.map((text, index) => ({
89
87
  id: state.id,
@@ -101,18 +99,17 @@ function dialogMessageLines(message, maxWidth) {
101
99
  const lines = sanitizeText(message).split("\n").flatMap((line) => wrapDisplayLine(line, safeMaxWidth));
102
100
  return lines.length > 0 ? lines : [""];
103
101
  }
104
- function dialogTopLine(title, closeLabel, width) {
102
+ function dialogTopLine(closeLabel, width) {
105
103
  const innerWidth = Math.max(0, width - 2);
106
- const closeOffset = dialogTopCloseOffset(title, closeLabel, width);
107
- const leftLabel = ` ${title} `;
108
- const spacer = " ".repeat(Math.max(0, closeOffset - stringDisplayWidth(leftLabel)));
109
- return `╭${padOrTrimPlain(`${leftLabel}${spacer}${closeLabel} `, innerWidth)}╮`;
104
+ const closeOffset = dialogTopCloseOffset(closeLabel, width);
105
+ const closeWidth = stringDisplayWidth(closeLabel);
106
+ const rightWidth = Math.max(0, innerWidth - closeOffset - closeWidth);
107
+ return `╭${"─".repeat(closeOffset)}${padOrTrimPlain(closeLabel, Math.min(closeWidth, innerWidth - closeOffset))}${"─".repeat(rightWidth)}╮`;
110
108
  }
111
- function dialogTopCloseOffset(title, closeLabel, width) {
109
+ function dialogTopCloseOffset(closeLabel, width) {
112
110
  const innerWidth = Math.max(0, width - 2);
113
- const leftLabel = ` ${title} `;
114
111
  const closeWidth = stringDisplayWidth(closeLabel);
115
- return Math.max(stringDisplayWidth(leftLabel), innerWidth - closeWidth - 1);
112
+ return Math.max(0, innerWidth - closeWidth - 1);
116
113
  }
117
114
  function toastKindIcon(kind) {
118
115
  switch (kind) {
@@ -6,6 +6,7 @@ import type { ToolBodyLineStyle, ToolHeaderSegment } from "../../tool-renderers/
6
6
  export type ToolBlockEntry = {
7
7
  id: string;
8
8
  toolName: string;
9
+ headerLabel?: string | undefined;
9
10
  headerArgs?: string | undefined;
10
11
  headerArgsSegments?: readonly ToolHeaderSegment[] | undefined;
11
12
  bodyLineStyles?: readonly ToolBodyLineStyle[] | undefined;
@@ -1,16 +1,17 @@
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";
4
- const TRUNCATED_PREVIEW_MARKER = " ";
3
+ import { alertIconPrefixLength, hasToolLspDiagnosticsAfterMutation, lspDiagnosticSeverityForLine, sanitizeText, toolStatusIcon, toolStatusIconColor, wrapLine } from "./render-text.js";
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;
13
- const headerPrefix = `${stateIcon} ${entry.toolName}`;
13
+ const headerLabel = entry.headerLabel ?? entry.toolName;
14
+ const headerPrefix = headerLabel ? `${stateIcon} ${headerLabel}` : stateIcon;
14
15
  const headerArgs = formatToolHeaderArgs(entry.headerArgs);
15
16
  const headerArgsWidth = width - stringDisplayWidth(headerPrefix) - 1;
16
17
  const clippedHeaderArgs = headerArgsWidth > 0 ? sliceByDisplayWidth(headerArgs, headerArgsWidth) : "";
@@ -31,7 +32,7 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
31
32
  headerLines.push(...renderToolBodyLines(entry.expandedText, width, target, toolOutputColor, entry.bodyStyle, colors, entry.syntaxHighlight, entry.bodyWrap, hasLspDiagnostics, entry.bodyLineStyles, entry.preserveAnsi));
32
33
  return headerLines;
33
34
  }
34
- if (rule.compactHidden || rule.defaultExpanded === true)
35
+ if (rule.compactHidden || (rule.defaultExpanded === true && !options.superCompact))
35
36
  return headerLines;
36
37
  const body = entry.collapsedBody.trimEnd();
37
38
  if (!body || rule.previewLines === 0)
@@ -111,7 +112,10 @@ function renderToolBodyLines(text, width, target, color, style, colors, syntaxHi
111
112
  segment.bold = diffStyle.bold;
112
113
  line.segments = [segment];
113
114
  }
114
- else if (lspDiagnosticStyle) {
115
+ else if (lspDiagnosticStyle?.kind === "alert" && wrapIndex === 0) {
116
+ line.segments = [{ start: 2, end: 2 + lspDiagnosticStyle.length, foreground: colors.warning, bold: true }];
117
+ }
118
+ else if (lspDiagnosticStyle?.kind === "severity") {
115
119
  line.segments = [{ start: 2, end: line.text.length, foreground: lspDiagnosticStyle.foreground }];
116
120
  }
117
121
  else if (bodyLineStyle && line.text.length > 2) {
@@ -348,37 +352,16 @@ function bodyLineStyleForLine(styles, lineIndex, colors) {
348
352
  return resolvedForeground ? { ...segment, foreground: resolvedForeground } : segment;
349
353
  }
350
354
  function lspDiagnosticLineStyle(line, colors) {
355
+ const alertLength = alertIconPrefixLength(line);
356
+ if (alertLength != null)
357
+ return { kind: "alert", length: alertLength };
351
358
  const severity = lspDiagnosticSeverityForLine(line);
352
359
  if (severity === "error")
353
- return { foreground: colors.error };
360
+ return { kind: "severity", foreground: colors.error };
354
361
  if (severity === "warning")
355
- return { foreground: colors.warning };
362
+ return { kind: "severity", foreground: colors.warning };
356
363
  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
- }
364
+ return { kind: "severity", foreground: colors.muted };
382
365
  return undefined;
383
366
  }
384
367
  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();