pi-ui-extend 0.1.20 → 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.
package/dist/app/app.d.ts CHANGED
@@ -60,6 +60,9 @@ export declare class PiUiExtendApp {
60
60
  private resumeLoading;
61
61
  constructor(options: AppOptions);
62
62
  private createRuntime;
63
+ private loadStartupConfig;
64
+ private applyPixConfig;
65
+ private updateOutputFilters;
63
66
  start(): Promise<void>;
64
67
  private checkPixUpdateOnStartup;
65
68
  private bindCurrentSession;
package/dist/app/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { THEMES } from "../theme.js";
2
2
  import { InputEditor } from "../input-editor.js";
3
- import { compileOutputFilterPatterns, loadPixConfig, resolveToolRule, } from "../config.js";
3
+ import { compileOutputFilterPatterns, defaultPixConfig, loadPixConfig, resolveToolRule, } from "../config.js";
4
4
  import { AppCommandController } from "./commands/command-controller.js";
5
5
  import { ConversationViewport } from "./rendering/conversation-viewport.js";
6
6
  import { EditorLayoutRenderer } from "./rendering/editor-layout-renderer.js";
@@ -68,7 +68,7 @@ export class PiUiExtendApp {
68
68
  extensionUiController;
69
69
  extensionActions;
70
70
  pixConfig;
71
- outputFilters;
71
+ outputFilters = [];
72
72
  commandController;
73
73
  inputActions;
74
74
  inputController;
@@ -127,7 +127,7 @@ export class PiUiExtendApp {
127
127
  constructor(options) {
128
128
  this.options = options;
129
129
  this.theme = THEMES[options.themeName];
130
- this.pixConfig = loadPixConfig(this.options.cwd);
130
+ this.pixConfig = defaultPixConfig();
131
131
  setAppIconTheme(this.pixConfig.iconTheme.name);
132
132
  const app = this;
133
133
  this.blinkController = new AppBlinkController({
@@ -160,7 +160,7 @@ export class PiUiExtendApp {
160
160
  });
161
161
  this.tabsController = new AppTabsController({
162
162
  options: this.options,
163
- maxProjectSessions: this.pixConfig.maxProjectSessions,
163
+ maxProjectSessions: () => this.pixConfig.maxProjectSessions,
164
164
  blinkController: this.blinkController,
165
165
  runtime: () => this.runtime,
166
166
  createRuntimeForNewSession: () => this.createRuntime(newTabRuntimeOptions(this.options)),
@@ -398,18 +398,16 @@ export class PiUiExtendApp {
398
398
  get subagentsWidgetState() { return app.subagentsWidgetController.widgetState; },
399
399
  get voicePartialText() { return app.voicePartialText; },
400
400
  get autocompleteSuggestion() { return app.autocompleteController.suggestionText(); },
401
- get queuedMessageWidgetEntries() { return app.queuedMessages.deferredQueuedEntries(); },
401
+ get queuedMessageWidgetEntries() { return app.queuedMessages.queuedEntries(); },
402
402
  renderExtensionInputComponent: (width) => this.extensionUiController.renderActiveCustomUi(width),
403
403
  extensionInputUsesEditor: () => this.extensionUiController.activeCustomUiUsesEditor(),
404
404
  widgetTuiHandle: () => this.extensionUiController.widgetTuiHandle(),
405
405
  createExtensionTheme: () => this.extensionUiController.createExtensionTheme(),
406
406
  suppressExtensionWidget: (key) => this.extensionUiController.suppressWidget(key),
407
407
  });
408
- this.outputFilters = compileOutputFilterPatterns(this.pixConfig.outputFilters.patterns);
408
+ this.updateOutputFilters();
409
409
  this.conversationViewport = new ConversationViewport({
410
410
  get entries() { return app.entries; },
411
- get session() { return app.runtime?.session; },
412
- get deferredUserMessages() { return app.queuedMessages.deferredUserMessages; },
413
411
  get entryRenderVersions() { return app.sessionEvents.entryRenderVersions; },
414
412
  get superCompactTools() { return app.superCompactTools; },
415
413
  get allThinkingExpanded() { return app.allThinkingExpanded; },
@@ -700,6 +698,7 @@ export class PiUiExtendApp {
700
698
  inputEditor: () => this.inputEditor,
701
699
  enableTerminal: () => this.terminalController.enableTerminal(),
702
700
  disposeRuntimeForQuit: (runtime) => this.terminalController.disposeRuntimeForQuit(runtime),
701
+ loadStartupConfig: () => this.loadStartupConfig(),
703
702
  loadRequestHistory: () => this.requestHistory.load(),
704
703
  startSubagentsPolling: () => this.subagentsWidgetController.startPolling(),
705
704
  closeSdkMenuForBind: () => this.popupMenus.closeSdkMenu(undefined, { render: false, restoreStatus: false }),
@@ -750,8 +749,35 @@ export class PiUiExtendApp {
750
749
  createRuntime(options) {
751
750
  return createPixRuntime(options, {
752
751
  eventBus: this.createExtensionEventBus(),
752
+ config: this.pixConfig,
753
753
  });
754
754
  }
755
+ async loadStartupConfig() {
756
+ await yieldToEventLoop();
757
+ this.applyPixConfig(loadPixConfig(this.options.cwd));
758
+ }
759
+ applyPixConfig(config) {
760
+ replaceRecord(this.pixConfig.toolRenderer, config.toolRenderer);
761
+ replaceRecord(this.pixConfig.outputFilters, config.outputFilters);
762
+ replaceRecord(this.pixConfig.promptEnhancer, config.promptEnhancer);
763
+ replaceRecord(this.pixConfig.autocomplete, config.autocomplete);
764
+ replaceRecord(this.pixConfig.modelColors, config.modelColors);
765
+ replaceRecord(this.pixConfig.iconTheme, config.iconTheme);
766
+ replaceRecord(this.pixConfig.dictation, config.dictation);
767
+ if (config.defaultModel === undefined)
768
+ delete this.pixConfig.defaultModel;
769
+ else
770
+ this.pixConfig.defaultModel = { ...config.defaultModel };
771
+ this.pixConfig.ignoreContextFiles = config.ignoreContextFiles;
772
+ this.pixConfig.maxProjectSessions = config.maxProjectSessions;
773
+ this.updateOutputFilters();
774
+ this.voiceController.updateDictationConfig(this.pixConfig.dictation);
775
+ setAppIconTheme(this.pixConfig.iconTheme.name);
776
+ this.conversationViewport.clear();
777
+ }
778
+ updateOutputFilters() {
779
+ this.outputFilters.splice(0, this.outputFilters.length, ...compileOutputFilterPatterns(this.pixConfig.outputFilters.patterns));
780
+ }
755
781
  async start() {
756
782
  await this.sessionLifecycle.start();
757
783
  this.modelUsageController.startPolling();
@@ -1053,3 +1079,37 @@ function newTabRuntimeOptions(options) {
1053
1079
  ...(options.modelRef === undefined ? {} : { modelRef: options.modelRef }),
1054
1080
  };
1055
1081
  }
1082
+ async function yieldToEventLoop() {
1083
+ await new Promise((resolve) => { setTimeout(resolve, 0); });
1084
+ }
1085
+ function replaceRecord(target, source) {
1086
+ for (const key of Object.keys(target)) {
1087
+ if (!(key in source))
1088
+ delete target[key];
1089
+ }
1090
+ for (const [key, value] of Object.entries(source)) {
1091
+ const existing = target[key];
1092
+ if (isMutableRecord(existing) && isMutableRecord(value)) {
1093
+ replaceRecord(existing, value);
1094
+ }
1095
+ else {
1096
+ target[key] = cloneConfigValue(value);
1097
+ }
1098
+ }
1099
+ }
1100
+ function cloneConfigValue(value) {
1101
+ if (Array.isArray(value))
1102
+ return value.map(cloneConfigValue);
1103
+ if (isMutableRecord(value))
1104
+ return cloneConfigRecord(value);
1105
+ return value;
1106
+ }
1107
+ function cloneConfigRecord(value) {
1108
+ const cloned = {};
1109
+ for (const [key, nested] of Object.entries(value))
1110
+ cloned[key] = cloneConfigValue(nested);
1111
+ return cloned;
1112
+ }
1113
+ function isMutableRecord(value) {
1114
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1115
+ }
@@ -1,4 +1,4 @@
1
- import { ANSI_RESET, colorize, THEMES } from "../../theme.js";
1
+ import { ANSI_RESET, ansiStylePrefix, colorize, THEMES } from "../../theme.js";
2
2
  import { isToastKind } from "../../ui.js";
3
3
  import { ellipsizeDisplay, sanitizeText } from "../rendering/render-text.js";
4
4
  const CUSTOM_UI_WIDGET_KEY = "pix-custom-ui";
@@ -20,7 +20,7 @@ export class ExtensionUiController {
20
20
  const colors = this.host.theme.colors;
21
21
  const foreground = (color) => extensionForegroundColor(colors, String(color));
22
22
  const background = (color) => extensionBackgroundColor(colors, String(color));
23
- const prefix = (options) => (colorize("", options).replace(new RegExp(`${escapeRegExp(ANSI_RESET)}$`), ""));
23
+ const prefix = (options) => ansiStylePrefix(options);
24
24
  return {
25
25
  fg: (color, text) => colorize(text, { foreground: foreground(color) }),
26
26
  bg: (color, text) => colorize(text, { background: background(color) }),
@@ -54,8 +54,8 @@ type VoiceControllerTestDeps = {
54
54
  export declare function setVoiceControllerTestDeps(overrides?: Partial<VoiceControllerTestDeps>): void;
55
55
  export declare class AppVoiceController {
56
56
  private readonly host;
57
- private readonly modelDefinitions;
58
- private readonly languages;
57
+ private modelDefinitions;
58
+ private languages;
59
59
  private language;
60
60
  private state;
61
61
  private readonly modelCache;
@@ -69,6 +69,7 @@ export declare class AppVoiceController {
69
69
  private partialTranscriptTimer;
70
70
  private startGeneration;
71
71
  constructor(host: AppVoiceControllerHost, dictationConfig: DictationConfig);
72
+ updateDictationConfig(dictationConfig: DictationConfig): void;
72
73
  statusWidgetText(): string;
73
74
  showLanguageSwitcher(): boolean;
74
75
  statusWidgetActive(): boolean;
@@ -51,6 +51,15 @@ export class AppVoiceController {
51
51
  this.languages = Object.keys(this.modelDefinitions);
52
52
  this.language = this.initialLanguage(dictationConfig.language);
53
53
  }
54
+ updateDictationConfig(dictationConfig) {
55
+ this.modelDefinitions = dictationConfig.languages;
56
+ this.languages = Object.keys(this.modelDefinitions);
57
+ this.language = this.initialLanguage(dictationConfig.language ?? this.language);
58
+ for (const language of this.modelCache.keys()) {
59
+ if (!this.modelDefinitions[language])
60
+ this.modelCache.delete(language);
61
+ }
62
+ }
54
63
  statusWidgetText() {
55
64
  const languageLabel = this.showLanguageSwitcher() ? ` ${this.language.toUpperCase()}` : "";
56
65
  switch (this.state) {
@@ -1,5 +1,6 @@
1
1
  import { applyOutputFilters } from "../../config.js";
2
2
  import { renderMarkdownTextLines } from "../../markdown-format.js";
3
+ import { stringDisplayWidth } from "../../terminal-width.js";
3
4
  import { attachImageClickTargets } from "../screen/image-click-targets.js";
4
5
  import { APP_ICONS } from "../icons.js";
5
6
  import { horizontalPaddingLayout, padHorizontalText, wrapText } from "./render-text.js";
@@ -7,13 +8,36 @@ import { renderConversationShellEntry } from "./conversation-shell-renderer.js";
7
8
  import { renderConversationToolEntry, renderThinkingEntry } from "./conversation-tool-renderer.js";
8
9
  export function renderConversationEntry(entry, width, options) {
9
10
  const { left: userContentLeft, contentWidth: userContentWidth } = horizontalPaddingLayout(width);
10
- const userLine = (text, entryId, syntaxHighlight, segments) => ({
11
- text: padHorizontalText(text, width),
12
- colorOverride: options.colors.userForeground,
13
- ...(segments && segments.length > 0 ? { segments: segments.map((segment) => ({ ...segment, start: segment.start + userContentLeft, end: segment.end + userContentLeft })) } : {}),
14
- ...(syntaxHighlight === undefined ? {} : { syntaxHighlight }),
15
- ...(entryId === undefined ? {} : { target: { kind: "user-message", id: entryId } }),
16
- });
11
+ const userLine = (text, entryId, syntaxHighlight, segments) => {
12
+ const textWidth = stringDisplayWidth(text);
13
+ const padding = Math.max(0, width - textWidth);
14
+ const paddedText = " ".repeat(padding) + text;
15
+ const offset = padding;
16
+ return {
17
+ text: paddedText,
18
+ colorOverride: options.colors.userForeground,
19
+ backgroundOverride: options.colors.userMessageBackground,
20
+ ...(segments && segments.length > 0
21
+ ? {
22
+ segments: segments.map((segment) => ({
23
+ ...segment,
24
+ start: segment.start + offset,
25
+ end: segment.end + offset,
26
+ foreground: options.colors.userForeground,
27
+ })),
28
+ }
29
+ : {}),
30
+ ...(syntaxHighlight === undefined
31
+ ? {}
32
+ : {
33
+ syntaxHighlight: {
34
+ ...syntaxHighlight,
35
+ start: syntaxHighlight.start + offset,
36
+ },
37
+ }),
38
+ ...(entryId === undefined ? {} : { target: { kind: "user-message", id: entryId } }),
39
+ };
40
+ };
17
41
  const queuedLine = (text, entryId, segments) => ({
18
42
  text,
19
43
  colorOverride: options.colors.userForeground,
@@ -69,10 +93,16 @@ function renderAssistantLines(text, width, options) {
69
93
  return [];
70
94
  const lines = [];
71
95
  for (const line of contentLines) {
96
+ const headingSegment = line.heading
97
+ ? { start: contentLeft, end: contentLeft + line.text.length, foreground: options.colors.assistantForeground, bold: true }
98
+ : undefined;
99
+ const existingSegments = line.segments?.map((segment) => ({ ...segment, start: segment.start + contentLeft, end: segment.end + contentLeft })) ?? [];
100
+ const allSegments = headingSegment ? [headingSegment, ...existingSegments] : existingSegments;
72
101
  lines.push({
73
102
  text: padHorizontalText(line.text, width),
74
103
  colorOverride: options.colors.assistantForeground,
75
- ...(line.segments && line.segments.length > 0 ? { segments: line.segments.map((segment) => ({ ...segment, start: segment.start + contentLeft, end: segment.end + contentLeft })) } : {}),
104
+ backgroundOverride: options.colors.assistantMessageBackground,
105
+ ...(allSegments.length > 0 ? { segments: allSegments } : {}),
76
106
  ...(line.syntaxHighlight ? { syntaxHighlight: line.syntaxHighlight } : {}),
77
107
  });
78
108
  }
@@ -63,7 +63,7 @@ export function renderThinkingEntry(entry, width, options) {
63
63
  expandedText: compactExpandedText || "(empty)",
64
64
  bodyWrap: "word",
65
65
  syntaxHighlight: compactExpandedText ? markdownSyntaxHighlightsForText(compactExpandedText) : undefined,
66
- }, rule, width, options.colors, { superCompact: Boolean(options.superCompactTools && !forceExpanded) });
66
+ }, rule, width, options.colors, { superCompact: Boolean(options.superCompactTools && !forceExpanded), backgroundOverride: options.colors.thinkingMessageBackground, skipHeaderBackground: true });
67
67
  }
68
68
  function trimTrailingBlankLines(text) {
69
69
  return text.replace(/(?:\r?\n[ \t]*)+$/u, "");
@@ -1,12 +1,9 @@
1
- import type { AgentSession } from "@earendil-works/pi-coding-agent";
2
1
  import { type PixConfig } from "../../config.js";
3
2
  import type { Theme } from "../../theme.js";
4
3
  import { type InlineUserMessageMenuContext } from "./conversation-entry-renderer.js";
5
- import type { ConversationBlockCache, Entry, RenderedLine, SubmittedUserMessage } from "../types.js";
4
+ import type { ConversationBlockCache, Entry, RenderedLine } from "../types.js";
6
5
  export type ConversationViewportHost = {
7
6
  readonly entries: readonly Entry[];
8
- readonly session: AgentSession | undefined;
9
- readonly deferredUserMessages: readonly SubmittedUserMessage[];
10
7
  readonly entryRenderVersions: ReadonlyMap<string, number>;
11
8
  readonly cwd: string;
12
9
  readonly colors: Theme["colors"];
@@ -40,7 +37,6 @@ export declare class ConversationViewport {
40
37
  blockForEntry(entry: Entry, width: number): ConversationBlockCache;
41
38
  entryBlockPositions(width: number): ConversationEntryBlockPosition[];
42
39
  measuredLineCountForEntries(width: number, entryIds: readonly string[]): number;
43
- private queuedEntries;
44
40
  private layoutForWidth;
45
41
  private buildLayout;
46
42
  private previousMeasuredLineCount;
@@ -2,7 +2,6 @@ import { resolveToolRule } from "../../config.js";
2
2
  import { stringDisplayWidth } from "../../terminal-width.js";
3
3
  import { renderConversationEntry as renderConversationEntryLines } from "./conversation-entry-renderer.js";
4
4
  import { horizontalPaddingLayout } from "./render-text.js";
5
- import { sdkQueuedMessageEntries } from "../session/queued-message-entries.js";
6
5
  export class ConversationViewport {
7
6
  host;
8
7
  blockCachesByWidth = new Map();
@@ -63,8 +62,7 @@ export class ConversationViewport {
63
62
  return { lines: visible, changed: false };
64
63
  }
65
64
  entries() {
66
- const queued = this.queuedEntries();
67
- return queued.length === 0 ? [...this.host.entries] : [...this.host.entries, ...queued];
65
+ return [...this.host.entries];
68
66
  }
69
67
  blockForEntry(entry, width) {
70
68
  const blockCache = this.blockCacheForWidth(width);
@@ -119,21 +117,16 @@ export class ConversationViewport {
119
117
  }
120
118
  return lineCount;
121
119
  }
122
- queuedEntries() {
123
- return sdkQueuedMessageEntries(this.host.session);
124
- }
125
120
  layoutForWidth(width) {
126
- const queued = this.queuedEntries();
127
- const entries = queued.length === 0 ? this.host.entries : [...this.host.entries, ...queued];
128
- const queuedSignature = queued.map((entry) => entry.id).join("\n");
121
+ const entries = this.host.entries;
129
122
  const superCompactTools = Boolean(this.host.superCompactTools);
130
123
  const allThinkingExpanded = Boolean(this.host.allThinkingExpanded);
131
124
  let layout = this.layoutCachesByWidth.get(width);
132
- if (!layout || this.layoutStructureChanged(layout, entries, queuedSignature, superCompactTools, allThinkingExpanded)) {
133
- const previousLayout = layout && layout.queuedSignature === queuedSignature && layout.superCompactTools === superCompactTools && layout.allThinkingExpanded === allThinkingExpanded
125
+ if (!layout || this.layoutStructureChanged(layout, entries, superCompactTools, allThinkingExpanded)) {
126
+ const previousLayout = layout && layout.superCompactTools === superCompactTools && layout.allThinkingExpanded === allThinkingExpanded
134
127
  ? layout
135
128
  : undefined;
136
- layout = this.buildLayout(entries, width, queuedSignature, superCompactTools, allThinkingExpanded, previousLayout);
129
+ layout = this.buildLayout(entries, width, superCompactTools, allThinkingExpanded, previousLayout);
137
130
  this.layoutCachesByWidth.set(width, layout);
138
131
  }
139
132
  else {
@@ -144,7 +137,7 @@ export class ConversationViewport {
144
137
  }
145
138
  return layout;
146
139
  }
147
- buildLayout(entries, width, queuedSignature, superCompactTools, allThinkingExpanded, previousLayout) {
140
+ buildLayout(entries, width, superCompactTools, allThinkingExpanded, previousLayout) {
148
141
  const entryIds = [];
149
142
  const lineCounts = [];
150
143
  const measuredLineCounts = [];
@@ -163,7 +156,7 @@ export class ConversationViewport {
163
156
  totalLineCount += lineCount;
164
157
  }
165
158
  offsets.push(totalLineCount);
166
- return { entries, entryIds, lineCounts, measuredLineCounts, offsets, positions, dirtyEntryIds: new Set(), totalLineCount, queuedSignature, superCompactTools, allThinkingExpanded };
159
+ return { entries, entryIds, lineCounts, measuredLineCounts, offsets, positions, dirtyEntryIds: new Set(), totalLineCount, superCompactTools, allThinkingExpanded };
167
160
  }
168
161
  previousMeasuredLineCount(previousLayout, entries, index, entry) {
169
162
  const previousIndex = previousLayout?.positions.get(entry.id);
@@ -179,8 +172,8 @@ export class ConversationViewport {
179
172
  return undefined;
180
173
  return previousLayout.lineCounts[previousIndex];
181
174
  }
182
- layoutStructureChanged(layout, entries, queuedSignature, superCompactTools, allThinkingExpanded) {
183
- if (layout.entries.length !== entries.length || layout.queuedSignature !== queuedSignature || layout.superCompactTools !== superCompactTools || layout.allThinkingExpanded !== allThinkingExpanded)
175
+ layoutStructureChanged(layout, entries, superCompactTools, allThinkingExpanded) {
176
+ if (layout.entries.length !== entries.length || layout.superCompactTools !== superCompactTools || layout.allThinkingExpanded !== allThinkingExpanded)
184
177
  return true;
185
178
  if (layout.entries.length === 0)
186
179
  return false;
@@ -111,7 +111,7 @@ export class EditorLayoutRenderer {
111
111
  for (const [index, text] of wrapped.entries()) {
112
112
  lines.push({
113
113
  text: padHorizontalText(text, width),
114
- colorOverride: this.host.theme.colors.warning,
114
+ colorOverride: this.host.theme.colors.userForeground,
115
115
  target: { kind: "queue-message", id: entry.id },
116
116
  ...(index === 0 ? { segments: [{ start: 0, end: icon.length, foreground: this.host.theme.colors.info }] } : {}),
117
117
  });
@@ -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,
@@ -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";
@@ -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
  }
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;
@@ -46,6 +46,7 @@ export function renderMarkdownLine(text, start = 0) {
46
46
  let rendered = text.slice(0, safeStart);
47
47
  let index = safeStart;
48
48
  let inCode = false;
49
+ const isHeading = /^\s{0,3}#{1,6}\s/.test(text);
49
50
  while (index < text.length) {
50
51
  const char = text[index] ?? "";
51
52
  if (char === "`" && !isEscaped(text, index)) {
@@ -67,7 +68,7 @@ export function renderMarkdownLine(text, start = 0) {
67
68
  rendered += char;
68
69
  index += 1;
69
70
  }
70
- return { text: rendered, segments };
71
+ return { text: rendered, segments, ...(isHeading ? { heading: true } : {}) };
71
72
  }
72
73
  export function renderMarkdownTextLines(text, width, start = 0) {
73
74
  const lines = [];
@@ -80,12 +81,14 @@ export function renderMarkdownTextLines(text, width, start = 0) {
80
81
  const closesFence = Boolean(fence && nextFence && fence.marker === nextFence.marker && nextFence.length >= fence.length);
81
82
  const opensFence = !fence && nextFence !== undefined;
82
83
  const syntaxHighlight = markdownLineSyntaxHighlight(fence, Boolean(opensFence || closesFence), start);
83
- const markdownLine = syntaxHighlight?.language === "markdown" ? renderMarkdownLine(rawLine) : undefined;
84
+ const isHeadingLine = !fence && /^\s{0,3}#{1,6}\s/.test(rawLine);
85
+ const markdownLine = syntaxHighlight?.language === "markdown" || isHeadingLine ? renderMarkdownLine(rawLine) : undefined;
84
86
  for (const wrapped of wrapRenderedMarkdownLine(markdownLine ?? { text: rawLine, segments: [] }, width)) {
85
87
  lines.push({
86
88
  text: wrapped.text,
87
89
  ...(wrapped.segments.length > 0 ? { segments: wrapped.segments } : {}),
88
90
  ...(syntaxHighlight ? { syntaxHighlight } : {}),
91
+ ...(isHeadingLine ? { heading: true } : {}),
89
92
  });
90
93
  }
91
94
  if (opensFence && nextFence) {
@@ -44,6 +44,8 @@ const MARKDOWN_FENCE_LANGUAGES = {
44
44
  javascript: "javascript",
45
45
  js: "javascript",
46
46
  json: "json",
47
+ markdown: "markdown",
48
+ md: "markdown",
47
49
  jsonc: "json",
48
50
  jsx: "javascript",
49
51
  python: "python",
@@ -227,7 +229,7 @@ function markdownSegments(code, colors) {
227
229
  const segments = [];
228
230
  const heading = /^(\s{0,3}#{1,6}\s+)(.*)$/.exec(code);
229
231
  if (heading) {
230
- addSegment(segments, 0, code.length, "tag", colors, true);
232
+ segments.push({ start: 0, end: code.length, foreground: colors.heading, bold: true });
231
233
  return segments;
232
234
  }
233
235
  const fence = /^\s*`{3,}/.exec(code);
package/dist/theme.d.ts CHANGED
@@ -17,6 +17,7 @@ export type Theme = {
17
17
  tabBorder: string;
18
18
  assistantMessageBackground: string;
19
19
  userMessageBackground: string;
20
+ thinkingMessageBackground: string;
20
21
  inputCursorBackground: string;
21
22
  popupForeground: string;
22
23
  popupBackground: string;
@@ -32,6 +33,7 @@ export type Theme = {
32
33
  accent: string;
33
34
  success: string;
34
35
  warning: string;
36
+ heading: string;
35
37
  info: string;
36
38
  toolMutation: string;
37
39
  toolSearch: string;
@@ -63,5 +65,6 @@ export type TextStyleOptions = {
63
65
  };
64
66
  export declare function parseThemeName(value: string): ThemeName | undefined;
65
67
  export declare function colorize(text: string, options: TextStyleOptions): string;
68
+ export declare function ansiStylePrefix(options: TextStyleOptions): string;
66
69
  export declare function colorLine(text: string, width: number, options: TextStyleOptions): string;
67
70
  export declare function padOrTrimPlain(text: string, width: number): string;
package/dist/theme.js CHANGED
@@ -5,7 +5,7 @@ export const THEMES = {
5
5
  colors: {
6
6
  background: "#090d13",
7
7
  foreground: "#d6deeb",
8
- assistantForeground: "#c9d1d9",
8
+ assistantForeground: "#a4bce0",
9
9
  muted: "#7d8590",
10
10
  headerForeground: "#c9d1d9",
11
11
  headerBackground: "#161b22",
@@ -16,8 +16,9 @@ export const THEMES = {
16
16
  inputBorder: "#30363d",
17
17
  inputBorderWidgetBackground: "#2a2f36",
18
18
  tabBorder: "#7d8590",
19
- assistantMessageBackground: "#161b22",
20
- userMessageBackground: "#262224",
19
+ assistantMessageBackground: "",
20
+ userMessageBackground: "",
21
+ thinkingMessageBackground: "",
21
22
  inputCursorBackground: "#7fb3c8",
22
23
  popupForeground: "#e6edf3",
23
24
  popupBackground: "#1e1e1e",
@@ -33,18 +34,19 @@ export const THEMES = {
33
34
  accent: "#7aa2d6",
34
35
  success: "#7ca982",
35
36
  warning: "#d49a4a",
37
+ heading: "#d4b35e",
36
38
  info: "#7fb3c8",
37
- toolMutation: "#d47aa2",
38
- toolSearch: "#a889d6",
39
- toolTitle: "#9aa7b4",
40
- toolBash: "#c99670",
41
- toolRead: "#6daa8a",
42
- toolIndex: "#7a9ec7",
43
- toolEdit: "#c76a8a",
44
- toolWeb: "#8a9cc7",
45
- toolMeta: "#8b8fa3",
46
- thinkingForeground: "#b8a0d4",
47
- userForeground: "#88b4dc",
39
+ toolMutation: "#b8899e", // ~10% brighter from #a67c8f
40
+ toolSearch: "#9d8abb", // ~10% brighter from #8c7aa8
41
+ toolTitle: "#899199", // ~10% brighter from #7b848c
42
+ toolBash: "#b89071", // ~10% brighter from #a68266
43
+ toolRead: "#6d9b82", // ~10% brighter from #628c76
44
+ toolIndex: "#7692b4", // ~10% brighter from #6a84a3
45
+ toolEdit: "#b47389", // ~10% brighter from #a3687c
46
+ toolWeb: "#8192b6", // ~10% brighter from #7584a6
47
+ toolMeta: "#7d8192", // ~10% brighter from #707485
48
+ thinkingForeground: "#64748b",
49
+ userForeground: "#d97706",
48
50
  thinkingXHigh: "#ff8a86",
49
51
  modelOpenAI: "#c8b45a",
50
52
  statusDotBase: "#30363d",
@@ -69,8 +71,9 @@ export const THEMES = {
69
71
  inputBorder: "#334155",
70
72
  inputBorderWidgetBackground: "#f1f5f9",
71
73
  tabBorder: "#64748b",
72
- assistantMessageBackground: "#eef2f7",
73
- userMessageBackground: "#f9f0ee",
74
+ assistantMessageBackground: "",
75
+ userMessageBackground: "",
76
+ thinkingMessageBackground: "",
74
77
  inputCursorBackground: "#0284c7",
75
78
  popupForeground: "#0f172a",
76
79
  popupBackground: "#ffffff",
@@ -86,18 +89,19 @@ export const THEMES = {
86
89
  accent: "#315f9f",
87
90
  success: "#47794c",
88
91
  warning: "#9a631d",
92
+ heading: "#b88a28",
89
93
  info: "#246b8e",
90
- toolMutation: "#a33a68",
91
- toolSearch: "#6d52a5",
92
- toolTitle: "#526070",
93
- toolBash: "#8a6535",
94
- toolRead: "#3d7a56",
95
- toolIndex: "#3a6d96",
96
- toolEdit: "#963a5e",
97
- toolWeb: "#4a6096",
98
- toolMeta: "#6b7280",
94
+ toolMutation: "#8c5c70", // reverted
95
+ toolSearch: "#6e608c", // reverted
96
+ toolTitle: "#626c78", // reverted
97
+ toolBash: "#8c7556", // reverted
98
+ toolRead: "#507a62", // reverted
99
+ toolIndex: "#567a96", // reverted
100
+ toolEdit: "#8c586c", // reverted
101
+ toolWeb: "#5c6a8c", // reverted
102
+ toolMeta: "#787e8a", // reverted
99
103
  thinkingForeground: "#6b5491",
100
- userForeground: "#4a78b5",
104
+ userForeground: "#854d0e",
101
105
  thinkingXHigh: "#cf333d",
102
106
  modelOpenAI: "#75671f",
103
107
  statusDotBase: "#334155",
@@ -108,10 +112,20 @@ export const THEMES = {
108
112
  },
109
113
  };
110
114
  export const ANSI_RESET = "\x1b[0m";
115
+ const rgbCodeCache = new Map();
116
+ const stylePrefixCache = new Map();
111
117
  export function parseThemeName(value) {
112
118
  return value === "dark" || value === "light" ? value : undefined;
113
119
  }
114
120
  export function colorize(text, options) {
121
+ const prefix = ansiStylePrefix(options);
122
+ return prefix ? `${prefix}${text}${ANSI_RESET}` : text;
123
+ }
124
+ export function ansiStylePrefix(options) {
125
+ const cacheKey = styleCacheKey(options);
126
+ const cached = stylePrefixCache.get(cacheKey);
127
+ if (cached !== undefined)
128
+ return cached;
115
129
  const codes = [];
116
130
  if (options.bold)
117
131
  codes.push("1");
@@ -123,7 +137,9 @@ export function colorize(text, options) {
123
137
  codes.push(rgbCode("38", options.foreground));
124
138
  if (options.background)
125
139
  codes.push(rgbCode("48", options.background));
126
- return codes.length === 0 ? text : `\x1b[${codes.join(";")}m${text}${ANSI_RESET}`;
140
+ const prefix = codes.length === 0 ? "" : `\x1b[${codes.join(";")}m`;
141
+ stylePrefixCache.set(cacheKey, prefix);
142
+ return prefix;
127
143
  }
128
144
  export function colorLine(text, width, options) {
129
145
  return colorize(padOrTrimPlain(text, width), options);
@@ -133,8 +149,17 @@ export function padOrTrimPlain(text, width) {
133
149
  }
134
150
  function rgbCode(prefix, hex) {
135
151
  const normalized = hex.replace(/^#/, "");
152
+ const cacheKey = `${prefix}:${normalized}`;
153
+ const cached = rgbCodeCache.get(cacheKey);
154
+ if (cached)
155
+ return cached;
136
156
  const red = Number.parseInt(normalized.slice(0, 2), 16);
137
157
  const green = Number.parseInt(normalized.slice(2, 4), 16);
138
158
  const blue = Number.parseInt(normalized.slice(4, 6), 16);
139
- return `${prefix};2;${red};${green};${blue}`;
159
+ const code = `${prefix};2;${red};${green};${blue}`;
160
+ rgbCodeCache.set(cacheKey, code);
161
+ return code;
162
+ }
163
+ function styleCacheKey(options) {
164
+ return `${options.bold ? 1 : 0}|${options.underline ? 1 : 0}|${options.strikethrough ? 1 : 0}|${options.foreground ?? ""}|${options.background ?? ""}`;
140
165
  }
@@ -96,6 +96,9 @@ export function getGoogleOAuthClientCredentials(...sources: Array<unknown>): Goo
96
96
  const clientSecret = nestedClientSecret ?? stringProperty(source, ["clientSecret", "client_secret", "googleClientSecret", "google_client_secret", "oauthClientSecret", "oauth_client_secret"]);
97
97
  if (clientId) return { clientId, ...(clientSecret ? { clientSecret } : {}) };
98
98
  }
99
+ const clientId = process.env.PI_ANTIGRAVITY_GOOGLE_CLIENT_ID;
100
+ const clientSecret = process.env.PI_ANTIGRAVITY_GOOGLE_CLIENT_SECRET;
101
+ if (clientId) return { clientId, ...(clientSecret ? { clientSecret } : {}) };
99
102
  return undefined;
100
103
  }
101
104
 
@@ -215,13 +218,15 @@ export async function importOpencodeAntigravityAccount(options: {
215
218
  };
216
219
  }
217
220
 
221
+ const oauthClient = getGoogleOAuthClientCredentials(selected.account, existing);
218
222
  piAuth[PROVIDER_ID] = {
223
+ ...existing,
219
224
  type: "oauth",
220
225
  refresh,
221
226
  access: "",
222
227
  expires: 0,
223
228
  email: selected.account.email,
224
- ...getGoogleOAuthClientCredentials(selected.account),
229
+ ...(oauthClient ? { oauthClient } : {}),
225
230
  accounts: storage.accounts.filter((account) => account.enabled !== false && getAccountRefreshToken(account)),
226
231
  activeIndex: selected.index,
227
232
  };
@@ -224,7 +224,7 @@ export async function addAntigravityAccount(
224
224
  async function refreshAccountToken(account: OpencodeAntigravityAccount, oauthClient?: GoogleOAuthClientCredentials): Promise<RefreshedAntigravityAccount> {
225
225
  const refreshToken = getAccountRefreshToken(account);
226
226
  if (!refreshToken) throw new Error(`Missing refresh token for Antigravity account ${account.email ?? "<unknown>"}`);
227
- const clientCredentials = getGoogleOAuthClientCredentials(account) ?? oauthClient;
227
+ const clientCredentials = getGoogleOAuthClientCredentials(account, oauthClient);
228
228
  assertGoogleOAuthCredentialsConfigured(clientCredentials);
229
229
  const projectId = getAccountProjectId(account);
230
230
  const start = Date.now();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-ui-extend",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {