pi-ui-extend 0.1.32 → 0.1.33

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 (93) hide show
  1. package/README.md +1 -1
  2. package/dist/app/app.d.ts +2 -0
  3. package/dist/app/app.js +28 -0
  4. package/dist/app/commands/command-session-actions.js +29 -1
  5. package/dist/app/constants.d.ts +1 -1
  6. package/dist/app/constants.js +2 -2
  7. package/dist/app/icons.d.ts +4 -9
  8. package/dist/app/icons.js +11 -34
  9. package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
  10. package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
  11. package/dist/app/rendering/conversation-tool-renderer.js +12 -18
  12. package/dist/app/rendering/conversation-viewport.d.ts +4 -0
  13. package/dist/app/rendering/conversation-viewport.js +144 -13
  14. package/dist/app/rendering/dcp-stats.js +42 -16
  15. package/dist/app/rendering/render-controller.js +4 -0
  16. package/dist/app/rendering/status-line-renderer.d.ts +7 -1
  17. package/dist/app/rendering/status-line-renderer.js +21 -0
  18. package/dist/app/rendering/tab-line-renderer.js +2 -2
  19. package/dist/app/rendering/tool-block-renderer.d.ts +1 -0
  20. package/dist/app/rendering/tool-block-renderer.js +37 -11
  21. package/dist/app/runtime.js +1 -1
  22. package/dist/app/screen/mouse-controller.d.ts +5 -1
  23. package/dist/app/screen/mouse-controller.js +16 -0
  24. package/dist/app/screen/scroll-controller.d.ts +20 -0
  25. package/dist/app/screen/scroll-controller.js +127 -10
  26. package/dist/app/session/lazy-session-manager.js +35 -5
  27. package/dist/app/session/pix-system-message.d.ts +1 -0
  28. package/dist/app/session/pix-system-message.js +14 -3
  29. package/dist/app/session/queued-message-controller.d.ts +11 -4
  30. package/dist/app/session/queued-message-controller.js +74 -59
  31. package/dist/app/session/queued-message-entries.d.ts +2 -1
  32. package/dist/app/session/queued-message-entries.js +12 -1
  33. package/dist/app/session/session-event-controller.d.ts +42 -1
  34. package/dist/app/session/session-event-controller.js +473 -29
  35. package/dist/app/session/session-history.js +23 -4
  36. package/dist/app/session/tabs-controller.d.ts +11 -1
  37. package/dist/app/session/tabs-controller.js +102 -21
  38. package/dist/app/types.d.ts +14 -1
  39. package/dist/bundled-extensions/question/contract.d.ts +25 -0
  40. package/dist/bundled-extensions/question/contract.js +94 -0
  41. package/dist/bundled-extensions/question/index.d.ts +7 -0
  42. package/dist/bundled-extensions/question/index.js +28 -0
  43. package/dist/bundled-extensions/question/render.d.ts +4 -0
  44. package/dist/bundled-extensions/question/render.js +27 -0
  45. package/dist/bundled-extensions/question/result.d.ts +6 -0
  46. package/dist/bundled-extensions/question/result.js +84 -0
  47. package/dist/bundled-extensions/question/tool-description.d.ts +7 -0
  48. package/dist/bundled-extensions/question/tool-description.js +11 -0
  49. package/dist/bundled-extensions/question/tui.d.ts +2 -0
  50. package/dist/bundled-extensions/question/tui.js +577 -0
  51. package/dist/bundled-extensions/question/types.d.ts +103 -0
  52. package/dist/bundled-extensions/question/types.js +1 -0
  53. package/dist/bundled-extensions/session-title/config.d.ts +17 -0
  54. package/dist/bundled-extensions/session-title/config.js +150 -0
  55. package/dist/bundled-extensions/session-title/index.d.ts +5 -0
  56. package/dist/bundled-extensions/session-title/index.js +384 -0
  57. package/dist/bundled-extensions/session-title/title-generation.d.ts +26 -0
  58. package/dist/bundled-extensions/session-title/title-generation.js +141 -0
  59. package/dist/bundled-extensions/terminal-bell/index.d.ts +14 -0
  60. package/dist/bundled-extensions/terminal-bell/index.js +491 -0
  61. package/dist/config.d.ts +1 -1
  62. package/dist/config.js +2 -1
  63. package/dist/default-pix-config.js +2 -1
  64. package/dist/icon-theme.d.ts +7 -0
  65. package/dist/icon-theme.js +36 -0
  66. package/dist/schemas/pi-tools-suite-schema.d.ts +4 -0
  67. package/dist/schemas/pi-tools-suite-schema.js +5 -0
  68. package/dist/schemas/pix-schema.d.ts +1 -0
  69. package/dist/schemas/pix-schema.js +1 -0
  70. package/external/pi-tools-suite/README.md +7 -7
  71. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +16 -16
  72. package/external/pi-tools-suite/src/async-subagents/core/state.ts +18 -4
  73. package/external/pi-tools-suite/src/async-subagents/core/types.ts +4 -0
  74. package/external/pi-tools-suite/src/async-subagents/tools/result.ts +14 -26
  75. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +0 -1
  76. package/external/pi-tools-suite/src/dcp/config.ts +14 -14
  77. package/external/pi-tools-suite/src/dcp/index.ts +31 -43
  78. package/external/pi-tools-suite/src/dcp/state-persistence.ts +151 -0
  79. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +25 -18
  80. package/external/pi-tools-suite/src/tool-descriptions.ts +6 -7
  81. package/package.json +3 -2
  82. package/schemas/pi-tools-suite.json +14 -0
  83. package/schemas/pix.json +7 -0
  84. package/extensions/question/contract.ts +0 -100
  85. package/extensions/question/index.ts +0 -34
  86. package/extensions/question/render.ts +0 -28
  87. package/extensions/question/result.ts +0 -86
  88. package/extensions/question/tool-description.ts +0 -11
  89. package/extensions/question/tui.ts +0 -629
  90. package/extensions/question/types.ts +0 -123
  91. package/extensions/session-title/config.ts +0 -164
  92. package/extensions/session-title/index.ts +0 -502
  93. package/extensions/terminal-bell/index.ts +0 -345
package/README.md CHANGED
@@ -212,7 +212,7 @@ Type `/` in the prompt to open the command picker. Commands that accept argument
212
212
  | `/import` | `<path.jsonl>` | Import and resume a session from a JSONL file. |
213
213
  | `/share` | — | Share the session as a private GitHub gist (requires `gh` CLI). |
214
214
  | `/copy` | — | Copy the last agent message to the clipboard. |
215
- | `/name` | `[name]` | Show or set the session display name. |
215
+ | `/name` | `[name]` | Set the session display name. Without arguments, generate one automatically using the session-title logic. |
216
216
  | `/session` | — | Show session info: message counts, token usage, and cost. |
217
217
  | `/usage` | — | Show local account quota usage and context window utilization. |
218
218
  | `/changelog` | — | Show the Pi package changelog. |
package/dist/app/app.d.ts CHANGED
@@ -109,6 +109,8 @@ export declare class PiUiExtendApp {
109
109
  private render;
110
110
  private scheduleRender;
111
111
  private syncScrollAfterInputEditorChange;
112
+ private conversationQuickScrollDirections;
113
+ private scrollConversationQuick;
112
114
  private renderStatusLine;
113
115
  private terminalColumns;
114
116
  private terminalRows;
package/dist/app/app.js CHANGED
@@ -183,10 +183,13 @@ export class PiUiExtendApp {
183
183
  loadSessionHistoryAsync: (options) => this.loadSessionHistoryAsync(options),
184
184
  captureSessionView: () => this.captureSessionView(),
185
185
  restoreSessionView: (view) => this.restoreSessionView(view),
186
+ restoreScrollState: (state) => this.scrollController.restoreState(state),
186
187
  syncUserSessionEntryMetadata: () => this.workspaceActions.syncUserSessionEntryMetadata(),
187
188
  captureInputState: () => this.inputEditor.draftState,
188
189
  restoreInputState: (state) => this.restoreTabInputState(state),
189
190
  closeMenusForTabSwitch: () => this.popupMenus.closeMenusForTabSwitch(),
191
+ captureAutoUserMessages: () => this.queuedMessages.captureAutoUserMessages(),
192
+ restoreAutoUserMessages: (messages) => this.queuedMessages.restoreAutoUserMessages(messages),
190
193
  captureDeferredUserMessages: () => this.queuedMessages.captureDeferredUserMessages(),
191
194
  restoreDeferredUserMessages: (messages) => this.queuedMessages.restoreDeferredUserMessages(messages),
192
195
  addEntry: (entry) => this.addEntry(entry),
@@ -283,6 +286,7 @@ export class PiUiExtendApp {
283
286
  terminalBellSoundStatusWidgetEnabled: () => this.terminalBellSoundController.isEnabled(),
284
287
  voiceStatusWidgetText: () => this.voiceController.statusWidgetText(),
285
288
  voiceStatusWidgetActive: () => this.voiceController.statusWidgetActive(),
289
+ conversationQuickScrollDirections: () => this.conversationQuickScrollDirections(),
286
290
  queueableInputActive: () => this.inputEditor.promptText.trimEnd().length > 0 || this.inputEditor.images.length > 0,
287
291
  userMessageJumpMenuActive: () => this.popupMenus.directMenu === "user-message-jump",
288
292
  allThinkingExpandedActive: () => this.allThinkingExpanded,
@@ -364,6 +368,7 @@ export class PiUiExtendApp {
364
368
  setSessionStatus: (session) => this.setSessionStatus(session),
365
369
  setSessionActivity: (activity) => this.setSessionActivity(activity),
366
370
  updateQueuedMessageStatus: () => this.queuedMessages.updateQueuedMessageStatus(),
371
+ flushAutoUserMessages: () => { void this.queuedMessages.flushAutoUserMessages(); },
367
372
  prepareWorkspaceMutation: (toolName, args) => this.workspaceActions.prepareWorkspaceMutation(toolName, args),
368
373
  workspaceMutationFromToolExecution: (input) => this.workspaceActions.workspaceMutationFromToolExecution(input),
369
374
  recordWorkspaceMutationForUserEntry: (entryId, mutation) => this.workspaceActions.recordWorkspaceMutationForUserEntry(entryId, mutation),
@@ -420,6 +425,7 @@ export class PiUiExtendApp {
420
425
  colors: this.theme.colors,
421
426
  pixConfig: this.pixConfig,
422
427
  outputFilters: this.outputFilters,
428
+ availableThinkingLevels: () => this.runtime?.session.getAvailableThinkingLevels(),
423
429
  hasDynamicConversationBlock: () => this.popupMenus.hasDynamicConversationBlock(),
424
430
  isDynamicConversationBlock: (entry) => this.popupMenus.isDynamicConversationBlock(entry),
425
431
  renderInlineUserMessageMenu: (entry, context) => this.popupMenus.renderInlineUserMessageMenu(entry, context),
@@ -433,6 +439,9 @@ export class PiUiExtendApp {
433
439
  hasOlderSessionHistory: () => this.sessionEvents.hasOlderSessionHistory(),
434
440
  isLoadingOlderSessionHistory: () => this.sessionEvents.isLoadingOlderSessionHistory(),
435
441
  loadOlderSessionHistory: (options) => this.sessionEvents.loadOlderSessionHistory(options),
442
+ hasNewerSessionHistory: () => this.sessionEvents.hasNewerSessionHistory(),
443
+ isLoadingNewerSessionHistory: () => this.sessionEvents.isLoadingNewerSessionHistory(),
444
+ loadNewerSessionHistory: (options) => this.sessionEvents.loadNewerSessionHistory(options),
436
445
  render: () => this.render(),
437
446
  });
438
447
  this.commandController = new AppCommandController({
@@ -564,6 +573,7 @@ export class PiUiExtendApp {
564
573
  this.render();
565
574
  });
566
575
  },
576
+ scrollConversationQuick: (direction) => this.scrollConversationQuick(direction),
567
577
  toggleAllThinkingExpanded: () => {
568
578
  this.allThinkingExpanded = !this.allThinkingExpanded;
569
579
  this.render();
@@ -904,12 +914,14 @@ export class PiUiExtendApp {
904
914
  return {
905
915
  entries: [...this.entries],
906
916
  eventState: this.sessionEvents.snapshotState(),
917
+ scrollState: this.scrollController.captureState(),
907
918
  };
908
919
  }
909
920
  restoreSessionView(view) {
910
921
  this.entries.splice(0, this.entries.length, ...view.entries);
911
922
  this.sessionEvents.restoreState(view.eventState);
912
923
  this.conversationViewport.clear();
924
+ this.scrollController.restoreState(view.scrollState);
913
925
  this.workspaceActions.syncUserSessionEntryMetadata();
914
926
  }
915
927
  loadSessionHistory() {
@@ -1109,6 +1121,22 @@ export class PiUiExtendApp {
1109
1121
  this.lastInputEditorContentVersion = contentVersion;
1110
1122
  this.scrollController.scrollToBottom();
1111
1123
  }
1124
+ conversationQuickScrollDirections() {
1125
+ const columns = this.terminalColumns();
1126
+ const terminalRows = this.terminalRows();
1127
+ const rows = Math.max(1, terminalRows - this.tabLineRenderer.panelRows(terminalRows));
1128
+ const { bodyHeight } = this.editorLayoutRenderer.computeLayout(columns, rows);
1129
+ if (bodyHeight <= 0)
1130
+ return { up: false, down: false };
1131
+ return this.scrollController.quickScrollDirections(columns, bodyHeight);
1132
+ }
1133
+ async scrollConversationQuick(direction) {
1134
+ const changed = direction === "up"
1135
+ ? await this.scrollController.scrollToAbsoluteTop()
1136
+ : await this.scrollController.scrollToAbsoluteBottom();
1137
+ if (changed)
1138
+ this.render();
1139
+ }
1112
1140
  renderStatusLine() {
1113
1141
  this.renderController.renderStatusLine();
1114
1142
  }
@@ -10,6 +10,8 @@ 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
12
  import { createStartupInfoMessage } from "../cli/startup-info.js";
13
+ import { loadSessionTitleConfig } from "../../bundled-extensions/session-title/config.js";
14
+ import { fallbackSessionTitleFromInput, firstUserMessageText, generateSessionTitle, sessionTitleModelRefs, } from "../../bundled-extensions/session-title/title-generation.js";
13
15
  export class SessionCommandActions {
14
16
  host;
15
17
  constructor(host) {
@@ -100,8 +102,34 @@ export class SessionCommandActions {
100
102
  return;
101
103
  const name = argumentsText.trim();
102
104
  if (!name) {
103
- this.host.addEntry({ id: createId("system"), kind: "system", text: `Session name: ${runtime.session.sessionName ?? "(none)"}` });
105
+ const branch = runtime.session.sessionManager.getBranch();
106
+ const firstPrompt = firstUserMessageText(branch);
107
+ if (!firstPrompt)
108
+ throw new Error("Cannot auto-name this session yet: no user messages found.");
109
+ const config = loadSessionTitleConfig(runtime.cwd);
110
+ const fallbackTitle = fallbackSessionTitleFromInput(firstPrompt, config.maxTitleChars);
111
+ const modelRefs = sessionTitleModelRefs(config);
112
+ let generatedName;
113
+ this.host.setStatus("generating session name");
114
+ this.host.render();
115
+ if (config.enabled) {
116
+ for (const modelRef of modelRefs) {
117
+ for (let attempt = 0; attempt < config.generationAttempts; attempt++) {
118
+ generatedName = await generateSessionTitle(firstPrompt.slice(0, config.maxInputChars).trim(), runtime.services.modelRegistry, config, modelRef, AbortSignal.timeout(config.timeoutMs));
119
+ if (generatedName)
120
+ break;
121
+ }
122
+ if (generatedName)
123
+ break;
124
+ }
125
+ }
126
+ const nextName = generatedName ?? fallbackTitle;
127
+ if (!nextName)
128
+ throw new Error("Could not generate a session name for this session.");
129
+ runtime.session.setSessionName(nextName);
130
+ this.host.addEntry({ id: createId("system"), kind: "system", text: `Session name set: ${nextName}` });
104
131
  this.host.setSessionStatus(runtime.session);
132
+ this.host.render();
105
133
  return;
106
134
  }
107
135
  runtime.session.setSessionName(name);
@@ -1,7 +1,7 @@
1
1
  import type { ResolvedToolRule } from "../config.js";
2
2
  export declare const THINKING_LEVELS: readonly ["off", "minimal", "low", "medium", "high", "xhigh"];
3
3
  export declare const THINKING_MENU_MAX_ROWS: number;
4
- export declare const PI_FAVORITE_MODEL_REFS: readonly ["amazon-bedrock/us.anthropic.claude-opus-4-6-v1", "anthropic/claude-opus-4-8", "openai/gpt-5.4", "azure-openai-responses/gpt-5.4", "openai-codex/gpt-5.5", "deepseek/deepseek-v4-pro", "google/gemini-3.1-pro-preview", "google-vertex/gemini-3.1-pro-preview", "github-copilot/gpt-5.4", "openrouter/moonshotai/kimi-k2.6", "vercel-ai-gateway/zai/glm-5.1", "xai/grok-4.20-0309-reasoning", "groq/openai/gpt-oss-120b", "cerebras/zai-glm-4.7", "zai/glm-5.1", "mistral/devstral-medium-latest", "minimax/MiniMax-M2.7", "minimax-cn/MiniMax-M2.7", "moonshotai/kimi-k2.6", "moonshotai-cn/kimi-k2.6", "huggingface/moonshotai/Kimi-K2.6", "fireworks/accounts/fireworks/models/kimi-k2p6", "together/moonshotai/Kimi-K2.6", "opencode/kimi-k2.6", "opencode-go/kimi-k2.6", "kimi-coding/kimi-for-coding", "cloudflare-workers-ai/@cf/moonshotai/kimi-k2.6", "cloudflare-ai-gateway/workers-ai/@cf/moonshotai/kimi-k2.6", "xiaomi/mimo-v2.5-pro", "xiaomi-token-plan-cn/mimo-v2.5-pro", "xiaomi-token-plan-ams/mimo-v2.5-pro", "xiaomi-token-plan-sgp/mimo-v2.5-pro"];
4
+ export declare const PI_FAVORITE_MODEL_REFS: readonly ["amazon-bedrock/us.anthropic.claude-opus-4-6-v1", "anthropic/claude-opus-4-8", "openai/gpt-5.4", "azure-openai-responses/gpt-5.4", "openai-codex/gpt-5.5", "deepseek/deepseek-v4-pro", "google/gemini-3.1-pro-preview", "google-vertex/gemini-3.1-pro-preview", "github-copilot/gpt-5.4", "openrouter/moonshotai/kimi-k2.6", "vercel-ai-gateway/zai/glm-5.2", "xai/grok-4.20-0309-reasoning", "groq/openai/gpt-oss-120b", "cerebras/zai-glm-4.7", "zai/glm-5.2", "mistral/devstral-medium-latest", "minimax/MiniMax-M2.7", "minimax-cn/MiniMax-M2.7", "moonshotai/kimi-k2.6", "moonshotai-cn/kimi-k2.6", "huggingface/moonshotai/Kimi-K2.6", "fireworks/accounts/fireworks/models/kimi-k2p6", "together/moonshotai/Kimi-K2.6", "opencode/kimi-k2.6", "opencode-go/kimi-k2.6", "kimi-coding/kimi-for-coding", "cloudflare-workers-ai/@cf/moonshotai/kimi-k2.6", "cloudflare-ai-gateway/workers-ai/@cf/moonshotai/kimi-k2.6", "xiaomi/mimo-v2.5-pro", "xiaomi-token-plan-cn/mimo-v2.5-pro", "xiaomi-token-plan-ams/mimo-v2.5-pro", "xiaomi-token-plan-sgp/mimo-v2.5-pro"];
5
5
  export declare const SLASH_COMMAND_MENU_MAX_ROWS = 6;
6
6
  export declare const RESUME_MENU_MAX_ROWS = 20;
7
7
  export declare const RESUME_MENU_INITIAL_SESSION_ROWS = 30;
@@ -12,11 +12,11 @@ export const PI_FAVORITE_MODEL_REFS = [
12
12
  "google-vertex/gemini-3.1-pro-preview",
13
13
  "github-copilot/gpt-5.4",
14
14
  "openrouter/moonshotai/kimi-k2.6",
15
- "vercel-ai-gateway/zai/glm-5.1",
15
+ "vercel-ai-gateway/zai/glm-5.2",
16
16
  "xai/grok-4.20-0309-reasoning",
17
17
  "groq/openai/gpt-oss-120b",
18
18
  "cerebras/zai-glm-4.7",
19
- "zai/glm-5.1",
19
+ "zai/glm-5.2",
20
20
  "mistral/devstral-medium-latest",
21
21
  "minimax/MiniMax-M2.7",
22
22
  "minimax-cn/MiniMax-M2.7",
@@ -1,5 +1,5 @@
1
- export declare const PIX_ICON_THEME_ENV = "PIX_ICON_THEME";
2
- export declare const PIX_USE_FALLBACK_ICONS_ENV = "PIX_USE_FALLBACK_ICONS";
1
+ import { type AppIconThemeName } from "../icon-theme.js";
2
+ export { appIconThemeFromFallbackFlag, appIconThemeOverrideFromEnv, parseAppIconThemeName, resolveAppIconThemeNameFromEnv, PIX_ICON_THEME_ENV, PIX_USE_FALLBACK_ICONS_ENV, type AppIconThemeName, } from "../icon-theme.js";
3
3
  declare const NERD_FONT_ICONS: {
4
4
  readonly alert: "󰀦";
5
5
  readonly autoFix: "󰁨";
@@ -28,17 +28,12 @@ declare const NERD_FONT_ICONS: {
28
28
  readonly toolBodyEnd: "└";
29
29
  readonly toolBodyGutter: "│";
30
30
  readonly toolPreviewTruncated: "⊞";
31
- readonly down: "󰁅";
31
+ readonly up: "";
32
+ readonly down: "↓";
32
33
  };
33
34
  export type AppIconName = keyof typeof NERD_FONT_ICONS;
34
35
  export type AppIconMap = Record<AppIconName, string>;
35
- export type AppIconThemeName = "nerdFont" | "fallback";
36
36
  export declare const APP_ICON_THEMES: Record<AppIconThemeName, AppIconMap>;
37
37
  export declare const APP_ICONS: AppIconMap;
38
38
  export declare function currentAppIconTheme(): AppIconThemeName;
39
- export declare function parseAppIconThemeName(value: unknown): AppIconThemeName | undefined;
40
- export declare function appIconThemeFromFallbackFlag(value: unknown): AppIconThemeName | undefined;
41
- export declare function resolveAppIconThemeNameFromEnv(env?: NodeJS.ProcessEnv): AppIconThemeName;
42
- export declare function appIconThemeOverrideFromEnv(env?: NodeJS.ProcessEnv): AppIconThemeName | undefined;
43
39
  export declare function setAppIconTheme(themeName: AppIconThemeName): void;
44
- export {};
package/dist/app/icons.js CHANGED
@@ -4,8 +4,13 @@
4
4
  //
5
5
  // Use codepoint escapes for private-use characters so editors cannot silently
6
6
  // substitute visually similar glyphs.
7
- export const PIX_ICON_THEME_ENV = "PIX_ICON_THEME";
8
- export const PIX_USE_FALLBACK_ICONS_ENV = "PIX_USE_FALLBACK_ICONS";
7
+ //
8
+ // Theme-name parsing/resolution lives in the dependency-free src/icon-theme.ts so
9
+ // configuration loading can resolve the theme without importing this app/ module
10
+ // (breaks the src/config.ts <-> src/app/icons.ts import cycle).
11
+ import { resolveAppIconThemeNameFromEnv, } from "../icon-theme.js";
12
+ // Re-exported for existing importers; the canonical home is src/icon-theme.ts.
13
+ export { appIconThemeFromFallbackFlag, appIconThemeOverrideFromEnv, parseAppIconThemeName, resolveAppIconThemeNameFromEnv, PIX_ICON_THEME_ENV, PIX_USE_FALLBACK_ICONS_ENV, } from "../icon-theme.js";
9
14
  const NERD_FONT_ICONS = {
10
15
  alert: "\u{f0026}",
11
16
  autoFix: "\u{f0068}",
@@ -34,7 +39,8 @@ const NERD_FONT_ICONS = {
34
39
  toolBodyEnd: "└",
35
40
  toolBodyGutter: "│",
36
41
  toolPreviewTruncated: "⊞",
37
- down: "\u{f0045}",
42
+ up: "",
43
+ down: "↓",
38
44
  };
39
45
  const FALLBACK_ICONS = {
40
46
  alert: "!",
@@ -64,7 +70,8 @@ const FALLBACK_ICONS = {
64
70
  toolBodyEnd: "`",
65
71
  toolBodyGutter: "|",
66
72
  toolPreviewTruncated: "+",
67
- down: "v",
73
+ up: "",
74
+ down: "↓",
68
75
  };
69
76
  export const APP_ICON_THEMES = {
70
77
  nerdFont: NERD_FONT_ICONS,
@@ -75,36 +82,6 @@ export const APP_ICONS = { ...APP_ICON_THEMES[currentAppIconThemeName] };
75
82
  export function currentAppIconTheme() {
76
83
  return currentAppIconThemeName;
77
84
  }
78
- export function parseAppIconThemeName(value) {
79
- if (typeof value !== "string")
80
- return undefined;
81
- const normalized = value.trim().toLowerCase().replace(/[\s_-]+/gu, "");
82
- if (normalized === "fallback" || normalized === "plain" || normalized === "ascii")
83
- return "fallback";
84
- if (normalized === "nerdfont" || normalized === "font" || normalized === "icons")
85
- return "nerdFont";
86
- return undefined;
87
- }
88
- export function appIconThemeFromFallbackFlag(value) {
89
- if (typeof value === "boolean")
90
- return value ? "fallback" : "nerdFont";
91
- if (typeof value !== "string")
92
- return undefined;
93
- const normalized = value.trim().toLowerCase();
94
- if (["1", "true", "yes", "on", "fallback"].includes(normalized))
95
- return "fallback";
96
- if (["0", "false", "no", "off", "nerdfont", "nerd-font"].includes(normalized))
97
- return "nerdFont";
98
- return undefined;
99
- }
100
- export function resolveAppIconThemeNameFromEnv(env = process.env) {
101
- return appIconThemeOverrideFromEnv(env) ?? "nerdFont";
102
- }
103
- export function appIconThemeOverrideFromEnv(env = process.env) {
104
- return appIconThemeFromFallbackFlag(env[PIX_USE_FALLBACK_ICONS_ENV])
105
- ?? parseAppIconThemeName(env[PIX_ICON_THEME_ENV])
106
- ?? undefined;
107
- }
108
85
  export function setAppIconTheme(themeName) {
109
86
  currentAppIconThemeName = themeName;
110
87
  Object.assign(APP_ICONS, APP_ICON_THEMES[themeName]);
@@ -11,6 +11,7 @@ export type ConversationEntryRenderOptions = {
11
11
  colors: Theme["colors"];
12
12
  pixConfig: PixConfig;
13
13
  outputFilters: readonly RegExp[];
14
+ availableThinkingLevels?: readonly string[];
14
15
  superCompactTools?: boolean;
15
16
  allThinkingExpanded?: boolean;
16
17
  renderInlineUserMessageMenu: (entry: Extract<Entry, {
@@ -5,6 +5,7 @@ export type ConversationToolRenderOptions = {
5
5
  cwd: string;
6
6
  pixConfig: PixConfig;
7
7
  colors: Theme["colors"];
8
+ availableThinkingLevels?: readonly string[];
8
9
  superCompactTools?: boolean;
9
10
  allThinkingExpanded?: boolean;
10
11
  };
@@ -1,12 +1,13 @@
1
1
  import { resolveColor, resolveToolRule } from "../../config.js";
2
2
  import { formatMarkdownTables, markdownSyntaxHighlightsForText } from "../../markdown-format.js";
3
3
  import { renderToolDisplay } from "../../tool-renderers/index.js";
4
- import { DEFAULT_THINKING_TOOL_RULE, SUBAGENT_STATUSES, THINKING_TOOL_NAME, TODO_TOOL_NAME } from "../constants.js";
4
+ import { SUBAGENT_STATUSES, THINKING_TOOL_NAME, TODO_TOOL_NAME } from "../constants.js";
5
5
  import { attachImageClickTargets } from "../screen/image-click-targets.js";
6
6
  import { formatStructuredText } from "./message-content.js";
7
7
  import { formatSubagentTimestamp, isSubagentRunRenderDetails, isSubagentsToolName, subagentRunName, subagentStatusIcon, taskPreviewMap, } from "../subagents/subagents-model.js";
8
8
  import { formatTodoTaskLine, isTodoDetails, visibleTodoTasks } from "../todo/todo-model.js";
9
9
  import { renderToolBlock } from "./tool-block-renderer.js";
10
+ import { thinkingLevelThemeColor } from "./status-line-renderer.js";
10
11
  export function renderConversationToolEntry(entry, width, options) {
11
12
  const todoLines = renderTodoToolEntry(entry, width, options);
12
13
  if (todoLines)
@@ -46,12 +47,15 @@ export function renderConversationToolEntry(entry, width, options) {
46
47
  return attachImageClickTargets(lines, entry.id, entry.images, { foreground: options.colors.info, underline: true });
47
48
  }
48
49
  export function renderThinkingEntry(entry, width, options) {
49
- const rule = resolveThinkingToolRule(options.pixConfig);
50
+ const rule = resolveToolRule(THINKING_TOOL_NAME, options.pixConfig.toolRenderer);
50
51
  const markdownText = entry.text ? formatMarkdownTables(entry.text, Math.max(1, width - 2)) : "";
51
52
  const expandedText = trimTrailingBlankLines(markdownText);
52
53
  const forceExpanded = Boolean(options.allThinkingExpanded);
53
54
  const compactExpandedText = options.superCompactTools && forceExpanded ? removeBlankLines(expandedText) : expandedText;
54
55
  const expanded = forceExpanded || (entry.expanded && expandedText.trim().length > 0);
56
+ const headerColorOverride = entry.level
57
+ ? thinkingLevelThemeColor(entry.level, options.colors, options.availableThinkingLevels)
58
+ : undefined;
55
59
  return renderToolBlock({
56
60
  id: entry.id,
57
61
  toolName: THINKING_TOOL_NAME,
@@ -63,7 +67,12 @@ export function renderThinkingEntry(entry, width, options) {
63
67
  expandedText: compactExpandedText || "(empty)",
64
68
  bodyWrap: "word",
65
69
  syntaxHighlight: compactExpandedText ? markdownSyntaxHighlightsForText(compactExpandedText) : undefined,
66
- }, rule, width, options.colors, { superCompact: Boolean(options.superCompactTools && !forceExpanded), backgroundOverride: options.colors.thinkingMessageBackground, skipHeaderBackground: true, showGutter: false });
70
+ }, rule, width, options.colors, {
71
+ superCompact: Boolean(options.superCompactTools && !forceExpanded),
72
+ backgroundOverride: options.colors.thinkingMessageBackground,
73
+ showGutter: true,
74
+ ...(headerColorOverride === undefined ? {} : { headerColorOverride }),
75
+ });
67
76
  }
68
77
  function trimTrailingBlankLines(text) {
69
78
  return text.replace(/(?:\r?\n[ \t]*)+$/u, "");
@@ -199,18 +208,3 @@ function formatSubagentToolLine(agent, preview) {
199
208
  parts.push(`retry:${agent.retryCount}`);
200
209
  return parts.join(" ");
201
210
  }
202
- function resolveThinkingToolRule(pixConfig) {
203
- const configured = pixConfig.toolRenderer.tools[THINKING_TOOL_NAME];
204
- if (!configured)
205
- return DEFAULT_THINKING_TOOL_RULE;
206
- const rule = {
207
- previewLines: configured.previewLines ?? DEFAULT_THINKING_TOOL_RULE.previewLines,
208
- direction: configured.direction ?? DEFAULT_THINKING_TOOL_RULE.direction,
209
- color: configured.color ?? DEFAULT_THINKING_TOOL_RULE.color,
210
- };
211
- if (configured.compactHidden != null)
212
- rule.compactHidden = configured.compactHidden;
213
- if (configured.hidden != null)
214
- rule.hidden = configured.hidden;
215
- return rule;
216
- }
@@ -9,6 +9,7 @@ export type ConversationViewportHost = {
9
9
  readonly colors: Theme["colors"];
10
10
  readonly pixConfig: PixConfig;
11
11
  readonly outputFilters: readonly RegExp[];
12
+ availableThinkingLevels?(): readonly string[] | undefined;
12
13
  readonly superCompactTools?: boolean;
13
14
  readonly allThinkingExpanded?: boolean;
14
15
  hasDynamicConversationBlock?(): boolean;
@@ -36,11 +37,14 @@ export declare class ConversationViewport {
36
37
  entries(): Entry[];
37
38
  blockForEntry(entry: Entry, width: number): ConversationBlockCache;
38
39
  entryBlockPositions(width: number): ConversationEntryBlockPosition[];
40
+ entryBlockPositionById(width: number, entryId: string): ConversationEntryBlockPosition | undefined;
39
41
  measuredLineCountForEntries(width: number, entryIds: readonly string[]): number;
40
42
  private layoutForWidth;
41
43
  private buildLayout;
42
44
  private previousMeasuredLineCount;
43
45
  private layoutStructureChanged;
46
+ private syncLayoutStructure;
47
+ private rebuildOffsets;
44
48
  private refreshDirtyLayoutEntries;
45
49
  private blockCacheForWidth;
46
50
  private refreshDynamicLayoutEntries;
@@ -73,11 +73,13 @@ export class ConversationViewport {
73
73
  const dynamic = this.host.isDynamicConversationBlock(entry);
74
74
  if (!dynamic && cached?.version === version)
75
75
  return cached;
76
+ const availableThinkingLevels = this.host.availableThinkingLevels?.();
76
77
  const lines = renderConversationEntryLines(entry, width, {
77
78
  cwd: this.host.cwd,
78
79
  colors: this.host.colors,
79
80
  pixConfig: this.host.pixConfig,
80
81
  outputFilters: this.host.outputFilters,
82
+ ...(availableThinkingLevels ? { availableThinkingLevels } : {}),
81
83
  superCompactTools: Boolean(this.host.superCompactTools),
82
84
  allThinkingExpanded: Boolean(this.host.allThinkingExpanded),
83
85
  renderInlineUserMessageMenu: (userEntry, context) => this.host.renderInlineUserMessageMenu(userEntry, context),
@@ -102,6 +104,23 @@ export class ConversationViewport {
102
104
  block: this.blockForEntry(entry, width),
103
105
  }));
104
106
  }
107
+ entryBlockPositionById(width, entryId) {
108
+ const layout = this.layoutForWidth(width);
109
+ const targetIndex = layout.positions.get(entryId);
110
+ if (targetIndex === undefined)
111
+ return undefined;
112
+ for (let index = 0; index <= targetIndex; index += 1)
113
+ this.ensureEntryMeasured(layout, width, index);
114
+ const entry = layout.entries[targetIndex];
115
+ if (!entry)
116
+ return undefined;
117
+ return {
118
+ entry,
119
+ offset: layout.offsets[targetIndex] ?? 0,
120
+ lineCount: layout.lineCounts[targetIndex] ?? 0,
121
+ block: this.blockForEntry(entry, width),
122
+ };
123
+ }
105
124
  measuredLineCountForEntries(width, entryIds) {
106
125
  if (entryIds.length === 0)
107
126
  return 0;
@@ -122,16 +141,21 @@ export class ConversationViewport {
122
141
  const superCompactTools = Boolean(this.host.superCompactTools);
123
142
  const allThinkingExpanded = Boolean(this.host.allThinkingExpanded);
124
143
  let layout = this.layoutCachesByWidth.get(width);
125
- if (!layout || this.layoutStructureChanged(layout, entries, superCompactTools, allThinkingExpanded)) {
126
- const previousLayout = layout && layout.superCompactTools === superCompactTools && layout.allThinkingExpanded === allThinkingExpanded
127
- ? layout
128
- : undefined;
129
- layout = this.buildLayout(entries, width, superCompactTools, allThinkingExpanded, previousLayout);
144
+ if (!layout) {
145
+ layout = this.buildLayout(entries, width, superCompactTools, allThinkingExpanded);
130
146
  this.layoutCachesByWidth.set(width, layout);
131
147
  }
132
- else {
133
- this.refreshDirtyLayoutEntries(layout, width);
148
+ else if (this.layoutStructureChanged(layout, entries, superCompactTools, allThinkingExpanded)) {
149
+ const synced = this.syncLayoutStructure(layout, entries, width, superCompactTools, allThinkingExpanded);
150
+ if (!synced) {
151
+ const previousLayout = layout.superCompactTools === superCompactTools && layout.allThinkingExpanded === allThinkingExpanded
152
+ ? layout
153
+ : undefined;
154
+ layout = this.buildLayout(entries, width, superCompactTools, allThinkingExpanded, previousLayout);
155
+ this.layoutCachesByWidth.set(width, layout);
156
+ }
134
157
  }
158
+ this.refreshDirtyLayoutEntries(layout, width);
135
159
  if (this.host.hasDynamicConversationBlock?.()) {
136
160
  this.refreshDynamicLayoutEntries(layout, width);
137
161
  }
@@ -173,12 +197,61 @@ export class ConversationViewport {
173
197
  return previousLayout.lineCounts[previousIndex];
174
198
  }
175
199
  layoutStructureChanged(layout, entries, superCompactTools, allThinkingExpanded) {
176
- if (layout.entries.length !== entries.length || layout.superCompactTools !== superCompactTools || layout.allThinkingExpanded !== allThinkingExpanded)
200
+ if (layout.entryIds.length !== entries.length || layout.superCompactTools !== superCompactTools || layout.allThinkingExpanded !== allThinkingExpanded)
177
201
  return true;
178
- if (layout.entries.length === 0)
202
+ if (layout.entryIds.length === 0)
179
203
  return false;
180
204
  return layout.entryIds[0] !== entries[0]?.id || layout.entryIds[layout.entryIds.length - 1] !== entries[entries.length - 1]?.id;
181
205
  }
206
+ syncLayoutStructure(layout, entries, width, superCompactTools, allThinkingExpanded) {
207
+ if (layout.superCompactTools !== superCompactTools || layout.allThinkingExpanded !== allThinkingExpanded)
208
+ return false;
209
+ const currentEntryIds = entries.map((entry) => entry.id);
210
+ const overlap = layoutOverlap(layout.entryIds, currentEntryIds);
211
+ if (!overlap)
212
+ return false;
213
+ const estimatedBlockLineCounts = entries.map((entry) => this.estimatedBlockLineCountForEntry(entry, width));
214
+ const lineCounts = [];
215
+ const measuredLineCounts = [];
216
+ const positions = new Map();
217
+ for (let index = 0; index < entries.length; index += 1) {
218
+ const entry = entries[index];
219
+ positions.set(entry.id, index);
220
+ const oldIndex = index >= overlap.currentStart && index < overlap.currentStart + overlap.length
221
+ ? overlap.previousStart + index - overlap.currentStart
222
+ : undefined;
223
+ if (oldIndex !== undefined) {
224
+ lineCounts.push(layout.lineCounts[oldIndex] ?? 0);
225
+ measuredLineCounts.push(layout.measuredLineCounts[oldIndex] === true);
226
+ continue;
227
+ }
228
+ lineCounts.push(this.estimatedLineCountForEntry(entry, entries, index, estimatedBlockLineCounts));
229
+ measuredLineCounts.push(false);
230
+ }
231
+ layout.entries = entries;
232
+ layout.entryIds = currentEntryIds;
233
+ layout.lineCounts = lineCounts;
234
+ layout.measuredLineCounts = measuredLineCounts;
235
+ layout.positions = positions;
236
+ this.rebuildOffsets(layout);
237
+ const changedNextIndexes = new Set([overlap.currentStart - 1, overlap.currentStart + overlap.length - 1]);
238
+ for (const index of changedNextIndexes) {
239
+ if (index >= 0 && index < entries.length)
240
+ this.refreshLayoutEntry(layout, width, index, layout.measuredLineCounts[index] === true);
241
+ }
242
+ return true;
243
+ }
244
+ rebuildOffsets(layout) {
245
+ const offsets = [];
246
+ let totalLineCount = 0;
247
+ for (const lineCount of layout.lineCounts) {
248
+ offsets.push(totalLineCount);
249
+ totalLineCount += lineCount;
250
+ }
251
+ offsets.push(totalLineCount);
252
+ layout.offsets = offsets;
253
+ layout.totalLineCount = totalLineCount;
254
+ }
182
255
  refreshDirtyLayoutEntries(layout, width) {
183
256
  if (layout.dirtyEntryIds.size === 0)
184
257
  return;
@@ -224,7 +297,7 @@ export class ConversationViewport {
224
297
  const previousLineCount = layout.lineCounts[index] ?? 0;
225
298
  const nextLineCount = measure
226
299
  ? this.measuredLineCountForEntry(entry, layout.entries, index, width)
227
- : this.estimatedLineCountForEntry(entry, layout.entries, index, width);
300
+ : this.estimatedLineCountForEntry(entry, layout.entries, index, layout.entries.map((candidate) => this.estimatedBlockLineCountForEntry(candidate, width)));
228
301
  layout.measuredLineCounts[index] = measure;
229
302
  if (previousLineCount === nextLineCount)
230
303
  return false;
@@ -240,9 +313,8 @@ export class ConversationViewport {
240
313
  const block = this.blockForEntry(entry, width);
241
314
  return this.lineCountWithGap(entry, block.lineCount, this.nextVisibleEntry(entries, index, width));
242
315
  }
243
- estimatedLineCountForEntry(entry, entries, index, width) {
244
- const blockLineCount = this.estimatedBlockLineCountForEntry(entry, width);
245
- const blockLineCounts = entries.map((candidate) => this.estimatedBlockLineCountForEntry(candidate, width));
316
+ estimatedLineCountForEntry(entry, entries, index, blockLineCounts) {
317
+ const blockLineCount = blockLineCounts[index] ?? 0;
246
318
  return this.lineCountWithGap(entry, blockLineCount, this.nextEstimatedVisibleEntry(entries, blockLineCounts, index));
247
319
  }
248
320
  lineCountWithGap(entry, blockLineCount, nextEntry) {
@@ -339,6 +411,65 @@ function estimateWrappedLineCount(text, width) {
339
411
  }
340
412
  return count;
341
413
  }
414
+ function layoutOverlap(previousIds, currentIds) {
415
+ if (currentIds.length === 0 || previousIds.length === 0)
416
+ return { previousStart: 0, currentStart: 0, length: 0 };
417
+ const candidates = [];
418
+ const prefixLength = commonPrefixLength(previousIds, currentIds);
419
+ if (prefixLength > 0)
420
+ candidates.push({ previousStart: 0, currentStart: 0, length: prefixLength });
421
+ const suffixLength = commonSuffixLength(previousIds, currentIds);
422
+ if (suffixLength > 0)
423
+ candidates.push({
424
+ previousStart: previousIds.length - suffixLength,
425
+ currentStart: currentIds.length - suffixLength,
426
+ length: suffixLength,
427
+ });
428
+ const suffixPrefixLength = maxSuffixPrefixOverlap(previousIds, currentIds);
429
+ if (suffixPrefixLength > 0)
430
+ candidates.push({
431
+ previousStart: previousIds.length - suffixPrefixLength,
432
+ currentStart: 0,
433
+ length: suffixPrefixLength,
434
+ });
435
+ const prefixSuffixLength = maxSuffixPrefixOverlap(currentIds, previousIds);
436
+ if (prefixSuffixLength > 0)
437
+ candidates.push({
438
+ previousStart: 0,
439
+ currentStart: currentIds.length - prefixSuffixLength,
440
+ length: prefixSuffixLength,
441
+ });
442
+ return candidates.sort((left, right) => right.length - left.length)[0];
443
+ }
444
+ function commonPrefixLength(left, right) {
445
+ const maxLength = Math.min(left.length, right.length);
446
+ let length = 0;
447
+ while (length < maxLength && left[length] === right[length])
448
+ length += 1;
449
+ return length;
450
+ }
451
+ function commonSuffixLength(left, right) {
452
+ const maxLength = Math.min(left.length, right.length);
453
+ let length = 0;
454
+ while (length < maxLength && left[left.length - length - 1] === right[right.length - length - 1])
455
+ length += 1;
456
+ return length;
457
+ }
458
+ function maxSuffixPrefixOverlap(left, right) {
459
+ const maxLength = Math.min(left.length, right.length);
460
+ for (let length = maxLength; length > 0; length -= 1) {
461
+ let matches = true;
462
+ for (let offset = 0; offset < length; offset += 1) {
463
+ if (left[left.length - length + offset] !== right[offset]) {
464
+ matches = false;
465
+ break;
466
+ }
467
+ }
468
+ if (matches)
469
+ return length;
470
+ }
471
+ return 0;
472
+ }
342
473
  function estimateToolLikeLineCount(toolName, expanded, output, width, pixConfig, superCompactTools, includeStatusLine) {
343
474
  const rule = resolveToolRule(toolName, pixConfig.toolRenderer);
344
475
  if (rule.hidden)