pi-ui-extend 0.1.19 → 0.1.21

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 (38) hide show
  1. package/dist/app/app.d.ts +3 -0
  2. package/dist/app/app.js +68 -8
  3. package/dist/app/constants.js +1 -1
  4. package/dist/app/extensions/extension-ui-controller.js +2 -2
  5. package/dist/app/input/voice-controller.d.ts +3 -2
  6. package/dist/app/input/voice-controller.js +9 -0
  7. package/dist/app/rendering/conversation-entry-renderer.js +39 -9
  8. package/dist/app/rendering/conversation-tool-renderer.js +1 -1
  9. package/dist/app/rendering/conversation-viewport.d.ts +1 -5
  10. package/dist/app/rendering/conversation-viewport.js +9 -16
  11. package/dist/app/rendering/editor-layout-renderer.js +5 -5
  12. package/dist/app/rendering/render-controller.js +14 -24
  13. package/dist/app/rendering/status-line-renderer.d.ts +2 -0
  14. package/dist/app/rendering/status-line-renderer.js +75 -29
  15. package/dist/app/rendering/tool-block-renderer.d.ts +2 -0
  16. package/dist/app/rendering/tool-block-renderer.js +13 -1
  17. package/dist/app/runtime.d.ts +2 -0
  18. package/dist/app/runtime.js +27 -4
  19. package/dist/app/screen/mouse-controller.d.ts +1 -1
  20. package/dist/app/screen/mouse-controller.js +9 -3
  21. package/dist/app/screen/screen-styler.js +3 -3
  22. package/dist/app/session/session-lifecycle-controller.d.ts +2 -0
  23. package/dist/app/session/session-lifecycle-controller.js +43 -16
  24. package/dist/app/session/tabs-controller.d.ts +1 -1
  25. package/dist/app/session/tabs-controller.js +3 -7
  26. package/dist/app/types.d.ts +1 -1
  27. package/dist/config.d.ts +1 -0
  28. package/dist/config.js +19 -7
  29. package/dist/markdown-format.d.ts +2 -0
  30. package/dist/markdown-format.js +5 -2
  31. package/dist/syntax-highlight.js +3 -1
  32. package/dist/theme.d.ts +11 -0
  33. package/dist/theme.js +56 -15
  34. package/extensions/question/tui.ts +1 -1
  35. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +6 -1
  36. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +1 -1
  37. package/external/pi-tools-suite/src/glm-coding-discipline/index.ts +123 -20
  38. package/package.json +1 -1
@@ -1,12 +1,10 @@
1
1
  import { compactProgressBarSegments, formatCompactProgressBar } from "../../context-progress-bar.js";
2
- import { padOrTrimPlain } from "./render-text.js";
2
+ import { ellipsizeDisplay, padOrTrimPlain } from "./render-text.js";
3
3
  import { displayIndexForColumn, stringDisplayWidth } from "../../terminal-width.js";
4
4
  import { APP_ICONS } from "../icons.js";
5
5
  import { resolveColor, resolveModelColor } from "../../config.js";
6
6
  const MODEL_USAGE_PROGRESS_BAR_WIDTH = stringDisplayWidth(formatCompactProgressBar(100));
7
- const INPUT_BORDER_WIDGET_INSET = 2;
8
- const INPUT_BORDER_WIDGET_SEPARATOR = "─";
9
- const STATUS_ICON_BUTTON_WIDTH = 3;
7
+ const STATUS_WIDGET_GAP = " ";
10
8
  export class StatusLineRenderer {
11
9
  host;
12
10
  constructor(host) {
@@ -18,17 +16,29 @@ export class StatusLineRenderer {
18
16
  const baseStatus = this.host.currentStatus();
19
17
  const workspaceLabel = this.host.statusWorkspaceLabel();
20
18
  const modelUsageLabel = this.host.modelUsageStatusLabel();
21
- const workspaceDetailsLabel = modelUsageLabel ? `${workspaceLabel} ${modelUsageLabel}` : workspaceLabel;
22
- const contextBarLabel = this.contextBarLabel(baseStatus, contentWidth, workspaceDetailsLabel);
19
+ const widgetsLayout = this.inputBorderWidgetsLayout(contentWidth);
20
+ const leftWidth = widgetsLayout?.inputBorderWidgetStartColumn
21
+ ? Math.max(0, widgetsLayout.inputBorderWidgetStartColumn - 2)
22
+ : contentWidth;
23
+ const fullWorkspaceDetailsLabel = modelUsageLabel ? `${workspaceLabel} ${modelUsageLabel}` : workspaceLabel;
24
+ const contextBarLabel = this.contextBarLabel(baseStatus, leftWidth, fullWorkspaceDetailsLabel);
23
25
  const status = contextBarLabel ? `${baseStatus} ${contextBarLabel}` : baseStatus;
24
26
  const sessionLabel = "";
25
- const details = `${status} ${workspaceDetailsLabel}`;
26
- const text = padOrTrimPlain(`${statusDot} ${details}`, width);
27
+ const fittedWorkspaceLabel = this.fitWorkspaceLabel(statusDot, status, workspaceLabel, modelUsageLabel, leftWidth);
28
+ const workspaceDetailsLabel = modelUsageLabel
29
+ ? `${fittedWorkspaceLabel ? `${fittedWorkspaceLabel} ` : ""}${modelUsageLabel}`
30
+ : fittedWorkspaceLabel;
31
+ const details = workspaceDetailsLabel ? `${status} ${workspaceDetailsLabel}` : status;
32
+ const leftText = padOrTrimPlain(`${statusDot} ${details}`, leftWidth);
33
+ const text = widgetsLayout?.inputBorderWidgetStartColumn
34
+ ? overlayText(padOrTrimPlain(leftText, contentWidth), widgetsLayout.inputBorderWidgetStartColumn, widgetsLayout.text)
35
+ : padOrTrimPlain(leftText, contentWidth);
27
36
  return {
37
+ ...widgetsLayout,
28
38
  details,
29
39
  text,
30
40
  sessionLabel,
31
- workspaceLabel,
41
+ workspaceLabel: fittedWorkspaceLabel,
32
42
  ...(modelUsageLabel ? { modelUsageLabel } : {}),
33
43
  ...(contextBarLabel ? { contextBarLabel } : {}),
34
44
  };
@@ -76,17 +86,18 @@ export class StatusLineRenderer {
76
86
  if (!hasParts)
77
87
  return undefined;
78
88
  const parts = [];
79
- const totalWidth = widgets.reduce((total, widget, index) => total + (index > 0 ? 1 : 0) + stringDisplayWidth(widget.text), 0);
80
- const endColumn = Math.max(1, width - INPUT_BORDER_WIDGET_INSET);
89
+ const gapWidth = stringDisplayWidth(STATUS_WIDGET_GAP);
90
+ const totalWidth = widgets.reduce((total, widget, index) => total + stringDisplayWidth(widget.text) + (index > 0 ? gapWidth : 0), 0);
91
+ const endColumn = Math.max(1, width + 1);
81
92
  const startColumn = endColumn - totalWidth;
82
- if (startColumn < 2)
93
+ if (startColumn < 1)
83
94
  return undefined;
84
95
  layout.inputBorderWidgetStartColumn = startColumn;
85
96
  let nextColumn = startColumn;
86
97
  for (const [index, widget] of widgets.entries()) {
87
98
  if (index > 0) {
88
- parts.push(INPUT_BORDER_WIDGET_SEPARATOR);
89
- nextColumn += 1;
99
+ parts.push(STATUS_WIDGET_GAP);
100
+ nextColumn += gapWidth;
90
101
  }
91
102
  parts.push(widget.text);
92
103
  widget.assign(nextColumn, widget.text);
@@ -110,7 +121,6 @@ export class StatusLineRenderer {
110
121
  }
111
122
  inputBorderWidgetSegments(layout, text) {
112
123
  const colors = this.host.theme.colors;
113
- const background = colors.inputBorderWidgetBackground;
114
124
  const segments = [];
115
125
  const pushWidgetSegment = (widget, foreground) => {
116
126
  if (!widget)
@@ -119,7 +129,6 @@ export class StatusLineRenderer {
119
129
  start: displayIndexForColumn(text, widget.startColumn),
120
130
  end: displayIndexForColumn(text, widget.endColumn),
121
131
  foreground,
122
- background,
123
132
  });
124
133
  };
125
134
  pushWidgetSegment(layout.draftQueueWidget, colors.info);
@@ -138,14 +147,12 @@ export class StatusLineRenderer {
138
147
  start: displayIndexForColumn(text, voiceWidget.startColumn),
139
148
  end: displayIndexForColumn(text, voiceWidget.micEndColumn),
140
149
  foreground: this.host.voiceStatusWidgetActive() ? colors.error : colors.muted,
141
- background,
142
150
  });
143
151
  if (voiceWidget.languageEndColumn > voiceWidget.languageStartColumn) {
144
152
  segments.push({
145
153
  start: displayIndexForColumn(text, voiceWidget.languageStartColumn),
146
154
  end: displayIndexForColumn(text, voiceWidget.languageEndColumn),
147
155
  foreground: colors.statusForeground,
148
- background,
149
156
  });
150
157
  }
151
158
  }
@@ -271,16 +278,10 @@ export class StatusLineRenderer {
271
278
  end: statusDotStart + APP_ICONS.record.length,
272
279
  foreground: this.statusDotColor(),
273
280
  }] : [];
274
- this.pushDraftQueueWidgetSegment(segments, statusText);
275
- this.pushUserJumpWidgetSegment(segments, statusText);
276
- this.pushThinkingExpandWidgetSegment(segments, statusText, layout);
277
- this.pushCompactToolsWidgetSegment(segments, statusText);
278
- this.pushTerminalBellSoundWidgetSegment(segments, statusText);
281
+ this.pushStatusWidgetSegments(segments, statusText, layout);
279
282
  this.pushWorkspaceSegments(segments, statusText, layout.workspaceLabel);
280
283
  if (layout.modelUsageLabel)
281
284
  this.pushModelUsageSegments(segments, statusText, layout.modelUsageLabel);
282
- this.pushPromptEnhancerWidgetSegment(segments, statusText);
283
- this.pushVoiceWidgetSegment(segments, statusText);
284
285
  const session = this.host.session;
285
286
  if (!session)
286
287
  return segments;
@@ -304,6 +305,50 @@ export class StatusLineRenderer {
304
305
  this.pushSegment(segments, statusText.lastIndexOf(layout.sessionLabel), layout.sessionLabel.length, this.host.theme.colors.selectionForeground);
305
306
  return segments;
306
307
  }
308
+ fitWorkspaceLabel(statusDot, status, workspaceLabel, modelUsageLabel, width) {
309
+ const modelUsageSuffix = modelUsageLabel ? ` ${modelUsageLabel}` : "";
310
+ const available = width - stringDisplayWidth(`${statusDot} ${status} `) - stringDisplayWidth(modelUsageSuffix);
311
+ if (available <= 0)
312
+ return "";
313
+ return ellipsizeDisplay(workspaceLabel, available);
314
+ }
315
+ pushStatusWidgetSegments(segments, statusText, layout) {
316
+ const colors = this.host.theme.colors;
317
+ const widgets = [
318
+ { widget: layout.draftQueueWidget, foreground: colors.info },
319
+ { widget: layout.promptEnhancerWidget, foreground: this.host.promptEnhancerStatusWidgetActive()
320
+ ? colors.warning
321
+ : this.host.promptEnhancerStatusWidgetEnabled()
322
+ ? colors.info
323
+ : colors.muted },
324
+ { widget: layout.userJumpWidget, foreground: this.host.userMessageJumpMenuActive?.() ? colors.info : colors.muted },
325
+ { widget: layout.terminalBellSoundWidget, foreground: this.host.terminalBellSoundStatusWidgetEnabled() ? colors.info : colors.muted },
326
+ { widget: layout.thinkingExpandWidget, foreground: this.host.allThinkingExpandedActive?.() ? colors.info : colors.muted },
327
+ { widget: layout.compactToolsWidget, foreground: this.host.superCompactToolsActive?.() ? colors.info : colors.muted },
328
+ ].filter((entry) => Boolean(entry.widget));
329
+ for (const { widget, foreground } of widgets) {
330
+ segments.push({
331
+ start: displayIndexForColumn(statusText, widget.startColumn),
332
+ end: displayIndexForColumn(statusText, widget.endColumn),
333
+ foreground,
334
+ });
335
+ }
336
+ const voiceWidget = layout.voiceWidget;
337
+ if (voiceWidget) {
338
+ segments.push({
339
+ start: displayIndexForColumn(statusText, voiceWidget.startColumn),
340
+ end: displayIndexForColumn(statusText, voiceWidget.micEndColumn),
341
+ foreground: this.host.voiceStatusWidgetActive() ? colors.error : colors.muted,
342
+ });
343
+ if (voiceWidget.languageEndColumn > voiceWidget.languageStartColumn) {
344
+ segments.push({
345
+ start: displayIndexForColumn(statusText, voiceWidget.languageStartColumn),
346
+ end: displayIndexForColumn(statusText, voiceWidget.languageEndColumn),
347
+ foreground: colors.statusForeground,
348
+ });
349
+ }
350
+ }
351
+ }
307
352
  pushPromptEnhancerWidgetSegment(segments, statusText) {
308
353
  const widgetText = this.host.promptEnhancerStatusWidgetText();
309
354
  const start = statusText.lastIndexOf(widgetText);
@@ -497,7 +542,7 @@ export class StatusLineRenderer {
497
542
  };
498
543
  }
499
544
  iconButtonText(icon) {
500
- return padOrTrimPlain(` ${icon} `, STATUS_ICON_BUTTON_WIDTH);
545
+ return icon;
501
546
  }
502
547
  voiceBorderWidgetText(widgetText) {
503
548
  const parts = this.voiceBorderWidgetParts(widgetText);
@@ -505,21 +550,22 @@ export class StatusLineRenderer {
505
550
  return "";
506
551
  const micButton = this.iconButtonText(parts.buttonIconText);
507
552
  if (parts.languageText) {
508
- return `${micButton}${INPUT_BORDER_WIDGET_SEPARATOR}${parts.languageText}`;
553
+ return `${micButton}${STATUS_WIDGET_GAP}${parts.languageText}`;
509
554
  }
510
555
  return micButton;
511
556
  }
512
557
  voiceWidgetLayout(startColumn, sourceText, widgetText) {
513
558
  const parts = this.voiceBorderWidgetParts(sourceText);
559
+ const micWidth = parts?.buttonIconText ? stringDisplayWidth(parts.buttonIconText) : stringDisplayWidth(widgetText);
514
560
  const languageStartOffset = parts?.languageText
515
- ? STATUS_ICON_BUTTON_WIDTH + stringDisplayWidth(INPUT_BORDER_WIDGET_SEPARATOR)
561
+ ? micWidth + stringDisplayWidth(STATUS_WIDGET_GAP)
516
562
  : stringDisplayWidth(widgetText);
517
563
  const languageEndOffset = parts?.languageText
518
564
  ? languageStartOffset + stringDisplayWidth(parts.languageText)
519
565
  : languageStartOffset;
520
566
  return {
521
567
  startColumn,
522
- micEndColumn: startColumn + STATUS_ICON_BUTTON_WIDTH,
568
+ micEndColumn: startColumn + micWidth,
523
569
  languageStartColumn: startColumn + languageStartOffset,
524
570
  languageEndColumn: startColumn + languageEndOffset,
525
571
  endColumn: startColumn + stringDisplayWidth(widgetText),
@@ -23,5 +23,7 @@ export type ToolBlockEntry = {
23
23
  };
24
24
  export type ToolBlockRenderOptions = {
25
25
  superCompact?: boolean;
26
+ backgroundOverride?: string;
27
+ skipHeaderBackground?: boolean;
26
28
  };
27
29
  export declare function renderToolBlock(entry: ToolBlockEntry, rule: ResolvedToolRule, width: number, colors: Theme["colors"], options?: ToolBlockRenderOptions): RenderedLine[];
@@ -10,7 +10,10 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
10
10
  const stateIcon = toolStatusIcon(entry);
11
11
  const toolColor = resolveColor(rule.color, colors);
12
12
  const toolOutputColor = colors.statusForeground;
13
- const headerLabel = entry.headerLabel ?? entry.toolName;
13
+ const headerLabel = (entry.headerLabel ?? entry.toolName).toLowerCase();
14
+ const bg = options.backgroundOverride;
15
+ const applyBackground = bg ? (lines) => { for (const line of lines)
16
+ line.backgroundOverride = bg; } : (_lines) => { };
14
17
  const headerPrefix = headerLabel ? `${stateIcon} ${headerLabel}` : stateIcon;
15
18
  const headerArgs = formatToolHeaderArgs(entry.headerArgs);
16
19
  const headerArgsWidth = width - stringDisplayWidth(headerPrefix) - 1;
@@ -22,14 +25,22 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
22
25
  text: header,
23
26
  target,
24
27
  colorOverride: toolColor,
28
+ ...(options.backgroundOverride && !options.skipHeaderBackground ? { backgroundOverride: options.backgroundOverride } : {}),
25
29
  segments: [
26
30
  { start: 0, end: stateIcon.length, foreground: toolStatusIconColor(entry, colors), bold: true },
31
+ { start: stateIcon.length, end: headerPrefix.length, bold: true },
27
32
  ...headerArgsStyledSegments(headerArgsStart, clippedHeaderArgs.length, entry.headerArgsSegments, colors),
28
33
  ],
29
34
  };
30
35
  const headerLines = [headerLine];
31
36
  if (expanded) {
32
37
  headerLines.push(...renderToolBodyLines(entry.expandedText, width, target, toolOutputColor, entry.bodyStyle, colors, entry.syntaxHighlight, entry.bodyWrap, hasLspDiagnostics, entry.bodyLineStyles, entry.preserveAnsi));
38
+ if (options.skipHeaderBackground && headerLines.length > 1) {
39
+ applyBackground(headerLines.slice(1));
40
+ }
41
+ else {
42
+ applyBackground(headerLines);
43
+ }
33
44
  return headerLines;
34
45
  }
35
46
  if (rule.compactHidden || (rule.defaultExpanded === true && !options.superCompact))
@@ -39,6 +50,7 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
39
50
  return headerLines;
40
51
  if (!options.superCompact) {
41
52
  headerLines.push(...renderCollapsedPreviewLines(entry, body, rule, width, target, toolOutputColor, colors, hasLspDiagnostics));
53
+ applyBackground(headerLines);
42
54
  return headerLines;
43
55
  }
44
56
  const preview = collapsedInlinePreview(body, rule, entry.preserveAnsi);
@@ -33,9 +33,11 @@ export declare function bundledSkillsInstallPath(homeDir?: string): string;
33
33
  export declare function ensurePiToolsSuiteExtensionInstalled(options?: PiToolsSuiteInstallOptions): Promise<PiToolsSuiteInstallResult>;
34
34
  export declare function ensureBundledSkillsInstalled(options?: BundledSkillsInstallOptions): Promise<BundledSkillsInstallResult>;
35
35
  export declare function getBundledExtensionPaths(): string[];
36
+ export declare function getBundledExtensionPathsAsync(): Promise<string[]>;
36
37
  export declare function prioritizeBundledQuestionExtension(base: LoadExtensionsResult, questionExtensionPath?: string): LoadExtensionsResult;
37
38
  export type CreatePixRuntimeOptions = {
38
39
  eventBus?: EventBus;
40
+ config?: PixConfig;
39
41
  };
40
42
  type RuntimeSessionManagerModelState = Pick<SessionManager, "getEntries" | "getBranch">;
41
43
  export declare function resolvePixRuntimeModelRef(options: Pick<AppOptions, "modelRef">, sessionManager: RuntimeSessionManagerModelState, config?: PixConfig): string | undefined;
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from "node:fs";
2
- import { cp, lstat, mkdir, readlink, realpath, rm, symlink } from "node:fs/promises";
2
+ import { access, cp, lstat, mkdir, readlink, realpath, rm, symlink } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -40,7 +40,7 @@ export function bundledSkillsInstallPath(homeDir = homedir()) {
40
40
  export async function ensurePiToolsSuiteExtensionInstalled(options = {}) {
41
41
  const sourcePath = resolve(options.sourcePath ?? piToolsSuiteExtensionSourcePath());
42
42
  const targetPath = resolve(options.targetPath ?? piToolsSuiteExtensionInstallPath(options.agentDir));
43
- if (!extensionEntryExists(sourcePath)) {
43
+ if (!(await extensionEntryExistsAsync(sourcePath))) {
44
44
  return { action: "missing-source", sourcePath, targetPath };
45
45
  }
46
46
  await mkdir(dirname(targetPath), { recursive: true });
@@ -84,6 +84,14 @@ export function getBundledExtensionPaths() {
84
84
  bundledTerminalBellExtensionPath(),
85
85
  ].filter(extensionEntryExists);
86
86
  }
87
+ export async function getBundledExtensionPathsAsync() {
88
+ const paths = await Promise.all([
89
+ bundledQuestionExtensionPath(),
90
+ bundledSessionTitleExtensionPath(),
91
+ bundledTerminalBellExtensionPath(),
92
+ ].map(async (extensionPath) => await extensionEntryExistsAsync(extensionPath) ? extensionPath : undefined));
93
+ return paths.filter((path) => path !== undefined);
94
+ }
87
95
  export function prioritizeBundledQuestionExtension(base, questionExtensionPath = bundledQuestionExtensionPath()) {
88
96
  const bundledQuestionExtensions = base.extensions.filter((extension) => isBundledQuestionExtension(extension, questionExtensionPath));
89
97
  if (bundledQuestionExtensions.length === 0)
@@ -101,6 +109,21 @@ export function prioritizeBundledQuestionExtension(base, questionExtensionPath =
101
109
  function extensionEntryExists(extensionPath) {
102
110
  return existsSync(join(extensionPath, "index.ts")) || existsSync(join(extensionPath, "index.js"));
103
111
  }
112
+ async function extensionEntryExistsAsync(extensionPath) {
113
+ try {
114
+ await access(join(extensionPath, "index.ts"));
115
+ return true;
116
+ }
117
+ catch {
118
+ try {
119
+ await access(join(extensionPath, "index.js"));
120
+ return true;
121
+ }
122
+ catch {
123
+ return false;
124
+ }
125
+ }
126
+ }
104
127
  function extensionSymlinkType() {
105
128
  return process.platform === "win32" ? "junction" : "dir";
106
129
  }
@@ -198,13 +221,13 @@ export function resolveSessionModelRefFromTail(entries) {
198
221
  export async function createPixRuntime(options, runtimeOptions = {}) {
199
222
  const agentDir = getAgentDir();
200
223
  const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
201
- const config = loadPixConfig(cwd);
224
+ const config = runtimeOptions.config ?? loadPixConfig(cwd);
202
225
  const effectiveModelRef = resolvePixRuntimeModelRef(options, sessionManager, config);
203
226
  const parsedModel = effectiveModelRef ? parseModelRef(effectiveModelRef) : undefined;
204
227
  const initialThinkingLevel = resolvePixRuntimeInitialThinkingLevel(options, sessionManager, config);
205
228
  await ensureBundledSkillsInstalledOnce();
206
229
  await ensurePiToolsSuiteExtensionInstalledOnce({ agentDir });
207
- const bundledExtensionPaths = getBundledExtensionPaths();
230
+ const bundledExtensionPaths = await getBundledExtensionPathsAsync();
208
231
  const services = await createAgentSessionServices({
209
232
  cwd,
210
233
  agentDir,
@@ -20,7 +20,7 @@ export type InputFrameCopyRows = {
20
20
  inputStartRow: number;
21
21
  inputEndRow: number;
22
22
  inputSeparatorRow: number;
23
- inputBottomSeparatorRow: number;
23
+ inputBottomSeparatorRow?: number;
24
24
  };
25
25
  export type AppMouseControllerHost = {
26
26
  terminalColumns(): number;
@@ -784,12 +784,15 @@ export class AppMouseController {
784
784
  const topOffset = editorLayoutTopOffset(tabPanelRows);
785
785
  const toScreenRow = (layoutRow) => Math.max(1, Math.min(terminalRows, topOffset + layoutRow));
786
786
  const toScreenRowExclusive = (layoutRow) => Math.max(1, Math.min(terminalRows + 1, topOffset + layoutRow));
787
- return {
787
+ const inputFrame = {
788
788
  inputStartRow: toScreenRow(layout.inputStartRow),
789
789
  inputEndRow: toScreenRowExclusive(layout.inputStartRow + layout.renderedInput.lines.length),
790
790
  inputSeparatorRow: toScreenRow(layout.inputSeparatorRow),
791
- inputBottomSeparatorRow: toScreenRow(layout.inputBottomSeparatorRow),
792
791
  };
792
+ if (layout.inputBottomSeparatorRow !== undefined) {
793
+ inputFrame.inputBottomSeparatorRow = toScreenRow(layout.inputBottomSeparatorRow);
794
+ }
795
+ return inputFrame;
793
796
  }
794
797
  getSelectedConversationText(anchor, current) {
795
798
  const range = orderedConversationSelection(anchor, current);
@@ -935,7 +938,10 @@ function orderedConversationSelection(anchor, current) {
935
938
  return anchor.x <= current.x ? { start: anchor, end: current } : { start: current, end: anchor };
936
939
  }
937
940
  export function screenSelectionLineText(row, text, startColumn, endColumn, inputFrame) {
938
- if (inputFrame && (row === inputFrame.inputSeparatorRow || row === inputFrame.inputBottomSeparatorRow)) {
941
+ if (inputFrame && row === inputFrame.inputSeparatorRow) {
942
+ return undefined;
943
+ }
944
+ if (inputFrame?.inputBottomSeparatorRow !== undefined && row === inputFrame.inputBottomSeparatorRow) {
939
945
  return undefined;
940
946
  }
941
947
  return sliceByDisplayColumns(text, startColumn, endColumn);
@@ -1,4 +1,4 @@
1
- import { ANSI_RESET, colorize } from "../../theme.js";
1
+ import { ANSI_RESET, ansiStylePrefix, colorize } from "../../theme.js";
2
2
  import { renderMarkdownLine } from "../../markdown-format.js";
3
3
  import { syntaxHighlightSegmentsForLine } from "../../syntax-highlight.js";
4
4
  import { displayIndexForColumn } from "../../terminal-width.js";
@@ -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.warning };
78
+ const baseOptions = { foreground: colors.userForeground };
79
79
  if (this.selectionRangeForRow(row, width, text))
80
80
  return this.styleLine(row, text, width, baseOptions);
81
81
  const plain = padOrTrimPlain(text, width);
@@ -105,7 +105,7 @@ export class ScreenStyler {
105
105
  return chunks.join("");
106
106
  }
107
107
  styleAnsiLine(text, options) {
108
- const prefix = colorize("", options).replace(new RegExp(`${escapeRegExp(ANSI_RESET)}$`), "");
108
+ const prefix = ansiStylePrefix(options);
109
109
  if (!prefix)
110
110
  return text;
111
111
  return `${prefix}${text.replaceAll(ANSI_RESET, `${ANSI_RESET}${prefix}`)}${ANSI_RESET}`;
@@ -14,6 +14,7 @@ export type AppSessionLifecycleHost = {
14
14
  inputEditor(): InputEditor;
15
15
  enableTerminal(): void;
16
16
  disposeRuntimeForQuit(runtime: AgentSessionRuntime): Promise<void>;
17
+ loadStartupConfig(): Promise<void>;
17
18
  loadRequestHistory(): Promise<void>;
18
19
  startSubagentsPolling(): void;
19
20
  closeSdkMenuForBind(): void;
@@ -67,6 +68,7 @@ export declare class AppSessionLifecycleController {
67
68
  loadSessionHistory(): void;
68
69
  requireRuntime(): AgentSessionRuntime;
69
70
  private bindSessionExtensions;
71
+ private collectAvailabilityIssues;
70
72
  private isCurrentRuntimeSession;
71
73
  private extensionUiScope;
72
74
  }
@@ -16,11 +16,17 @@ export class AppSessionLifecycleController {
16
16
  throw new Error("pi-ui-extend needs an interactive TTY");
17
17
  }
18
18
  this.host.enableTerminal();
19
- await this.host.loadRequestHistory();
20
19
  this.host.setRunning(true);
21
20
  this.host.startSubagentsPolling();
22
21
  this.host.render();
22
+ void this.host.loadRequestHistory().catch((error) => {
23
+ if (!this.host.isRunning())
24
+ return;
25
+ this.host.addEntry({ id: createId("warning"), kind: "system", text: `Request history failed to load: ${stringifyUnknown(error)}` });
26
+ this.host.render();
27
+ });
23
28
  try {
29
+ await this.host.loadStartupConfig();
24
30
  const runtime = await this.host.createRuntime();
25
31
  if (!this.host.isRunning()) {
26
32
  await this.host.disposeRuntimeForQuit(runtime);
@@ -34,21 +40,6 @@ export class AppSessionLifecycleController {
34
40
  if (isEmptyStartupSession(runtime)) {
35
41
  this.host.addEntry({ id: createId("system"), kind: "system", text: createStartupInfoMessage(runtime) });
36
42
  }
37
- await this.host.restoreTabsAfterStartup();
38
- const availabilityIssues = await collectStartupAvailabilityIssues(runtime);
39
- for (const issue of availabilityIssues) {
40
- this.host.addEntry({
41
- id: createId(issue.kind),
42
- kind: issue.kind === "error" ? "error" : "system",
43
- text: issue.message,
44
- });
45
- }
46
- if (availabilityIssues.some((issue) => issue.kind === "error")) {
47
- this.host.showToast("Startup dependency unavailable", "error");
48
- }
49
- else if (availabilityIssues.length > 0) {
50
- this.host.showToast("Startup dependency warning", "warning");
51
- }
52
43
  if (runtime.modelFallbackMessage) {
53
44
  this.host.addEntry({ id: createId("system"), kind: "system", text: runtime.modelFallbackMessage });
54
45
  }
@@ -59,6 +50,14 @@ export class AppSessionLifecycleController {
59
50
  this.host.setSessionStatus(runtime.session);
60
51
  this.host.setSessionActivity(runtime.session.isStreaming ? "running" : "idle");
61
52
  this.host.render();
53
+ void this.collectAvailabilityIssues(runtime);
54
+ void this.host.restoreTabsAfterStartup().catch((error) => {
55
+ if (!this.host.isRunning())
56
+ return;
57
+ this.host.addEntry({ id: createId("warning"), kind: "system", text: `Tab restore failed: ${stringifyUnknown(error)}` });
58
+ this.host.showToast("Could not restore tabs", "warning");
59
+ this.host.render();
60
+ });
62
61
  }
63
62
  catch (error) {
64
63
  this.host.addEntry({ id: createId("error"), kind: "error", text: stringifyUnknown(error) });
@@ -154,6 +153,34 @@ export class AppSessionLifecycleController {
154
153
  this.extensionBindSession = session;
155
154
  return promise;
156
155
  }
156
+ async collectAvailabilityIssues(runtime) {
157
+ try {
158
+ const availabilityIssues = await collectStartupAvailabilityIssues(runtime);
159
+ if (!this.host.isRunning() || this.host.runtime() !== runtime)
160
+ return;
161
+ for (const issue of availabilityIssues) {
162
+ this.host.addEntry({
163
+ id: createId(issue.kind),
164
+ kind: issue.kind === "error" ? "error" : "system",
165
+ text: issue.message,
166
+ });
167
+ }
168
+ if (availabilityIssues.some((issue) => issue.kind === "error")) {
169
+ this.host.showToast("Startup dependency unavailable", "error");
170
+ }
171
+ else if (availabilityIssues.length > 0) {
172
+ this.host.showToast("Startup dependency warning", "warning");
173
+ }
174
+ if (availabilityIssues.length > 0)
175
+ this.host.render();
176
+ }
177
+ catch (error) {
178
+ if (!this.host.isRunning() || this.host.runtime() !== runtime)
179
+ return;
180
+ this.host.addEntry({ id: createId("warning"), kind: "system", text: `Startup dependency check failed: ${stringifyUnknown(error)}` });
181
+ this.host.render();
182
+ }
183
+ }
157
184
  isCurrentRuntimeSession(runtime, session) {
158
185
  return this.host.isRunning() && this.host.runtime() === runtime && runtime.session === session;
159
186
  }
@@ -6,7 +6,7 @@ import type { AppOptions, Entry, SessionActivity, SessionTab, SubmittedUserMessa
6
6
  export type TabInputState = InputEditorDraftState;
7
7
  export type AppTabsControllerHost = {
8
8
  readonly options: AppOptions;
9
- readonly maxProjectSessions?: number;
9
+ readonly maxProjectSessions?: number | (() => number | undefined);
10
10
  readonly blinkController: AppBlinkController;
11
11
  runtime(): AgentSessionRuntime | undefined;
12
12
  createRuntimeForNewSession(): Promise<AgentSessionRuntime>;
@@ -220,17 +220,12 @@ export class AppTabsController {
220
220
  }
221
221
  this.syncActiveTabFromRuntime({ save: false });
222
222
  this.settleStartupTabPlaceholders();
223
- this.host.resetSessionView();
224
- if (this.activeTabId)
225
- this.restoreDeferredUserMessages(this.activeTabId);
226
- this.host.loadSessionHistory();
227
- this.host.setSessionStatus(restoredRuntime.session);
228
- this.host.setSessionActivity(this.sessionActivity(restoredRuntime.session));
229
223
  if (this.activeTabId)
230
224
  this.restoreInputState(this.activeTabId);
231
225
  await this.saveTabs();
232
226
  this.scheduleProjectSessionRetention();
233
227
  this.scheduleTabPrewarm();
228
+ await this.loadActiveSessionHistory(restoredRuntime);
234
229
  }
235
230
  async openNewTab() {
236
231
  if (this.pendingActiveTabId) {
@@ -1377,7 +1372,8 @@ export class AppTabsController {
1377
1372
  return preserved;
1378
1373
  }
1379
1374
  maxProjectSessions() {
1380
- const value = this.host.maxProjectSessions;
1375
+ const configured = this.host.maxProjectSessions;
1376
+ const value = typeof configured === "function" ? configured() : configured;
1381
1377
  return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
1382
1378
  }
1383
1379
  }
@@ -447,7 +447,7 @@ export type EditorLayout = {
447
447
  belowEditorLines: readonly RenderedLine[];
448
448
  inputStartRow: number;
449
449
  inputSeparatorRow: number;
450
- inputBottomSeparatorRow: number;
450
+ inputBottomSeparatorRow?: number;
451
451
  bodyHeight: number;
452
452
  };
453
453
  export type MouseEvent = {
package/dist/config.d.ts CHANGED
@@ -72,6 +72,7 @@ export type PixConfig = {
72
72
  };
73
73
  export declare function getPixConfigPath(homeDir?: string): string;
74
74
  export declare function getProjectPixConfigPath(cwd: string): string;
75
+ export declare function defaultPixConfig(): PixConfig;
75
76
  export declare function resolveDefaultModelRef(config: PixConfig): string | undefined;
76
77
  export declare function savePixDefaultModel(modelRef: string): DefaultModelConfig | undefined;
77
78
  export declare function savePixDefaultThinking(thinking: string, fallbackModelRef?: string): DefaultModelConfig | undefined;
package/dist/config.js CHANGED
@@ -285,19 +285,31 @@ function numberInRange(value, fallback, min, max) {
285
285
  const rounded = Math.round(value);
286
286
  return Math.min(max, Math.max(min, rounded));
287
287
  }
288
- function defaultPixConfig() {
288
+ export function defaultPixConfig() {
289
289
  return {
290
- toolRenderer: DEFAULT_TOOL_RENDERER,
291
- outputFilters: DEFAULT_OUTPUT_FILTERS,
292
- promptEnhancer: DEFAULT_PROMPT_ENHANCER,
293
- autocomplete: DEFAULT_AUTOCOMPLETE,
294
- modelColors: DEFAULT_MODEL_COLORS,
290
+ toolRenderer: cloneToolRendererConfig(DEFAULT_TOOL_RENDERER),
291
+ outputFilters: { patterns: [...DEFAULT_OUTPUT_FILTERS.patterns] },
292
+ promptEnhancer: { ...DEFAULT_PROMPT_ENHANCER },
293
+ autocomplete: { ...DEFAULT_AUTOCOMPLETE },
294
+ modelColors: { rules: { ...DEFAULT_MODEL_COLORS.rules } },
295
295
  iconTheme: { name: resolveAppIconThemeNameFromEnv() },
296
- dictation: DEFAULT_DICTATION,
296
+ dictation: cloneDictationConfig(DEFAULT_DICTATION),
297
297
  ignoreContextFiles: false,
298
298
  maxProjectSessions: 0,
299
299
  };
300
300
  }
301
+ function cloneToolRendererConfig(config) {
302
+ return {
303
+ default: { ...config.default },
304
+ tools: Object.fromEntries(Object.entries(config.tools).map(([name, rule]) => [name, { ...rule }])),
305
+ };
306
+ }
307
+ function cloneDictationConfig(config) {
308
+ return {
309
+ languages: Object.fromEntries(Object.entries(config.languages).map(([language, model]) => [language, { ...model }])),
310
+ ...(config.language === undefined ? {} : { language: config.language }),
311
+ };
312
+ }
301
313
  function pixConfigFromParsed(parsed, fallback = defaultPixConfig()) {
302
314
  const toolRenderer = extractToolRendererConfig(parsed) ?? fallback.toolRenderer;
303
315
  const outputFilters = extractOutputFiltersConfig(parsed) ?? fallback.outputFilters;
@@ -6,6 +6,7 @@ export type RenderedMarkdownLine = {
6
6
  end: number;
7
7
  bold: true;
8
8
  }[];
9
+ heading?: boolean;
9
10
  };
10
11
  export type RenderedMarkdownTextLine = {
11
12
  text: string;
@@ -15,6 +16,7 @@ export type RenderedMarkdownTextLine = {
15
16
  bold: true;
16
17
  }[] | undefined;
17
18
  syntaxHighlight?: SyntaxLineHighlight | undefined;
19
+ heading?: boolean;
18
20
  };
19
21
  export declare function formatMarkdownTables(text: string, maxWidth?: number): string;
20
22
  export declare function renderMarkdownLine(text: string, start?: number): RenderedMarkdownLine;