pi-ui-extend 0.1.15 → 0.1.17

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 (70) hide show
  1. package/dist/app/app.d.ts +2 -0
  2. package/dist/app/app.js +21 -6
  3. package/dist/app/commands/command-controller.js +1 -0
  4. package/dist/app/commands/command-host.d.ts +2 -0
  5. package/dist/app/commands/command-navigation-actions.d.ts +9 -0
  6. package/dist/app/commands/command-navigation-actions.js +62 -3
  7. package/dist/app/commands/command-registry.d.ts +1 -0
  8. package/dist/app/commands/command-registry.js +8 -0
  9. package/dist/app/constants.d.ts +0 -1
  10. package/dist/app/constants.js +0 -1
  11. package/dist/app/icons.d.ts +1 -0
  12. package/dist/app/icons.js +2 -0
  13. package/dist/app/input/input-action-controller.d.ts +1 -0
  14. package/dist/app/input/input-action-controller.js +5 -4
  15. package/dist/app/popup/menu-items-controller.d.ts +2 -0
  16. package/dist/app/popup/menu-items-controller.js +37 -14
  17. package/dist/app/popup/popup-menu-controller.js +30 -5
  18. package/dist/app/rendering/editor-panels.js +20 -9
  19. package/dist/app/rendering/popup-menu-renderer.d.ts +12 -0
  20. package/dist/app/rendering/popup-menu-renderer.js +151 -53
  21. package/dist/app/rendering/render-controller.js +25 -15
  22. package/dist/app/rendering/render-text.js +5 -2
  23. package/dist/app/rendering/status-line-renderer.d.ts +7 -0
  24. package/dist/app/rendering/status-line-renderer.js +191 -94
  25. package/dist/app/rendering/toast-controller.d.ts +1 -0
  26. package/dist/app/rendering/toast-controller.js +17 -0
  27. package/dist/app/screen/mouse-controller.js +4 -4
  28. package/dist/app/screen/scroll-controller.d.ts +1 -0
  29. package/dist/app/screen/scroll-controller.js +6 -0
  30. package/dist/app/screen/status-controller.js +2 -1
  31. package/dist/app/session/request-history.d.ts +4 -0
  32. package/dist/app/session/request-history.js +11 -0
  33. package/dist/app/session/session-search.js +10 -0
  34. package/dist/app/session/tabs-controller.d.ts +4 -4
  35. package/dist/app/session/tabs-controller.js +64 -6
  36. package/dist/app/todo/todo-model.d.ts +2 -2
  37. package/dist/app/todo/todo-model.js +15 -17
  38. package/dist/app/types.d.ts +12 -4
  39. package/dist/config.d.ts +1 -0
  40. package/dist/config.js +10 -1
  41. package/dist/default-pix-config.js +2 -0
  42. package/dist/fuzzy.d.ts +2 -0
  43. package/dist/fuzzy.js +27 -7
  44. package/dist/input-editor.d.ts +9 -0
  45. package/dist/input-editor.js +52 -0
  46. package/dist/schemas/pix-schema.d.ts +1 -0
  47. package/dist/schemas/pix-schema.js +1 -0
  48. package/dist/theme.js +6 -6
  49. package/dist/ui.d.ts +8 -0
  50. package/external/pi-tools-suite/README.md +2 -2
  51. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +40 -5
  52. package/external/pi-tools-suite/src/antigravity-auth/commands.ts +1 -37
  53. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +0 -2
  54. package/external/pi-tools-suite/src/antigravity-auth/index.ts +3 -16
  55. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +33 -17
  56. package/external/pi-tools-suite/src/antigravity-auth/status.ts +1 -1
  57. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +4 -12
  58. package/external/pi-tools-suite/src/antigravity-auth/types.ts +21 -0
  59. package/external/pi-tools-suite/src/todo/index.ts +3 -64
  60. package/external/pi-tools-suite/src/todo/state/persistence.ts +0 -1
  61. package/external/pi-tools-suite/src/todo/state/state-reducer.ts +7 -37
  62. package/external/pi-tools-suite/src/todo/todo.ts +2 -18
  63. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +2 -11
  64. package/external/pi-tools-suite/src/todo/tool/types.ts +0 -29
  65. package/external/pi-tools-suite/src/todo/view/format.ts +1 -3
  66. package/external/pi-tools-suite/src/tool-descriptions.ts +5 -4
  67. package/external/pi-tools-suite/src/usage/lib/google.ts +50 -30
  68. package/external/pi-tools-suite/src/usage/lib/types.ts +12 -2
  69. package/package.json +1 -1
  70. package/schemas/pix.json +5 -0
package/dist/app/app.d.ts CHANGED
@@ -44,6 +44,7 @@ export declare class PiUiExtendApp {
44
44
  private readonly extensionShutdownHandler;
45
45
  private runtime;
46
46
  private readonly inputEditor;
47
+ private lastInputEditorContentVersion;
47
48
  private readonly requestHistory;
48
49
  /** Shortcut: get/set the editor text as a plain string. */
49
50
  private get input();
@@ -101,6 +102,7 @@ export declare class PiUiExtendApp {
101
102
  private clearToastTimers;
102
103
  private render;
103
104
  private scheduleRender;
105
+ private syncScrollAfterInputEditorChange;
104
106
  private renderStatusLine;
105
107
  private terminalColumns;
106
108
  private terminalRows;
package/dist/app/app.js CHANGED
@@ -110,6 +110,7 @@ export class PiUiExtendApp {
110
110
  extensionShutdownHandler = () => { };
111
111
  runtime;
112
112
  inputEditor = new InputEditor();
113
+ lastInputEditorContentVersion = this.inputEditor.contentVersion;
113
114
  requestHistory;
114
115
  /** Shortcut: get/set the editor text as a plain string. */
115
116
  get input() { return this.inputEditor.text; }
@@ -126,6 +127,8 @@ export class PiUiExtendApp {
126
127
  constructor(options) {
127
128
  this.options = options;
128
129
  this.theme = THEMES[options.themeName];
130
+ this.pixConfig = loadPixConfig(this.options.cwd);
131
+ setAppIconTheme(this.pixConfig.iconTheme.name);
129
132
  const app = this;
130
133
  this.blinkController = new AppBlinkController({
131
134
  render: () => this.render(),
@@ -157,6 +160,7 @@ export class PiUiExtendApp {
157
160
  });
158
161
  this.tabsController = new AppTabsController({
159
162
  options: this.options,
163
+ maxProjectSessions: this.pixConfig.maxProjectSessions,
160
164
  blinkController: this.blinkController,
161
165
  runtime: () => this.runtime,
162
166
  createRuntimeForNewSession: () => this.createRuntime(newTabRuntimeOptions(this.options)),
@@ -175,8 +179,8 @@ export class PiUiExtendApp {
175
179
  loadSessionHistory: () => this.loadSessionHistory(),
176
180
  loadSessionHistoryAsync: (options) => this.loadSessionHistoryAsync(options),
177
181
  syncUserSessionEntryMetadata: () => this.workspaceActions.syncUserSessionEntryMetadata(),
178
- captureInputState: () => ({ text: this.inputEditor.text, cursor: this.inputEditor.cursor }),
179
- restoreInputState: (state) => this.restoreTabInputState(state.text, state.cursor),
182
+ captureInputState: () => this.inputEditor.draftState,
183
+ restoreInputState: (state) => this.restoreTabInputState(state),
180
184
  closeMenusForTabSwitch: () => this.popupMenus.closeMenusForTabSwitch(),
181
185
  captureDeferredUserMessages: () => this.queuedMessages.captureDeferredUserMessages(),
182
186
  restoreDeferredUserMessages: (messages) => this.queuedMessages.restoreDeferredUserMessages(messages),
@@ -184,8 +188,6 @@ export class PiUiExtendApp {
184
188
  showToast: (message, kind) => this.showToast(message, kind),
185
189
  render: () => this.render(),
186
190
  });
187
- this.pixConfig = loadPixConfig(this.options.cwd);
188
- setAppIconTheme(this.pixConfig.iconTheme.name);
189
191
  this.terminalBellSoundController = new TerminalBellSoundController();
190
192
  this.promptEnhancer = new AppPromptEnhancerController({
191
193
  runtime: () => this.runtime,
@@ -224,10 +226,12 @@ export class PiUiExtendApp {
224
226
  const popupMenuRenderer = new PopupMenuRenderer({
225
227
  theme: this.theme,
226
228
  screenStyler: this.screenStyler,
229
+ modelColors: this.pixConfig.modelColors,
227
230
  get entries() { return app.entries; },
228
231
  get session() { return app.runtime?.session; },
229
232
  get resumeLoading() { return app.resumeLoading; },
230
233
  get resumeSessionCount() { return app.resumeSessions.length; },
234
+ get userMessageJumpLoading() { return app.menuItems.isUserMessageJumpLoading(); },
231
235
  });
232
236
  this.popupMenus = new AppPopupMenuController({
233
237
  get entries() { return app.entries; },
@@ -430,6 +434,7 @@ export class PiUiExtendApp {
430
434
  this.commandController = new AppCommandController({
431
435
  options: this.options,
432
436
  runtime: () => this.runtime,
437
+ requestHistory: () => this.requestHistory,
433
438
  getInput: () => this.input,
434
439
  setInput: (value) => this.setInput(value),
435
440
  promptEnhancerModelRef: () => this.pixConfig.promptEnhancer.modelRef,
@@ -614,6 +619,7 @@ export class PiUiExtendApp {
614
619
  addEntry: (entry) => this.addEntry(entry),
615
620
  addSessionAbortedEntry: () => this.sessionEvents.addSessionAbortedEntry(),
616
621
  showToast: (message, kind) => this.showToast(message, kind),
622
+ dismissActiveDialog: () => this.toastController.dismissActiveDialog(),
617
623
  stopVoiceInput: () => this.voiceController.stopRecording(),
618
624
  isShellCommandRunning: () => this.shellController.isRunning(),
619
625
  runChatShellCommand: (command) => this.shellController.run(command),
@@ -814,10 +820,10 @@ export class PiUiExtendApp {
814
820
  this.popupMenus.resetInputMenuDismissals();
815
821
  this.autocompleteController.dispose();
816
822
  }
817
- restoreTabInputState(text, cursor) {
823
+ restoreTabInputState(state) {
818
824
  this.requestHistory.resetNavigation();
819
825
  this.popupMenus.resetInputMenuDismissals();
820
- this.inputEditor.setText(text, cursor);
826
+ this.inputEditor.setDraftState(state);
821
827
  this.autocompleteController.dispose();
822
828
  }
823
829
  async clearPersistedInputDraft() {
@@ -1007,6 +1013,7 @@ export class PiUiExtendApp {
1007
1013
  this.scheduledRenderTimer = undefined;
1008
1014
  }
1009
1015
  this.autocompleteController.observeInput();
1016
+ this.syncScrollAfterInputEditorChange();
1010
1017
  this.renderController.render();
1011
1018
  }
1012
1019
  scheduleRender() {
@@ -1014,10 +1021,18 @@ export class PiUiExtendApp {
1014
1021
  return;
1015
1022
  this.scheduledRenderTimer = setTimeout(() => {
1016
1023
  this.scheduledRenderTimer = undefined;
1024
+ this.syncScrollAfterInputEditorChange();
1017
1025
  this.renderController.render();
1018
1026
  }, COALESCED_RENDER_DELAY_MS);
1019
1027
  this.scheduledRenderTimer.unref?.();
1020
1028
  }
1029
+ syncScrollAfterInputEditorChange() {
1030
+ const contentVersion = this.inputEditor.contentVersion;
1031
+ if (contentVersion === this.lastInputEditorContentVersion)
1032
+ return;
1033
+ this.lastInputEditorContentVersion = contentVersion;
1034
+ this.scrollController.scrollToBottom();
1035
+ }
1021
1036
  renderStatusLine() {
1022
1037
  this.renderController.renderStatusLine();
1023
1038
  }
@@ -54,6 +54,7 @@ export class AppCommandController {
54
54
  runCloneCommand: () => this.navigationActions.runCloneCommand(),
55
55
  runTreeCommand: (argumentsText) => this.navigationActions.runTreeCommand(argumentsText),
56
56
  runJumpCommand: (argumentsText) => this.navigationActions.runJumpCommand(argumentsText),
57
+ runHistoryCommand: (argumentsText) => this.navigationActions.runHistoryCommand(argumentsText),
57
58
  runSearchCommand: (argumentsText) => this.navigationActions.runSearchCommand(argumentsText),
58
59
  runUnsupportedBuiltinCommand: (commandName, message) => this.navigationActions.runUnsupportedBuiltinCommand(commandName, message),
59
60
  runResumePathCommand: (sessionPath) => this.navigationActions.runResumePathCommand(sessionPath),
@@ -1,11 +1,13 @@
1
1
  import type { AgentSession, AgentSessionRuntime, SessionInfo } from "@earendil-works/pi-coding-agent";
2
2
  import type { SessionSearchResult } from "../session/session-search.js";
3
+ import type { AppRequestHistory } from "../session/request-history.js";
3
4
  import type { ActivePopupMenu, AppOptions, Entry, ModelMenuValue, PixMenuItem, PixMenuOptions, PopupMenuPlacement, ScopedSessionModel, SessionModel, ThinkingMenuValue } from "../types.js";
4
5
  import type { ToastNotifier } from "../../ui.js";
5
6
  export type DirectPopupMenu = Exclude<ActivePopupMenu, "slash">;
6
7
  export type CommandControllerHost = {
7
8
  readonly options: AppOptions;
8
9
  runtime(): AgentSessionRuntime | undefined;
10
+ requestHistory(): AppRequestHistory;
9
11
  getInput(): string;
10
12
  setInput(value: string): void;
11
13
  promptEnhancerModelRef(): string;
@@ -2,6 +2,14 @@ import type { SessionInfo } from "@earendil-works/pi-coding-agent";
2
2
  import type { CommandControllerHost } from "./command-host.js";
3
3
  import { type ResumeSessionLoaderOptions } from "../session/resume-session-loader.js";
4
4
  import type { PopupMenuPlacement } from "../types.js";
5
+ export declare function formatHistoryMenuLabel(text: string): string;
6
+ export declare function historyHighlightRanges(ranges: readonly {
7
+ start: number;
8
+ end: number;
9
+ }[], text: string): {
10
+ start: number;
11
+ end: number;
12
+ }[];
5
13
  export declare class NavigationCommandActions {
6
14
  private readonly host;
7
15
  private readonly resumeSessionLoader;
@@ -11,6 +19,7 @@ export declare class NavigationCommandActions {
11
19
  runCloneCommand(): Promise<void>;
12
20
  runTreeCommand(argumentsText: string): Promise<void>;
13
21
  runJumpCommand(argumentsText: string): Promise<void>;
22
+ runHistoryCommand(argumentsText: string): Promise<void>;
14
23
  runSearchCommand(argumentsText: string): Promise<void>;
15
24
  runUnsupportedBuiltinCommand(commandName: string, message: string): Promise<void>;
16
25
  runResumePathCommand(sessionPath: string): Promise<void>;
@@ -11,6 +11,24 @@ function nextTick() {
11
11
  setImmediate(resolve);
12
12
  });
13
13
  }
14
+ export function formatHistoryMenuLabel(text) {
15
+ return sanitizeText(text).replace(/\n/g, " ↵ ");
16
+ }
17
+ export function historyHighlightRanges(ranges, text) {
18
+ return ranges.map((range) => ({
19
+ start: historyLabelIndex(range.start, text),
20
+ end: historyLabelIndex(range.end, text),
21
+ })).filter((range) => range.end > range.start);
22
+ }
23
+ function historyLabelIndex(index, text) {
24
+ const before = text.slice(0, Math.max(0, Math.min(index, text.length)));
25
+ const newlineCount = before.split("\n").length - 1;
26
+ return before.length + newlineCount * 2;
27
+ }
28
+ function formatHistoryMenuDescription(text) {
29
+ const lines = sanitizeText(text).split("\n");
30
+ return lines.length > 1 ? `${lines.length} lines` : undefined;
31
+ }
14
32
  export class NavigationCommandActions {
15
33
  host;
16
34
  resumeSessionLoader;
@@ -98,12 +116,53 @@ export class NavigationCommandActions {
98
116
  const runtime = getRuntime(this.host, "jump");
99
117
  if (!runtime)
100
118
  return;
101
- this.host.setStatus("scanning session messages…");
102
- this.host.render();
103
- await this.host.refreshUserMessageJumpMenuItems();
104
119
  this.host.openDirectPopupMenu("user-message-jump", { preserveStatus: true });
105
120
  this.host.setDirectPopupMenuQuery(argumentsText.trim());
106
121
  this.host.render();
122
+ try {
123
+ await this.host.refreshUserMessageJumpMenuItems();
124
+ }
125
+ catch (error) {
126
+ this.host.toast.error(`Could not load jump messages: ${error instanceof Error ? error.message : String(error)}`);
127
+ }
128
+ finally {
129
+ this.host.render();
130
+ }
131
+ }
132
+ async runHistoryCommand(argumentsText) {
133
+ const query = argumentsText.trim();
134
+ const matches = this.host.requestHistory().searchMatches(query, 100);
135
+ if (matches.length === 0) {
136
+ this.host.addEntry({ id: createId("system"), kind: "system", text: query ? `No command history found for: ${query}` : "Command history is empty." });
137
+ this.host.toast.info(query ? "No matching command history" : "Command history is empty");
138
+ this.host.setSessionStatus(this.host.runtime()?.session);
139
+ this.host.render();
140
+ return;
141
+ }
142
+ const selected = await this.host.showMenu(matches.map((match) => {
143
+ const description = formatHistoryMenuDescription(match.value);
144
+ return {
145
+ value: match.value,
146
+ label: formatHistoryMenuLabel(match.value),
147
+ labelHighlightRanges: match.matchedText === match.label ? historyHighlightRanges(match.matchedRanges, match.value) : [],
148
+ ...(description === undefined ? {} : { description }),
149
+ };
150
+ }), {
151
+ title: query ? `Search command history: ${query}` : "Command history",
152
+ placeholder: "Filter history",
153
+ emptyText: "No matching command history",
154
+ searchable: true,
155
+ minScorePerCharacter: 8,
156
+ preferKeyboardLayoutMatches: true,
157
+ });
158
+ if (!selected) {
159
+ this.host.setSessionStatus(this.host.runtime()?.session);
160
+ return;
161
+ }
162
+ this.host.setInput(selected);
163
+ this.host.toast.info("Restored command from history");
164
+ this.host.setSessionStatus(this.host.runtime()?.session);
165
+ this.host.render();
107
166
  }
108
167
  async runSearchCommand(argumentsText) {
109
168
  const runtime = getIdleRuntime(this.host, "search");
@@ -25,6 +25,7 @@ export type CommandRegistryActions = {
25
25
  runCloneCommand(): Promise<void>;
26
26
  runTreeCommand(argumentsText: string): Promise<void>;
27
27
  runJumpCommand(argumentsText: string): Promise<void>;
28
+ runHistoryCommand(argumentsText: string): Promise<void>;
28
29
  runSearchCommand(argumentsText: string): Promise<void>;
29
30
  runUnsupportedBuiltinCommand(commandName: string, message: string): Promise<void>;
30
31
  runReloadCommand(): Promise<void>;
@@ -185,6 +185,14 @@ export function createSlashCommands(actions, host) {
185
185
  allowArguments: true,
186
186
  run: (argumentsText) => actions.runJumpCommand(argumentsText),
187
187
  },
188
+ {
189
+ name: "history",
190
+ description: "Search command history and restore a match",
191
+ kind: "builtin",
192
+ keywords: ["command", "request", "prompt", "find", "recent"],
193
+ allowArguments: true,
194
+ run: (argumentsText) => actions.runHistoryCommand(argumentsText),
195
+ },
188
196
  {
189
197
  name: "search",
190
198
  description: "Search sessions and open a match in a new tab",
@@ -42,7 +42,6 @@ export declare const GIT_BRANCH_CACHE_MS = 30000;
42
42
  export declare const TODO_TOOL_NAME = "todo";
43
43
  export declare const TODO_ACTIONS: readonly ["create", "update", "batch_create", "batch_update", "list", "get", "delete", "clear", "export", "import"];
44
44
  export declare const TODO_STATUSES: readonly ["pending", "in_progress", "deferred", "completed", "deleted"];
45
- export declare const TODO_PRIORITIES: readonly ["low", "medium", "high", "urgent"];
46
45
  export declare const SUBAGENT_STATUSES: readonly ["planned", "running", "retrying", "done", "failed", "stopped"];
47
46
  export declare const SUBAGENT_ACTIVE_STATUSES: readonly ["planned", "running", "retrying"];
48
47
  export declare const SUBAGENT_TERMINAL_STATUSES: readonly ["done", "failed", "stopped"];
@@ -90,7 +90,6 @@ export const TODO_ACTIONS = [
90
90
  "import",
91
91
  ];
92
92
  export const TODO_STATUSES = ["pending", "in_progress", "deferred", "completed", "deleted"];
93
- export const TODO_PRIORITIES = ["low", "medium", "high", "urgent"];
94
93
  export const SUBAGENT_STATUSES = ["planned", "running", "retrying", "done", "failed", "stopped"];
95
94
  export const SUBAGENT_ACTIVE_STATUSES = ["planned", "running", "retrying"];
96
95
  export const SUBAGENT_TERMINAL_STATUSES = ["done", "failed", "stopped"];
@@ -12,6 +12,7 @@ declare const NERD_FONT_ICONS: {
12
12
  readonly deleted: "󰅙";
13
13
  readonly deferred: "󰍷";
14
14
  readonly info: "󰋼";
15
+ readonly lightbulb: "󰌵";
15
16
  readonly microphone: "󰍬";
16
17
  readonly plus: "󰐕";
17
18
  readonly pause: "󰏤";
package/dist/app/icons.js CHANGED
@@ -18,6 +18,7 @@ const NERD_FONT_ICONS = {
18
18
  deleted: "\u{f0159}",
19
19
  deferred: "\u{f0377}",
20
20
  info: "\u{f02fc}",
21
+ lightbulb: "\u{f0335}",
21
22
  microphone: "\u{f036c}",
22
23
  plus: "\u{f0415}",
23
24
  pause: "\u{f03e4}",
@@ -44,6 +45,7 @@ const FALLBACK_ICONS = {
44
45
  deleted: "×",
45
46
  deferred: "↷",
46
47
  info: "i",
48
+ lightbulb: "💡",
47
49
  microphone: "m",
48
50
  plus: "+",
49
51
  pause: "⏸",
@@ -19,6 +19,7 @@ export type AppInputActionControllerHost = {
19
19
  addEntry(entry: Entry): void;
20
20
  addSessionAbortedEntry(): void;
21
21
  showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
22
+ dismissActiveDialog?(): boolean;
22
23
  stopVoiceInput(): Promise<void>;
23
24
  isShellCommandRunning(): boolean;
24
25
  runChatShellCommand(command: string): Promise<InteractiveShellCommandResult>;
@@ -80,10 +80,11 @@ export class AppInputActionController {
80
80
  }
81
81
  }
82
82
  closeActiveGlobalUi() {
83
- if (!this.popupMenus.syncActivePopupMenu())
84
- return false;
85
- this.popupMenus.cancelActivePopupMenu();
86
- return true;
83
+ if (this.popupMenus.syncActivePopupMenu()) {
84
+ this.popupMenus.cancelActivePopupMenu();
85
+ return true;
86
+ }
87
+ return this.host.dismissActiveDialog?.() ?? false;
87
88
  }
88
89
  async abortStreamingSession(runtime, options) {
89
90
  const session = runtime.session;
@@ -11,6 +11,7 @@ export declare class AppMenuItemsController {
11
11
  private readonly host;
12
12
  private resumeMenuLoaderCache;
13
13
  private userMessageJumpItems;
14
+ private userMessageJumpLoading;
14
15
  constructor(host: AppMenuItemsControllerHost);
15
16
  parseSlashInput(text: string): import("../types.js").ParsedSlashInput | undefined;
16
17
  getResourceSlashCommands(): SlashCommand[];
@@ -22,6 +23,7 @@ export declare class AppMenuItemsController {
22
23
  getThinkingMenuItems(query: string): PopupMenuItem<ThinkingMenuValue>[];
23
24
  getUserMessageMenuItems(): PopupMenuItem<UserMessageMenuValue>[];
24
25
  getUserMessageJumpMenuItems(query: string): PopupMenuItem<UserMessageJumpMenuValue>[];
26
+ isUserMessageJumpLoading(): boolean;
25
27
  refreshUserMessageJumpMenuItems(): Promise<void>;
26
28
  getQueueMessageMenuItems(): PopupMenuItem<QueueMessageMenuValue>[];
27
29
  getResumeMenuItems(query: string, limit?: number): PopupMenuItem<ResumeMenuValue>[];
@@ -12,6 +12,7 @@ export class AppMenuItemsController {
12
12
  host;
13
13
  resumeMenuLoaderCache;
14
14
  userMessageJumpItems;
15
+ userMessageJumpLoading = false;
15
16
  constructor(host) {
16
17
  this.host = host;
17
18
  }
@@ -29,6 +30,9 @@ export class AppMenuItemsController {
29
30
  value: match.value,
30
31
  label: `/${match.value.name}`,
31
32
  description: match.value.description,
33
+ labelHighlightRanges: match.matchedText === match.label
34
+ ? match.matchedRanges.map((range) => ({ start: range.start + 1, end: range.end + 1 }))
35
+ : [],
32
36
  }));
33
37
  }
34
38
  modelRef(model) {
@@ -64,6 +68,7 @@ export class AppMenuItemsController {
64
68
  value: match.value,
65
69
  label: `${match.value.ref}${match.value.current ? ` ${APP_ICONS.check}` : ""}`,
66
70
  description: match.value.model.name,
71
+ labelHighlightRanges: labelHighlightRangesFromMatch(match.matchedText, match.matchedRanges, match.label),
67
72
  }));
68
73
  }
69
74
  getThinkingMenuItems(query) {
@@ -96,26 +101,36 @@ export class AppMenuItemsController {
96
101
  getUserMessageJumpMenuItems(query) {
97
102
  return filterUserMessageJumpItems(this.userMessageJumpItems ?? buildUserMessageJumpItems(this.host.getEntries()), query);
98
103
  }
104
+ isUserMessageJumpLoading() {
105
+ return this.userMessageJumpLoading;
106
+ }
99
107
  async refreshUserMessageJumpMenuItems() {
100
108
  const runtime = this.host.runtime();
101
109
  if (!runtime) {
102
110
  this.userMessageJumpItems = undefined;
111
+ this.userMessageJumpLoading = false;
103
112
  return;
104
113
  }
105
- const entries = await sessionHistoryFullBranchEntries(runtime.session);
106
- const loadedBySessionEntryId = new Map(this.host.getEntries()
107
- .filter((entry) => entry.kind === "user" && typeof entry.sessionEntryId === "string")
108
- .map((entry) => [entry.sessionEntryId, entry]));
109
- const sources = entries.flatMap((entry) => {
110
- if (entry.type !== "message" || !isRecord(entry.message) || entry.message.role !== "user")
111
- return [];
112
- const text = renderUserMessageContent(entry.message.content);
113
- if (!text)
114
- return [];
115
- const loaded = loadedBySessionEntryId.get(entry.id);
116
- return [{ text, ...(loaded ? { entryId: loaded.id } : {}), sessionEntryId: entry.id }];
117
- });
118
- this.userMessageJumpItems = buildUserMessageJumpItems(sources);
114
+ this.userMessageJumpLoading = true;
115
+ try {
116
+ const entries = await sessionHistoryFullBranchEntries(runtime.session);
117
+ const loadedBySessionEntryId = new Map(this.host.getEntries()
118
+ .filter((entry) => entry.kind === "user" && typeof entry.sessionEntryId === "string")
119
+ .map((entry) => [entry.sessionEntryId, entry]));
120
+ const sources = entries.flatMap((entry) => {
121
+ if (entry.type !== "message" || !isRecord(entry.message) || entry.message.role !== "user")
122
+ return [];
123
+ const text = renderUserMessageContent(entry.message.content);
124
+ if (!text)
125
+ return [];
126
+ const loaded = loadedBySessionEntryId.get(entry.id);
127
+ return [{ text, ...(loaded ? { entryId: loaded.id } : {}), sessionEntryId: entry.id }];
128
+ });
129
+ this.userMessageJumpItems = buildUserMessageJumpItems(sources);
130
+ }
131
+ finally {
132
+ this.userMessageJumpLoading = false;
133
+ }
119
134
  }
120
135
  getQueueMessageMenuItems() {
121
136
  return [
@@ -205,6 +220,14 @@ export class AppMenuItemsController {
205
220
  }
206
221
  }
207
222
  }
223
+ function labelHighlightRangesFromMatch(matchedText, matchedRanges, label) {
224
+ if (matchedText === label)
225
+ return matchedRanges;
226
+ const offset = label.toLocaleLowerCase().indexOf(matchedText.toLocaleLowerCase());
227
+ if (offset < 0)
228
+ return [];
229
+ return matchedRanges.map((range) => ({ start: offset + range.start, end: offset + range.end }));
230
+ }
208
231
  function normalizeAvailableThinkingLevels(levels) {
209
232
  const seen = new Set();
210
233
  const normalized = [];
@@ -666,10 +666,18 @@ export class AppPopupMenuController {
666
666
  value: item,
667
667
  label: item.label,
668
668
  ...(item.keywords === undefined ? {} : { keywords: item.keywords }),
669
- })), query).map((match) => match.value);
669
+ })), query, {
670
+ ...(request.options.minScorePerCharacter === undefined ? {} : { minScorePerCharacter: request.options.minScorePerCharacter }),
671
+ preferKeyboardLayoutMatches: request.options.preferKeyboardLayoutMatches ?? false,
672
+ }).map((match) => ({
673
+ ...match.value,
674
+ labelHighlightRanges: match.matchedText === match.label ? match.matchedRanges : [],
675
+ }));
670
676
  return this.withoutCloseMenuItems(items.map((item) => ({
671
677
  value: item,
672
678
  label: item.label,
679
+ ...(item.labelHighlightRanges === undefined ? {} : { labelHighlightRanges: item.labelHighlightRanges }),
680
+ ...(item.descriptionHighlightRanges === undefined ? {} : { descriptionHighlightRanges: item.descriptionHighlightRanges }),
673
681
  ...(item.description === undefined ? {} : { description: item.description }),
674
682
  })));
675
683
  }
@@ -733,7 +741,8 @@ function formatSessionMenuDateTime(dateTime) {
733
741
  time: dateTime.toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit", hourCycle: "h23" }),
734
742
  };
735
743
  }
736
- function formatSessionInfoMenuItem(session, labelPrefix = "") {
744
+ function formatSessionInfoMenuItem(source) {
745
+ const { session, labelPrefix } = source;
737
746
  const { date, time } = formatSessionMenuDateTime(session.modified);
738
747
  const messages = `${session.messageCount} msg${session.messageCount !== 1 ? "s" : ""}`;
739
748
  const label = session.name ?? session.firstMessage.slice(0, 50);
@@ -741,6 +750,7 @@ function formatSessionInfoMenuItem(session, labelPrefix = "") {
741
750
  value: session,
742
751
  label: `${labelPrefix}${label}`,
743
752
  description: `${date} ${time} · ${messages} · ${session.id.slice(0, 8)}`,
753
+ ...(source.labelHighlightRanges === undefined ? {} : { labelHighlightRanges: source.labelHighlightRanges }),
744
754
  };
745
755
  }
746
756
  function buildSessionInfoMenuSource(sessions, currentSessionFile, query) {
@@ -761,7 +771,11 @@ function buildSessionInfoMenuSource(sessions, currentSessionFile, query) {
761
771
  session.id,
762
772
  ],
763
773
  }));
764
- return fuzzySearch(items, query).map((match) => ({ session: match.value, labelPrefix: "" }));
774
+ return fuzzySearch(items, query).map((match) => ({
775
+ session: match.value,
776
+ labelPrefix: "",
777
+ labelHighlightRanges: match.matchedText === match.label ? match.matchedRanges : [],
778
+ }));
765
779
  }
766
780
  export function createSessionInfoMenuItemsLoader(sessions, currentSessionFile, query) {
767
781
  const source = buildSessionInfoMenuSource(sessions, currentSessionFile, query);
@@ -775,7 +789,7 @@ export function createSessionInfoMenuItemsLoader(sessions, currentSessionFile, q
775
789
  const cached = cachedItems.get(effectiveLimit);
776
790
  if (cached)
777
791
  return cached;
778
- const result = source.slice(0, effectiveLimit).map((item) => formatSessionInfoMenuItem(item.session, item.labelPrefix));
792
+ const result = source.slice(0, effectiveLimit).map((item) => formatSessionInfoMenuItem(item));
779
793
  cachedItems.set(effectiveLimit, result);
780
794
  return result;
781
795
  },
@@ -812,5 +826,16 @@ export function filterUserMessageJumpItems(items, query) {
812
826
  ...(item.aliases === undefined ? {} : { aliases: item.aliases }),
813
827
  ...(item.keywords === undefined ? {} : { keywords: item.keywords }),
814
828
  }));
815
- return fuzzySearch(searchableItems, query).map((match) => match.value);
829
+ return fuzzySearch(searchableItems, query).map((match) => ({
830
+ ...match.value,
831
+ labelHighlightRanges: labelHighlightRangesFromMatch(match.matchedText, match.matchedRanges, match.label),
832
+ }));
833
+ }
834
+ function labelHighlightRangesFromMatch(matchedText, matchedRanges, label) {
835
+ if (matchedText === label)
836
+ return matchedRanges;
837
+ const offset = label.toLocaleLowerCase().indexOf(matchedText.toLocaleLowerCase());
838
+ if (offset < 0)
839
+ return [];
840
+ return matchedRanges.map((range) => ({ start: offset + range.start, end: offset + range.end }));
816
841
  }
@@ -20,35 +20,46 @@ export function renderTodoPanel(details, expanded, width, colors) {
20
20
  const todoPanelColor = colors.warning;
21
21
  const todoMetaColor = colors.muted;
22
22
  const todoThinkingColor = (level) => thinkingLevelThemeColor(level, colors);
23
+ const todoStatusThemeColor = (status) => {
24
+ switch (status) {
25
+ case "pending": return colors.muted;
26
+ case "in_progress": return colors.warning;
27
+ case "deferred": return colors.muted;
28
+ case "completed": return colors.success;
29
+ case "deleted": return colors.error;
30
+ }
31
+ };
23
32
  if (!expanded) {
24
33
  const prefix = `${headerText} — current: `;
25
34
  const current = activeTask ? formatTodoTaskLine(activeTask) : "no active todo";
26
35
  const collapsedText = `${prefix}${current}`;
27
- const segments = activeTask
28
- ? todoTaskLineSegments(activeTask, todoMetaColor, { thinkingColor: todoThinkingColor }).map((segment) => ({
36
+ const segments = [
37
+ { start: 0, end: headerText.length, foreground: todoPanelColor },
38
+ { start: headerText.length, end: prefix.length, foreground: todoMetaColor },
39
+ ];
40
+ if (activeTask) {
41
+ const activeSegments = todoTaskLineSegments(activeTask, todoMetaColor, { thinkingColor: todoThinkingColor, statusColor: todoStatusThemeColor }).map((segment) => ({
29
42
  ...segment,
30
43
  start: segment.start + prefix.length,
31
44
  end: segment.end + prefix.length,
32
- }))
33
- : undefined;
45
+ }));
46
+ segments.push(...activeSegments);
47
+ }
34
48
  const line = {
35
49
  text: padOrTrimPlain(ellipsizeDisplay(collapsedText, contentWidth), width),
36
- colorOverride: todoPanelColor,
50
+ segments,
37
51
  target,
38
52
  };
39
- if (segments)
40
- line.segments = segments;
41
53
  return [line];
42
54
  }
43
55
  const lines = [];
44
56
  for (const { task, depth } of visibleTodoTaskRows(details)) {
45
57
  const text = formatTodoTaskLine(task, { depth });
46
- const segments = todoTaskLineSegments(task, todoMetaColor, { depth, thinkingColor: todoThinkingColor });
58
+ const segments = todoTaskLineSegments(task, todoMetaColor, { depth, thinkingColor: todoThinkingColor, statusColor: todoStatusThemeColor });
47
59
  let start = 0;
48
60
  for (const wrapped of wrapLine(text, contentWidth)) {
49
61
  lines.push({
50
62
  text: padOrTrimPlain(wrapped, width),
51
- colorOverride: todoPanelColor,
52
63
  segments: shiftSegmentsToSlice(segments, start, wrapped.length),
53
64
  target,
54
65
  });
@@ -1,4 +1,5 @@
1
1
  import { type Theme } from "../../theme.js";
2
+ import { type ModelColorsConfig } from "../../config.js";
2
3
  import type { PopupMenu } from "../../ui.js";
3
4
  import type { ScreenStyler } from "../screen/screen-styler.js";
4
5
  import type { Entry, ModelMenuValue, PixMenuItem, PixMenuOptions, QueueMessageMenuValue, RenderedLine, ResumeMenuValue, SlashCommand, ThinkingMenuValue, UserMessageJumpMenuValue, UserMessageMenuValue } from "../types.js";
@@ -8,8 +9,10 @@ export type PopupMenuRendererHost = {
8
9
  readonly screenStyler: ScreenStyler;
9
10
  readonly entries: readonly Entry[];
10
11
  readonly session: AgentSession | undefined;
12
+ readonly modelColors?: ModelColorsConfig;
11
13
  readonly resumeLoading: boolean;
12
14
  readonly resumeSessionCount: number;
15
+ readonly userMessageJumpLoading: boolean;
13
16
  };
14
17
  export declare class PopupMenuRenderer {
15
18
  private readonly host;
@@ -38,10 +41,19 @@ export declare class PopupMenuRenderer {
38
41
  options: PixMenuOptions;
39
42
  } | undefined, directQuery: string): RenderedLine[];
40
43
  private hasPopupActionItems;
44
+ private labelDescriptionText;
41
45
  private userMessageActionForeground;
42
46
  private selectableItemVariant;
47
+ private thinkingMenuItemSegments;
48
+ private modelMenuItemSegments;
49
+ private modelMenuItemColor;
50
+ private availableThinkingLevels;
43
51
  private queueMessageItemVariant;
44
52
  private sdkItemVariant;
53
+ private sdkMenuItemSegments;
54
+ private itemHighlightSegments;
55
+ private highlightSegments;
56
+ private descriptionHighlightSegments;
45
57
  private resumeMenuItemSegments;
46
58
  private popupMenuHeader;
47
59
  private popupLineForeground;