pi-ui-extend 0.1.13 → 0.1.15

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 (92) hide show
  1. package/README.md +1 -1
  2. package/dist/app/app.d.ts +5 -0
  3. package/dist/app/app.js +82 -12
  4. package/dist/app/commands/command-controller.js +1 -0
  5. package/dist/app/commands/command-host.d.ts +3 -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.js +3 -0
  9. package/dist/app/commands/command-registry.d.ts +1 -0
  10. package/dist/app/commands/command-registry.js +8 -0
  11. package/dist/app/extensions/extension-ui-controller.d.ts +16 -5
  12. package/dist/app/extensions/extension-ui-controller.js +99 -61
  13. package/dist/app/input/input-action-controller.d.ts +1 -0
  14. package/dist/app/input/input-action-controller.js +8 -2
  15. package/dist/app/logger.d.ts +25 -0
  16. package/dist/app/logger.js +90 -0
  17. package/dist/app/model/model-usage-status.js +30 -15
  18. package/dist/app/popup/menu-items-controller.d.ts +2 -0
  19. package/dist/app/popup/menu-items-controller.js +45 -6
  20. package/dist/app/popup/popup-action-controller.d.ts +2 -1
  21. package/dist/app/popup/popup-action-controller.js +7 -4
  22. package/dist/app/popup/popup-menu-controller.d.ts +36 -23
  23. package/dist/app/popup/popup-menu-controller.js +68 -322
  24. package/dist/app/rendering/conversation-entry-renderer.js +3 -3
  25. package/dist/app/rendering/conversation-viewport.d.ts +10 -2
  26. package/dist/app/rendering/conversation-viewport.js +157 -16
  27. package/dist/app/rendering/editor-panels.js +4 -2
  28. package/dist/app/rendering/popup-menu-renderer.d.ts +50 -0
  29. package/dist/app/rendering/popup-menu-renderer.js +307 -0
  30. package/dist/app/rendering/render-controller.js +5 -13
  31. package/dist/app/rendering/status-line-renderer.d.ts +1 -1
  32. package/dist/app/rendering/status-line-renderer.js +27 -24
  33. package/dist/app/rendering/toast-controller.d.ts +11 -3
  34. package/dist/app/rendering/toast-controller.js +53 -12
  35. package/dist/app/runtime.d.ts +2 -1
  36. package/dist/app/runtime.js +20 -10
  37. package/dist/app/screen/mouse-controller.d.ts +2 -2
  38. package/dist/app/screen/mouse-controller.js +27 -48
  39. package/dist/app/screen/screen-styler.d.ts +1 -1
  40. package/dist/app/screen/screen-styler.js +9 -7
  41. package/dist/app/screen/scroll-controller.d.ts +11 -9
  42. package/dist/app/screen/scroll-controller.js +50 -45
  43. package/dist/app/session/lazy-session-manager.d.ts +11 -0
  44. package/dist/app/session/lazy-session-manager.js +539 -0
  45. package/dist/app/session/pix-system-message.d.ts +16 -0
  46. package/dist/app/session/pix-system-message.js +64 -0
  47. package/dist/app/session/session-event-controller.d.ts +11 -0
  48. package/dist/app/session/session-event-controller.js +58 -2
  49. package/dist/app/session/session-history.d.ts +18 -0
  50. package/dist/app/session/session-history.js +72 -3
  51. package/dist/app/session/session-lifecycle-controller.d.ts +6 -2
  52. package/dist/app/session/session-lifecycle-controller.js +7 -2
  53. package/dist/app/session/tabs-controller.d.ts +13 -1
  54. package/dist/app/session/tabs-controller.js +248 -27
  55. package/dist/app/todo/todo-model.d.ts +3 -1
  56. package/dist/app/todo/todo-model.js +14 -2
  57. package/dist/app/types.d.ts +5 -2
  58. package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
  59. package/dist/app/workspace/workspace-actions-controller.js +12 -0
  60. package/dist/config.d.ts +5 -1
  61. package/dist/config.js +73 -25
  62. package/dist/default-pix-config.js +2 -0
  63. package/dist/schemas/pi-tools-suite-schema.d.ts +1 -0
  64. package/dist/schemas/pi-tools-suite-schema.js +1 -0
  65. package/dist/schemas/pix-schema.d.ts +2 -1
  66. package/dist/schemas/pix-schema.js +5 -4
  67. package/dist/terminal-width.d.ts +2 -0
  68. package/dist/terminal-width.js +64 -3
  69. package/external/pi-tools-suite/README.md +1 -0
  70. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +12 -3
  71. package/external/pi-tools-suite/src/antigravity-auth/commands.ts +2 -4
  72. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +2 -2
  73. package/external/pi-tools-suite/src/antigravity-auth/index.ts +8 -2
  74. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +102 -50
  75. package/external/pi-tools-suite/src/antigravity-auth/status.ts +81 -2
  76. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +29 -8
  77. package/external/pi-tools-suite/src/config.ts +8 -0
  78. package/external/pi-tools-suite/src/dcp/index.ts +16 -1
  79. package/external/pi-tools-suite/src/dcp/state.ts +35 -0
  80. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -0
  81. package/external/pi-tools-suite/src/todo/index.ts +181 -11
  82. package/external/pi-tools-suite/src/todo/state/state-reducer.ts +23 -10
  83. package/external/pi-tools-suite/src/todo/todo.ts +10 -5
  84. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +33 -6
  85. package/external/pi-tools-suite/src/todo/tool/types.ts +9 -1
  86. package/external/pi-tools-suite/src/todo/view/format.ts +2 -1
  87. package/external/pi-tools-suite/src/tool-descriptions.ts +2 -1
  88. package/external/pi-tools-suite/src/usage/index.ts +5 -2
  89. package/external/pi-tools-suite/src/usage/lib/google.ts +6 -13
  90. package/package.json +1 -1
  91. package/schemas/pi-tools-suite.json +4 -0
  92. package/schemas/pix.json +6 -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");
@@ -751,195 +673,6 @@ export class AppPopupMenuController {
751
673
  ...(item.description === undefined ? {} : { description: item.description }),
752
674
  })));
753
675
  }
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
676
  }
944
677
  function canonicalSessionPath(sessionPath) {
945
678
  return sessionPath ? resolve(sessionPath) : undefined;
@@ -1051,20 +784,33 @@ export function createSessionInfoMenuItemsLoader(sessions, currentSessionFile, q
1051
784
  export function formatSessionInfoMenuItems(sessions, currentSessionFile, query, options = {}) {
1052
785
  return createSessionInfoMenuItemsLoader(sessions, currentSessionFile, query).items(options.limit);
1053
786
  }
1054
- export function buildUserMessageJumpItems(entries, query) {
1055
- const userEntries = entries.filter((entry) => entry.kind === "user");
1056
- const items = userEntries.map((entry, index) => {
787
+ export function buildUserMessageJumpItems(entries) {
788
+ const userEntries = entries.flatMap((entry) => {
789
+ if ("kind" in entry) {
790
+ return entry.kind === "user"
791
+ ? [{ text: entry.text, entryId: entry.id, ...(entry.sessionEntryId === undefined ? {} : { sessionEntryId: entry.sessionEntryId }) }]
792
+ : [];
793
+ }
794
+ return [entry];
795
+ });
796
+ return userEntries.map((entry, index) => {
1057
797
  const preview = sanitizeText(entry.text).replace(/\s+/g, " ").trim();
1058
798
  const label = `${index + 1}. ${preview || "(empty message)"}`;
1059
799
  return {
1060
- value: { entryId: entry.id },
800
+ value: { ...(entry.entryId === undefined ? {} : { entryId: entry.entryId }), ...(entry.sessionEntryId === undefined ? {} : { sessionEntryId: entry.sessionEntryId }) },
1061
801
  label,
1062
- aliases: [entry.sessionEntryId ?? "", entry.id],
802
+ ...(entry.entryId ? {} : { description: "load older history and jump" }),
803
+ aliases: [entry.sessionEntryId ?? "", entry.entryId ?? ""],
1063
804
  keywords: [entry.text],
1064
805
  };
1065
806
  });
1066
- return fuzzySearch(items, query).map((match) => ({
1067
- value: match.value,
1068
- label: match.label,
807
+ }
808
+ export function filterUserMessageJumpItems(items, query) {
809
+ const searchableItems = items.map((item) => ({
810
+ value: item,
811
+ label: item.label,
812
+ ...(item.aliases === undefined ? {} : { aliases: item.aliases }),
813
+ ...(item.keywords === undefined ? {} : { keywords: item.keywords }),
1069
814
  }));
815
+ return fuzzySearch(searchableItems, query).map((match) => match.value);
1070
816
  }
@@ -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;