@oh-my-pi/pi-coding-agent 16.2.1 → 16.2.3

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 (187) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/dist/cli.js +3621 -3579
  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/runtime.d.ts +15 -1
  8. package/dist/types/advisor/transcript-recorder.d.ts +13 -2
  9. package/dist/types/advisor/watchdog.d.ts +20 -0
  10. package/dist/types/collab/guest.d.ts +29 -0
  11. package/dist/types/config/model-roles.d.ts +1 -1
  12. package/dist/types/config/settings-schema.d.ts +113 -12
  13. package/dist/types/debug/log-viewer.d.ts +1 -0
  14. package/dist/types/debug/raw-sse.d.ts +1 -0
  15. package/dist/types/discovery/omp-extension-roots.d.ts +3 -3
  16. package/dist/types/edit/hashline/diff.d.ts +0 -11
  17. package/dist/types/edit/index.d.ts +18 -0
  18. package/dist/types/edit/streaming.d.ts +30 -0
  19. package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
  20. package/dist/types/extensibility/shared-events.d.ts +1 -0
  21. package/dist/types/extensibility/tool-event-input.d.ts +7 -0
  22. package/dist/types/extensibility/utils.d.ts +12 -0
  23. package/dist/types/mcp/oauth-discovery.d.ts +0 -11
  24. package/dist/types/mcp/transports/index.d.ts +1 -0
  25. package/dist/types/mcp/transports/sse.d.ts +20 -0
  26. package/dist/types/modes/components/advisor-config.d.ts +59 -0
  27. package/dist/types/modes/components/index.d.ts +1 -0
  28. package/dist/types/modes/components/model-selector.d.ts +9 -1
  29. package/dist/types/modes/components/settings-selector.d.ts +1 -0
  30. package/dist/types/modes/components/status-line/component.d.ts +30 -3
  31. package/dist/types/modes/components/status-line/types.d.ts +13 -1
  32. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  33. package/dist/types/modes/interactive-mode.d.ts +10 -4
  34. package/dist/types/modes/skill-command.d.ts +32 -0
  35. package/dist/types/modes/types.d.ts +7 -2
  36. package/dist/types/sdk.d.ts +1 -1
  37. package/dist/types/session/agent-session.d.ts +84 -12
  38. package/dist/types/session/indexed-session-storage.d.ts +7 -1
  39. package/dist/types/session/messages.d.ts +32 -7
  40. package/dist/types/session/messages.test.d.ts +1 -0
  41. package/dist/types/session/session-entries.d.ts +31 -3
  42. package/dist/types/session/session-history-format.d.ts +6 -0
  43. package/dist/types/session/session-loader.d.ts +9 -1
  44. package/dist/types/session/session-manager.d.ts +6 -4
  45. package/dist/types/session/session-storage.d.ts +11 -0
  46. package/dist/types/session/session-title-slot.d.ts +19 -0
  47. package/dist/types/session/settings-stream-fn.d.ts +21 -0
  48. package/dist/types/session/turn-persistence.d.ts +88 -0
  49. package/dist/types/ssh/connection-manager.d.ts +47 -0
  50. package/dist/types/ssh/utils.d.ts +16 -0
  51. package/dist/types/task/executor.d.ts +3 -16
  52. package/dist/types/task/render.d.ts +0 -5
  53. package/dist/types/task/renderer.d.ts +13 -0
  54. package/dist/types/task/types.d.ts +16 -0
  55. package/dist/types/task/yield-assembly.d.ts +28 -0
  56. package/dist/types/tiny/text.d.ts +8 -0
  57. package/dist/types/tools/render-utils.d.ts +2 -0
  58. package/dist/types/tools/review.d.ts +6 -4
  59. package/dist/types/tools/ssh.d.ts +1 -1
  60. package/dist/types/tools/todo.d.ts +6 -0
  61. package/dist/types/tools/yield.d.ts +8 -3
  62. package/dist/types/utils/thinking-display.d.ts +4 -0
  63. package/package.json +12 -12
  64. package/src/advisor/__tests__/advisor.test.ts +438 -10
  65. package/src/advisor/__tests__/config.test.ts +173 -0
  66. package/src/advisor/advise-tool.ts +11 -6
  67. package/src/advisor/config.ts +256 -0
  68. package/src/advisor/index.ts +1 -0
  69. package/src/advisor/runtime.ts +77 -4
  70. package/src/advisor/transcript-recorder.ts +25 -2
  71. package/src/advisor/watchdog.ts +57 -31
  72. package/src/auto-thinking/classifier.ts +2 -2
  73. package/src/autoresearch/index.ts +7 -2
  74. package/src/cli/gc-cli.ts +17 -10
  75. package/src/collab/guest.ts +43 -7
  76. package/src/config/model-registry.ts +80 -18
  77. package/src/config/model-resolver.ts +5 -1
  78. package/src/config/model-roles.ts +3 -3
  79. package/src/config/settings-schema.ts +107 -8
  80. package/src/debug/index.ts +32 -7
  81. package/src/debug/log-viewer.ts +111 -53
  82. package/src/debug/raw-sse.ts +68 -48
  83. package/src/discovery/codex.ts +13 -5
  84. package/src/discovery/omp-extension-roots.ts +38 -13
  85. package/src/edit/hashline/diff.ts +57 -4
  86. package/src/edit/index.ts +21 -0
  87. package/src/edit/streaming.ts +170 -0
  88. package/src/eval/js/shared/local-module-loader.ts +23 -1
  89. package/src/export/html/template.js +13 -7
  90. package/src/extensibility/custom-tools/types.ts +1 -0
  91. package/src/extensibility/extensions/loader.ts +5 -3
  92. package/src/extensibility/extensions/wrapper.ts +9 -3
  93. package/src/extensibility/hooks/loader.ts +3 -3
  94. package/src/extensibility/hooks/tool-wrapper.ts +13 -4
  95. package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +18 -1
  96. package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +59 -2
  97. package/src/extensibility/plugins/manager.ts +76 -5
  98. package/src/extensibility/shared-events.ts +1 -0
  99. package/src/extensibility/tool-event-input.ts +23 -0
  100. package/src/extensibility/utils.ts +74 -0
  101. package/src/internal-urls/docs-index.generated.txt +1 -1
  102. package/src/mcp/client.ts +3 -1
  103. package/src/mcp/manager.ts +12 -5
  104. package/src/mcp/oauth-discovery.ts +5 -29
  105. package/src/mcp/transports/http.ts +3 -1
  106. package/src/mcp/transports/index.ts +1 -0
  107. package/src/mcp/transports/sse.ts +377 -0
  108. package/src/memories/index.ts +15 -6
  109. package/src/mnemopi/backend.ts +2 -2
  110. package/src/modes/acp/acp-agent.ts +1 -1
  111. package/src/modes/components/advisor-config.ts +555 -0
  112. package/src/modes/components/advisor-message.ts +9 -2
  113. package/src/modes/components/agent-hub.ts +9 -4
  114. package/src/modes/components/assistant-message.ts +5 -5
  115. package/src/modes/components/index.ts +2 -0
  116. package/src/modes/components/model-selector.ts +79 -48
  117. package/src/modes/components/settings-selector.ts +1 -0
  118. package/src/modes/components/status-line/component.ts +145 -14
  119. package/src/modes/components/status-line/segments.ts +47 -22
  120. package/src/modes/components/status-line/types.ts +13 -1
  121. package/src/modes/components/tool-execution.ts +47 -6
  122. package/src/modes/controllers/command-controller.ts +23 -2
  123. package/src/modes/controllers/event-controller.ts +114 -11
  124. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  125. package/src/modes/controllers/input-controller.ts +61 -61
  126. package/src/modes/controllers/selector-controller.ts +100 -9
  127. package/src/modes/interactive-mode.ts +65 -10
  128. package/src/modes/print-mode.ts +1 -1
  129. package/src/modes/skill-command.ts +116 -0
  130. package/src/modes/types.ts +7 -2
  131. package/src/modes/utils/transcript-render-helpers.ts +2 -2
  132. package/src/modes/utils/ui-helpers.ts +46 -27
  133. package/src/prompts/agents/reviewer.md +11 -10
  134. package/src/prompts/review-custom-request.md +1 -2
  135. package/src/prompts/review-request.md +1 -2
  136. package/src/prompts/system/interrupted-thinking.md +7 -0
  137. package/src/prompts/system/recap-user.md +9 -0
  138. package/src/prompts/system/subagent-system-prompt.md +8 -5
  139. package/src/prompts/system/subagent-yield-reminder.md +6 -5
  140. package/src/prompts/system/system-prompt.md +0 -1
  141. package/src/prompts/tools/irc.md +2 -2
  142. package/src/prompts/tools/read.md +2 -2
  143. package/src/sdk.ts +40 -50
  144. package/src/session/agent-session.ts +1139 -600
  145. package/src/session/indexed-session-storage.ts +86 -13
  146. package/src/session/messages.test.ts +125 -0
  147. package/src/session/messages.ts +192 -21
  148. package/src/session/redis-session-storage.ts +49 -2
  149. package/src/session/session-entries.ts +39 -2
  150. package/src/session/session-history-format.ts +29 -2
  151. package/src/session/session-listing.ts +54 -24
  152. package/src/session/session-loader.ts +66 -3
  153. package/src/session/session-manager.ts +113 -19
  154. package/src/session/session-persistence.ts +96 -3
  155. package/src/session/session-storage.ts +36 -0
  156. package/src/session/session-title-slot.ts +141 -0
  157. package/src/session/settings-stream-fn.ts +49 -0
  158. package/src/session/sql-session-storage.ts +71 -11
  159. package/src/session/turn-persistence.ts +142 -0
  160. package/src/session/unexpected-stop-classifier.ts +2 -2
  161. package/src/slash-commands/builtin-registry.ts +16 -3
  162. package/src/slash-commands/helpers/mcp.ts +2 -1
  163. package/src/ssh/__tests__/connection-manager-args.test.ts +123 -1
  164. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +55 -18
  165. package/src/ssh/connection-manager.ts +139 -12
  166. package/src/ssh/file-transfer.ts +23 -18
  167. package/src/ssh/ssh-executor.ts +2 -13
  168. package/src/ssh/utils.ts +19 -0
  169. package/src/task/executor.ts +21 -23
  170. package/src/task/render.ts +162 -20
  171. package/src/task/renderer.ts +14 -0
  172. package/src/task/types.ts +17 -0
  173. package/src/task/yield-assembly.ts +207 -0
  174. package/src/tiny/models.ts +8 -6
  175. package/src/tiny/text.ts +37 -7
  176. package/src/tools/ask.ts +55 -4
  177. package/src/tools/image-gen.ts +2 -1
  178. package/src/tools/render-utils.ts +2 -0
  179. package/src/tools/renderers.ts +8 -2
  180. package/src/tools/review.ts +17 -7
  181. package/src/tools/ssh.ts +8 -4
  182. package/src/tools/todo.ts +17 -1
  183. package/src/tools/tts.ts +2 -1
  184. package/src/tools/yield.ts +140 -31
  185. package/src/utils/thinking-display.ts +15 -0
  186. package/src/utils/title-generator.ts +1 -1
  187. package/src/web/search/providers/tavily.ts +36 -19
@@ -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,
@@ -1183,6 +1194,23 @@ export const SETTINGS_SCHEMA = {
1183
1194
  },
1184
1195
  },
1185
1196
 
1197
+ textVerbosity: {
1198
+ type: "enum",
1199
+ values: ["low", "medium", "high"] as const,
1200
+ default: "high",
1201
+ ui: {
1202
+ tab: "model",
1203
+ group: "Sampling",
1204
+ label: "Text Verbosity",
1205
+ description: "OpenAI Responses and Codex response verbosity (low, medium, or high)",
1206
+ options: [
1207
+ { value: "low", label: "Low", description: "Prefer concise responses" },
1208
+ { value: "medium", label: "Medium", description: "Balance brevity and detail" },
1209
+ { value: "high", label: "High", description: "Prefer detailed responses (default)" },
1210
+ ],
1211
+ },
1212
+ },
1213
+
1186
1214
  serviceTier: {
1187
1215
  type: "enum",
1188
1216
  values: SERVICE_TIER_SETTING_VALUES,
@@ -1616,6 +1644,35 @@ export const SETTINGS_SCHEMA = {
1616
1644
  },
1617
1645
  },
1618
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
+
1619
1676
  // Collab
1620
1677
  "collab.relayUrl": {
1621
1678
  type: "string",
@@ -1872,6 +1929,17 @@ export const SETTINGS_SCHEMA = {
1872
1929
  },
1873
1930
  },
1874
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
+
1875
1943
  "compaction.reserveTokens": { type: "number", default: 16384 },
1876
1944
 
1877
1945
  "compaction.keepRecentTokens": { type: "number", default: 20000 },
@@ -1880,6 +1948,8 @@ export const SETTINGS_SCHEMA = {
1880
1948
 
1881
1949
  "compaction.remoteEndpoint": { type: "string", default: undefined },
1882
1950
 
1951
+ "compaction.v2RetainedMessageBudget": { type: "number", default: 64000 },
1952
+
1883
1953
  // Idle compaction
1884
1954
  "compaction.idleEnabled": {
1885
1955
  type: "boolean",
@@ -2007,7 +2077,6 @@ export const SETTINGS_SCHEMA = {
2007
2077
  "anthropic",
2008
2078
  "deepseek",
2009
2079
  "harmony",
2010
- "pi",
2011
2080
  "qwen3",
2012
2081
  "gemini",
2013
2082
  "gemma",
@@ -2034,7 +2103,6 @@ export const SETTINGS_SCHEMA = {
2034
2103
  { value: "anthropic", label: "Anthropic", description: "Use Anthropic-style in-band tool calls." },
2035
2104
  { value: "deepseek", label: "DeepSeek", description: "Use DeepSeek-style in-band tool calls." },
2036
2105
  { value: "harmony", label: "Harmony", description: "Use Harmony-style in-band tool calls." },
2037
- { value: "pi", label: "Pi", description: "Use the Pi owned dialect (compact sigil-delimited tool calls)." },
2038
2106
  { value: "qwen3", label: "Qwen3", description: "Use the Qwen3 owned dialect." },
2039
2107
  { value: "gemini", label: "Gemini", description: "Use the Gemini owned dialect." },
2040
2108
  { value: "gemma", label: "Gemma", description: "Use the Gemma owned dialect." },
@@ -2440,11 +2508,16 @@ export const SETTINGS_SCHEMA = {
2440
2508
  tab: "memory",
2441
2509
  group: "Mnemopi",
2442
2510
  label: "Mnemopi LLM Mode",
2443
- description: "Use no LLM, the configured smol model, or a remote OpenAI-compatible endpoint",
2511
+ description:
2512
+ "Use no LLM, the online tiny model (the TINY role from /models, else pi/smol), or a remote OpenAI-compatible endpoint",
2444
2513
  condition: "mnemopiActive",
2445
2514
  options: [
2446
2515
  { value: "none", label: "None", description: "Disable Mnemopi LLM-backed extraction" },
2447
- { value: "smol", label: "Smol", description: "Use the configured pi-ai smol model" },
2516
+ {
2517
+ value: "smol",
2518
+ label: "Online (tiny)",
2519
+ description: "Use the online tiny model (the TINY role from /models, else pi/smol)",
2520
+ },
2448
2521
  { value: "remote", label: "Remote", description: "Use the Mnemopi remote LLM settings below" },
2449
2522
  ],
2450
2523
  },
@@ -3776,6 +3849,17 @@ export const SETTINGS_SCHEMA = {
3776
3849
  },
3777
3850
  },
3778
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
+
3779
3863
  // Delegation
3780
3864
  "task.isolation.mode": {
3781
3865
  type: "enum",
@@ -4342,7 +4426,8 @@ export const SETTINGS_SCHEMA = {
4342
4426
  tab: "providers",
4343
4427
  group: "Tiny Model",
4344
4428
  label: "Tiny Model",
4345
- description: "Session-title model: online pi/smol by default, or a local on-device model",
4429
+ description:
4430
+ "Session-title model: online (the TINY role from /models, else pi/smol) by default, or a local on-device model",
4346
4431
  options: TINY_TITLE_MODEL_OPTIONS,
4347
4432
  },
4348
4433
  },
@@ -4381,7 +4466,7 @@ export const SETTINGS_SCHEMA = {
4381
4466
  group: "General",
4382
4467
  label: "Memory Model",
4383
4468
  description:
4384
- "Mnemopi LLM for fact extraction + consolidation: online (smol/remote) by default, or a local on-device model",
4469
+ "Mnemopi LLM for fact extraction + consolidation: online (the TINY role from /models, else smol/remote) by default, or a local on-device model",
4385
4470
  condition: "mnemopiActive",
4386
4471
  options: TINY_MEMORY_MODEL_OPTIONS,
4387
4472
  },
@@ -4396,7 +4481,7 @@ export const SETTINGS_SCHEMA = {
4396
4481
  group: "Thinking",
4397
4482
  label: "Auto Thinking Model",
4398
4483
  description:
4399
- "Difficulty classifier for the `auto` thinking level: online smol by default, or a local on-device model",
4484
+ "Difficulty classifier for the `auto` thinking level: online (the TINY role from /models, else smol) by default, or a local on-device model",
4400
4485
  condition: "autoThinkingActive",
4401
4486
  options: AUTO_THINKING_MODEL_OPTIONS,
4402
4487
  },
@@ -4420,7 +4505,8 @@ export const SETTINGS_SCHEMA = {
4420
4505
  tab: "providers",
4421
4506
  group: "Tiny Model",
4422
4507
  label: "Unexpected Stop Model",
4423
- description: "Classifier for unexpected-stop detection: online smol by default, or a local on-device model.",
4508
+ description:
4509
+ "Classifier for unexpected-stop detection: online (the TINY role from /models, else smol) by default, or a local on-device model.",
4424
4510
  condition: "unexpectedStopDetection",
4425
4511
  options: TINY_MEMORY_MODEL_OPTIONS,
4426
4512
  },
@@ -4828,6 +4914,8 @@ export interface CompactionSettings {
4828
4914
  autoContinue: boolean;
4829
4915
  remoteEnabled: boolean;
4830
4916
  remoteEndpoint: string | undefined;
4917
+ remoteStreamingV2Enabled: boolean;
4918
+ v2RetainedMessageBudget: number;
4831
4919
  idleEnabled: boolean;
4832
4920
  idleThresholdTokens: number;
4833
4921
  idleTimeoutSeconds: number;
@@ -4835,6 +4923,15 @@ export interface CompactionSettings {
4835
4923
  dropUseless: boolean;
4836
4924
  }
4837
4925
 
4926
+ export interface RecapSettings {
4927
+ enabled: boolean;
4928
+ idleSeconds: number;
4929
+ }
4930
+
4931
+ export interface TitleSettings {
4932
+ refreshOnReplan: boolean;
4933
+ }
4934
+
4838
4935
  export interface ContextPromotionSettings {
4839
4936
  enabled: boolean;
4840
4937
  }
@@ -4980,6 +5077,8 @@ export interface GcSettings {
4980
5077
  /** Map group prefix -> typed settings interface */
4981
5078
  export interface GroupTypeMap {
4982
5079
  compaction: CompactionSettings;
5080
+ recap: RecapSettings;
5081
+ title: TitleSettings;
4983
5082
  contextPromotion: ContextPromotionSettings;
4984
5083
  retry: RetrySettings;
4985
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");