@oh-my-pi/pi-coding-agent 16.2.2 → 16.2.4

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 (164) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/cli.js +3682 -3615
  3. package/dist/types/advisor/__tests__/config.test.d.ts +1 -0
  4. package/dist/types/advisor/advise-tool.d.ts +8 -4
  5. package/dist/types/advisor/config.d.ts +88 -0
  6. package/dist/types/advisor/index.d.ts +1 -0
  7. package/dist/types/advisor/transcript-recorder.d.ts +13 -2
  8. package/dist/types/advisor/watchdog.d.ts +20 -0
  9. package/dist/types/cli/update-cli.d.ts +15 -0
  10. package/dist/types/collab/guest.d.ts +29 -0
  11. package/dist/types/collab/replication-shrink.d.ts +39 -0
  12. package/dist/types/config/provider-globals.d.ts +7 -0
  13. package/dist/types/config/settings-schema.d.ts +81 -0
  14. package/dist/types/debug/log-viewer.d.ts +1 -0
  15. package/dist/types/debug/raw-sse.d.ts +1 -0
  16. package/dist/types/edit/hashline/diff.d.ts +0 -11
  17. package/dist/types/extensibility/tool-event-input.d.ts +7 -0
  18. package/dist/types/extensibility/utils.d.ts +12 -0
  19. package/dist/types/mcp/transports/index.d.ts +1 -0
  20. package/dist/types/mcp/transports/sse.d.ts +20 -0
  21. package/dist/types/memories/index.d.ts +20 -1
  22. package/dist/types/modes/components/advisor-config.d.ts +59 -0
  23. package/dist/types/modes/components/index.d.ts +1 -0
  24. package/dist/types/modes/components/model-selector.d.ts +9 -1
  25. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  26. package/dist/types/modes/components/status-line/component.d.ts +35 -1
  27. package/dist/types/modes/components/status-line/types.d.ts +13 -1
  28. package/dist/types/modes/controllers/command-controller.d.ts +3 -3
  29. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  30. package/dist/types/modes/interactive-mode.d.ts +11 -4
  31. package/dist/types/modes/skill-command.d.ts +32 -0
  32. package/dist/types/modes/types.d.ts +9 -3
  33. package/dist/types/session/agent-session.d.ts +58 -10
  34. package/dist/types/session/indexed-session-storage.d.ts +7 -1
  35. package/dist/types/session/messages.d.ts +26 -0
  36. package/dist/types/session/messages.test.d.ts +1 -0
  37. package/dist/types/session/session-entries.d.ts +31 -3
  38. package/dist/types/session/session-history-format.d.ts +6 -0
  39. package/dist/types/session/session-loader.d.ts +9 -1
  40. package/dist/types/session/session-manager.d.ts +8 -7
  41. package/dist/types/session/session-storage.d.ts +11 -0
  42. package/dist/types/session/session-title-slot.d.ts +19 -0
  43. package/dist/types/ssh/connection-manager.d.ts +47 -0
  44. package/dist/types/ssh/utils.d.ts +16 -0
  45. package/dist/types/task/executor.d.ts +3 -16
  46. package/dist/types/task/render.d.ts +0 -5
  47. package/dist/types/task/renderer.d.ts +13 -0
  48. package/dist/types/task/types.d.ts +16 -0
  49. package/dist/types/task/yield-assembly.d.ts +28 -0
  50. package/dist/types/tiny/text.d.ts +8 -0
  51. package/dist/types/tools/render-utils.d.ts +2 -0
  52. package/dist/types/tools/review.d.ts +6 -4
  53. package/dist/types/tools/ssh.d.ts +1 -1
  54. package/dist/types/tools/todo.d.ts +6 -0
  55. package/dist/types/tools/yield.d.ts +8 -3
  56. package/dist/types/utils/thinking-display.d.ts +4 -0
  57. package/package.json +12 -12
  58. package/src/advisor/__tests__/advisor.test.ts +242 -10
  59. package/src/advisor/__tests__/config.test.ts +173 -0
  60. package/src/advisor/advise-tool.ts +11 -6
  61. package/src/advisor/config.ts +256 -0
  62. package/src/advisor/index.ts +1 -0
  63. package/src/advisor/runtime.ts +12 -2
  64. package/src/advisor/transcript-recorder.ts +25 -2
  65. package/src/advisor/watchdog.ts +57 -31
  66. package/src/autolearn/controller.ts +13 -22
  67. package/src/autoresearch/index.ts +7 -2
  68. package/src/cli/gc-cli.ts +17 -10
  69. package/src/cli/update-cli.ts +254 -0
  70. package/src/collab/guest.ts +43 -7
  71. package/src/collab/host.ts +13 -8
  72. package/src/collab/replication-shrink.ts +111 -0
  73. package/src/config/model-registry.ts +80 -18
  74. package/src/config/provider-globals.ts +25 -0
  75. package/src/config/settings-schema.ts +77 -0
  76. package/src/debug/index.ts +32 -7
  77. package/src/debug/log-viewer.ts +111 -53
  78. package/src/debug/raw-sse.ts +68 -48
  79. package/src/discovery/codex.ts +13 -5
  80. package/src/edit/hashline/diff.ts +57 -4
  81. package/src/eval/__tests__/julia-prelude.test.ts +2 -2
  82. package/src/eval/js/shared/local-module-loader.ts +23 -1
  83. package/src/export/html/template.js +13 -7
  84. package/src/extensibility/extensions/loader.ts +5 -3
  85. package/src/extensibility/extensions/wrapper.ts +9 -3
  86. package/src/extensibility/hooks/loader.ts +3 -3
  87. package/src/extensibility/hooks/tool-wrapper.ts +13 -4
  88. package/src/extensibility/plugins/manager.ts +2 -1
  89. package/src/extensibility/tool-event-input.ts +23 -0
  90. package/src/extensibility/utils.ts +74 -0
  91. package/src/internal-urls/docs-index.generated.txt +1 -1
  92. package/src/mcp/client.ts +3 -1
  93. package/src/mcp/manager.ts +12 -5
  94. package/src/mcp/transports/index.ts +1 -0
  95. package/src/mcp/transports/sse.ts +377 -0
  96. package/src/memories/index.ts +130 -15
  97. package/src/memory-backend/local-backend.ts +5 -3
  98. package/src/modes/components/advisor-config.ts +555 -0
  99. package/src/modes/components/advisor-message.ts +9 -2
  100. package/src/modes/components/agent-hub.ts +9 -4
  101. package/src/modes/components/index.ts +2 -0
  102. package/src/modes/components/model-selector.ts +79 -48
  103. package/src/modes/components/settings-selector.ts +1 -0
  104. package/src/modes/components/status-line/component.ts +150 -5
  105. package/src/modes/components/status-line/segments.ts +46 -21
  106. package/src/modes/components/status-line/types.ts +13 -1
  107. package/src/modes/components/tool-execution.ts +47 -6
  108. package/src/modes/controllers/command-controller.ts +27 -36
  109. package/src/modes/controllers/event-controller.ts +113 -1
  110. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  111. package/src/modes/controllers/input-controller.ts +61 -61
  112. package/src/modes/controllers/selector-controller.ts +100 -9
  113. package/src/modes/interactive-mode.ts +74 -25
  114. package/src/modes/skill-command.ts +116 -0
  115. package/src/modes/types.ts +9 -3
  116. package/src/modes/utils/ui-helpers.ts +41 -23
  117. package/src/prompts/agents/reviewer.md +11 -10
  118. package/src/prompts/goals/goal-todo-context.md +12 -0
  119. package/src/prompts/review-custom-request.md +1 -2
  120. package/src/prompts/review-request.md +1 -2
  121. package/src/prompts/system/interrupted-thinking.md +7 -0
  122. package/src/prompts/system/recap-user.md +9 -0
  123. package/src/prompts/system/subagent-system-prompt.md +8 -5
  124. package/src/prompts/system/subagent-yield-reminder.md +6 -5
  125. package/src/prompts/system/system-prompt.md +0 -1
  126. package/src/prompts/tools/irc.md +2 -2
  127. package/src/prompts/tools/read.md +2 -2
  128. package/src/sdk.ts +28 -24
  129. package/src/session/agent-session.ts +899 -429
  130. package/src/session/indexed-session-storage.ts +86 -13
  131. package/src/session/messages.test.ts +125 -0
  132. package/src/session/messages.ts +172 -9
  133. package/src/session/redis-session-storage.ts +49 -2
  134. package/src/session/session-entries.ts +39 -2
  135. package/src/session/session-history-format.ts +29 -2
  136. package/src/session/session-listing.ts +54 -24
  137. package/src/session/session-loader.ts +66 -3
  138. package/src/session/session-manager.ts +115 -22
  139. package/src/session/session-persistence.ts +95 -1
  140. package/src/session/session-storage.ts +36 -0
  141. package/src/session/session-title-slot.ts +141 -0
  142. package/src/session/sql-session-storage.ts +71 -11
  143. package/src/slash-commands/builtin-registry.ts +23 -24
  144. package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
  145. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
  146. package/src/ssh/connection-manager.ts +139 -12
  147. package/src/ssh/file-transfer.ts +23 -18
  148. package/src/ssh/ssh-executor.ts +2 -13
  149. package/src/ssh/utils.ts +19 -0
  150. package/src/task/executor.ts +21 -23
  151. package/src/task/render.ts +162 -20
  152. package/src/task/renderer.ts +14 -0
  153. package/src/task/types.ts +17 -0
  154. package/src/task/yield-assembly.ts +207 -0
  155. package/src/tiny/text.ts +23 -0
  156. package/src/tools/ask.ts +55 -4
  157. package/src/tools/render-utils.ts +2 -0
  158. package/src/tools/renderers.ts +8 -2
  159. package/src/tools/review.ts +17 -7
  160. package/src/tools/ssh.ts +8 -4
  161. package/src/tools/todo.ts +17 -1
  162. package/src/tools/yield.ts +140 -31
  163. package/src/utils/thinking-display.ts +15 -0
  164. package/src/prompts/system/autolearn-nudge.md +0 -5
@@ -600,6 +600,17 @@ export const SETTINGS_SCHEMA = {
600
600
  "Use the terminal's default background for the status line instead of the theme's `statusLineBg`. Powerline end caps are dropped because they need a contrasting fill to bridge into the surrounding terminal.",
601
601
  },
602
602
  },
603
+ "statusLine.compactThinkingLevel": {
604
+ type: "boolean",
605
+ default: false,
606
+ ui: {
607
+ tab: "appearance",
608
+ group: "Status Line",
609
+ label: "Compact Thinking Level",
610
+ description:
611
+ "Show the thinking level as a single icon on the model name instead of a separate ` · <level>` suffix.",
612
+ },
613
+ },
603
614
  "tools.artifactSpillThreshold": {
604
615
  type: "number",
605
616
  default: 50,
@@ -1633,6 +1644,35 @@ export const SETTINGS_SCHEMA = {
1633
1644
  },
1634
1645
  },
1635
1646
 
1647
+ "recap.enabled": {
1648
+ type: "boolean",
1649
+ default: true,
1650
+ ui: {
1651
+ tab: "interaction",
1652
+ group: "Notifications",
1653
+ label: "Idle Recap",
1654
+ description: "Generate a brief LLM recap of where things stand after the terminal has been idle",
1655
+ },
1656
+ },
1657
+
1658
+ "recap.idleSeconds": {
1659
+ type: "number",
1660
+ default: 240,
1661
+ ui: {
1662
+ tab: "interaction",
1663
+ group: "Notifications",
1664
+ label: "Idle Recap Delay",
1665
+ description: "Seconds to wait while idle before showing the recap",
1666
+ options: [
1667
+ { value: "60", label: "1 minute" },
1668
+ { value: "120", label: "2 minutes" },
1669
+ { value: "240", label: "4 minutes" },
1670
+ { value: "300", label: "5 minutes" },
1671
+ { value: "600", label: "10 minutes" },
1672
+ ],
1673
+ },
1674
+ },
1675
+
1636
1676
  // Collab
1637
1677
  "collab.relayUrl": {
1638
1678
  type: "string",
@@ -1889,6 +1929,17 @@ export const SETTINGS_SCHEMA = {
1889
1929
  },
1890
1930
  },
1891
1931
 
1932
+ "compaction.remoteStreamingV2Enabled": {
1933
+ type: "boolean",
1934
+ default: true,
1935
+ ui: {
1936
+ tab: "context",
1937
+ group: "Compaction",
1938
+ label: "Remote Compaction V2",
1939
+ description: "Use Responses streaming compaction for compatible remote compaction models",
1940
+ },
1941
+ },
1942
+
1892
1943
  "compaction.reserveTokens": { type: "number", default: 16384 },
1893
1944
 
1894
1945
  "compaction.keepRecentTokens": { type: "number", default: 20000 },
@@ -1897,6 +1948,8 @@ export const SETTINGS_SCHEMA = {
1897
1948
 
1898
1949
  "compaction.remoteEndpoint": { type: "string", default: undefined },
1899
1950
 
1951
+ "compaction.v2RetainedMessageBudget": { type: "number", default: 64000 },
1952
+
1900
1953
  // Idle compaction
1901
1954
  "compaction.idleEnabled": {
1902
1955
  type: "boolean",
@@ -3796,6 +3849,17 @@ export const SETTINGS_SCHEMA = {
3796
3849
  },
3797
3850
  },
3798
3851
 
3852
+ "title.refreshOnReplan": {
3853
+ type: "boolean",
3854
+ default: true,
3855
+ ui: {
3856
+ tab: "tasks",
3857
+ group: "Modes",
3858
+ label: "Refresh Title on Replan",
3859
+ description: "Refresh generated session titles after todo init replans unless the title was set by the user",
3860
+ },
3861
+ },
3862
+
3799
3863
  // Delegation
3800
3864
  "task.isolation.mode": {
3801
3865
  type: "enum",
@@ -4850,6 +4914,8 @@ export interface CompactionSettings {
4850
4914
  autoContinue: boolean;
4851
4915
  remoteEnabled: boolean;
4852
4916
  remoteEndpoint: string | undefined;
4917
+ remoteStreamingV2Enabled: boolean;
4918
+ v2RetainedMessageBudget: number;
4853
4919
  idleEnabled: boolean;
4854
4920
  idleThresholdTokens: number;
4855
4921
  idleTimeoutSeconds: number;
@@ -4857,6 +4923,15 @@ export interface CompactionSettings {
4857
4923
  dropUseless: boolean;
4858
4924
  }
4859
4925
 
4926
+ export interface RecapSettings {
4927
+ enabled: boolean;
4928
+ idleSeconds: number;
4929
+ }
4930
+
4931
+ export interface TitleSettings {
4932
+ refreshOnReplan: boolean;
4933
+ }
4934
+
4860
4935
  export interface ContextPromotionSettings {
4861
4936
  enabled: boolean;
4862
4937
  }
@@ -5002,6 +5077,8 @@ export interface GcSettings {
5002
5077
  /** Map group prefix -> typed settings interface */
5003
5078
  export interface GroupTypeMap {
5004
5079
  compaction: CompactionSettings;
5080
+ recap: RecapSettings;
5081
+ title: TitleSettings;
5005
5082
  contextPromotion: ContextPromotionSettings;
5006
5083
  retry: RetrySettings;
5007
5084
  memories: MemoriesSettings;
@@ -10,6 +10,7 @@ import {
10
10
  Container,
11
11
  isNotificationSuppressed,
12
12
  Loader,
13
+ type OverlayHandle,
13
14
  type SelectItem,
14
15
  SelectList,
15
16
  Spacer,
@@ -327,18 +328,29 @@ export class DebugSelectorComponent extends Container {
327
328
  return;
328
329
  }
329
330
 
331
+ let overlay: OverlayHandle | undefined;
332
+ const close = (): void => {
333
+ overlay?.hide();
334
+ overlay = undefined;
335
+ void this.ctx.showDebugSelector();
336
+ };
330
337
  const viewer = new DebugLogViewerComponent({
331
338
  logs,
332
339
  terminalRows: this.ctx.ui.terminal.rows,
333
- onExit: () => this.ctx.showDebugSelector(),
340
+ onExit: close,
334
341
  onStatus: message => this.ctx.showStatus(message, { dim: true }),
335
342
  onError: message => this.ctx.showError(message),
336
343
  onUpdate: () => this.ctx.ui.requestRender(),
337
344
  logSource,
338
345
  });
339
346
 
340
- this.ctx.editorContainer.clear();
341
- this.ctx.editorContainer.addChild(viewer);
347
+ overlay = this.ctx.ui.showOverlay(viewer, {
348
+ anchor: "top-left",
349
+ width: "100%",
350
+ maxHeight: "100%",
351
+ margin: 0,
352
+ fullscreen: true,
353
+ });
342
354
  this.ctx.ui.setFocus(viewer);
343
355
  } catch (err) {
344
356
  this.ctx.showError(`Failed to read logs: ${err instanceof Error ? err.message : String(err)}`);
@@ -348,16 +360,29 @@ export class DebugSelectorComponent extends Container {
348
360
  }
349
361
 
350
362
  async #handleViewRawSse(): Promise<void> {
351
- const viewer = new RawSseViewerComponent({
363
+ let overlay: OverlayHandle | undefined;
364
+ let viewer: RawSseViewerComponent | undefined;
365
+ const close = (): void => {
366
+ viewer?.dispose();
367
+ overlay?.hide();
368
+ overlay = undefined;
369
+ void this.ctx.showDebugSelector();
370
+ };
371
+ viewer = new RawSseViewerComponent({
352
372
  buffer: resolveRawSseDebugBuffer(this.ctx.session),
353
373
  terminalRows: this.ctx.ui.terminal.rows,
354
- onExit: () => this.ctx.showDebugSelector(),
374
+ onExit: close,
355
375
  onStatus: message => this.ctx.showStatus(message, { dim: true }),
356
376
  onUpdate: () => this.ctx.ui.requestRender(),
357
377
  });
358
378
 
359
- this.ctx.editorContainer.clear();
360
- this.ctx.editorContainer.addChild(viewer);
379
+ overlay = this.ctx.ui.showOverlay(viewer, {
380
+ anchor: "top-left",
381
+ width: "100%",
382
+ maxHeight: "100%",
383
+ margin: 0,
384
+ fullscreen: true,
385
+ });
361
386
  this.ctx.ui.setFocus(viewer);
362
387
  this.ctx.ui.requestRender();
363
388
  }
@@ -3,11 +3,13 @@ import {
3
3
  extractPrintableText,
4
4
  matchesKey,
5
5
  padding,
6
+ parseSgrMouse,
6
7
  replaceTabs,
7
8
  truncateToWidth,
8
9
  visibleWidth,
9
10
  } from "@oh-my-pi/pi-tui";
10
11
  import { sanitizeText } from "@oh-my-pi/pi-utils";
12
+ import { bottomBorder, divider, row, topBorder } from "../modes/components/overlay-box";
11
13
  import { theme } from "../modes/theme/theme";
12
14
  import { copyToClipboard } from "../utils/clipboard";
13
15
  import {
@@ -23,6 +25,8 @@ export const LOAD_OLDER_LABEL = "### MOVE UP TO LOAD MORE...";
23
25
 
24
26
  const INITIAL_LOG_CHUNK = 50;
25
27
  const LOAD_OLDER_CHUNK = 50;
28
+ const MIN_LOG_VIEWER_WIDTH = 48;
29
+ const LOG_VIEWER_CHROME_LINES = 8;
26
30
 
27
31
  type LogEntry = {
28
32
  rawLine: string;
@@ -184,6 +188,31 @@ export class DebugLogViewerModel {
184
188
  }
185
189
  }
186
190
 
191
+ moveCursorToRow(rowIndex: number, extendSelection: boolean): boolean {
192
+ const selectableIndex = this.#selectableRowIndices.indexOf(rowIndex);
193
+ if (selectableIndex < 0) {
194
+ return false;
195
+ }
196
+
197
+ if (extendSelection && this.#selectionAnchorSelectableIndex === undefined) {
198
+ const row = this.#getCursorRow();
199
+ if (row?.kind === "log") {
200
+ this.#selectionAnchorSelectableIndex = this.#cursorSelectableIndex;
201
+ }
202
+ }
203
+
204
+ this.#cursorSelectableIndex = selectableIndex;
205
+
206
+ if (!extendSelection) {
207
+ this.#selectionAnchorSelectableIndex = undefined;
208
+ }
209
+
210
+ if (this.#getCursorRow()?.kind !== "log" && !extendSelection) {
211
+ this.#selectionAnchorSelectableIndex = undefined;
212
+ }
213
+ return true;
214
+ }
215
+
187
216
  getSelectedLogIndices(): number[] {
188
217
  if (this.#selectableRowIndices.length === 0) {
189
218
  return [];
@@ -484,6 +513,9 @@ export class DebugLogViewerComponent implements Component {
484
513
  #scrollRowOffset = 0;
485
514
  #statusMessage: string | undefined;
486
515
  #loadingOlder = false;
516
+ #bodyRowStart = 0;
517
+ #bodyRowCount = 0;
518
+ #bodyLineToRowIndex: Array<number | undefined> = [];
487
519
 
488
520
  constructor(options: DebugLogViewerComponentOptions) {
489
521
  this.#logSource = options.logSource;
@@ -501,6 +533,10 @@ export class DebugLogViewerComponent implements Component {
501
533
  }
502
534
 
503
535
  handleInput(keyData: string): void {
536
+ if (keyData.startsWith("\x1b[<") && this.#handleMouse(keyData)) {
537
+ return;
538
+ }
539
+
504
540
  if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
505
541
  this.#onExit();
506
542
  return;
@@ -598,59 +634,99 @@ export class DebugLogViewerComponent implements Component {
598
634
  }
599
635
  }
600
636
 
637
+ #handleMouse(keyData: string): boolean {
638
+ const event = parseSgrMouse(keyData);
639
+ if (!event) return false;
640
+
641
+ const overBody = event.row >= this.#bodyRowStart && event.row < this.#bodyRowStart + this.#bodyRowCount;
642
+ if (event.wheel !== null && overBody) {
643
+ this.#statusMessage = undefined;
644
+ const maxOffset = Math.max(0, this.#model.rows.length - this.#bodyHeight());
645
+ this.#scrollRowOffset = Math.max(0, Math.min(maxOffset, this.#scrollRowOffset + event.wheel * 3));
646
+ this.#onUpdate?.();
647
+ return true;
648
+ }
649
+
650
+ if (!event.leftClick || !overBody) return false;
651
+ const rowIndex = this.#bodyLineToRowIndex[event.row - this.#bodyRowStart];
652
+ if (rowIndex === undefined) return false;
653
+
654
+ const target = this.#model.rows[rowIndex];
655
+ if (!target || target.kind === "warning") return false;
656
+ this.#statusMessage = undefined;
657
+ this.#model.moveCursorToRow(rowIndex, false);
658
+ if (target.kind === "load-older") {
659
+ void this.#handleLoadOlder();
660
+ return true;
661
+ }
662
+
663
+ if (this.#model.isExpanded(target.logIndex)) {
664
+ this.#model.collapseSelected();
665
+ } else {
666
+ this.#model.expandSelected();
667
+ }
668
+ this.#ensureCursorVisible();
669
+ this.#onUpdate?.();
670
+ return true;
671
+ }
672
+
601
673
  invalidate(): void {
602
674
  // no cached child state
603
675
  }
604
676
 
605
677
  render(width: number): readonly string[] {
606
- this.#lastRenderWidth = Math.max(20, width);
678
+ this.#lastRenderWidth = Math.max(MIN_LOG_VIEWER_WIDTH, width);
607
679
  this.#ensureCursorVisible();
608
680
 
609
- const innerWidth = Math.max(1, this.#lastRenderWidth - 2);
681
+ const contentWidth = Math.max(1, this.#lastRenderWidth - 4);
610
682
  const bodyHeight = this.#bodyHeight();
611
683
 
612
- const rows = this.#renderRows(innerWidth);
613
- const visibleBodyLines = this.#renderVisibleBodyLines(rows, innerWidth, bodyHeight);
684
+ const rows = this.#renderRows(contentWidth);
685
+ this.#bodyRowStart = 4;
686
+ this.#bodyRowCount = bodyHeight;
687
+ const visibleBodyLines = this.#renderVisibleBodyLines(rows, bodyHeight);
614
688
 
615
689
  return [
616
- this.#frameTop(innerWidth),
617
- this.#frameLine(this.#summaryText(), innerWidth),
618
- this.#frameSeparator(innerWidth),
619
- this.#frameLine(this.#filterText(), innerWidth),
620
- this.#frameSeparator(innerWidth),
690
+ topBorder(this.#lastRenderWidth, "Recent Logs"),
691
+ row(this.#summaryText(), this.#lastRenderWidth),
692
+ row(this.#filterText(), this.#lastRenderWidth),
693
+ divider(this.#lastRenderWidth),
621
694
  ...visibleBodyLines,
622
- this.#frameLine(this.#statusText(), innerWidth),
623
- this.#frameBottom(innerWidth),
695
+ divider(this.#lastRenderWidth),
696
+ row(this.#statusText(), this.#lastRenderWidth),
697
+ row(theme.fg("dim", this.#controlsText()), this.#lastRenderWidth),
698
+ bottomBorder(this.#lastRenderWidth),
624
699
  ];
625
700
  }
626
701
 
627
702
  #summaryText(): string {
628
- return ` # ${this.#model.visibleLogCount}/${this.#model.logCount} logs | ${this.#controlsText()}`;
703
+ const selected = this.#model.getSelectedCount();
704
+ const expanded = this.#model.expandedCount;
705
+ return `${theme.fg("muted", "showing")} ${theme.fg("accent", `${this.#model.visibleLogCount}/${this.#model.logCount}`)} ${theme.fg("muted", "selected")} ${theme.fg(selected > 0 ? "accent" : "muted", String(selected))} ${theme.fg("muted", "expanded")} ${theme.fg(expanded > 0 ? "accent" : "muted", String(expanded))}`;
629
706
  }
630
707
 
631
708
  #controlsText(): string {
632
- return "Esc: back Ctrl+C: copy Up/Down: move Shift+Up/Down: select range Left/Right: collapse/expand Ctrl+A: select all Ctrl+O: load older Ctrl+P: pid filter";
709
+ return "Esc close · Ctrl+C copy · ↑/↓/wheel move · click toggle · Shift+↑/↓ select · ←/→ collapse/expand · Ctrl+A all · Ctrl+O older · Ctrl+P pid";
633
710
  }
634
711
 
635
712
  #filterText(): string {
636
713
  const sanitized = replaceTabs(sanitizeText(this.#model.filterQuery));
637
- const query = sanitized.length === 0 ? "" : theme.fg("accent", sanitized);
714
+ const query = sanitized.length === 0 ? theme.fg("muted", "type to filter") : theme.fg("accent", sanitized);
638
715
  const pidStatus = this.#model.isProcessFilterEnabled()
639
- ? theme.fg("success", "pid:on")
640
- : theme.fg("muted", "pid:off");
641
- return ` filter: ${query} ${pidStatus}`;
716
+ ? theme.fg("success", "pid on")
717
+ : theme.fg("muted", "pid off");
718
+ const loading = this.#loadingOlder ? ` ${theme.fg("warning", "loading older…")}` : "";
719
+ return `${theme.fg("muted", "filter")} ${query} ${pidStatus}${loading}`;
642
720
  }
643
721
 
644
722
  #statusText(): string {
645
- const base = ` Selected: ${this.#model.getSelectedCount()} Expanded: ${this.#model.expandedCount}`;
646
- if (this.#statusMessage) {
647
- return `${base} ${this.#statusMessage}`;
648
- }
649
- return base;
723
+ return this.#statusMessage
724
+ ? theme.fg("success", this.#statusMessage)
725
+ : theme.fg("dim", "Enter loads older when highlighted; printable keys update filter");
650
726
  }
651
727
 
652
728
  #bodyHeight(): number {
653
- return Math.max(3, this.#terminalRows - 8);
729
+ return Math.max(3, (process.stdout.rows || this.#terminalRows || 24) - LOG_VIEWER_CHROME_LINES);
654
730
  }
655
731
 
656
732
  async #handleLoadOlder(additionalCount: number = LOAD_OLDER_CHUNK): Promise<void> {
@@ -780,26 +856,25 @@ export class DebugLogViewerComponent implements Component {
780
856
  return rendered;
781
857
  }
782
858
 
783
- #renderVisibleBodyLines(
784
- rows: Array<{ lines: string[]; rowIndex: number }>,
785
- innerWidth: number,
786
- bodyHeight: number,
787
- ): string[] {
859
+ #renderVisibleBodyLines(rows: Array<{ lines: string[]; rowIndex: number }>, bodyHeight: number): string[] {
860
+ this.#bodyLineToRowIndex = [];
788
861
  const lines: string[] = [];
789
862
  if (rows.length === 0) {
790
- lines.push(this.#frameLine(theme.fg("muted", "no matches"), innerWidth));
863
+ this.#bodyLineToRowIndex.push(undefined);
864
+ lines.push(row(theme.fg("muted", "no matches"), this.#lastRenderWidth));
791
865
  }
792
866
  for (let i = this.#scrollRowOffset; i < rows.length; i++) {
793
- const row = rows[i];
794
- if (!row) {
867
+ const renderedRow = rows[i];
868
+ if (!renderedRow) {
795
869
  continue;
796
870
  }
797
871
 
798
- for (const line of row.lines) {
872
+ for (const line of renderedRow.lines) {
799
873
  if (lines.length >= bodyHeight) {
800
874
  break;
801
875
  }
802
- lines.push(this.#frameLine(line, innerWidth));
876
+ this.#bodyLineToRowIndex.push(renderedRow.rowIndex);
877
+ lines.push(row(line, this.#lastRenderWidth));
803
878
  }
804
879
 
805
880
  if (lines.length >= bodyHeight) {
@@ -808,7 +883,8 @@ export class DebugLogViewerComponent implements Component {
808
883
  }
809
884
 
810
885
  while (lines.length < bodyHeight) {
811
- lines.push(this.#frameLine("", innerWidth));
886
+ this.#bodyLineToRowIndex.push(undefined);
887
+ lines.push(row("", this.#lastRenderWidth));
812
888
  }
813
889
 
814
890
  return lines;
@@ -840,7 +916,7 @@ export class DebugLogViewerComponent implements Component {
840
916
  return;
841
917
  }
842
918
  const bodyHeight = Math.max(1, this.#bodyHeight());
843
- const innerWidth = Math.max(1, this.#lastRenderWidth - 2);
919
+ const innerWidth = Math.max(1, this.#lastRenderWidth - 4);
844
920
 
845
921
  // Scroll up: cursor is above viewport
846
922
  if (cursorRowIndex < this.#scrollRowOffset) {
@@ -865,24 +941,6 @@ export class DebugLogViewerComponent implements Component {
865
941
  }
866
942
  }
867
943
 
868
- #frameTop(innerWidth: number): string {
869
- return `${theme.boxRound.topLeft}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.topRight}`;
870
- }
871
-
872
- #frameSeparator(innerWidth: number): string {
873
- return `${theme.boxRound.teeRight}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.teeLeft}`;
874
- }
875
-
876
- #frameBottom(innerWidth: number): string {
877
- return `${theme.boxRound.bottomLeft}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.bottomRight}`;
878
- }
879
-
880
- #frameLine(content: string, innerWidth: number): string {
881
- const truncated = truncateToWidth(content, innerWidth);
882
- const remaining = Math.max(0, innerWidth - visibleWidth(truncated));
883
- return `${theme.boxRound.vertical}${truncated}${padding(remaining)}${theme.boxRound.vertical}`;
884
- }
885
-
886
944
  #copySelected() {
887
945
  const selectedPayload = buildLogCopyPayload(this.#model.getSelectedRawLines());
888
946
  const selected = selectedPayload.length === 0 ? [] : selectedPayload.split("\n");
@@ -1,13 +1,6 @@
1
- import {
2
- type Component,
3
- matchesKey,
4
- padding,
5
- replaceTabs,
6
- ScrollView,
7
- truncateToWidth,
8
- visibleWidth,
9
- } from "@oh-my-pi/pi-tui";
1
+ import { type Component, matchesKey, parseSgrMouse, replaceTabs, ScrollView, truncateToWidth } from "@oh-my-pi/pi-tui";
10
2
  import { sanitizeText } from "@oh-my-pi/pi-utils";
3
+ import { bottomBorder, divider, row, topBorder } from "../modes/components/overlay-box";
11
4
  import { theme } from "../modes/theme/theme";
12
5
  import { copyToClipboard } from "../utils/clipboard";
13
6
  import {
@@ -17,8 +10,8 @@ import {
17
10
  rawSseRecordLines,
18
11
  } from "./raw-sse-buffer";
19
12
 
20
- const MIN_VIEWER_WIDTH = 20;
21
- const VIEWER_FRAME_LINES = 5;
13
+ const MIN_VIEWER_WIDTH = 40;
14
+ const VIEWER_CHROME_LINES = 6;
22
15
  // `data:` lines below this width render fine on a single row; anything wider gets pretty-printed
23
16
  // across multiple `data:` lines so streamed JSON blobs stop getting clipped by `truncateToWidth`.
24
17
  const PRETTY_PRINT_DATA_THRESHOLD = 100;
@@ -79,6 +72,8 @@ export class RawSseViewerComponent implements Component {
79
72
  #followTail = true;
80
73
  #lastRenderWidth = MIN_VIEWER_WIDTH;
81
74
  #statusMessage: string | undefined;
75
+ #bodyRowStart = 0;
76
+ #bodyRowCount = 0;
82
77
  // Pretty-printed wire lines keyed by `record.sequence`. Pretty-printing is
83
78
  // the JSON.parse + JSON.stringify per `data:` line, so we cache the result —
84
79
  // the render path runs on every keypress and from `#maxScrollOffset()`.
@@ -98,9 +93,17 @@ export class RawSseViewerComponent implements Component {
98
93
  });
99
94
  }
100
95
 
96
+ dispose(): void {
97
+ this.#unsubscribe();
98
+ }
99
+
101
100
  handleInput(keyData: string): void {
101
+ if (keyData.startsWith("\x1b[<") && this.#handleMouse(keyData)) {
102
+ return;
103
+ }
104
+
102
105
  if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc")) {
103
- this.#unsubscribe();
106
+ this.dispose();
104
107
  this.#onExit();
105
108
  return;
106
109
  }
@@ -145,15 +148,44 @@ export class RawSseViewerComponent implements Component {
145
148
  }
146
149
  }
147
150
 
151
+ #handleMouse(keyData: string): boolean {
152
+ const event = parseSgrMouse(keyData);
153
+ if (!event) return false;
154
+
155
+ const overBody = event.row >= this.#bodyRowStart && event.row < this.#bodyRowStart + this.#bodyRowCount;
156
+ if (event.wheel !== null && overBody) {
157
+ this.#followTail = false;
158
+ this.#scrollOffset = Math.max(0, Math.min(this.#maxScrollOffset(), this.#scrollOffset + event.wheel * 3));
159
+ this.#onUpdate?.();
160
+ return true;
161
+ }
162
+
163
+ if (!event.leftClick) return false;
164
+ if (event.row === 1) {
165
+ this.#followTail = !this.#followTail;
166
+ this.#followIfNeeded();
167
+ this.#onUpdate?.();
168
+ return true;
169
+ }
170
+ if (overBody) {
171
+ this.#followTail = false;
172
+ const clickedOffset = this.#scrollOffset + event.row - this.#bodyRowStart;
173
+ this.#scrollOffset = Math.max(0, Math.min(this.#maxScrollOffset(), clickedOffset));
174
+ this.#onUpdate?.();
175
+ return true;
176
+ }
177
+ return false;
178
+ }
179
+
148
180
  invalidate(): void {}
149
181
 
150
182
  render(width: number): readonly string[] {
151
183
  this.#lastRenderWidth = Math.max(MIN_VIEWER_WIDTH, width);
152
184
  this.#followIfNeeded();
153
185
 
154
- const innerWidth = Math.max(1, this.#lastRenderWidth - 2);
186
+ const contentWidth = Math.max(1, this.#lastRenderWidth - 4);
155
187
  const bodyHeight = this.#bodyHeight();
156
- const rawLines = this.#renderRawLines(innerWidth);
188
+ const rawLines = this.#renderRawLines(contentWidth);
157
189
  const sv = new ScrollView(rawLines.slice(this.#scrollOffset, this.#scrollOffset + bodyHeight), {
158
190
  height: bodyHeight,
159
191
  scrollbar: "auto",
@@ -161,15 +193,18 @@ export class RawSseViewerComponent implements Component {
161
193
  theme: { track: t => theme.fg("muted", t), thumb: t => theme.fg("accent", t) },
162
194
  });
163
195
  sv.setScrollOffset(this.#scrollOffset);
164
- const bodyRows = sv.render(innerWidth);
196
+ const bodyRows = sv.render(contentWidth);
197
+ this.#bodyRowStart = 3;
198
+ this.#bodyRowCount = bodyHeight;
165
199
 
166
200
  return [
167
- this.#frameTop(innerWidth),
168
- this.#frameLine(this.#summaryText(), innerWidth),
169
- this.#frameSeparator(innerWidth),
170
- ...bodyRows.map(line => this.#frameLine(line, innerWidth)),
171
- this.#frameLine(this.#statusText(), innerWidth),
172
- this.#frameBottom(innerWidth),
201
+ topBorder(this.#lastRenderWidth, "Raw Provider Stream"),
202
+ row(this.#summaryText(), this.#lastRenderWidth),
203
+ divider(this.#lastRenderWidth),
204
+ ...bodyRows.map(line => row(line, this.#lastRenderWidth)),
205
+ divider(this.#lastRenderWidth),
206
+ row(this.#statusText(), this.#lastRenderWidth),
207
+ bottomBorder(this.#lastRenderWidth),
173
208
  ];
174
209
  }
175
210
 
@@ -221,22 +256,25 @@ export class RawSseViewerComponent implements Component {
221
256
  if (key < firstSequence) this.#prettyLinesCache.delete(key);
222
257
  }
223
258
  }
224
-
225
259
  #summaryText(): string {
226
260
  const snapshot = this.#buffer.snapshot();
227
- const last = snapshot.lastUpdatedAt ? ` last=${formatRawSseIsoTime(snapshot.lastUpdatedAt)}` : "";
228
- const follow = this.#followTail ? "follow:on" : "follow:off";
229
- return ` # raw provider stream (SSE + WS) | events=${snapshot.totalEvents} records=${snapshot.records.length}${last} | ${follow} | Esc back Ctrl+C copy End follow`;
261
+ const last = snapshot.lastUpdatedAt
262
+ ? `${theme.fg("muted", "last")} ${theme.fg("accent", formatRawSseIsoTime(snapshot.lastUpdatedAt))}`
263
+ : theme.fg("muted", "waiting for first frame");
264
+ const follow = this.#followTail ? theme.fg("success", "follow on") : theme.fg("warning", "follow off");
265
+ return `${theme.fg("muted", "events")} ${theme.fg("accent", String(snapshot.totalEvents))} ${theme.fg("muted", "records")} ${theme.fg("accent", String(snapshot.records.length))} ${last} ${follow}`;
230
266
  }
231
267
 
232
268
  #statusText(): string {
233
- return this.#statusMessage ?? " Up/Down scroll PgUp/PgDn page";
269
+ const help = "Esc close · Ctrl+C copy raw · End follow tail · wheel scroll · click summary toggles follow";
270
+ return this.#statusMessage
271
+ ? `${theme.fg("success", this.#statusMessage)} ${theme.fg("dim", help)}`
272
+ : theme.fg("dim", help);
234
273
  }
235
274
 
236
275
  #bodyHeight(): number {
237
- return Math.max(3, this.#terminalRows - VIEWER_FRAME_LINES);
276
+ return Math.max(3, (process.stdout.rows || this.#terminalRows || 24) - VIEWER_CHROME_LINES);
238
277
  }
239
-
240
278
  #followIfNeeded(): void {
241
279
  if (this.#followTail) this.#scrollToTail();
242
280
  }
@@ -246,8 +284,8 @@ export class RawSseViewerComponent implements Component {
246
284
  }
247
285
 
248
286
  #maxScrollOffset(): number {
249
- const innerWidth = Math.max(1, this.#lastRenderWidth - 2);
250
- return Math.max(0, this.#renderRawLines(innerWidth).length - this.#bodyHeight());
287
+ const contentWidth = Math.max(1, this.#lastRenderWidth - 4);
288
+ return Math.max(0, this.#renderRawLines(contentWidth).length - this.#bodyHeight());
251
289
  }
252
290
 
253
291
  #copyAll(): void {
@@ -271,22 +309,4 @@ export class RawSseViewerComponent implements Component {
271
309
  }
272
310
  this.#onUpdate?.();
273
311
  }
274
-
275
- #frameTop(innerWidth: number): string {
276
- return `${theme.boxRound.topLeft}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.topRight}`;
277
- }
278
-
279
- #frameSeparator(innerWidth: number): string {
280
- return `${theme.boxRound.teeRight}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.teeLeft}`;
281
- }
282
-
283
- #frameBottom(innerWidth: number): string {
284
- return `${theme.boxRound.bottomLeft}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.bottomRight}`;
285
- }
286
-
287
- #frameLine(content: string, innerWidth: number): string {
288
- const truncated = truncateToWidth(content, innerWidth);
289
- const remaining = Math.max(0, innerWidth - visibleWidth(truncated));
290
- return `${theme.boxRound.vertical}${truncated}${padding(remaining)}${theme.boxRound.vertical}`;
291
- }
292
312
  }