pi-ui-extend 0.1.20 → 0.1.24

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 (54) hide show
  1. package/README.md +1 -10
  2. package/bin/pix.mjs +11 -154
  3. package/dist/app/app.d.ts +4 -0
  4. package/dist/app/app.js +102 -17
  5. package/dist/app/cli/startup-info.d.ts +0 -1
  6. package/dist/app/cli/startup-info.js +0 -3
  7. package/dist/app/commands/command-session-actions.js +3 -0
  8. package/dist/app/extensions/extension-ui-controller.js +2 -2
  9. package/dist/app/input/voice-controller.d.ts +3 -2
  10. package/dist/app/input/voice-controller.js +9 -0
  11. package/dist/app/popup/popup-menu-controller.js +7 -1
  12. package/dist/app/rendering/conversation-entry-renderer.js +29 -10
  13. package/dist/app/rendering/conversation-tool-renderer.js +1 -1
  14. package/dist/app/rendering/conversation-viewport.d.ts +1 -5
  15. package/dist/app/rendering/conversation-viewport.js +9 -16
  16. package/dist/app/rendering/editor-layout-renderer.js +1 -1
  17. package/dist/app/rendering/render-text.d.ts +6 -0
  18. package/dist/app/rendering/render-text.js +9 -0
  19. package/dist/app/rendering/tab-line-renderer.js +1 -5
  20. package/dist/app/rendering/tool-block-renderer.d.ts +2 -0
  21. package/dist/app/rendering/tool-block-renderer.js +20 -2
  22. package/dist/app/runtime.d.ts +2 -0
  23. package/dist/app/runtime.js +27 -4
  24. package/dist/app/screen/mouse-controller.js +14 -6
  25. package/dist/app/screen/screen-styler.js +2 -2
  26. package/dist/app/session/session-event-controller.js +5 -4
  27. package/dist/app/session/session-lifecycle-controller.d.ts +2 -0
  28. package/dist/app/session/session-lifecycle-controller.js +43 -20
  29. package/dist/app/session/tabs-controller.d.ts +6 -2
  30. package/dist/app/session/tabs-controller.js +114 -30
  31. package/dist/app/types.d.ts +5 -0
  32. package/dist/app/workspace/workspace-actions-controller.d.ts +3 -0
  33. package/dist/app/workspace/workspace-actions-controller.js +71 -16
  34. package/dist/app/workspace/workspace-undo.js +41 -6
  35. package/dist/config.d.ts +1 -0
  36. package/dist/config.js +19 -7
  37. package/dist/markdown-format.d.ts +6 -0
  38. package/dist/markdown-format.js +11 -3
  39. package/dist/syntax-highlight.js +3 -1
  40. package/dist/theme.d.ts +3 -0
  41. package/dist/theme.js +53 -28
  42. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +6 -1
  43. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +2 -1
  44. package/external/pi-tools-suite/src/telegram-mirror/README.md +81 -46
  45. package/external/pi-tools-suite/src/telegram-mirror/bot.ts +81 -10
  46. package/external/pi-tools-suite/src/telegram-mirror/events.ts +6 -38
  47. package/external/pi-tools-suite/src/telegram-mirror/index.ts +246 -40
  48. package/external/pi-tools-suite/src/telegram-mirror/ipc.ts +20 -0
  49. package/external/pi-tools-suite/src/telegram-mirror/multiplexer.ts +247 -17
  50. package/external/pi-tools-suite/src/telegram-mirror/renderer.ts +75 -78
  51. package/external/pi-tools-suite/src/todo/index.ts +7 -6
  52. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +1 -1
  53. package/external/pi-tools-suite/src/web-search/index.ts +139 -2
  54. package/package.json +7 -7
package/README.md CHANGED
@@ -87,7 +87,6 @@ Useful flags:
87
87
  - `--cwd <path>`: workspace used for Pi tools, settings, resources, and sessions.
88
88
  - `--no-session`: run with an in-memory SDK session.
89
89
  - `--model <provider/model[:thinking]>`: request a specific model, for example `anthropic/claude-sonnet-4-20250514:medium`.
90
- - `--reload-on-build`: restart the running Pix process after a successful watcher build.
91
90
 
92
91
  ## Updating Pix
93
92
 
@@ -143,20 +142,12 @@ Run Pix against a workspace:
143
142
  pix --cwd /path/to/workspace
144
143
  ```
145
144
 
146
- During UI development, run the watcher in another terminal:
145
+ During UI development, run the watcher in another terminal, then restart Pix after rebuilds when needed:
147
146
 
148
147
  ```bash
149
148
  npm run watch:pix
150
149
  ```
151
150
 
152
- Each running instance can reload after successful builds:
153
-
154
- ```bash
155
- PIX_RELOAD_ON_BUILD=1 pix --cwd /path/to/workspace
156
- # or
157
- pix --reload-on-build --cwd /path/to/workspace
158
- ```
159
-
160
151
  For a one-shot dev launch that rebuilds and refreshes the global link first:
161
152
 
162
153
  ```bash
package/bin/pix.mjs CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from "node:child_process";
3
- import { existsSync, readdirSync, statSync } from "node:fs";
2
+ import { existsSync } from "node:fs";
4
3
  import { delimiter, dirname, join } from "node:path";
5
4
  import { fileURLToPath } from "node:url";
6
5
 
@@ -11,10 +10,7 @@ const packageRoot = dirname(dirname(launcherPath));
11
10
  const mainPath = fileURLToPath(new URL("../dist/main.js", import.meta.url));
12
11
  const updatePath = fileURLToPath(new URL("../dist/app/cli/update.js", import.meta.url));
13
12
  const installPath = fileURLToPath(new URL("../dist/app/cli/install.js", import.meta.url));
14
- const distPath = dirname(mainPath);
15
- const rawArgs = process.argv.slice(2);
16
- const childArgs = [];
17
- let reloadOnBuild = truthyEnv(process.env.PIX_RELOAD_ON_BUILD);
13
+ const cliArgs = process.argv.slice(2);
18
14
 
19
15
  if (!isCurrentNodeSupported()) {
20
16
  console.error(`[pix] Node ${minimumNodeVersionLabel}+ is required; current Node is ${process.versions.node}.`);
@@ -22,63 +18,31 @@ if (!isCurrentNodeSupported()) {
22
18
  process.exit(1);
23
19
  }
24
20
 
25
- for (const arg of rawArgs) {
26
- if (arg === "--reload-on-build") {
27
- reloadOnBuild = true;
28
- continue;
29
- }
30
- if (arg === "--no-reload-on-build") {
31
- reloadOnBuild = false;
32
- continue;
33
- }
34
- childArgs.push(arg);
35
- }
21
+ applyPixRuntimeEnv();
36
22
 
37
- if (childArgs[0] === "update") {
23
+ if (cliArgs[0] === "update") {
38
24
  if (!existsSync(updatePath)) {
39
25
  console.error("pix update is not built yet. Run `npm run build:pix` or update from a published package.");
40
26
  process.exit(1);
41
27
  }
42
28
  const { runPixUpdateCli } = await import(new URL("../dist/app/cli/update.js", import.meta.url));
43
- process.exit(await runPixUpdateCli(childArgs.slice(1)));
29
+ process.exit(await runPixUpdateCli(cliArgs.slice(1)));
44
30
  }
45
31
 
46
- if (childArgs[0] === "install" || childArgs[0] === "setup") {
32
+ if (cliArgs[0] === "install" || cliArgs[0] === "setup") {
47
33
  if (!existsSync(installPath)) {
48
34
  console.error("pix install is not built yet. Run `npm run build:pix` or update from a published package.");
49
35
  process.exit(1);
50
36
  }
51
37
  const { runPixInstallCli } = await import(new URL("../dist/app/cli/install.js", import.meta.url));
52
- process.exit(await runPixInstallCli(childArgs.slice(1), { env: pixChildEnv() }));
38
+ process.exit(await runPixInstallCli(cliArgs.slice(1), { env: process.env }));
53
39
  }
54
40
 
55
41
  if (!existsSync(mainPath)) {
56
42
  console.error("pix is not built yet. Run `npm run build:pix` or `npm run watch:pix`.");
57
43
  process.exit(1);
58
44
  }
59
-
60
- let child = undefined;
61
- let reloadTimer = undefined;
62
- let distPollTimer = undefined;
63
- let distSnapshot = snapshotDist();
64
- let restarting = false;
65
- let shuttingDown = false;
66
-
67
- startChild();
68
- if (reloadOnBuild) startDistPolling();
69
-
70
- for (const signal of ["SIGINT", "SIGTERM"]) {
71
- process.on(signal, () => {
72
- shuttingDown = true;
73
- stopDistPolling();
74
- child?.kill(signal);
75
- });
76
- }
77
-
78
- function truthyEnv(value) {
79
- if (!value) return false;
80
- return !["0", "false", "no", "off"].includes(value.toLowerCase());
81
- }
45
+ await import(new URL("../dist/main.js", import.meta.url));
82
46
 
83
47
  function isCurrentNodeSupported() {
84
48
  const parts = process.versions.node.split(".").map((part) => Number.parseInt(part, 10));
@@ -91,117 +55,10 @@ function isCurrentNodeSupported() {
91
55
  return true;
92
56
  }
93
57
 
94
- function startChild() {
95
- child = spawn(process.execPath, [mainPath, ...childArgs], {
96
- stdio: "inherit",
97
- env: pixChildEnv(),
98
- });
99
-
100
- child.on("error", (error) => {
101
- console.error(error.message);
102
- process.exitCode = 1;
103
- });
104
-
105
- child.on("exit", (code, signal) => {
106
- child = undefined;
107
- if (restarting) return;
108
-
109
- shuttingDown = true;
110
- stopDistPolling();
111
- if (signal) {
112
- process.exitCode = signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1;
113
- return;
114
- }
115
- process.exitCode = code ?? 1;
116
- });
117
- }
118
-
119
- function pixChildEnv() {
120
- const env = { ...process.env };
58
+ function applyPixRuntimeEnv() {
121
59
  const bundledBinPath = join(packageRoot, "node_modules", ".bin");
122
60
  if (existsSync(bundledBinPath)) {
123
- env.PATH = [bundledBinPath, env.PATH ?? ""].filter(Boolean).join(delimiter);
124
- env.PIX_BUNDLED_PI_BIN = bundledBinPath;
61
+ process.env.PATH = [bundledBinPath, process.env.PATH ?? ""].filter(Boolean).join(delimiter);
62
+ process.env.PIX_BUNDLED_PI_BIN = bundledBinPath;
125
63
  }
126
- return env;
127
- }
128
-
129
- function startDistPolling() {
130
- const pollInterval = Number(process.env.PIX_RELOAD_POLL_MS ?? 1000);
131
- distPollTimer = setInterval(() => {
132
- const nextSnapshot = snapshotDist();
133
- if (nextSnapshot === distSnapshot) return;
134
-
135
- distSnapshot = nextSnapshot;
136
- queueReload();
137
- }, Number.isFinite(pollInterval) && pollInterval > 0 ? pollInterval : 1000);
138
- }
139
-
140
- function stopDistPolling() {
141
- if (reloadTimer) clearTimeout(reloadTimer);
142
- if (distPollTimer) clearInterval(distPollTimer);
143
- reloadTimer = undefined;
144
- distPollTimer = undefined;
145
- }
146
-
147
- function queueReload() {
148
- if (shuttingDown) return;
149
- if (reloadTimer) clearTimeout(reloadTimer);
150
- reloadTimer = setTimeout(() => {
151
- reloadTimer = undefined;
152
- restartChild();
153
- }, 250);
154
- }
155
-
156
- function restartChild() {
157
- if (shuttingDown) return;
158
- if (!child) {
159
- startChild();
160
- return;
161
- }
162
-
163
- restarting = true;
164
- const currentChild = child;
165
- currentChild.once("exit", () => {
166
- restarting = false;
167
- if (!shuttingDown) startChild();
168
- });
169
- console.error("[pix] dist changed; restarting renderer");
170
- currentChild.kill("SIGTERM");
171
- }
172
-
173
- function snapshotDist() {
174
- let newestMtime = 0;
175
- let runtimeFileCount = 0;
176
- const pendingDirs = [distPath];
177
-
178
- while (pendingDirs.length > 0) {
179
- const currentDir = pendingDirs.pop();
180
- if (!currentDir) continue;
181
-
182
- let entries;
183
- try {
184
- entries = readdirSync(currentDir, { withFileTypes: true });
185
- } catch {
186
- continue;
187
- }
188
-
189
- for (const entry of entries) {
190
- const entryPath = join(currentDir, entry.name);
191
- if (entry.isDirectory()) {
192
- pendingDirs.push(entryPath);
193
- continue;
194
- }
195
- if (!entry.isFile() || !entry.name.endsWith(".js")) continue;
196
-
197
- runtimeFileCount += 1;
198
- try {
199
- newestMtime = Math.max(newestMtime, statSync(entryPath).mtimeMs);
200
- } catch {
201
- // If tsc is replacing a file while we scan, the next poll will see the final state.
202
- }
203
- }
204
- }
205
-
206
- return `${runtimeFileCount}:${newestMtime}`;
207
64
  }
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;
@@ -82,6 +85,7 @@ export declare class PiUiExtendApp {
82
85
  private openSearchResultInNewTab;
83
86
  private scrollToUserMessageJumpTarget;
84
87
  private findUserEntryBySessionEntryId;
88
+ private findUserEntryByJumpText;
85
89
  private loadSessionHistoryAsync;
86
90
  private handleSessionEvent;
87
91
  private findEntry;
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";
@@ -46,6 +46,9 @@ import { AppVoiceController } from "./input/voice-controller.js";
46
46
  import { createIsolatedExtensionEventBus } from "./extensions/extension-event-bus.js";
47
47
  import { setAppIconTheme } from "./icons.js";
48
48
  const TERMINAL_BELL_ATTENTION_EVENT = "pix:terminal-bell:attention";
49
+ function normalizeJumpTargetText(text) {
50
+ return text.replace(/\s+/gu, " ").trim();
51
+ }
49
52
  const SUBAGENTS_LIVE_STATE_EVENT = "pi-tools-suite:async-subagents:live-state";
50
53
  const TODO_STATE_EVENT = "pi-tools-suite:todo:state";
51
54
  const COALESCED_RENDER_DELAY_MS = 16;
@@ -68,7 +71,7 @@ export class PiUiExtendApp {
68
71
  extensionUiController;
69
72
  extensionActions;
70
73
  pixConfig;
71
- outputFilters;
74
+ outputFilters = [];
72
75
  commandController;
73
76
  inputActions;
74
77
  inputController;
@@ -127,7 +130,7 @@ export class PiUiExtendApp {
127
130
  constructor(options) {
128
131
  this.options = options;
129
132
  this.theme = THEMES[options.themeName];
130
- this.pixConfig = loadPixConfig(this.options.cwd);
133
+ this.pixConfig = defaultPixConfig();
131
134
  setAppIconTheme(this.pixConfig.iconTheme.name);
132
135
  const app = this;
133
136
  this.blinkController = new AppBlinkController({
@@ -160,7 +163,7 @@ export class PiUiExtendApp {
160
163
  });
161
164
  this.tabsController = new AppTabsController({
162
165
  options: this.options,
163
- maxProjectSessions: this.pixConfig.maxProjectSessions,
166
+ maxProjectSessions: () => this.pixConfig.maxProjectSessions,
164
167
  blinkController: this.blinkController,
165
168
  runtime: () => this.runtime,
166
169
  createRuntimeForNewSession: () => this.createRuntime(newTabRuntimeOptions(this.options)),
@@ -398,18 +401,16 @@ export class PiUiExtendApp {
398
401
  get subagentsWidgetState() { return app.subagentsWidgetController.widgetState; },
399
402
  get voicePartialText() { return app.voicePartialText; },
400
403
  get autocompleteSuggestion() { return app.autocompleteController.suggestionText(); },
401
- get queuedMessageWidgetEntries() { return app.queuedMessages.deferredQueuedEntries(); },
404
+ get queuedMessageWidgetEntries() { return app.queuedMessages.queuedEntries(); },
402
405
  renderExtensionInputComponent: (width) => this.extensionUiController.renderActiveCustomUi(width),
403
406
  extensionInputUsesEditor: () => this.extensionUiController.activeCustomUiUsesEditor(),
404
407
  widgetTuiHandle: () => this.extensionUiController.widgetTuiHandle(),
405
408
  createExtensionTheme: () => this.extensionUiController.createExtensionTheme(),
406
409
  suppressExtensionWidget: (key) => this.extensionUiController.suppressWidget(key),
407
410
  });
408
- this.outputFilters = compileOutputFilterPatterns(this.pixConfig.outputFilters.patterns);
411
+ this.updateOutputFilters();
409
412
  this.conversationViewport = new ConversationViewport({
410
413
  get entries() { return app.entries; },
411
- get session() { return app.runtime?.session; },
412
- get deferredUserMessages() { return app.queuedMessages.deferredUserMessages; },
413
414
  get entryRenderVersions() { return app.sessionEvents.entryRenderVersions; },
414
415
  get superCompactTools() { return app.superCompactTools; },
415
416
  get allThinkingExpanded() { return app.allThinkingExpanded; },
@@ -700,6 +701,7 @@ export class PiUiExtendApp {
700
701
  inputEditor: () => this.inputEditor,
701
702
  enableTerminal: () => this.terminalController.enableTerminal(),
702
703
  disposeRuntimeForQuit: (runtime) => this.terminalController.disposeRuntimeForQuit(runtime),
704
+ loadStartupConfig: () => this.loadStartupConfig(),
703
705
  loadRequestHistory: () => this.requestHistory.load(),
704
706
  startSubagentsPolling: () => this.subagentsWidgetController.startPolling(),
705
707
  closeSdkMenuForBind: () => this.popupMenus.closeSdkMenu(undefined, { render: false, restoreStatus: false }),
@@ -750,8 +752,35 @@ export class PiUiExtendApp {
750
752
  createRuntime(options) {
751
753
  return createPixRuntime(options, {
752
754
  eventBus: this.createExtensionEventBus(),
755
+ config: this.pixConfig,
753
756
  });
754
757
  }
758
+ async loadStartupConfig() {
759
+ await yieldToEventLoop();
760
+ this.applyPixConfig(loadPixConfig(this.options.cwd));
761
+ }
762
+ applyPixConfig(config) {
763
+ replaceRecord(this.pixConfig.toolRenderer, config.toolRenderer);
764
+ replaceRecord(this.pixConfig.outputFilters, config.outputFilters);
765
+ replaceRecord(this.pixConfig.promptEnhancer, config.promptEnhancer);
766
+ replaceRecord(this.pixConfig.autocomplete, config.autocomplete);
767
+ replaceRecord(this.pixConfig.modelColors, config.modelColors);
768
+ replaceRecord(this.pixConfig.iconTheme, config.iconTheme);
769
+ replaceRecord(this.pixConfig.dictation, config.dictation);
770
+ if (config.defaultModel === undefined)
771
+ delete this.pixConfig.defaultModel;
772
+ else
773
+ this.pixConfig.defaultModel = { ...config.defaultModel };
774
+ this.pixConfig.ignoreContextFiles = config.ignoreContextFiles;
775
+ this.pixConfig.maxProjectSessions = config.maxProjectSessions;
776
+ this.updateOutputFilters();
777
+ this.voiceController.updateDictationConfig(this.pixConfig.dictation);
778
+ setAppIconTheme(this.pixConfig.iconTheme.name);
779
+ this.conversationViewport.clear();
780
+ }
781
+ updateOutputFilters() {
782
+ this.outputFilters.splice(0, this.outputFilters.length, ...compileOutputFilterPatterns(this.pixConfig.outputFilters.patterns));
783
+ }
755
784
  async start() {
756
785
  await this.sessionLifecycle.start();
757
786
  this.modelUsageController.startPolling();
@@ -888,20 +917,42 @@ export class PiUiExtendApp {
888
917
  async scrollToUserMessageJumpTarget(target) {
889
918
  if (target.entryId && this.scrollController.scrollToConversationEntry(target.entryId))
890
919
  return true;
891
- if (!target.sessionEntryId)
892
- return false;
893
- let entry = this.findUserEntryBySessionEntryId(target.sessionEntryId);
894
- while (!entry && this.sessionEvents.hasOlderSessionHistory() && !this.sessionEvents.isLoadingOlderSessionHistory()) {
895
- const loaded = await this.sessionEvents.loadOlderSessionHistory({ render: false });
896
- if (!loaded)
897
- break;
898
- entry = this.findUserEntryBySessionEntryId(target.sessionEntryId);
920
+ this.workspaceActions.syncUserSessionEntryMetadata();
921
+ if (target.sessionEntryId) {
922
+ let entry = this.findUserEntryBySessionEntryId(target.sessionEntryId);
923
+ while (!entry && this.sessionEvents.hasOlderSessionHistory() && !this.sessionEvents.isLoadingOlderSessionHistory()) {
924
+ const loaded = await this.sessionEvents.loadOlderSessionHistory({ render: false });
925
+ if (!loaded)
926
+ break;
927
+ entry = this.findUserEntryBySessionEntryId(target.sessionEntryId);
928
+ }
929
+ if (entry && this.scrollController.scrollToConversationEntry(entry.id))
930
+ return true;
899
931
  }
900
- return entry ? this.scrollController.scrollToConversationEntry(entry.id) : false;
932
+ const fallbackEntry = this.findUserEntryByJumpText(target);
933
+ return fallbackEntry ? this.scrollController.scrollToConversationEntry(fallbackEntry.id) : false;
901
934
  }
902
935
  findUserEntryBySessionEntryId(sessionEntryId) {
903
936
  return this.entries.find((entry) => entry.kind === "user" && entry.sessionEntryId === sessionEntryId);
904
937
  }
938
+ findUserEntryByJumpText(target) {
939
+ if (!target.text)
940
+ return undefined;
941
+ const userEntries = this.entries.filter((entry) => entry.kind === "user");
942
+ if (target.userIndex !== undefined && target.userCount !== undefined) {
943
+ const visibleIndex = target.userIndex - (target.userCount - userEntries.length);
944
+ const entry = userEntries[visibleIndex];
945
+ if (entry && normalizeJumpTargetText(entry.text) === normalizeJumpTargetText(target.text))
946
+ return entry;
947
+ }
948
+ const normalizedTargetText = normalizeJumpTargetText(target.text);
949
+ for (let index = userEntries.length - 1; index >= 0; index -= 1) {
950
+ const entry = userEntries[index];
951
+ if (entry && normalizeJumpTargetText(entry.text) === normalizedTargetText)
952
+ return entry;
953
+ }
954
+ return undefined;
955
+ }
905
956
  async loadSessionHistoryAsync(options) {
906
957
  return this.sessionEvents.loadSessionHistoryAsync(options);
907
958
  }
@@ -1053,3 +1104,37 @@ function newTabRuntimeOptions(options) {
1053
1104
  ...(options.modelRef === undefined ? {} : { modelRef: options.modelRef }),
1054
1105
  };
1055
1106
  }
1107
+ async function yieldToEventLoop() {
1108
+ await new Promise((resolve) => { setTimeout(resolve, 0); });
1109
+ }
1110
+ function replaceRecord(target, source) {
1111
+ for (const key of Object.keys(target)) {
1112
+ if (!(key in source))
1113
+ delete target[key];
1114
+ }
1115
+ for (const [key, value] of Object.entries(source)) {
1116
+ const existing = target[key];
1117
+ if (isMutableRecord(existing) && isMutableRecord(value)) {
1118
+ replaceRecord(existing, value);
1119
+ }
1120
+ else {
1121
+ target[key] = cloneConfigValue(value);
1122
+ }
1123
+ }
1124
+ }
1125
+ function cloneConfigValue(value) {
1126
+ if (Array.isArray(value))
1127
+ return value.map(cloneConfigValue);
1128
+ if (isMutableRecord(value))
1129
+ return cloneConfigRecord(value);
1130
+ return value;
1131
+ }
1132
+ function cloneConfigRecord(value) {
1133
+ const cloned = {};
1134
+ for (const [key, nested] of Object.entries(value))
1135
+ cloned[key] = cloneConfigValue(nested);
1136
+ return cloned;
1137
+ }
1138
+ function isMutableRecord(value) {
1139
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1140
+ }
@@ -1,3 +1,2 @@
1
1
  import type { AgentSessionRuntime } from "@earendil-works/pi-coding-agent";
2
2
  export declare function createStartupInfoMessage(runtime: AgentSessionRuntime): string;
3
- export declare function isEmptyStartupSession(runtime: AgentSessionRuntime): boolean;
@@ -12,9 +12,6 @@ export function createStartupInfoMessage(runtime) {
12
12
  ...sections.flatMap(formatSection),
13
13
  ].join("\n").trimEnd();
14
14
  }
15
- export function isEmptyStartupSession(runtime) {
16
- return Array.isArray(runtime.session.messages) && runtime.session.messages.length === 0;
17
- }
18
15
  function startupSections(runtime) {
19
16
  const loader = runtime.session.resourceLoader;
20
17
  const context = loader.getAgentsFiles();
@@ -9,6 +9,7 @@ import { runProcess } from "../process.js";
9
9
  import { copyTextToClipboard } from "../screen/clipboard.js";
10
10
  import { formatAccountUsageReport, queryAccountUsageReport } from "../model/model-usage-status.js";
11
11
  import { checkPixUpdate, formatPixUpdateCheck, parsePixUpdateArgs, pixUpdateUsage } from "../cli/update.js";
12
+ import { createStartupInfoMessage } from "../cli/startup-info.js";
12
13
  export class SessionCommandActions {
13
14
  host;
14
15
  constructor(host) {
@@ -114,6 +115,8 @@ export class SessionCommandActions {
114
115
  return;
115
116
  const stats = runtime.session.getSessionStats();
116
117
  const lines = [
118
+ createStartupInfoMessage(runtime),
119
+ "",
117
120
  "Session info",
118
121
  ...(runtime.session.sessionName ? [`name: ${runtime.session.sessionName}`] : []),
119
122
  `file: ${stats.sessionFile ?? "in-memory"}`,
@@ -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) {
@@ -810,7 +810,13 @@ export function buildUserMessageJumpItems(entries) {
810
810
  const preview = sanitizeText(entry.text).replace(/\s+/g, " ").trim();
811
811
  const label = `${index + 1}. ${preview || "(empty message)"}`;
812
812
  return {
813
- value: { ...(entry.entryId === undefined ? {} : { entryId: entry.entryId }), ...(entry.sessionEntryId === undefined ? {} : { sessionEntryId: entry.sessionEntryId }) },
813
+ value: {
814
+ ...(entry.entryId === undefined ? {} : { entryId: entry.entryId }),
815
+ ...(entry.sessionEntryId === undefined ? {} : { sessionEntryId: entry.sessionEntryId }),
816
+ text: entry.text,
817
+ userIndex: index,
818
+ userCount: userEntries.length,
819
+ },
814
820
  label,
815
821
  ...(entry.entryId ? {} : { description: "load older history and jump" }),
816
822
  aliases: [entry.sessionEntryId ?? "", entry.entryId ?? ""],
@@ -2,7 +2,7 @@ import { applyOutputFilters } from "../../config.js";
2
2
  import { renderMarkdownTextLines } from "../../markdown-format.js";
3
3
  import { attachImageClickTargets } from "../screen/image-click-targets.js";
4
4
  import { APP_ICONS } from "../icons.js";
5
- import { horizontalPaddingLayout, padHorizontalText, wrapText } from "./render-text.js";
5
+ import { horizontalPaddingLayout, padHorizontalText, wrapTextLines } from "./render-text.js";
6
6
  import { renderConversationShellEntry } from "./conversation-shell-renderer.js";
7
7
  import { renderConversationToolEntry, renderThinkingEntry } from "./conversation-tool-renderer.js";
8
8
  export function renderConversationEntry(entry, width, options) {
@@ -10,6 +10,7 @@ export function renderConversationEntry(entry, width, options) {
10
10
  const userLine = (text, entryId, syntaxHighlight, segments) => ({
11
11
  text: padHorizontalText(text, width),
12
12
  colorOverride: options.colors.userForeground,
13
+ backgroundOverride: options.colors.userMessageBackground,
13
14
  ...(segments && segments.length > 0 ? { segments: segments.map((segment) => ({ ...segment, start: segment.start + userContentLeft, end: segment.end + userContentLeft })) } : {}),
14
15
  ...(syntaxHighlight === undefined ? {} : { syntaxHighlight }),
15
16
  ...(entryId === undefined ? {} : { target: { kind: "user-message", id: entryId } }),
@@ -21,17 +22,25 @@ export function renderConversationEntry(entry, width, options) {
21
22
  target: { kind: "queue-message", id: entryId },
22
23
  });
23
24
  const userMessageLines = (userEntry) => {
24
- const lines = renderMarkdownTextLines(userEntry.text, userContentWidth, userContentLeft).map((line) => userLine(line.text, userEntry.id, line.syntaxHighlight, line.segments));
25
+ const lines = renderMarkdownTextLines(userEntry.text, userContentWidth, userContentLeft).map((line) => ({
26
+ ...userLine(line.text, userEntry.id, line.syntaxHighlight, line.segments),
27
+ ...(line.copyText === undefined ? {} : { copyText: line.copyText }),
28
+ ...(line.continuesOnNextLine ? { continuesOnNextLine: true } : {}),
29
+ }));
25
30
  return attachImageClickTargets(lines, userEntry.id, userEntry.images, { foreground: options.colors.info, underline: true });
26
31
  };
27
32
  const queuedMessageLines = (queuedEntry) => {
28
33
  const icon = queuedEntry.queueSource === "deferred" ? APP_ICONS.pause : APP_ICONS.timerSand;
29
- const contentLines = wrapText(`${icon} ${queuedEntry.text}`, width);
30
- return contentLines.map((text, index) => queuedLine(text, queuedEntry.id, index === 0 ? [{ start: 0, end: icon.length, foreground: options.colors.info }] : undefined));
34
+ const contentLines = wrapTextLines(`${icon} ${queuedEntry.text}`, width);
35
+ return contentLines.map((line, index) => ({
36
+ ...queuedLine(line.text, queuedEntry.id, index === 0 ? [{ start: 0, end: icon.length, foreground: options.colors.info }] : undefined),
37
+ copyText: line.copyText,
38
+ ...(line.continuesOnNextLine ? { continuesOnNextLine: true } : {}),
39
+ }));
31
40
  };
32
41
  switch (entry.kind) {
33
42
  case "system":
34
- return wrapText(`system: ${entry.text}`, width).map((text) => ({ text, variant: "muted" }));
43
+ return wrapTextLines(`system: ${entry.text}`, width).map((line) => ({ text: line.text, copyText: line.copyText, ...(line.continuesOnNextLine ? { continuesOnNextLine: true } : {}), variant: "muted" }));
35
44
  case "user":
36
45
  return userMessageLines(entry);
37
46
  case "queued":
@@ -41,21 +50,23 @@ export function renderConversationEntry(entry, width, options) {
41
50
  case "custom":
42
51
  return renderCustomEntry(entry, width);
43
52
  case "session-aborted":
44
- return wrapText(entry.text, width).map((text) => ({ text, variant: "error" }));
53
+ return wrapTextLines(entry.text, width).map((line) => ({ text: line.text, copyText: line.copyText, ...(line.continuesOnNextLine ? { continuesOnNextLine: true } : {}), variant: "error" }));
45
54
  case "shell":
46
55
  return renderConversationShellEntry(entry, width, options);
47
56
  case "thinking":
48
57
  return renderThinkingEntry(entry, width, options);
49
58
  case "error":
50
- return wrapText(`error: ${entry.text}`, width).map((text) => ({ text, variant: "error" }));
59
+ return wrapTextLines(`error: ${entry.text}`, width).map((line) => ({ text: line.text, copyText: line.copyText, ...(line.continuesOnNextLine ? { continuesOnNextLine: true } : {}), variant: "error" }));
51
60
  case "tool":
52
61
  return renderConversationToolEntry(entry, width, options);
53
62
  }
54
63
  }
55
64
  function renderCustomEntry(entry, width) {
56
65
  const label = `[${entry.customType}]`;
57
- return wrapText(`${label}\n${entry.text}`, width).map((text, index) => ({
58
- text,
66
+ return wrapTextLines(`${label}\n${entry.text}`, width).map((line, index) => ({
67
+ text: line.text,
68
+ copyText: line.copyText,
69
+ ...(line.continuesOnNextLine ? { continuesOnNextLine: true } : {}),
59
70
  variant: index === 0 ? "accent" : "normal",
60
71
  }));
61
72
  }
@@ -69,10 +80,18 @@ function renderAssistantLines(text, width, options) {
69
80
  return [];
70
81
  const lines = [];
71
82
  for (const line of contentLines) {
83
+ const headingSegment = line.heading
84
+ ? { start: contentLeft, end: contentLeft + line.text.length, foreground: options.colors.assistantForeground, bold: true }
85
+ : undefined;
86
+ const existingSegments = line.segments?.map((segment) => ({ ...segment, start: segment.start + contentLeft, end: segment.end + contentLeft })) ?? [];
87
+ const allSegments = headingSegment ? [headingSegment, ...existingSegments] : existingSegments;
72
88
  lines.push({
73
89
  text: padHorizontalText(line.text, width),
90
+ ...(line.copyText === undefined ? {} : { copyText: line.copyText }),
91
+ ...(line.continuesOnNextLine ? { continuesOnNextLine: true } : {}),
74
92
  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 })) } : {}),
93
+ backgroundOverride: options.colors.assistantMessageBackground,
94
+ ...(allSegments.length > 0 ? { segments: allSegments } : {}),
76
95
  ...(line.syntaxHighlight ? { syntaxHighlight: line.syntaxHighlight } : {}),
77
96
  });
78
97
  }
@@ -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, "");