pi-ui-extend 0.1.13 → 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 (111) hide show
  1. package/README.md +1 -1
  2. package/dist/app/app.d.ts +7 -0
  3. package/dist/app/app.js +102 -17
  4. package/dist/app/commands/command-controller.js +2 -0
  5. package/dist/app/commands/command-host.d.ts +5 -0
  6. package/dist/app/commands/command-model-actions.d.ts +2 -0
  7. package/dist/app/commands/command-model-actions.js +40 -4
  8. package/dist/app/commands/command-navigation-actions.d.ts +9 -0
  9. package/dist/app/commands/command-navigation-actions.js +62 -0
  10. package/dist/app/commands/command-registry.d.ts +2 -0
  11. package/dist/app/commands/command-registry.js +16 -0
  12. package/dist/app/constants.d.ts +0 -1
  13. package/dist/app/constants.js +0 -1
  14. package/dist/app/extensions/extension-ui-controller.d.ts +16 -5
  15. package/dist/app/extensions/extension-ui-controller.js +99 -61
  16. package/dist/app/icons.d.ts +1 -0
  17. package/dist/app/icons.js +2 -0
  18. package/dist/app/input/input-action-controller.d.ts +2 -0
  19. package/dist/app/input/input-action-controller.js +8 -1
  20. package/dist/app/logger.d.ts +25 -0
  21. package/dist/app/logger.js +90 -0
  22. package/dist/app/model/model-usage-status.js +30 -15
  23. package/dist/app/popup/menu-items-controller.d.ts +4 -0
  24. package/dist/app/popup/menu-items-controller.js +68 -6
  25. package/dist/app/popup/popup-action-controller.d.ts +2 -1
  26. package/dist/app/popup/popup-action-controller.js +7 -4
  27. package/dist/app/popup/popup-menu-controller.d.ts +36 -23
  28. package/dist/app/popup/popup-menu-controller.js +97 -326
  29. package/dist/app/rendering/conversation-entry-renderer.js +3 -3
  30. package/dist/app/rendering/conversation-viewport.d.ts +10 -2
  31. package/dist/app/rendering/conversation-viewport.js +157 -16
  32. package/dist/app/rendering/editor-panels.js +22 -9
  33. package/dist/app/rendering/popup-menu-renderer.d.ts +62 -0
  34. package/dist/app/rendering/popup-menu-renderer.js +405 -0
  35. package/dist/app/rendering/render-controller.js +30 -28
  36. package/dist/app/rendering/render-text.js +5 -2
  37. package/dist/app/rendering/status-line-renderer.d.ts +8 -1
  38. package/dist/app/rendering/status-line-renderer.js +217 -117
  39. package/dist/app/rendering/toast-controller.d.ts +12 -3
  40. package/dist/app/rendering/toast-controller.js +70 -12
  41. package/dist/app/runtime.d.ts +2 -1
  42. package/dist/app/runtime.js +20 -10
  43. package/dist/app/screen/mouse-controller.d.ts +2 -2
  44. package/dist/app/screen/mouse-controller.js +27 -48
  45. package/dist/app/screen/screen-styler.d.ts +1 -1
  46. package/dist/app/screen/screen-styler.js +9 -7
  47. package/dist/app/screen/scroll-controller.d.ts +12 -9
  48. package/dist/app/screen/scroll-controller.js +56 -45
  49. package/dist/app/screen/status-controller.js +2 -1
  50. package/dist/app/session/lazy-session-manager.d.ts +11 -0
  51. package/dist/app/session/lazy-session-manager.js +539 -0
  52. package/dist/app/session/pix-system-message.d.ts +16 -0
  53. package/dist/app/session/pix-system-message.js +64 -0
  54. package/dist/app/session/request-history.d.ts +4 -0
  55. package/dist/app/session/request-history.js +11 -0
  56. package/dist/app/session/session-event-controller.d.ts +11 -0
  57. package/dist/app/session/session-event-controller.js +58 -2
  58. package/dist/app/session/session-history.d.ts +18 -0
  59. package/dist/app/session/session-history.js +72 -3
  60. package/dist/app/session/session-lifecycle-controller.d.ts +6 -2
  61. package/dist/app/session/session-lifecycle-controller.js +7 -2
  62. package/dist/app/session/session-search.js +10 -0
  63. package/dist/app/session/tabs-controller.d.ts +17 -5
  64. package/dist/app/session/tabs-controller.js +308 -29
  65. package/dist/app/todo/todo-model.d.ts +4 -2
  66. package/dist/app/todo/todo-model.js +23 -13
  67. package/dist/app/types.d.ts +17 -6
  68. package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
  69. package/dist/app/workspace/workspace-actions-controller.js +12 -0
  70. package/dist/config.d.ts +6 -1
  71. package/dist/config.js +82 -25
  72. package/dist/default-pix-config.js +4 -0
  73. package/dist/fuzzy.d.ts +2 -0
  74. package/dist/fuzzy.js +27 -7
  75. package/dist/input-editor.d.ts +9 -0
  76. package/dist/input-editor.js +52 -0
  77. package/dist/schemas/pi-tools-suite-schema.d.ts +1 -0
  78. package/dist/schemas/pi-tools-suite-schema.js +1 -0
  79. package/dist/schemas/pix-schema.d.ts +3 -1
  80. package/dist/schemas/pix-schema.js +6 -4
  81. package/dist/terminal-width.d.ts +2 -0
  82. package/dist/terminal-width.js +64 -3
  83. package/dist/theme.js +6 -6
  84. package/dist/ui.d.ts +8 -0
  85. package/external/pi-tools-suite/README.md +3 -2
  86. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +52 -8
  87. package/external/pi-tools-suite/src/antigravity-auth/commands.ts +3 -41
  88. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +0 -2
  89. package/external/pi-tools-suite/src/antigravity-auth/index.ts +11 -18
  90. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +129 -61
  91. package/external/pi-tools-suite/src/antigravity-auth/status.ts +82 -3
  92. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +20 -7
  93. package/external/pi-tools-suite/src/antigravity-auth/types.ts +21 -0
  94. package/external/pi-tools-suite/src/config.ts +8 -0
  95. package/external/pi-tools-suite/src/dcp/index.ts +16 -1
  96. package/external/pi-tools-suite/src/dcp/state.ts +35 -0
  97. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -0
  98. package/external/pi-tools-suite/src/todo/index.ts +123 -14
  99. package/external/pi-tools-suite/src/todo/state/persistence.ts +0 -1
  100. package/external/pi-tools-suite/src/todo/state/state-reducer.ts +26 -43
  101. package/external/pi-tools-suite/src/todo/todo.ts +12 -23
  102. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +34 -16
  103. package/external/pi-tools-suite/src/todo/tool/types.ts +7 -28
  104. package/external/pi-tools-suite/src/todo/view/format.ts +2 -3
  105. package/external/pi-tools-suite/src/tool-descriptions.ts +6 -4
  106. package/external/pi-tools-suite/src/usage/index.ts +5 -2
  107. package/external/pi-tools-suite/src/usage/lib/google.ts +53 -40
  108. package/external/pi-tools-suite/src/usage/lib/types.ts +12 -2
  109. package/package.json +1 -1
  110. package/schemas/pi-tools-suite.json +4 -0
  111. package/schemas/pix.json +11 -2
@@ -1,14 +1,11 @@
1
1
  import { resolve } from "node:path";
2
2
  import { fuzzySearch } from "../../fuzzy.js";
3
- import { colorLine } from "../../theme.js";
4
3
  import { PopupMenu } from "../../ui.js";
5
- import { padOrTrimPlain, ellipsizeDisplay, sanitizeText } from "../rendering/render-text.js";
6
- import { stringDisplayWidth } from "../../terminal-width.js";
7
- import { RESUME_MENU_INITIAL_SESSION_ROWS, RESUME_MENU_LOAD_BATCH_ROWS, RESUME_MENU_LOAD_THRESHOLD_ROWS, RESUME_MENU_MAX_ROWS, SLASH_COMMAND_DESCRIPTION_COLUMN, SLASH_COMMAND_MENU_MAX_ROWS, THINKING_MENU_MAX_ROWS, } from "../constants.js";
8
- import { APP_ICONS } from "../icons.js";
9
- const POPUP_MENU_ESCAPE_BUTTON = "Esc";
4
+ import { sanitizeText } from "../rendering/render-text.js";
5
+ import { RESUME_MENU_INITIAL_SESSION_ROWS, RESUME_MENU_LOAD_BATCH_ROWS, RESUME_MENU_LOAD_THRESHOLD_ROWS, RESUME_MENU_MAX_ROWS, SLASH_COMMAND_MENU_MAX_ROWS, THINKING_MENU_MAX_ROWS, } from "../constants.js";
10
6
  export class AppPopupMenuController {
11
7
  host;
8
+ renderer;
12
9
  menuController = {
13
10
  show: (items, options) => this.showSdkMenu(items, options),
14
11
  select: (title, options, menuOptions) => this.selectSdkMenu(title, options, menuOptions),
@@ -40,8 +37,9 @@ export class AppPopupMenuController {
40
37
  resumeMenuAllSessionsLoaded = false;
41
38
  activeUserMessageEntryId;
42
39
  activeQueuedMessageEntryId;
43
- constructor(host) {
40
+ constructor(host, renderer) {
44
41
  this.host = host;
42
+ this.renderer = renderer;
45
43
  }
46
44
  get directMenu() {
47
45
  return this.directPopupMenu;
@@ -121,7 +119,6 @@ export class AppPopupMenuController {
121
119
  case "slash":
122
120
  return this.slashCommandMenu;
123
121
  }
124
- throw new Error(`Unknown popup menu: ${active}`);
125
122
  }
126
123
  moveActivePopupMenuSelection(delta) {
127
124
  const active = this.syncActivePopupMenu();
@@ -266,6 +263,29 @@ export class AppPopupMenuController {
266
263
  this.slashCommandMenu.close();
267
264
  this.dismissedSlashCommandMenuInput = undefined;
268
265
  }
266
+ closeMenusForTabSwitch() {
267
+ if (this.directPopupMenu === "sdk-menu") {
268
+ this.closeSdkMenu(undefined, { render: false, restoreStatus: false });
269
+ }
270
+ this.directPopupMenu = undefined;
271
+ this.directPopupMenuQuery = "";
272
+ this.directPopupMenuPreserveStatus = false;
273
+ this.directPopupMenuPlacement = "default";
274
+ this.activeUserMessageEntryId = undefined;
275
+ this.activeQueuedMessageEntryId = undefined;
276
+ this.slashCommandMenu.close();
277
+ this.modelMenu.close();
278
+ this.thinkingMenu.close();
279
+ this.resumeMenu.close();
280
+ this.userMessageMenu.close();
281
+ this.userMessageJumpMenu.close();
282
+ this.queueMessageMenu.close();
283
+ this.sdkMenu.close();
284
+ const input = this.host.getInput();
285
+ this.dismissedSlashCommandMenuInput = input;
286
+ this.dismissedModelMenuInput = input;
287
+ this.dismissedThinkingMenuInput = input;
288
+ }
269
289
  cancelActivePopupMenu() {
270
290
  const active = this.syncActivePopupMenu();
271
291
  if (this.directPopupMenu === "sdk-menu") {
@@ -414,60 +434,44 @@ export class AppPopupMenuController {
414
434
  }
415
435
  renderActivePopupMenu(width) {
416
436
  if (this.syncQueueMessageMenu())
417
- return this.renderQueueMessageMenu(width);
437
+ return this.renderer.renderQueueMessageMenu(width, this.queueMessageMenu);
418
438
  // User-message actions are rendered inline inside the selected message block.
419
439
  // They must never also appear as the global popup above the input editor.
420
440
  if (this.syncUserMessageMenu())
421
441
  return [];
422
442
  if (this.syncUserMessageJumpMenu())
423
- return this.renderUserMessageJumpMenu(width);
424
- if (this.syncResumeMenu())
425
- return this.renderResumeMenu(width);
443
+ return this.renderer.renderUserMessageJumpMenu(width, this.userMessageJumpMenu, this.directPopupMenuQuery);
444
+ if (this.syncResumeMenu()) {
445
+ return this.renderer.renderResumeMenu(width, this.resumeMenu, {
446
+ directQuery: this.directPopupMenuQuery,
447
+ allSessionsLoaded: this.resumeMenuAllSessionsLoaded,
448
+ loadedSessionCount: this.resumeMenuLoadedSessionCount(),
449
+ });
450
+ }
426
451
  if (this.syncSdkMenu())
427
- return this.renderSdkMenu(width);
452
+ return this.renderer.renderSdkMenu(width, this.sdkMenu, this.sdkMenuRequest, this.directPopupMenuQuery);
428
453
  if (this.syncModelMenu())
429
- return this.renderModelMenu(width);
454
+ return this.renderer.renderModelMenu(width, this.modelMenu);
430
455
  if (this.syncThinkingMenu())
431
- return this.renderThinkingMenu(width);
432
- return this.renderSlashCommandMenu(width);
456
+ return this.renderer.renderThinkingMenu(width, this.thinkingMenu);
457
+ return this.syncSlashCommandMenu() ? this.renderer.renderSlashCommandMenu(width, this.slashCommandMenu) : [];
433
458
  }
434
459
  popupMenuWidth(columns) {
435
- return columns;
460
+ return this.renderer.popupMenuWidth(columns);
436
461
  }
437
462
  popupMenuMargin(columns) {
438
- return columns > 44 ? 2 : 0;
463
+ return this.renderer.popupMenuMargin(columns);
439
464
  }
440
465
  effectivePopupMenuWidth(columns) {
441
- const sideMargin = this.popupMenuMargin(columns);
442
- return Math.min(this.popupMenuWidth(columns), Math.max(1, columns - sideMargin * 2));
466
+ return this.renderer.effectivePopupMenuWidth(columns);
443
467
  }
444
468
  styleOverlayLine(row, line, width) {
445
- const colors = this.host.theme.colors;
446
- const margin = this.popupMenuMargin(width);
447
- const menuWidth = this.effectivePopupMenuWidth(width);
448
- const rightMargin = Math.max(0, width - margin - menuWidth);
449
469
  const activeMenuName = this.syncActivePopupMenu() ?? "slash";
450
470
  const activeMenu = this.getActivePopupMenu(activeMenuName);
451
- const selected = line.target?.kind === "popup-menu" && activeMenu.selectedIndex === line.target.index;
452
- const foreground = this.popupLineForeground(line, selected);
453
- const background = this.popupLineBackground(line, selected);
454
- const plain = `${" ".repeat(margin)}${padOrTrimPlain(line.text, menuWidth)}${" ".repeat(rightMargin)}`;
455
- if (this.host.screenStyler.selectionRangeForRow(row, width)) {
456
- return this.host.screenStyler.styleLine(row, plain, width, { foreground, background });
457
- }
458
- return [
459
- colorLine("", margin, { background: colors.background }),
460
- line.segments && line.segments.length > 0
461
- ? this.host.screenStyler.styleLineSegments(row, line.text, menuWidth, { foreground, background, bold: selected }, line.segments)
462
- : colorLine(line.text, menuWidth, { foreground, background, bold: selected }),
463
- colorLine("", rightMargin, { background: colors.background }),
464
- ].join("");
471
+ return this.renderer.styleOverlayLine(row, line, width, activeMenu);
465
472
  }
466
473
  overlayPlainText(line, width) {
467
- const margin = this.popupMenuMargin(width);
468
- const menuWidth = this.effectivePopupMenuWidth(width);
469
- const rightMargin = Math.max(0, width - margin - menuWidth);
470
- return `${" ".repeat(margin)}${padOrTrimPlain(line.text, menuWidth)}${" ".repeat(rightMargin)}`;
474
+ return this.renderer.overlayPlainText(line, width);
471
475
  }
472
476
  isDynamicConversationBlock(entry) {
473
477
  return entry.kind === "user" && this.directPopupMenu === "user-message" && this.activeUserMessageEntryId === entry.id;
@@ -478,46 +482,7 @@ export class AppPopupMenuController {
478
482
  renderInlineUserMessageMenu(entry, options) {
479
483
  if (!(this.directPopupMenu === "user-message" && this.activeUserMessageEntryId === entry.id && this.syncUserMessageMenu()))
480
484
  return [];
481
- const headerLine = options.userLine(formatPopupMenuHeader("Message actions", options.userContentWidth));
482
- headerLine.target = { kind: "popup-menu-close" };
483
- headerLine.segments = [{
484
- start: options.userContentLeft,
485
- end: options.userContentLeft + options.userContentWidth,
486
- foreground: this.host.theme.colors.accent,
487
- background: this.host.theme.colors.popupHeaderBackground,
488
- bold: true,
489
- }];
490
- const lines = [headerLine];
491
- for (const item of this.userMessageMenu.visibleItems()) {
492
- const label = item.label.padEnd(18, " ");
493
- const description = item.description ?? "";
494
- const marker = item.selected ? "›" : " ";
495
- const rawText = `${marker} ${label}${description}`;
496
- const text = ellipsizeDisplay(rawText, options.userContentWidth);
497
- const line = options.userLine(text);
498
- line.target = { kind: "popup-menu", index: item.index };
499
- const contentStart = options.userContentLeft;
500
- const labelStart = contentStart + 2;
501
- const labelEnd = Math.min(contentStart + text.length, labelStart + item.label.length);
502
- const descriptionStart = contentStart + 2 + label.length;
503
- line.segments = [
504
- ...(item.selected ? [{ start: contentStart, end: contentStart + 1, foreground: this.host.theme.colors.accent, bold: true }] : []),
505
- {
506
- start: labelStart,
507
- end: labelEnd,
508
- foreground: this.userMessageActionForeground(item.selected, item.value),
509
- bold: item.selected,
510
- },
511
- ...(descriptionStart < contentStart + text.length
512
- ? [{ start: descriptionStart, end: contentStart + text.length, foreground: this.host.theme.colors.muted }]
513
- : []),
514
- ];
515
- lines.push(line);
516
- }
517
- return lines;
518
- }
519
- hasPopupActionItems(items) {
520
- return items.length > 0;
485
+ return this.renderer.renderInlineUserMessageMenu(options, this.userMessageMenu);
521
486
  }
522
487
  withoutCloseMenuItems(items) {
523
488
  return items.filter((item) => item.label.trim().toLowerCase() !== "cancel");
@@ -551,49 +516,6 @@ export class AppPopupMenuController {
551
516
  resumeMenuLoadedSessionCount() {
552
517
  return this.resumeMenu.items.filter((item) => item.value.kind === "session").length;
553
518
  }
554
- userMessageActionForeground(selected, value) {
555
- if (selected)
556
- return this.host.theme.colors.accent;
557
- if (value === "undo")
558
- return this.host.theme.colors.error;
559
- return this.host.theme.colors.inputForeground;
560
- }
561
- selectableItemVariant(selected, value) {
562
- if (selected)
563
- return "accent";
564
- return value.current ? "muted" : "normal";
565
- }
566
- queueMessageItemVariant(selected, value) {
567
- if (selected)
568
- return "accent";
569
- return value === "cancel" ? "error" : "normal";
570
- }
571
- sdkItemVariant(selected, value) {
572
- if (selected)
573
- return "accent";
574
- return value.variant ?? "normal";
575
- }
576
- resumeMenuItemSegments(value, label, description, text) {
577
- if (value.kind !== "session")
578
- return undefined;
579
- const sessionLabel = value.session.name ?? value.session.firstMessage.slice(0, 50);
580
- const sessionLabelStart = Math.max(0, label.length - sessionLabel.length);
581
- const muted = this.host.theme.colors.popupMuted;
582
- const segments = [];
583
- if (sessionLabelStart > 0)
584
- segments.push({ start: 0, end: sessionLabelStart, foreground: muted });
585
- if (description.length > 0)
586
- segments.push({ start: label.length, end: text.length, foreground: muted });
587
- return segments.length > 0 ? segments : undefined;
588
- }
589
- popupMenuHeader(title, width) {
590
- return {
591
- text: formatPopupMenuHeader(title, width),
592
- variant: "accent",
593
- backgroundOverride: this.host.theme.colors.popupHeaderBackground,
594
- target: { kind: "popup-menu-close" },
595
- };
596
- }
597
519
  syncModelMenu() {
598
520
  if (this.directPopupMenu === "model") {
599
521
  this.closeMenusExcept("model");
@@ -744,202 +666,21 @@ export class AppPopupMenuController {
744
666
  value: item,
745
667
  label: item.label,
746
668
  ...(item.keywords === undefined ? {} : { keywords: item.keywords }),
747
- })), 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
+ }));
748
676
  return this.withoutCloseMenuItems(items.map((item) => ({
749
677
  value: item,
750
678
  label: item.label,
679
+ ...(item.labelHighlightRanges === undefined ? {} : { labelHighlightRanges: item.labelHighlightRanges }),
680
+ ...(item.descriptionHighlightRanges === undefined ? {} : { descriptionHighlightRanges: item.descriptionHighlightRanges }),
751
681
  ...(item.description === undefined ? {} : { description: item.description }),
752
682
  })));
753
683
  }
754
- renderSlashCommandMenu(_width) {
755
- if (!this.syncSlashCommandMenu())
756
- return [];
757
- const lines = [this.popupMenuHeader("Commands", _width)];
758
- const visibleItems = this.slashCommandMenu.visibleItems();
759
- if (!this.hasPopupActionItems(this.slashCommandMenu.items)) {
760
- lines.push({ text: " No matching slash commands", variant: "muted" });
761
- }
762
- for (const item of visibleItems) {
763
- const command = item.label.padEnd(SLASH_COMMAND_DESCRIPTION_COLUMN, " ");
764
- const description = item.description ?? "";
765
- lines.push({
766
- text: `${command}${description}`,
767
- variant: item.selected ? "accent" : "normal",
768
- target: { kind: "popup-menu", index: item.index },
769
- });
770
- }
771
- return lines;
772
- }
773
- renderModelMenu(_width) {
774
- if (!this.syncModelMenu())
775
- return [];
776
- const lines = [this.popupMenuHeader("Select model", _width)];
777
- const visibleItems = this.modelMenu.visibleItems();
778
- if (!this.hasPopupActionItems(this.modelMenu.items)) {
779
- lines.push({
780
- text: this.host.session ? " No matching favorite models" : " Model menu unavailable",
781
- variant: "muted",
782
- });
783
- }
784
- for (const item of visibleItems) {
785
- const model = item.label.padEnd(SLASH_COMMAND_DESCRIPTION_COLUMN, " ");
786
- const description = item.description ?? "";
787
- lines.push({
788
- text: `${model}${description}`,
789
- variant: this.selectableItemVariant(item.selected, item.value),
790
- target: { kind: "popup-menu", index: item.index },
791
- });
792
- }
793
- return lines;
794
- }
795
- renderThinkingMenu(_width) {
796
- if (!this.syncThinkingMenu())
797
- return [];
798
- const lines = [this.popupMenuHeader("Thinking level", _width)];
799
- const visibleItems = this.thinkingMenu.visibleItems();
800
- if (!this.hasPopupActionItems(this.thinkingMenu.items)) {
801
- lines.push({ text: " No matching thinking levels", variant: "muted" });
802
- }
803
- for (const item of visibleItems) {
804
- const level = item.label.padEnd(SLASH_COMMAND_DESCRIPTION_COLUMN, " ");
805
- const description = item.description ?? "";
806
- lines.push({
807
- text: `${level}${description}`,
808
- variant: this.selectableItemVariant(item.selected, item.value),
809
- target: { kind: "popup-menu", index: item.index },
810
- });
811
- }
812
- return lines;
813
- }
814
- renderResumeMenu(_width) {
815
- if (!this.syncResumeMenu())
816
- return [];
817
- const title = this.host.resumeLoading ? `Resume session ${APP_ICONS.timerSand}` : "Resume session";
818
- const lines = [this.popupMenuHeader(title, _width)];
819
- const visibleItems = this.resumeMenu.visibleItems();
820
- if (!this.host.resumeLoading && !this.hasPopupActionItems(this.resumeMenu.items)) {
821
- lines.push({
822
- text: this.host.resumeSessionCount === 0 ? " No sessions found" : " No matching sessions",
823
- variant: "muted",
824
- });
825
- }
826
- for (const item of visibleItems) {
827
- const label = item.label;
828
- const description = item.description ?? "";
829
- const text = `${label} ${description}`;
830
- const segments = this.resumeMenuItemSegments(item.value, label, description, text);
831
- lines.push({
832
- text,
833
- variant: item.selected ? "accent" : "normal",
834
- ...(segments ? { segments } : {}),
835
- target: { kind: "popup-menu", index: item.index },
836
- });
837
- }
838
- if (!this.resumeMenuAllSessionsLoaded && this.resumeMenuLoadedSessionCount() > 0) {
839
- lines.push({ text: ` Loaded ${this.resumeMenuLoadedSessionCount()} sessions · scroll for more`, variant: "muted" });
840
- }
841
- if (this.directPopupMenuQuery) {
842
- lines.push({ text: ` Search: ${this.directPopupMenuQuery}`, variant: "muted" });
843
- }
844
- return lines;
845
- }
846
- renderUserMessageJumpMenu(width) {
847
- if (!this.syncUserMessageJumpMenu())
848
- return [];
849
- const lines = [this.popupMenuHeader("Jump to user message", width)];
850
- if (!this.hasPopupActionItems(this.userMessageJumpMenu.items)) {
851
- lines.push({
852
- text: this.host.entries.some((entry) => entry.kind === "user") ? " No matching user messages" : " No user messages yet",
853
- variant: "muted",
854
- });
855
- }
856
- const labelWidth = Math.max(1, width);
857
- for (const item of this.userMessageJumpMenu.visibleItems()) {
858
- const label = ellipsizeDisplay(item.label, labelWidth);
859
- lines.push({
860
- text: label,
861
- variant: item.selected ? "accent" : "normal",
862
- target: { kind: "popup-menu", index: item.index },
863
- });
864
- }
865
- if (this.directPopupMenuQuery) {
866
- lines.push({ text: ` Search: ${this.directPopupMenuQuery}`, variant: "muted" });
867
- }
868
- return lines;
869
- }
870
- renderQueueMessageMenu(_width) {
871
- if (!this.syncQueueMessageMenu())
872
- return [];
873
- const lines = [this.popupMenuHeader("Queued message", _width)];
874
- for (const item of this.queueMessageMenu.visibleItems()) {
875
- const label = item.label.padEnd(18, " ");
876
- const description = item.description ?? "";
877
- lines.push({
878
- text: `${label}${description}`,
879
- variant: this.queueMessageItemVariant(item.selected, item.value),
880
- target: { kind: "popup-menu", index: item.index },
881
- });
882
- }
883
- return lines;
884
- }
885
- renderSdkMenu(_width) {
886
- if (!this.syncSdkMenu())
887
- return [];
888
- const request = this.sdkMenuRequest;
889
- const lines = [this.popupMenuHeader(request?.options.title ?? "Menu", _width)];
890
- if (!this.hasPopupActionItems(this.sdkMenu.items)) {
891
- lines.push({ text: ` ${request?.options.emptyText ?? "No matching items"}`, variant: "muted" });
892
- }
893
- for (const item of this.sdkMenu.visibleItems()) {
894
- const label = item.label.padEnd(SLASH_COMMAND_DESCRIPTION_COLUMN, " ");
895
- const description = item.description ?? "";
896
- lines.push({
897
- text: `${label}${description}`,
898
- variant: this.sdkItemVariant(item.selected, item.value),
899
- target: { kind: "popup-menu", index: item.index },
900
- });
901
- }
902
- if (request?.options.searchable !== false && this.directPopupMenuQuery) {
903
- lines.push({ text: ` ${request?.options.placeholder ?? "Search"}: ${this.directPopupMenuQuery}`, variant: "muted" });
904
- }
905
- return lines;
906
- }
907
- popupLineForeground(line, selected) {
908
- const colors = this.host.theme.colors;
909
- if (selected)
910
- return colors.popupSelectedForeground;
911
- if (line.colorOverride)
912
- return line.colorOverride;
913
- switch (line.variant) {
914
- case "accent":
915
- return colors.accent;
916
- case "muted":
917
- return colors.popupMuted;
918
- case "error":
919
- return colors.error;
920
- case "normal":
921
- case undefined:
922
- return colors.popupForeground;
923
- }
924
- return colors.popupForeground;
925
- }
926
- popupLineBackground(line, selected) {
927
- const colors = this.host.theme.colors;
928
- if (selected)
929
- return colors.popupSelectedBackground;
930
- return line.backgroundOverride ?? colors.popupBackground;
931
- }
932
- }
933
- export function formatPopupMenuHeader(title, width) {
934
- const safeWidth = Math.max(1, width);
935
- const sanitizedTitle = sanitizeText(title).replace(/\s+/g, " ").trim() || "Menu";
936
- const buttonWidth = stringDisplayWidth(POPUP_MENU_ESCAPE_BUTTON);
937
- if (safeWidth <= buttonWidth + 1)
938
- return padOrTrimPlain(POPUP_MENU_ESCAPE_BUTTON, safeWidth);
939
- const titleWidth = safeWidth - buttonWidth - 1;
940
- const titleText = ellipsizeDisplay(sanitizedTitle, titleWidth);
941
- const gapWidth = Math.max(1, safeWidth - stringDisplayWidth(titleText) - buttonWidth);
942
- return `${titleText}${" ".repeat(gapWidth)}${POPUP_MENU_ESCAPE_BUTTON}`;
943
684
  }
944
685
  function canonicalSessionPath(sessionPath) {
945
686
  return sessionPath ? resolve(sessionPath) : undefined;
@@ -1000,7 +741,8 @@ function formatSessionMenuDateTime(dateTime) {
1000
741
  time: dateTime.toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit", hourCycle: "h23" }),
1001
742
  };
1002
743
  }
1003
- function formatSessionInfoMenuItem(session, labelPrefix = "") {
744
+ function formatSessionInfoMenuItem(source) {
745
+ const { session, labelPrefix } = source;
1004
746
  const { date, time } = formatSessionMenuDateTime(session.modified);
1005
747
  const messages = `${session.messageCount} msg${session.messageCount !== 1 ? "s" : ""}`;
1006
748
  const label = session.name ?? session.firstMessage.slice(0, 50);
@@ -1008,6 +750,7 @@ function formatSessionInfoMenuItem(session, labelPrefix = "") {
1008
750
  value: session,
1009
751
  label: `${labelPrefix}${label}`,
1010
752
  description: `${date} ${time} · ${messages} · ${session.id.slice(0, 8)}`,
753
+ ...(source.labelHighlightRanges === undefined ? {} : { labelHighlightRanges: source.labelHighlightRanges }),
1011
754
  };
1012
755
  }
1013
756
  function buildSessionInfoMenuSource(sessions, currentSessionFile, query) {
@@ -1028,7 +771,11 @@ function buildSessionInfoMenuSource(sessions, currentSessionFile, query) {
1028
771
  session.id,
1029
772
  ],
1030
773
  }));
1031
- 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
+ }));
1032
779
  }
1033
780
  export function createSessionInfoMenuItemsLoader(sessions, currentSessionFile, query) {
1034
781
  const source = buildSessionInfoMenuSource(sessions, currentSessionFile, query);
@@ -1042,7 +789,7 @@ export function createSessionInfoMenuItemsLoader(sessions, currentSessionFile, q
1042
789
  const cached = cachedItems.get(effectiveLimit);
1043
790
  if (cached)
1044
791
  return cached;
1045
- const result = source.slice(0, effectiveLimit).map((item) => formatSessionInfoMenuItem(item.session, item.labelPrefix));
792
+ const result = source.slice(0, effectiveLimit).map((item) => formatSessionInfoMenuItem(item));
1046
793
  cachedItems.set(effectiveLimit, result);
1047
794
  return result;
1048
795
  },
@@ -1051,20 +798,44 @@ export function createSessionInfoMenuItemsLoader(sessions, currentSessionFile, q
1051
798
  export function formatSessionInfoMenuItems(sessions, currentSessionFile, query, options = {}) {
1052
799
  return createSessionInfoMenuItemsLoader(sessions, currentSessionFile, query).items(options.limit);
1053
800
  }
1054
- export function buildUserMessageJumpItems(entries, query) {
1055
- const userEntries = entries.filter((entry) => entry.kind === "user");
1056
- const items = userEntries.map((entry, index) => {
801
+ export function buildUserMessageJumpItems(entries) {
802
+ const userEntries = entries.flatMap((entry) => {
803
+ if ("kind" in entry) {
804
+ return entry.kind === "user"
805
+ ? [{ text: entry.text, entryId: entry.id, ...(entry.sessionEntryId === undefined ? {} : { sessionEntryId: entry.sessionEntryId }) }]
806
+ : [];
807
+ }
808
+ return [entry];
809
+ });
810
+ return userEntries.map((entry, index) => {
1057
811
  const preview = sanitizeText(entry.text).replace(/\s+/g, " ").trim();
1058
812
  const label = `${index + 1}. ${preview || "(empty message)"}`;
1059
813
  return {
1060
- value: { entryId: entry.id },
814
+ value: { ...(entry.entryId === undefined ? {} : { entryId: entry.entryId }), ...(entry.sessionEntryId === undefined ? {} : { sessionEntryId: entry.sessionEntryId }) },
1061
815
  label,
1062
- aliases: [entry.sessionEntryId ?? "", entry.id],
816
+ ...(entry.entryId ? {} : { description: "load older history and jump" }),
817
+ aliases: [entry.sessionEntryId ?? "", entry.entryId ?? ""],
1063
818
  keywords: [entry.text],
1064
819
  };
1065
820
  });
1066
- return fuzzySearch(items, query).map((match) => ({
1067
- value: match.value,
1068
- label: match.label,
821
+ }
822
+ export function filterUserMessageJumpItems(items, query) {
823
+ const searchableItems = items.map((item) => ({
824
+ value: item,
825
+ label: item.label,
826
+ ...(item.aliases === undefined ? {} : { aliases: item.aliases }),
827
+ ...(item.keywords === undefined ? {} : { keywords: item.keywords }),
1069
828
  }));
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 }));
1070
841
  }
@@ -16,10 +16,10 @@ export function renderConversationEntry(entry, width, options) {
16
16
  ...(entryId === undefined ? {} : { target: { kind: "user-message", id: entryId } }),
17
17
  });
18
18
  const queuedLine = (text, entryId, segments) => ({
19
- text: padHorizontalText(text, width),
19
+ text,
20
20
  variant: "muted",
21
21
  backgroundOverride: options.colors.userMessageBackground,
22
- ...(segments && segments.length > 0 ? { segments: segments.map((segment) => ({ ...segment, start: segment.start + userContentLeft, end: segment.end + userContentLeft })) } : {}),
22
+ ...(segments && segments.length > 0 ? { segments } : {}),
23
23
  target: { kind: "queue-message", id: entryId },
24
24
  });
25
25
  const userMessageLines = (userEntry) => {
@@ -33,7 +33,7 @@ export function renderConversationEntry(entry, width, options) {
33
33
  };
34
34
  const queuedMessageLines = (queuedEntry) => {
35
35
  const icon = queuedEntry.queueSource === "deferred" ? APP_ICONS.pause : APP_ICONS.timerSand;
36
- const contentLines = wrapText(`${icon} ${queuedEntry.text}`, userContentWidth);
36
+ const contentLines = wrapText(`${icon} ${queuedEntry.text}`, width);
37
37
  return contentLines.map((text, index) => queuedLine(text, queuedEntry.id, index === 0 ? [{ start: 0, end: icon.length, foreground: options.colors.info }] : undefined));
38
38
  };
39
39
  switch (entry.kind) {
@@ -1,5 +1,5 @@
1
1
  import type { AgentSession } from "@earendil-works/pi-coding-agent";
2
- import type { PixConfig } from "../../config.js";
2
+ import { type PixConfig } from "../../config.js";
3
3
  import type { Theme } from "../../theme.js";
4
4
  import { type InlineUserMessageMenuContext } from "./conversation-entry-renderer.js";
5
5
  import type { ConversationBlockCache, Entry, RenderedLine, SubmittedUserMessage } from "../types.js";
@@ -35,19 +35,27 @@ export declare class ConversationViewport {
35
35
  deleteEntry(entryId: string): void;
36
36
  lineCount(width: number): number;
37
37
  slice(width: number, start: number, count: number): RenderedLine[];
38
+ private sliceMeasured;
38
39
  entries(): Entry[];
39
40
  blockForEntry(entry: Entry, width: number): ConversationBlockCache;
40
41
  entryBlockPositions(width: number): ConversationEntryBlockPosition[];
42
+ measuredLineCountForEntries(width: number, entryIds: readonly string[]): number;
41
43
  private queuedEntries;
42
44
  private layoutForWidth;
43
45
  private buildLayout;
46
+ private previousMeasuredLineCount;
44
47
  private layoutStructureChanged;
45
48
  private refreshDirtyLayoutEntries;
46
49
  private blockCacheForWidth;
47
50
  private refreshDynamicLayoutEntries;
51
+ private ensureEntryMeasured;
48
52
  private refreshLayoutEntry;
49
- private lineCountForEntry;
53
+ private measuredLineCountForEntry;
54
+ private estimatedLineCountForEntry;
55
+ private lineCountWithGap;
56
+ private estimatedBlockLineCountForEntry;
50
57
  private nextVisibleEntry;
58
+ private nextEstimatedVisibleEntry;
51
59
  private gapAfterEntry;
52
60
  private isSuperCompactGaplessEntry;
53
61
  private entryIndexForOffset;