@code-yeongyu/senpi 2026.5.23 → 2026.5.24

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 (104) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/core/extensions/builtin/history-search/filter.d.ts +3 -0
  3. package/dist/core/extensions/builtin/history-search/filter.d.ts.map +1 -0
  4. package/dist/core/extensions/builtin/history-search/filter.js +22 -0
  5. package/dist/core/extensions/builtin/history-search/filter.js.map +1 -0
  6. package/dist/core/extensions/builtin/history-search/index.d.ts +7 -0
  7. package/dist/core/extensions/builtin/history-search/index.d.ts.map +1 -0
  8. package/dist/core/extensions/builtin/history-search/index.js +45 -0
  9. package/dist/core/extensions/builtin/history-search/index.js.map +1 -0
  10. package/dist/core/extensions/builtin/history-search/indexer.d.ts +3 -0
  11. package/dist/core/extensions/builtin/history-search/indexer.d.ts.map +1 -0
  12. package/dist/core/extensions/builtin/history-search/indexer.js +161 -0
  13. package/dist/core/extensions/builtin/history-search/indexer.js.map +1 -0
  14. package/dist/core/extensions/builtin/history-search/overlay.d.ts +30 -0
  15. package/dist/core/extensions/builtin/history-search/overlay.d.ts.map +1 -0
  16. package/dist/core/extensions/builtin/history-search/overlay.js +115 -0
  17. package/dist/core/extensions/builtin/history-search/overlay.js.map +1 -0
  18. package/dist/core/extensions/builtin/history-search/types.d.ts +8 -0
  19. package/dist/core/extensions/builtin/history-search/types.d.ts.map +1 -0
  20. package/dist/core/extensions/builtin/history-search/types.js +2 -0
  21. package/dist/core/extensions/builtin/history-search/types.js.map +1 -0
  22. package/dist/core/extensions/builtin/index.d.ts.map +1 -1
  23. package/dist/core/extensions/builtin/index.js +4 -0
  24. package/dist/core/extensions/builtin/index.js.map +1 -1
  25. package/dist/core/extensions/builtin/session-observer/index.d.ts +5 -0
  26. package/dist/core/extensions/builtin/session-observer/index.d.ts.map +1 -0
  27. package/dist/core/extensions/builtin/session-observer/index.js +36 -0
  28. package/dist/core/extensions/builtin/session-observer/index.js.map +1 -0
  29. package/dist/core/extensions/builtin/session-observer/loader.d.ts +3 -0
  30. package/dist/core/extensions/builtin/session-observer/loader.d.ts.map +1 -0
  31. package/dist/core/extensions/builtin/session-observer/loader.js +20 -0
  32. package/dist/core/extensions/builtin/session-observer/loader.js.map +1 -0
  33. package/dist/core/extensions/builtin/session-observer/overlay-format.d.ts +7 -0
  34. package/dist/core/extensions/builtin/session-observer/overlay-format.d.ts.map +1 -0
  35. package/dist/core/extensions/builtin/session-observer/overlay-format.js +30 -0
  36. package/dist/core/extensions/builtin/session-observer/overlay-format.js.map +1 -0
  37. package/dist/core/extensions/builtin/session-observer/overlay.d.ts +51 -0
  38. package/dist/core/extensions/builtin/session-observer/overlay.d.ts.map +1 -0
  39. package/dist/core/extensions/builtin/session-observer/overlay.js +239 -0
  40. package/dist/core/extensions/builtin/session-observer/overlay.js.map +1 -0
  41. package/dist/core/extensions/builtin/session-observer/scanner.d.ts +10 -0
  42. package/dist/core/extensions/builtin/session-observer/scanner.d.ts.map +1 -0
  43. package/dist/core/extensions/builtin/session-observer/scanner.js +140 -0
  44. package/dist/core/extensions/builtin/session-observer/scanner.js.map +1 -0
  45. package/dist/core/extensions/builtin/session-observer/text.d.ts +7 -0
  46. package/dist/core/extensions/builtin/session-observer/text.d.ts.map +1 -0
  47. package/dist/core/extensions/builtin/session-observer/text.js +37 -0
  48. package/dist/core/extensions/builtin/session-observer/text.js.map +1 -0
  49. package/dist/core/extensions/builtin/session-observer/transcript-entries.d.ts +7 -0
  50. package/dist/core/extensions/builtin/session-observer/transcript-entries.d.ts.map +1 -0
  51. package/dist/core/extensions/builtin/session-observer/transcript-entries.js +71 -0
  52. package/dist/core/extensions/builtin/session-observer/transcript-entries.js.map +1 -0
  53. package/dist/core/extensions/builtin/session-observer/transcript-format.d.ts +11 -0
  54. package/dist/core/extensions/builtin/session-observer/transcript-format.d.ts.map +1 -0
  55. package/dist/core/extensions/builtin/session-observer/transcript-format.js +65 -0
  56. package/dist/core/extensions/builtin/session-observer/transcript-format.js.map +1 -0
  57. package/dist/core/extensions/builtin/session-observer/transcript.d.ts +4 -0
  58. package/dist/core/extensions/builtin/session-observer/transcript.d.ts.map +1 -0
  59. package/dist/core/extensions/builtin/session-observer/transcript.js +81 -0
  60. package/dist/core/extensions/builtin/session-observer/transcript.js.map +1 -0
  61. package/dist/core/extensions/builtin/session-observer/types.d.ts +33 -0
  62. package/dist/core/extensions/builtin/session-observer/types.d.ts.map +1 -0
  63. package/dist/core/extensions/builtin/session-observer/types.js +2 -0
  64. package/dist/core/extensions/builtin/session-observer/types.js.map +1 -0
  65. package/dist/core/extensions/runner.d.ts.map +1 -1
  66. package/dist/core/extensions/runner.js +1 -0
  67. package/dist/core/extensions/runner.js.map +1 -1
  68. package/dist/core/keybindings.d.ts +10 -0
  69. package/dist/core/keybindings.d.ts.map +1 -1
  70. package/dist/core/keybindings.js +3 -0
  71. package/dist/core/keybindings.js.map +1 -1
  72. package/dist/core/package-manager.d.ts.map +1 -1
  73. package/dist/core/package-manager.js +16 -4
  74. package/dist/core/package-manager.js.map +1 -1
  75. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  76. package/dist/modes/interactive/components/footer.js +74 -63
  77. package/dist/modes/interactive/components/footer.js.map +1 -1
  78. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  79. package/dist/modes/interactive/interactive-mode.js +18 -0
  80. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  81. package/dist/utils/paths.d.ts +1 -0
  82. package/dist/utils/paths.d.ts.map +1 -1
  83. package/dist/utils/paths.js +8 -0
  84. package/dist/utils/paths.js.map +1 -1
  85. package/docs/terminal-setup.md +6 -0
  86. package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
  87. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts.map +1 -1
  88. package/node_modules/@earendil-works/pi-ai/dist/models.generated.js +1 -1
  89. package/node_modules/@earendil-works/pi-ai/dist/models.generated.js.map +1 -1
  90. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  91. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.js +53 -11
  92. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.js.map +1 -1
  93. package/node_modules/@earendil-works/pi-ai/package.json +1 -1
  94. package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.d.ts +3 -0
  95. package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.d.ts.map +1 -0
  96. package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.js +53 -0
  97. package/node_modules/@earendil-works/pi-tui/dist/native-modifiers.js.map +1 -0
  98. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts +2 -0
  99. package/node_modules/@earendil-works/pi-tui/dist/terminal.d.ts.map +1 -1
  100. package/node_modules/@earendil-works/pi-tui/dist/terminal.js +13 -1
  101. package/node_modules/@earendil-works/pi-tui/dist/terminal.js.map +1 -1
  102. package/node_modules/@earendil-works/pi-tui/package.json +2 -2
  103. package/npm-shrinkwrap.json +12 -12
  104. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/core/extensions/builtin/index.ts"],"names":[],"mappings":"AACA,OAAO,sBAAsB,MAAM,2BAA2B,CAAC;AAC/D,OAAO,2BAA2B,MAAM,iCAAiC,CAAC;AAC1E,OAAO,oBAAoB,MAAM,yBAAyB,CAAC;AAC3D,OAAO,mBAAmB,MAAM,uBAAuB,CAAC;AACxD,OAAO,aAAa,MAAM,WAAW,CAAC;AACtC,OAAO,cAAc,MAAM,YAAY,CAAC;AACxC,OAAO,sBAAsB,MAAM,4BAA4B,CAAC;AAChE,OAAO,sBAAsB,MAAM,4BAA4B,CAAC;AAChE,OAAO,wBAAwB,MAAM,8BAA8B,CAAC;AACpE,OAAO,yBAAyB,MAAM,8BAA8B,CAAC;AACrE,OAAO,qBAAqB,MAAM,0BAA0B,CAAC;AAC7D,OAAO,wBAAwB,MAAM,wBAAwB,CAAC;AAC9D,OAAO,gBAAgB,MAAM,cAAc,CAAC;AAC5C,OAAO,oBAAoB,MAAM,mBAAmB,CAAC;AACrD,OAAO,kBAAkB,MAAM,sBAAsB,CAAC;AACtD,OAAO,sBAAsB,MAAM,4BAA4B,CAAC;AAChE,OAAO,YAAY,MAAM,UAAU,CAAC;AAOpC,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,mBAAmB,EAAE,KAAK,CAAU,CAAC;AAEhG,MAAM,CAAC,MAAM,+BAA+B,GAAG;IAC9C,IAAI,EAAE,aAAa;IACnB,KAAK,EAAE,cAAc;IACrB,mBAAmB,EAAE,wBAAwB;IAC7C,GAAG,EAAE,YAAY;CAC8D,CAAC;AAEjF,MAAM,CAAC,MAAM,iBAAiB,GAA8B;IAC3D,EAAE,EAAE,EAAE,mBAAmB,EAAE,OAAO,EAAE,yBAAyB,EAAE;IAC/D,EAAE,EAAE,EAAE,iBAAiB,EAAE,OAAO,EAAE,sBAAsB,EAAE;IAC1D,EAAE,EAAE,EAAE,eAAe,EAAE,OAAO,EAAE,qBAAqB,EAAE;IACvD,EAAE,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE,kBAAkB,EAAE;IAChD,EAAE,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,gBAAgB,EAAE;IAC5C,EAAE,EAAE,EAAE,sBAAsB,EAAE,OAAO,EAAE,2BAA2B,EAAE;IACpE,EAAE,EAAE,EAAE,gBAAgB,EAAE,OAAO,EAAE,sBAAsB,EAAE;IACzD,EAAE,EAAE,EAAE,mBAAmB,EAAE,OAAO,EAAE,wBAAwB,EAAE;IAC9D,EAAE,EAAE,EAAE,cAAc,EAAE,OAAO,EAAE,oBAAoB,EAAE;IACrD,EAAE,EAAE,EAAE,cAAc,EAAE,OAAO,EAAE,oBAAoB,EAAE;IACrD,EAAE,EAAE,EAAE,iBAAiB,EAAE,OAAO,EAAE,sBAAsB,EAAE;IAC1D,EAAE,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,mBAAmB,EAAE;IAClD,EAAE,EAAE,EAAE,iBAAiB,EAAE,OAAO,EAAE,sBAAsB,EAAE;CAC1D,CAAC","sourcesContent":["import type { ExtensionFactory } from \"../types.ts\";\nimport anthropicBashExtension from \"./anthropic-bash/index.ts\";\nimport anthropicWebSearchExtension from \"./anthropic-web-search/index.ts\";\nimport bashTimeoutExtension from \"./bash-timeout/index.ts\";\nimport compactionExtension from \"./compaction/index.ts\";\nimport diffExtension from \"./diff.ts\";\nimport filesExtension from \"./files.ts\";\nimport gptApplyPatchExtension from \"./gpt-apply-patch/index.ts\";\nimport kimiWebSearchExtension from \"./kimi-web-search/index.ts\";\nimport openaiWebSearchExtension from \"./openai-web-search/index.ts\";\nimport permissionSystemExtension from \"./permission-system/index.ts\";\nimport promptPresetExtension from \"./prompt-preset/index.ts\";\nimport promptUrlWidgetExtension from \"./prompt-url-widget.ts\";\nimport redrawsExtension from \"./redraws.ts\";\nimport serviceTierExtension from \"./service-tier.ts\";\nimport todowriteExtension from \"./todotools/index.ts\";\nimport toolPairGuardExtension from \"./tool-pair-guard/index.ts\";\nimport tpsExtension from \"./tps.ts\";\n\nexport interface BuiltinExtensionFactory {\n\tid: string;\n\tfactory: ExtensionFactory;\n}\n\nexport const globalDefaultExtensionIds = [\"diff\", \"files\", \"prompt-url-widget\", \"tps\"] as const;\n\nexport const globalDefaultExtensionFactories = {\n\tdiff: diffExtension,\n\tfiles: filesExtension,\n\t\"prompt-url-widget\": promptUrlWidgetExtension,\n\ttps: tpsExtension,\n} satisfies Record<(typeof globalDefaultExtensionIds)[number], ExtensionFactory>;\n\nexport const builtinExtensions: BuiltinExtensionFactory[] = [\n\t{ id: \"permission-system\", factory: permissionSystemExtension },\n\t{ id: \"gpt-apply-patch\", factory: gptApplyPatchExtension },\n\t{ id: \"prompt-preset\", factory: promptPresetExtension },\n\t{ id: \"todowrite\", factory: todowriteExtension },\n\t{ id: \"redraws\", factory: redrawsExtension },\n\t{ id: \"anthropic-web-search\", factory: anthropicWebSearchExtension },\n\t{ id: \"anthropic-bash\", factory: anthropicBashExtension },\n\t{ id: \"openai-web-search\", factory: openaiWebSearchExtension },\n\t{ id: \"service-tier\", factory: serviceTierExtension },\n\t{ id: \"bash-timeout\", factory: bashTimeoutExtension },\n\t{ id: \"tool-pair-guard\", factory: toolPairGuardExtension },\n\t{ id: \"compaction\", factory: compactionExtension },\n\t{ id: \"kimi-web-search\", factory: kimiWebSearchExtension },\n];\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/core/extensions/builtin/index.ts"],"names":[],"mappings":"AACA,OAAO,sBAAsB,MAAM,2BAA2B,CAAC;AAC/D,OAAO,2BAA2B,MAAM,iCAAiC,CAAC;AAC1E,OAAO,oBAAoB,MAAM,yBAAyB,CAAC;AAC3D,OAAO,mBAAmB,MAAM,uBAAuB,CAAC;AACxD,OAAO,aAAa,MAAM,WAAW,CAAC;AACtC,OAAO,cAAc,MAAM,YAAY,CAAC;AACxC,OAAO,sBAAsB,MAAM,4BAA4B,CAAC;AAChE,OAAO,sBAAsB,MAAM,2BAA2B,CAAC;AAC/D,OAAO,sBAAsB,MAAM,4BAA4B,CAAC;AAChE,OAAO,wBAAwB,MAAM,8BAA8B,CAAC;AACpE,OAAO,yBAAyB,MAAM,8BAA8B,CAAC;AACrE,OAAO,qBAAqB,MAAM,0BAA0B,CAAC;AAC7D,OAAO,wBAAwB,MAAM,wBAAwB,CAAC;AAC9D,OAAO,gBAAgB,MAAM,cAAc,CAAC;AAC5C,OAAO,oBAAoB,MAAM,mBAAmB,CAAC;AACrD,OAAO,wBAAwB,MAAM,6BAA6B,CAAC;AACnE,OAAO,kBAAkB,MAAM,sBAAsB,CAAC;AACtD,OAAO,sBAAsB,MAAM,4BAA4B,CAAC;AAChE,OAAO,YAAY,MAAM,UAAU,CAAC;AAOpC,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,mBAAmB,EAAE,KAAK,CAAU,CAAC;AAEhG,MAAM,CAAC,MAAM,+BAA+B,GAAG;IAC9C,IAAI,EAAE,aAAa;IACnB,KAAK,EAAE,cAAc;IACrB,mBAAmB,EAAE,wBAAwB;IAC7C,GAAG,EAAE,YAAY;CAC8D,CAAC;AAEjF,MAAM,CAAC,MAAM,iBAAiB,GAA8B;IAC3D,EAAE,EAAE,EAAE,mBAAmB,EAAE,OAAO,EAAE,yBAAyB,EAAE;IAC/D,EAAE,EAAE,EAAE,iBAAiB,EAAE,OAAO,EAAE,sBAAsB,EAAE;IAC1D,EAAE,EAAE,EAAE,eAAe,EAAE,OAAO,EAAE,qBAAqB,EAAE;IACvD,EAAE,EAAE,EAAE,WAAW,EAAE,OAAO,EAAE,kBAAkB,EAAE;IAChD,EAAE,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,gBAAgB,EAAE;IAC5C,EAAE,EAAE,EAAE,sBAAsB,EAAE,OAAO,EAAE,2BAA2B,EAAE;IACpE,EAAE,EAAE,EAAE,gBAAgB,EAAE,OAAO,EAAE,sBAAsB,EAAE;IACzD,EAAE,EAAE,EAAE,mBAAmB,EAAE,OAAO,EAAE,wBAAwB,EAAE;IAC9D,EAAE,EAAE,EAAE,cAAc,EAAE,OAAO,EAAE,oBAAoB,EAAE;IACrD,EAAE,EAAE,EAAE,cAAc,EAAE,OAAO,EAAE,oBAAoB,EAAE;IACrD,EAAE,EAAE,EAAE,iBAAiB,EAAE,OAAO,EAAE,sBAAsB,EAAE;IAC1D,EAAE,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,mBAAmB,EAAE;IAClD,EAAE,EAAE,EAAE,gBAAgB,EAAE,OAAO,EAAE,sBAAsB,EAAE;IACzD,EAAE,EAAE,EAAE,kBAAkB,EAAE,OAAO,EAAE,wBAAwB,EAAE;IAC7D,EAAE,EAAE,EAAE,iBAAiB,EAAE,OAAO,EAAE,sBAAsB,EAAE;CAC1D,CAAC","sourcesContent":["import type { ExtensionFactory } from \"../types.ts\";\nimport anthropicBashExtension from \"./anthropic-bash/index.ts\";\nimport anthropicWebSearchExtension from \"./anthropic-web-search/index.ts\";\nimport bashTimeoutExtension from \"./bash-timeout/index.ts\";\nimport compactionExtension from \"./compaction/index.ts\";\nimport diffExtension from \"./diff.ts\";\nimport filesExtension from \"./files.ts\";\nimport gptApplyPatchExtension from \"./gpt-apply-patch/index.ts\";\nimport historySearchExtension from \"./history-search/index.ts\";\nimport kimiWebSearchExtension from \"./kimi-web-search/index.ts\";\nimport openaiWebSearchExtension from \"./openai-web-search/index.ts\";\nimport permissionSystemExtension from \"./permission-system/index.ts\";\nimport promptPresetExtension from \"./prompt-preset/index.ts\";\nimport promptUrlWidgetExtension from \"./prompt-url-widget.ts\";\nimport redrawsExtension from \"./redraws.ts\";\nimport serviceTierExtension from \"./service-tier.ts\";\nimport sessionObserverExtension from \"./session-observer/index.ts\";\nimport todowriteExtension from \"./todotools/index.ts\";\nimport toolPairGuardExtension from \"./tool-pair-guard/index.ts\";\nimport tpsExtension from \"./tps.ts\";\n\nexport interface BuiltinExtensionFactory {\n\tid: string;\n\tfactory: ExtensionFactory;\n}\n\nexport const globalDefaultExtensionIds = [\"diff\", \"files\", \"prompt-url-widget\", \"tps\"] as const;\n\nexport const globalDefaultExtensionFactories = {\n\tdiff: diffExtension,\n\tfiles: filesExtension,\n\t\"prompt-url-widget\": promptUrlWidgetExtension,\n\ttps: tpsExtension,\n} satisfies Record<(typeof globalDefaultExtensionIds)[number], ExtensionFactory>;\n\nexport const builtinExtensions: BuiltinExtensionFactory[] = [\n\t{ id: \"permission-system\", factory: permissionSystemExtension },\n\t{ id: \"gpt-apply-patch\", factory: gptApplyPatchExtension },\n\t{ id: \"prompt-preset\", factory: promptPresetExtension },\n\t{ id: \"todowrite\", factory: todowriteExtension },\n\t{ id: \"redraws\", factory: redrawsExtension },\n\t{ id: \"anthropic-web-search\", factory: anthropicWebSearchExtension },\n\t{ id: \"anthropic-bash\", factory: anthropicBashExtension },\n\t{ id: \"openai-web-search\", factory: openaiWebSearchExtension },\n\t{ id: \"service-tier\", factory: serviceTierExtension },\n\t{ id: \"bash-timeout\", factory: bashTimeoutExtension },\n\t{ id: \"tool-pair-guard\", factory: toolPairGuardExtension },\n\t{ id: \"compaction\", factory: compactionExtension },\n\t{ id: \"history-search\", factory: historySearchExtension },\n\t{ id: \"session-observer\", factory: sessionObserverExtension },\n\t{ id: \"kimi-web-search\", factory: kimiWebSearchExtension },\n];\n"]}
@@ -0,0 +1,5 @@
1
+ import type { ExtensionAPI } from "../../types.ts";
2
+ export { resolveSessionHudRoot, scanSessionHudEntries } from "./scanner.ts";
3
+ export { renderTranscript } from "./transcript.ts";
4
+ export default function sessionHudExtension(pi: ExtensionAPI): void;
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/session-observer/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAKnD,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC5E,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAAC,EAAE,EAAE,YAAY,GAAG,IAAI,CAmClE","sourcesContent":["import { getSessionsDir } from \"../../../../config.ts\";\nimport type { ExtensionAPI } from \"../../types.ts\";\nimport { SessionHudOverlay } from \"./overlay.ts\";\nimport { resolveSessionHudRoot, scanSessionHudEntries } from \"./scanner.ts\";\nimport type { SessionHudEntry } from \"./types.ts\";\n\nexport { resolveSessionHudRoot, scanSessionHudEntries } from \"./scanner.ts\";\nexport { renderTranscript } from \"./transcript.ts\";\n\nexport default function sessionHudExtension(pi: ExtensionAPI): void {\n\tpi.registerCommand(\"sessions\", {\n\t\tdescription: \"Peek at previous session transcripts in a HUD\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\tctx.ui.notify(\"No UI available\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet sessions: readonly SessionHudEntry[];\n\t\t\ttry {\n\t\t\t\tconst root = resolveSessionHudRoot(ctx.sessionManager.getSessionDir(), getSessionsDir());\n\t\t\t\tsessions = await scanSessionHudEntries(root, ctx.sessionManager.getSessionFile());\n\t\t\t} catch (error) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tctx.ui.notify(`Failed to read sessions: ${message}`, \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (sessions.length === 0) {\n\t\t\t\tctx.ui.notify(\"No sessions found\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tawait ctx.ui.custom<void>(\n\t\t\t\t(tui, _theme, _keybindings, done) =>\n\t\t\t\t\tnew SessionHudOverlay({\n\t\t\t\t\t\tsessions,\n\t\t\t\t\t\tdone,\n\t\t\t\t\t\trequestRender: () => tui.requestRender(),\n\t\t\t\t\t}),\n\t\t\t\t{ overlay: true, overlayOptions: { width: \"94%\", maxHeight: \"90%\", minWidth: 72, margin: 1 } },\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
@@ -0,0 +1,36 @@
1
+ import { getSessionsDir } from "../../../../config.js";
2
+ import { SessionHudOverlay } from "./overlay.js";
3
+ import { resolveSessionHudRoot, scanSessionHudEntries } from "./scanner.js";
4
+ export { resolveSessionHudRoot, scanSessionHudEntries } from "./scanner.js";
5
+ export { renderTranscript } from "./transcript.js";
6
+ export default function sessionHudExtension(pi) {
7
+ pi.registerCommand("sessions", {
8
+ description: "Peek at previous session transcripts in a HUD",
9
+ handler: async (_args, ctx) => {
10
+ if (!ctx.hasUI) {
11
+ ctx.ui.notify("No UI available", "info");
12
+ return;
13
+ }
14
+ let sessions;
15
+ try {
16
+ const root = resolveSessionHudRoot(ctx.sessionManager.getSessionDir(), getSessionsDir());
17
+ sessions = await scanSessionHudEntries(root, ctx.sessionManager.getSessionFile());
18
+ }
19
+ catch (error) {
20
+ const message = error instanceof Error ? error.message : String(error);
21
+ ctx.ui.notify(`Failed to read sessions: ${message}`, "error");
22
+ return;
23
+ }
24
+ if (sessions.length === 0) {
25
+ ctx.ui.notify("No sessions found", "info");
26
+ return;
27
+ }
28
+ await ctx.ui.custom((tui, _theme, _keybindings, done) => new SessionHudOverlay({
29
+ sessions,
30
+ done,
31
+ requestRender: () => tui.requestRender(),
32
+ }), { overlay: true, overlayOptions: { width: "94%", maxHeight: "90%", minWidth: 72, margin: 1 } });
33
+ },
34
+ });
35
+ }
36
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/session-observer/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAG5E,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAC5E,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAAC,EAAgB,EAAQ;IACnE,EAAE,CAAC,eAAe,CAAC,UAAU,EAAE;QAC9B,WAAW,EAAE,+CAA+C;QAC5D,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC;YAC9B,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;gBAChB,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;gBACzC,OAAO;YACR,CAAC;YAED,IAAI,QAAoC,CAAC;YACzC,IAAI,CAAC;gBACJ,MAAM,IAAI,GAAG,qBAAqB,CAAC,GAAG,CAAC,cAAc,CAAC,aAAa,EAAE,EAAE,cAAc,EAAE,CAAC,CAAC;gBACzF,QAAQ,GAAG,MAAM,qBAAqB,CAAC,IAAI,EAAE,GAAG,CAAC,cAAc,CAAC,cAAc,EAAE,CAAC,CAAC;YACnF,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACvE,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,4BAA4B,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;gBAC9D,OAAO;YACR,CAAC;YAED,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC3B,GAAG,CAAC,EAAE,CAAC,MAAM,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC;gBAC3C,OAAO;YACR,CAAC;YAED,MAAM,GAAG,CAAC,EAAE,CAAC,MAAM,CAClB,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,IAAI,EAAE,EAAE,CACnC,IAAI,iBAAiB,CAAC;gBACrB,QAAQ;gBACR,IAAI;gBACJ,aAAa,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,aAAa,EAAE;aACxC,CAAC,EACH,EAAE,OAAO,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,EAAE,CAC9F,CAAC;QAAA,CACF;KACD,CAAC,CAAC;AAAA,CACH","sourcesContent":["import { getSessionsDir } from \"../../../../config.ts\";\nimport type { ExtensionAPI } from \"../../types.ts\";\nimport { SessionHudOverlay } from \"./overlay.ts\";\nimport { resolveSessionHudRoot, scanSessionHudEntries } from \"./scanner.ts\";\nimport type { SessionHudEntry } from \"./types.ts\";\n\nexport { resolveSessionHudRoot, scanSessionHudEntries } from \"./scanner.ts\";\nexport { renderTranscript } from \"./transcript.ts\";\n\nexport default function sessionHudExtension(pi: ExtensionAPI): void {\n\tpi.registerCommand(\"sessions\", {\n\t\tdescription: \"Peek at previous session transcripts in a HUD\",\n\t\thandler: async (_args, ctx) => {\n\t\t\tif (!ctx.hasUI) {\n\t\t\t\tctx.ui.notify(\"No UI available\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlet sessions: readonly SessionHudEntry[];\n\t\t\ttry {\n\t\t\t\tconst root = resolveSessionHudRoot(ctx.sessionManager.getSessionDir(), getSessionsDir());\n\t\t\t\tsessions = await scanSessionHudEntries(root, ctx.sessionManager.getSessionFile());\n\t\t\t} catch (error) {\n\t\t\t\tconst message = error instanceof Error ? error.message : String(error);\n\t\t\t\tctx.ui.notify(`Failed to read sessions: ${message}`, \"error\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (sessions.length === 0) {\n\t\t\t\tctx.ui.notify(\"No sessions found\", \"info\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tawait ctx.ui.custom<void>(\n\t\t\t\t(tui, _theme, _keybindings, done) =>\n\t\t\t\t\tnew SessionHudOverlay({\n\t\t\t\t\t\tsessions,\n\t\t\t\t\t\tdone,\n\t\t\t\t\t\trequestRender: () => tui.requestRender(),\n\t\t\t\t\t}),\n\t\t\t\t{ overlay: true, overlayOptions: { width: \"94%\", maxHeight: \"90%\", minWidth: 72, margin: 1 } },\n\t\t\t);\n\t\t},\n\t});\n}\n"]}
@@ -0,0 +1,3 @@
1
+ import type { TranscriptSnapshot } from "./types.ts";
2
+ export declare function loadTranscriptSnapshot(filePath: string): Promise<TranscriptSnapshot>;
3
+ //# sourceMappingURL=loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/session-observer/loader.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAErD,wBAAsB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAc1F","sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport type { SessionMessageEntry } from \"../../../session-manager.ts\";\nimport { parseSessionEntries } from \"../../../session-manager.ts\";\nimport type { TranscriptSnapshot } from \"./types.ts\";\n\nexport async function loadTranscriptSnapshot(filePath: string): Promise<TranscriptSnapshot> {\n\tconst content = await readFile(filePath, \"utf-8\");\n\tconst entries = parseSessionEntries(content);\n\tconst messages: SessionMessageEntry[] = [];\n\tlet model: string | undefined;\n\tfor (const entry of entries) {\n\t\tif (entry.type === \"message\") {\n\t\t\tmessages.push(entry);\n\t\t\tif (!model && entry.message.role === \"assistant\") model = entry.message.responseModel ?? entry.message.model;\n\t\t} else if (entry.type === \"model_change\") {\n\t\t\tmodel = `${entry.provider}/${entry.modelId}`;\n\t\t}\n\t}\n\treturn { entries: messages, model };\n}\n"]}
@@ -0,0 +1,20 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { parseSessionEntries } from "../../../session-manager.js";
3
+ export async function loadTranscriptSnapshot(filePath) {
4
+ const content = await readFile(filePath, "utf-8");
5
+ const entries = parseSessionEntries(content);
6
+ const messages = [];
7
+ let model;
8
+ for (const entry of entries) {
9
+ if (entry.type === "message") {
10
+ messages.push(entry);
11
+ if (!model && entry.message.role === "assistant")
12
+ model = entry.message.responseModel ?? entry.message.model;
13
+ }
14
+ else if (entry.type === "model_change") {
15
+ model = `${entry.provider}/${entry.modelId}`;
16
+ }
17
+ }
18
+ return { entries: messages, model };
19
+ }
20
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/session-observer/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAGlE,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,QAAgB,EAA+B;IAC3F,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAClD,MAAM,OAAO,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC7C,MAAM,QAAQ,GAA0B,EAAE,CAAC;IAC3C,IAAI,KAAyB,CAAC;IAC9B,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC9B,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACrB,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,WAAW;gBAAE,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,aAAa,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAC9G,CAAC;aAAM,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;YAC1C,KAAK,GAAG,GAAG,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;QAC9C,CAAC;IACF,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;AAAA,CACpC","sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport type { SessionMessageEntry } from \"../../../session-manager.ts\";\nimport { parseSessionEntries } from \"../../../session-manager.ts\";\nimport type { TranscriptSnapshot } from \"./types.ts\";\n\nexport async function loadTranscriptSnapshot(filePath: string): Promise<TranscriptSnapshot> {\n\tconst content = await readFile(filePath, \"utf-8\");\n\tconst entries = parseSessionEntries(content);\n\tconst messages: SessionMessageEntry[] = [];\n\tlet model: string | undefined;\n\tfor (const entry of entries) {\n\t\tif (entry.type === \"message\") {\n\t\t\tmessages.push(entry);\n\t\t\tif (!model && entry.message.role === \"assistant\") model = entry.message.responseModel ?? entry.message.model;\n\t\t} else if (entry.type === \"model_change\") {\n\t\t\tmodel = `${entry.provider}/${entry.modelId}`;\n\t\t}\n\t}\n\treturn { entries: messages, model };\n}\n"]}
@@ -0,0 +1,7 @@
1
+ import type { SessionHudEntry } from "./types.ts";
2
+ export declare function sessionAge(session: SessionHudEntry): string;
3
+ export declare function describeSession(session: SessionHudEntry): string;
4
+ export declare function pickerLabel(session: SessionHudEntry): string;
5
+ export declare function renderLine(text: string, width: number): string;
6
+ export declare function viewerFooter(scroll: string): string;
7
+ //# sourceMappingURL=overlay-format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay-format.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/session-observer/overlay-format.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,wBAAgB,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAE3D;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAGhE;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAE5D;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAUnD","sourcesContent":["import { TruncatedText } from \"@earendil-works/pi-tui\";\nimport { keyHint, keyText } from \"../../../../modes/interactive/components/keybinding-hints.ts\";\nimport { theme } from \"../../../../modes/interactive/theme/theme.ts\";\nimport { shortenPath } from \"../../../../utils/paths.ts\";\nimport type {} from \"../../../keybindings.ts\";\nimport { compactWhitespace, formatSessionDate } from \"./text.ts\";\nimport type { SessionHudEntry } from \"./types.ts\";\n\nexport function sessionAge(session: SessionHudEntry): string {\n\treturn session.isCurrent ? \"live\" : formatSessionDate(new Date(session.modifiedAt));\n}\n\nexport function describeSession(session: SessionHudEntry): string {\n\tconst cwd = shortenPath(session.cwd) || \"unknown\";\n\treturn `${cwd} · ${sessionAge(session)} · ${session.messageCount} msg`;\n}\n\nexport function pickerLabel(session: SessionHudEntry): string {\n\treturn compactWhitespace(session.lastUserText) || \"(no user prompt)\";\n}\n\nexport function renderLine(text: string, width: number): string {\n\treturn new TruncatedText(text, 0, 0).render(width)[0] ?? \"\";\n}\n\nexport function viewerFooter(scroll: string): string {\n\tconst scrollKeys = `${theme.fg(\"dim\", `${keyText(\"tui.select.up\")}/${keyText(\"tui.select.down\")}`)}${theme.fg(\"muted\", \" scroll\")}`;\n\treturn [\n\t\tscrollKeys,\n\t\tkeyHint(\"tui.select.confirm\", \"expand\"),\n\t\tkeyHint(\"tui.select.cancel\", \"sessions\"),\n\t\tkeyHint(\"app.sessions.observe\", \"close\"),\n\t]\n\t\t.join(theme.fg(\"muted\", \" · \"))\n\t\t.concat(scroll);\n}\n"]}
@@ -0,0 +1,30 @@
1
+ import { TruncatedText } from "@earendil-works/pi-tui";
2
+ import { keyHint, keyText } from "../../../../modes/interactive/components/keybinding-hints.js";
3
+ import { theme } from "../../../../modes/interactive/theme/theme.js";
4
+ import { shortenPath } from "../../../../utils/paths.js";
5
+ import { compactWhitespace, formatSessionDate } from "./text.js";
6
+ export function sessionAge(session) {
7
+ return session.isCurrent ? "live" : formatSessionDate(new Date(session.modifiedAt));
8
+ }
9
+ export function describeSession(session) {
10
+ const cwd = shortenPath(session.cwd) || "unknown";
11
+ return `${cwd} · ${sessionAge(session)} · ${session.messageCount} msg`;
12
+ }
13
+ export function pickerLabel(session) {
14
+ return compactWhitespace(session.lastUserText) || "(no user prompt)";
15
+ }
16
+ export function renderLine(text, width) {
17
+ return new TruncatedText(text, 0, 0).render(width)[0] ?? "";
18
+ }
19
+ export function viewerFooter(scroll) {
20
+ const scrollKeys = `${theme.fg("dim", `${keyText("tui.select.up")}/${keyText("tui.select.down")}`)}${theme.fg("muted", " scroll")}`;
21
+ return [
22
+ scrollKeys,
23
+ keyHint("tui.select.confirm", "expand"),
24
+ keyHint("tui.select.cancel", "sessions"),
25
+ keyHint("app.sessions.observe", "close"),
26
+ ]
27
+ .join(theme.fg("muted", " · "))
28
+ .concat(scroll);
29
+ }
30
+ //# sourceMappingURL=overlay-format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay-format.js","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/session-observer/overlay-format.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,8DAA8D,CAAC;AAChG,OAAO,EAAE,KAAK,EAAE,MAAM,8CAA8C,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAEzD,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAGjE,MAAM,UAAU,UAAU,CAAC,OAAwB,EAAU;IAC5D,OAAO,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,iBAAiB,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;AAAA,CACpF;AAED,MAAM,UAAU,eAAe,CAAC,OAAwB,EAAU;IACjE,MAAM,GAAG,GAAG,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC;IAClD,OAAO,GAAG,GAAG,OAAM,UAAU,CAAC,OAAO,CAAC,OAAM,OAAO,CAAC,YAAY,MAAM,CAAC;AAAA,CACvE;AAED,MAAM,UAAU,WAAW,CAAC,OAAwB,EAAU;IAC7D,OAAO,iBAAiB,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,kBAAkB,CAAC;AAAA,CACrE;AAED,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,KAAa,EAAU;IAC/D,OAAO,IAAI,aAAa,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AAAA,CAC5D;AAED,MAAM,UAAU,YAAY,CAAC,MAAc,EAAU;IACpD,MAAM,UAAU,GAAG,GAAG,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,OAAO,CAAC,eAAe,CAAC,IAAI,OAAO,CAAC,iBAAiB,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;IACpI,OAAO;QACN,UAAU;QACV,OAAO,CAAC,oBAAoB,EAAE,QAAQ,CAAC;QACvC,OAAO,CAAC,mBAAmB,EAAE,UAAU,CAAC;QACxC,OAAO,CAAC,sBAAsB,EAAE,OAAO,CAAC;KACxC;SACC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAK,CAAC,CAAC;SAC9B,MAAM,CAAC,MAAM,CAAC,CAAC;AAAA,CACjB","sourcesContent":["import { TruncatedText } from \"@earendil-works/pi-tui\";\nimport { keyHint, keyText } from \"../../../../modes/interactive/components/keybinding-hints.ts\";\nimport { theme } from \"../../../../modes/interactive/theme/theme.ts\";\nimport { shortenPath } from \"../../../../utils/paths.ts\";\nimport type {} from \"../../../keybindings.ts\";\nimport { compactWhitespace, formatSessionDate } from \"./text.ts\";\nimport type { SessionHudEntry } from \"./types.ts\";\n\nexport function sessionAge(session: SessionHudEntry): string {\n\treturn session.isCurrent ? \"live\" : formatSessionDate(new Date(session.modifiedAt));\n}\n\nexport function describeSession(session: SessionHudEntry): string {\n\tconst cwd = shortenPath(session.cwd) || \"unknown\";\n\treturn `${cwd} · ${sessionAge(session)} · ${session.messageCount} msg`;\n}\n\nexport function pickerLabel(session: SessionHudEntry): string {\n\treturn compactWhitespace(session.lastUserText) || \"(no user prompt)\";\n}\n\nexport function renderLine(text: string, width: number): string {\n\treturn new TruncatedText(text, 0, 0).render(width)[0] ?? \"\";\n}\n\nexport function viewerFooter(scroll: string): string {\n\tconst scrollKeys = `${theme.fg(\"dim\", `${keyText(\"tui.select.up\")}/${keyText(\"tui.select.down\")}`)}${theme.fg(\"muted\", \" scroll\")}`;\n\treturn [\n\t\tscrollKeys,\n\t\tkeyHint(\"tui.select.confirm\", \"expand\"),\n\t\tkeyHint(\"tui.select.cancel\", \"sessions\"),\n\t\tkeyHint(\"app.sessions.observe\", \"close\"),\n\t]\n\t\t.join(theme.fg(\"muted\", \" · \"))\n\t\t.concat(scroll);\n}\n"]}
@@ -0,0 +1,51 @@
1
+ import { Container, type Focusable } from "@earendil-works/pi-tui";
2
+ import type { SessionHudEntry } from "./types.ts";
3
+ type Mode = "picker" | "viewer";
4
+ interface SessionHudOverlayOptions {
5
+ readonly sessions: readonly SessionHudEntry[];
6
+ readonly done: () => void;
7
+ readonly requestRender: () => void;
8
+ }
9
+ export declare class SessionHudOverlay extends Container implements Focusable {
10
+ private readonly options;
11
+ private readonly sessionsByValue;
12
+ private readonly topBorder;
13
+ private readonly middleBorder;
14
+ private readonly bottomBorder;
15
+ private list;
16
+ private mode;
17
+ private selectedSession;
18
+ private snapshot;
19
+ private renderedLines;
20
+ private ranges;
21
+ private selectedEntryIndex;
22
+ private shouldSelectLastOnLoad;
23
+ private expandedEntries;
24
+ private scrollOffset;
25
+ private viewportHeight;
26
+ private loadingText;
27
+ private _focused;
28
+ constructor(options: SessionHudOverlayOptions);
29
+ get focused(): boolean;
30
+ set focused(value: boolean);
31
+ handleInput(input: string): void;
32
+ render(width: number): string[];
33
+ getMode(): Mode;
34
+ getSelectedEntryIndex(): number;
35
+ getExpandedEntryCount(): number;
36
+ private handlePickerInput;
37
+ private rebuildPicker;
38
+ private toPickerItem;
39
+ private openSession;
40
+ private resetViewerState;
41
+ private rebuildTranscript;
42
+ private renderViewer;
43
+ private handleViewerInput;
44
+ private backToPicker;
45
+ private moveSelection;
46
+ private jumpTo;
47
+ private toggleExpanded;
48
+ private scrollToSelected;
49
+ }
50
+ export {};
51
+ //# sourceMappingURL=overlay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/session-observer/overlay.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,SAAS,EACT,KAAK,SAAS,EAOd,MAAM,wBAAwB,CAAC;AAUhC,OAAO,KAAK,EAAE,eAAe,EAAwC,MAAM,YAAY,CAAC;AAIxF,KAAK,IAAI,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAUhC,UAAU,wBAAwB;IACjC,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC;IAC1B,QAAQ,CAAC,aAAa,EAAE,MAAM,IAAI,CAAC;CACnC;AAED,qBAAa,iBAAkB,SAAQ,SAAU,YAAW,SAAS;IACpE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2B;IACnD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAsC;IACtE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyD;IACnF,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAyD;IACtF,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAyD;IACtF,OAAO,CAAC,IAAI,CAAyB;IACrC,OAAO,CAAC,IAAI,CAAkB;IAC9B,OAAO,CAAC,eAAe,CAA8B;IACrD,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,aAAa,CAAyB;IAC9C,OAAO,CAAC,MAAM,CAAmC;IACjD,OAAO,CAAC,kBAAkB,CAAM;IAChC,OAAO,CAAC,sBAAsB,CAAS;IACvC,OAAO,CAAC,eAAe,CAAqB;IAC5C,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,cAAc,CAAM;IAC5B,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,QAAQ,CAAS;IAEzB,YAAY,OAAO,EAAE,wBAAwB,EAI5C;IAED,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAEzB;IAED,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAM/B;IAEQ,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAGvC;IAED,OAAO,IAAI,IAAI,CAEd;IAED,qBAAqB,IAAI,MAAM,CAE9B;IAED,qBAAqB,IAAI,MAAM,CAE9B;IAED,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,aAAa;IAqCrB,OAAO,CAAC,YAAY;YAMN,WAAW;IAiBzB,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,iBAAiB;IAkBzB,OAAO,CAAC,YAAY;IA4BpB,OAAO,CAAC,iBAAiB;IAgBzB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,aAAa;IAMrB,OAAO,CAAC,MAAM;IAMd,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,gBAAgB;CAQxB","sourcesContent":["import {\n\tContainer,\n\ttype Focusable,\n\tgetKeybindings,\n\ttype SelectItem,\n\tSelectList,\n\tSpacer,\n\tText,\n\tTruncatedText,\n} from \"@earendil-works/pi-tui\";\nimport { DynamicBorder } from \"../../../../modes/interactive/components/dynamic-border.ts\";\nimport { keyHint } from \"../../../../modes/interactive/components/keybinding-hints.ts\";\nimport { getMarkdownTheme, theme } from \"../../../../modes/interactive/theme/theme.ts\";\nimport { shortenPath } from \"../../../../utils/paths.ts\";\nimport type {} from \"../../../keybindings.ts\";\nimport { loadTranscriptSnapshot } from \"./loader.ts\";\nimport { describeSession, pickerLabel, renderLine, sessionAge, viewerFooter } from \"./overlay-format.ts\";\nimport { sanitizeLine } from \"./text.ts\";\nimport { renderTranscript } from \"./transcript.ts\";\nimport type { SessionHudEntry, TranscriptSnapshot, ViewerEntryRange } from \"./types.ts\";\n\nconst MAX_VISIBLE_SESSIONS = 12;\n\ntype Mode = \"picker\" | \"viewer\";\ntype PickerAction = \"tui.select.up\" | \"tui.select.down\" | \"tui.select.confirm\" | \"tui.select.cancel\";\n\nconst PICKER_ACTIONS: readonly PickerAction[] = [\n\t\"tui.select.up\",\n\t\"tui.select.down\",\n\t\"tui.select.confirm\",\n\t\"tui.select.cancel\",\n];\n\ninterface SessionHudOverlayOptions {\n\treadonly sessions: readonly SessionHudEntry[];\n\treadonly done: () => void;\n\treadonly requestRender: () => void;\n}\n\nexport class SessionHudOverlay extends Container implements Focusable {\n\tprivate readonly options: SessionHudOverlayOptions;\n\tprivate readonly sessionsByValue = new Map<string, SessionHudEntry>();\n\tprivate readonly topBorder = new DynamicBorder((text) => theme.fg(\"accent\", text));\n\tprivate readonly middleBorder = new DynamicBorder((text) => theme.fg(\"accent\", text));\n\tprivate readonly bottomBorder = new DynamicBorder((text) => theme.fg(\"accent\", text));\n\tprivate list: SelectList | undefined;\n\tprivate mode: Mode = \"picker\";\n\tprivate selectedSession: SessionHudEntry | undefined;\n\tprivate snapshot: TranscriptSnapshot | undefined;\n\tprivate renderedLines: readonly string[] = [];\n\tprivate ranges: readonly ViewerEntryRange[] = [];\n\tprivate selectedEntryIndex = -1;\n\tprivate shouldSelectLastOnLoad = false;\n\tprivate expandedEntries = new Set<number>();\n\tprivate scrollOffset = 0;\n\tprivate viewportHeight = 12;\n\tprivate loadingText: string | undefined;\n\tprivate _focused = false;\n\n\tconstructor(options: SessionHudOverlayOptions) {\n\t\tsuper();\n\t\tthis.options = options;\n\t\tthis.rebuildPicker();\n\t}\n\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t}\n\n\thandleInput(input: string): void {\n\t\tif (this.mode === \"picker\") {\n\t\t\tthis.handlePickerInput(input);\n\t\t\treturn;\n\t\t}\n\t\tthis.handleViewerInput(input);\n\t}\n\n\toverride render(width: number): string[] {\n\t\tif (this.mode === \"picker\") return super.render(width);\n\t\treturn this.renderViewer(width);\n\t}\n\n\tgetMode(): Mode {\n\t\treturn this.mode;\n\t}\n\n\tgetSelectedEntryIndex(): number {\n\t\treturn this.selectedEntryIndex;\n\t}\n\n\tgetExpandedEntryCount(): number {\n\t\treturn this.expandedEntries.size;\n\t}\n\n\tprivate handlePickerInput(input: string): void {\n\t\tconst keybindings = getKeybindings();\n\t\tif (PICKER_ACTIONS.some((action) => keybindings.matches(input, action))) {\n\t\t\tthis.list?.handleInput(input);\n\t\t}\n\t}\n\n\tprivate rebuildPicker(): void {\n\t\tthis.sessionsByValue.clear();\n\t\tconst items = this.options.sessions.map((session, index) => this.toPickerItem(session, index));\n\t\tconst list = new SelectList(items, Math.min(MAX_VISIBLE_SESSIONS, Math.max(1, items.length)), {\n\t\t\tselectedPrefix: (text) => theme.fg(\"accent\", text),\n\t\t\tselectedText: (text) => text,\n\t\t\tdescription: (text) => theme.fg(\"muted\", text),\n\t\t\tscrollInfo: (text) => theme.fg(\"dim\", text),\n\t\t\tnoMatch: (text) => theme.fg(\"warning\", text.replace(\"commands\", \"sessions\")),\n\t\t});\n\t\tlist.onSelect = (item) => {\n\t\t\tconst session = this.sessionsByValue.get(item.value);\n\t\t\tif (session) void this.openSession(session);\n\t\t};\n\t\tlist.onCancel = () => this.options.done();\n\t\tthis.list = list;\n\t\tthis.clear();\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(this.topBorder);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(\n\t\t\tnew Text(\n\t\t\t\t`${theme.bold(theme.fg(\"accent\", \" Sessions\"))}${theme.fg(\"dim\", ` ${this.options.sessions.length} sessions`)}`,\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(list);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(\n\t\t\tnew TruncatedText(`${keyHint(\"tui.select.confirm\", \"view\")} ${keyHint(\"tui.select.cancel\", \"close\")}`, 0, 0),\n\t\t);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(this.bottomBorder);\n\t}\n\n\tprivate toPickerItem(session: SessionHudEntry, index: number): SelectItem {\n\t\tconst value = String(index);\n\t\tthis.sessionsByValue.set(value, session);\n\t\treturn { value, label: pickerLabel(session), description: describeSession(session) };\n\t}\n\n\tprivate async openSession(session: SessionHudEntry): Promise<void> {\n\t\tthis.mode = \"viewer\";\n\t\tthis.selectedSession = session;\n\t\tthis.snapshot = undefined;\n\t\tthis.loadingText = \"Loading session transcript...\";\n\t\tthis.resetViewerState();\n\t\tthis.options.requestRender();\n\t\ttry {\n\t\t\tthis.snapshot = await loadTranscriptSnapshot(session.path);\n\t\t\tthis.loadingText = undefined;\n\t\t} catch (error) {\n\t\t\tthis.loadingText = `Failed to read session: ${error instanceof Error ? error.message : String(error)}`;\n\t\t}\n\t\tthis.rebuildTranscript(process.stdout.columns || 80);\n\t\tthis.options.requestRender();\n\t}\n\n\tprivate resetViewerState(): void {\n\t\tthis.expandedEntries = new Set<number>();\n\t\tthis.scrollOffset = 0;\n\t\tthis.selectedEntryIndex = -1;\n\t\tthis.shouldSelectLastOnLoad = true;\n\t\tthis.renderedLines = [];\n\t\tthis.ranges = [];\n\t}\n\n\tprivate rebuildTranscript(width: number): void {\n\t\tif (!this.snapshot) return;\n\t\tconst rendered = renderTranscript(this.snapshot.entries, {\n\t\t\twidth,\n\t\t\tselectedIndex: this.selectedEntryIndex,\n\t\t\texpandedEntries: this.expandedEntries,\n\t\t\tmarkdownTheme: getMarkdownTheme(),\n\t\t});\n\t\tthis.renderedLines = rendered.lines;\n\t\tthis.ranges = rendered.ranges;\n\t\tif (this.ranges.length > 0 && this.shouldSelectLastOnLoad) {\n\t\t\tthis.shouldSelectLastOnLoad = false;\n\t\t\tthis.selectedEntryIndex = this.ranges.length - 1;\n\t\t\tthis.rebuildTranscript(width);\n\t\t}\n\t\tthis.scrollToSelected();\n\t}\n\n\tprivate renderViewer(width: number): string[] {\n\t\tthis.viewportHeight = Math.max(5, (process.stdout.rows || 32) - 8);\n\t\tthis.rebuildTranscript(width);\n\t\tconst session = this.selectedSession;\n\t\tconst title = session ? `Sessions > ${shortenPath(session.cwd) || \"unknown\"} · ${session.shortId}` : \"Sessions\";\n\t\tconst status = session\n\t\t\t? `${session.messageCount} messages · ${sessionAge(session)}${this.snapshot?.model ? ` · ${this.snapshot.model}` : \"\"}`\n\t\t\t: \"\";\n\t\tconst maxScroll = Math.max(0, this.renderedLines.length - this.viewportHeight);\n\t\tthis.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxScroll));\n\t\tconst content = this.loadingText ? [theme.fg(\"dim\", this.loadingText)] : this.renderedLines;\n\t\tconst visible = content.slice(this.scrollOffset, this.scrollOffset + this.viewportHeight);\n\t\tconst lines: string[] = [];\n\t\tlines.push(...this.topBorder.render(width));\n\t\tlines.push(renderLine(` ${theme.bold(theme.fg(\"accent\", title))}`, width));\n\t\tif (status) lines.push(renderLine(` ${theme.fg(\"dim\", status)}`, width));\n\t\tlines.push(...this.middleBorder.render(width));\n\t\tfor (const line of visible) lines.push(` ${sanitizeLine(line, width - 2)}`);\n\t\tfor (let index = visible.length; index < this.viewportHeight; index += 1) lines.push(\"\");\n\t\tconst scroll =\n\t\t\tcontent.length > this.viewportHeight\n\t\t\t\t? ` [${this.scrollOffset + 1}-${Math.min(this.scrollOffset + this.viewportHeight, content.length)}/${content.length}]`\n\t\t\t\t: \"\";\n\t\tlines.push(renderLine(` ${viewerFooter(scroll)}`, width));\n\t\tlines.push(...this.bottomBorder.render(width));\n\t\treturn lines;\n\t}\n\n\tprivate handleViewerInput(input: string): void {\n\t\tconst keybindings = getKeybindings();\n\t\tif (keybindings.matches(input, \"app.sessions.observe\")) {\n\t\t\tthis.options.done();\n\t\t\treturn;\n\t\t} else if (keybindings.matches(input, \"tui.select.cancel\")) this.backToPicker();\n\t\telse if (input === \"j\" || keybindings.matches(input, \"tui.select.down\")) this.moveSelection(1);\n\t\telse if (input === \"k\" || keybindings.matches(input, \"tui.select.up\")) this.moveSelection(-1);\n\t\telse if (keybindings.matches(input, \"tui.select.pageDown\")) this.moveSelection(5);\n\t\telse if (keybindings.matches(input, \"tui.select.pageUp\")) this.moveSelection(-5);\n\t\telse if (input === \"g\") this.jumpTo(0);\n\t\telse if (input === \"G\") this.jumpTo(this.ranges.length - 1);\n\t\telse if (keybindings.matches(input, \"tui.select.confirm\")) this.toggleExpanded();\n\t\tthis.options.requestRender();\n\t}\n\n\tprivate backToPicker(): void {\n\t\tthis.mode = \"picker\";\n\t\tthis.rebuildPicker();\n\t}\n\n\tprivate moveSelection(delta: number): void {\n\t\tif (this.ranges.length === 0) return;\n\t\tthis.selectedEntryIndex = Math.max(0, Math.min(this.selectedEntryIndex + delta, this.ranges.length - 1));\n\t\tthis.scrollToSelected();\n\t}\n\n\tprivate jumpTo(index: number): void {\n\t\tif (this.ranges.length === 0) return;\n\t\tthis.selectedEntryIndex = Math.max(0, Math.min(index, this.ranges.length - 1));\n\t\tthis.scrollToSelected();\n\t}\n\n\tprivate toggleExpanded(): void {\n\t\tif (this.ranges.length === 0) return;\n\t\tif (this.expandedEntries.has(this.selectedEntryIndex)) this.expandedEntries.delete(this.selectedEntryIndex);\n\t\telse this.expandedEntries.add(this.selectedEntryIndex);\n\t}\n\n\tprivate scrollToSelected(): void {\n\t\tconst selected = this.ranges[this.selectedEntryIndex];\n\t\tif (!selected) return;\n\t\tconst bottom = selected.lineStart + selected.lineCount;\n\t\tif (selected.lineStart < this.scrollOffset) this.scrollOffset = Math.max(0, selected.lineStart - 1);\n\t\tif (bottom > this.scrollOffset + this.viewportHeight)\n\t\t\tthis.scrollOffset = Math.max(0, bottom - this.viewportHeight + 1);\n\t}\n}\n"]}
@@ -0,0 +1,239 @@
1
+ import { Container, getKeybindings, SelectList, Spacer, Text, TruncatedText, } from "@earendil-works/pi-tui";
2
+ import { DynamicBorder } from "../../../../modes/interactive/components/dynamic-border.js";
3
+ import { keyHint } from "../../../../modes/interactive/components/keybinding-hints.js";
4
+ import { getMarkdownTheme, theme } from "../../../../modes/interactive/theme/theme.js";
5
+ import { shortenPath } from "../../../../utils/paths.js";
6
+ import { loadTranscriptSnapshot } from "./loader.js";
7
+ import { describeSession, pickerLabel, renderLine, sessionAge, viewerFooter } from "./overlay-format.js";
8
+ import { sanitizeLine } from "./text.js";
9
+ import { renderTranscript } from "./transcript.js";
10
+ const MAX_VISIBLE_SESSIONS = 12;
11
+ const PICKER_ACTIONS = [
12
+ "tui.select.up",
13
+ "tui.select.down",
14
+ "tui.select.confirm",
15
+ "tui.select.cancel",
16
+ ];
17
+ export class SessionHudOverlay extends Container {
18
+ options;
19
+ sessionsByValue = new Map();
20
+ topBorder = new DynamicBorder((text) => theme.fg("accent", text));
21
+ middleBorder = new DynamicBorder((text) => theme.fg("accent", text));
22
+ bottomBorder = new DynamicBorder((text) => theme.fg("accent", text));
23
+ list;
24
+ mode = "picker";
25
+ selectedSession;
26
+ snapshot;
27
+ renderedLines = [];
28
+ ranges = [];
29
+ selectedEntryIndex = -1;
30
+ shouldSelectLastOnLoad = false;
31
+ expandedEntries = new Set();
32
+ scrollOffset = 0;
33
+ viewportHeight = 12;
34
+ loadingText;
35
+ _focused = false;
36
+ constructor(options) {
37
+ super();
38
+ this.options = options;
39
+ this.rebuildPicker();
40
+ }
41
+ get focused() {
42
+ return this._focused;
43
+ }
44
+ set focused(value) {
45
+ this._focused = value;
46
+ }
47
+ handleInput(input) {
48
+ if (this.mode === "picker") {
49
+ this.handlePickerInput(input);
50
+ return;
51
+ }
52
+ this.handleViewerInput(input);
53
+ }
54
+ render(width) {
55
+ if (this.mode === "picker")
56
+ return super.render(width);
57
+ return this.renderViewer(width);
58
+ }
59
+ getMode() {
60
+ return this.mode;
61
+ }
62
+ getSelectedEntryIndex() {
63
+ return this.selectedEntryIndex;
64
+ }
65
+ getExpandedEntryCount() {
66
+ return this.expandedEntries.size;
67
+ }
68
+ handlePickerInput(input) {
69
+ const keybindings = getKeybindings();
70
+ if (PICKER_ACTIONS.some((action) => keybindings.matches(input, action))) {
71
+ this.list?.handleInput(input);
72
+ }
73
+ }
74
+ rebuildPicker() {
75
+ this.sessionsByValue.clear();
76
+ const items = this.options.sessions.map((session, index) => this.toPickerItem(session, index));
77
+ const list = new SelectList(items, Math.min(MAX_VISIBLE_SESSIONS, Math.max(1, items.length)), {
78
+ selectedPrefix: (text) => theme.fg("accent", text),
79
+ selectedText: (text) => text,
80
+ description: (text) => theme.fg("muted", text),
81
+ scrollInfo: (text) => theme.fg("dim", text),
82
+ noMatch: (text) => theme.fg("warning", text.replace("commands", "sessions")),
83
+ });
84
+ list.onSelect = (item) => {
85
+ const session = this.sessionsByValue.get(item.value);
86
+ if (session)
87
+ void this.openSession(session);
88
+ };
89
+ list.onCancel = () => this.options.done();
90
+ this.list = list;
91
+ this.clear();
92
+ this.addChild(new Spacer(1));
93
+ this.addChild(this.topBorder);
94
+ this.addChild(new Spacer(1));
95
+ this.addChild(new Text(`${theme.bold(theme.fg("accent", " Sessions"))}${theme.fg("dim", ` ${this.options.sessions.length} sessions`)}`, 0, 0));
96
+ this.addChild(new Spacer(1));
97
+ this.addChild(list);
98
+ this.addChild(new Spacer(1));
99
+ this.addChild(new TruncatedText(`${keyHint("tui.select.confirm", "view")} ${keyHint("tui.select.cancel", "close")}`, 0, 0));
100
+ this.addChild(new Spacer(1));
101
+ this.addChild(this.bottomBorder);
102
+ }
103
+ toPickerItem(session, index) {
104
+ const value = String(index);
105
+ this.sessionsByValue.set(value, session);
106
+ return { value, label: pickerLabel(session), description: describeSession(session) };
107
+ }
108
+ async openSession(session) {
109
+ this.mode = "viewer";
110
+ this.selectedSession = session;
111
+ this.snapshot = undefined;
112
+ this.loadingText = "Loading session transcript...";
113
+ this.resetViewerState();
114
+ this.options.requestRender();
115
+ try {
116
+ this.snapshot = await loadTranscriptSnapshot(session.path);
117
+ this.loadingText = undefined;
118
+ }
119
+ catch (error) {
120
+ this.loadingText = `Failed to read session: ${error instanceof Error ? error.message : String(error)}`;
121
+ }
122
+ this.rebuildTranscript(process.stdout.columns || 80);
123
+ this.options.requestRender();
124
+ }
125
+ resetViewerState() {
126
+ this.expandedEntries = new Set();
127
+ this.scrollOffset = 0;
128
+ this.selectedEntryIndex = -1;
129
+ this.shouldSelectLastOnLoad = true;
130
+ this.renderedLines = [];
131
+ this.ranges = [];
132
+ }
133
+ rebuildTranscript(width) {
134
+ if (!this.snapshot)
135
+ return;
136
+ const rendered = renderTranscript(this.snapshot.entries, {
137
+ width,
138
+ selectedIndex: this.selectedEntryIndex,
139
+ expandedEntries: this.expandedEntries,
140
+ markdownTheme: getMarkdownTheme(),
141
+ });
142
+ this.renderedLines = rendered.lines;
143
+ this.ranges = rendered.ranges;
144
+ if (this.ranges.length > 0 && this.shouldSelectLastOnLoad) {
145
+ this.shouldSelectLastOnLoad = false;
146
+ this.selectedEntryIndex = this.ranges.length - 1;
147
+ this.rebuildTranscript(width);
148
+ }
149
+ this.scrollToSelected();
150
+ }
151
+ renderViewer(width) {
152
+ this.viewportHeight = Math.max(5, (process.stdout.rows || 32) - 8);
153
+ this.rebuildTranscript(width);
154
+ const session = this.selectedSession;
155
+ const title = session ? `Sessions > ${shortenPath(session.cwd) || "unknown"} · ${session.shortId}` : "Sessions";
156
+ const status = session
157
+ ? `${session.messageCount} messages · ${sessionAge(session)}${this.snapshot?.model ? ` · ${this.snapshot.model}` : ""}`
158
+ : "";
159
+ const maxScroll = Math.max(0, this.renderedLines.length - this.viewportHeight);
160
+ this.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxScroll));
161
+ const content = this.loadingText ? [theme.fg("dim", this.loadingText)] : this.renderedLines;
162
+ const visible = content.slice(this.scrollOffset, this.scrollOffset + this.viewportHeight);
163
+ const lines = [];
164
+ lines.push(...this.topBorder.render(width));
165
+ lines.push(renderLine(` ${theme.bold(theme.fg("accent", title))}`, width));
166
+ if (status)
167
+ lines.push(renderLine(` ${theme.fg("dim", status)}`, width));
168
+ lines.push(...this.middleBorder.render(width));
169
+ for (const line of visible)
170
+ lines.push(` ${sanitizeLine(line, width - 2)}`);
171
+ for (let index = visible.length; index < this.viewportHeight; index += 1)
172
+ lines.push("");
173
+ const scroll = content.length > this.viewportHeight
174
+ ? ` [${this.scrollOffset + 1}-${Math.min(this.scrollOffset + this.viewportHeight, content.length)}/${content.length}]`
175
+ : "";
176
+ lines.push(renderLine(` ${viewerFooter(scroll)}`, width));
177
+ lines.push(...this.bottomBorder.render(width));
178
+ return lines;
179
+ }
180
+ handleViewerInput(input) {
181
+ const keybindings = getKeybindings();
182
+ if (keybindings.matches(input, "app.sessions.observe")) {
183
+ this.options.done();
184
+ return;
185
+ }
186
+ else if (keybindings.matches(input, "tui.select.cancel"))
187
+ this.backToPicker();
188
+ else if (input === "j" || keybindings.matches(input, "tui.select.down"))
189
+ this.moveSelection(1);
190
+ else if (input === "k" || keybindings.matches(input, "tui.select.up"))
191
+ this.moveSelection(-1);
192
+ else if (keybindings.matches(input, "tui.select.pageDown"))
193
+ this.moveSelection(5);
194
+ else if (keybindings.matches(input, "tui.select.pageUp"))
195
+ this.moveSelection(-5);
196
+ else if (input === "g")
197
+ this.jumpTo(0);
198
+ else if (input === "G")
199
+ this.jumpTo(this.ranges.length - 1);
200
+ else if (keybindings.matches(input, "tui.select.confirm"))
201
+ this.toggleExpanded();
202
+ this.options.requestRender();
203
+ }
204
+ backToPicker() {
205
+ this.mode = "picker";
206
+ this.rebuildPicker();
207
+ }
208
+ moveSelection(delta) {
209
+ if (this.ranges.length === 0)
210
+ return;
211
+ this.selectedEntryIndex = Math.max(0, Math.min(this.selectedEntryIndex + delta, this.ranges.length - 1));
212
+ this.scrollToSelected();
213
+ }
214
+ jumpTo(index) {
215
+ if (this.ranges.length === 0)
216
+ return;
217
+ this.selectedEntryIndex = Math.max(0, Math.min(index, this.ranges.length - 1));
218
+ this.scrollToSelected();
219
+ }
220
+ toggleExpanded() {
221
+ if (this.ranges.length === 0)
222
+ return;
223
+ if (this.expandedEntries.has(this.selectedEntryIndex))
224
+ this.expandedEntries.delete(this.selectedEntryIndex);
225
+ else
226
+ this.expandedEntries.add(this.selectedEntryIndex);
227
+ }
228
+ scrollToSelected() {
229
+ const selected = this.ranges[this.selectedEntryIndex];
230
+ if (!selected)
231
+ return;
232
+ const bottom = selected.lineStart + selected.lineCount;
233
+ if (selected.lineStart < this.scrollOffset)
234
+ this.scrollOffset = Math.max(0, selected.lineStart - 1);
235
+ if (bottom > this.scrollOffset + this.viewportHeight)
236
+ this.scrollOffset = Math.max(0, bottom - this.viewportHeight + 1);
237
+ }
238
+ }
239
+ //# sourceMappingURL=overlay.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"overlay.js","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/session-observer/overlay.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\tContainer,\n\ttype Focusable,\n\tgetKeybindings,\n\ttype SelectItem,\n\tSelectList,\n\tSpacer,\n\tText,\n\tTruncatedText,\n} from \"@earendil-works/pi-tui\";\nimport { DynamicBorder } from \"../../../../modes/interactive/components/dynamic-border.ts\";\nimport { keyHint } from \"../../../../modes/interactive/components/keybinding-hints.ts\";\nimport { getMarkdownTheme, theme } from \"../../../../modes/interactive/theme/theme.ts\";\nimport { shortenPath } from \"../../../../utils/paths.ts\";\nimport type {} from \"../../../keybindings.ts\";\nimport { loadTranscriptSnapshot } from \"./loader.ts\";\nimport { describeSession, pickerLabel, renderLine, sessionAge, viewerFooter } from \"./overlay-format.ts\";\nimport { sanitizeLine } from \"./text.ts\";\nimport { renderTranscript } from \"./transcript.ts\";\nimport type { SessionHudEntry, TranscriptSnapshot, ViewerEntryRange } from \"./types.ts\";\n\nconst MAX_VISIBLE_SESSIONS = 12;\n\ntype Mode = \"picker\" | \"viewer\";\ntype PickerAction = \"tui.select.up\" | \"tui.select.down\" | \"tui.select.confirm\" | \"tui.select.cancel\";\n\nconst PICKER_ACTIONS: readonly PickerAction[] = [\n\t\"tui.select.up\",\n\t\"tui.select.down\",\n\t\"tui.select.confirm\",\n\t\"tui.select.cancel\",\n];\n\ninterface SessionHudOverlayOptions {\n\treadonly sessions: readonly SessionHudEntry[];\n\treadonly done: () => void;\n\treadonly requestRender: () => void;\n}\n\nexport class SessionHudOverlay extends Container implements Focusable {\n\tprivate readonly options: SessionHudOverlayOptions;\n\tprivate readonly sessionsByValue = new Map<string, SessionHudEntry>();\n\tprivate readonly topBorder = new DynamicBorder((text) => theme.fg(\"accent\", text));\n\tprivate readonly middleBorder = new DynamicBorder((text) => theme.fg(\"accent\", text));\n\tprivate readonly bottomBorder = new DynamicBorder((text) => theme.fg(\"accent\", text));\n\tprivate list: SelectList | undefined;\n\tprivate mode: Mode = \"picker\";\n\tprivate selectedSession: SessionHudEntry | undefined;\n\tprivate snapshot: TranscriptSnapshot | undefined;\n\tprivate renderedLines: readonly string[] = [];\n\tprivate ranges: readonly ViewerEntryRange[] = [];\n\tprivate selectedEntryIndex = -1;\n\tprivate shouldSelectLastOnLoad = false;\n\tprivate expandedEntries = new Set<number>();\n\tprivate scrollOffset = 0;\n\tprivate viewportHeight = 12;\n\tprivate loadingText: string | undefined;\n\tprivate _focused = false;\n\n\tconstructor(options: SessionHudOverlayOptions) {\n\t\tsuper();\n\t\tthis.options = options;\n\t\tthis.rebuildPicker();\n\t}\n\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t}\n\n\thandleInput(input: string): void {\n\t\tif (this.mode === \"picker\") {\n\t\t\tthis.handlePickerInput(input);\n\t\t\treturn;\n\t\t}\n\t\tthis.handleViewerInput(input);\n\t}\n\n\toverride render(width: number): string[] {\n\t\tif (this.mode === \"picker\") return super.render(width);\n\t\treturn this.renderViewer(width);\n\t}\n\n\tgetMode(): Mode {\n\t\treturn this.mode;\n\t}\n\n\tgetSelectedEntryIndex(): number {\n\t\treturn this.selectedEntryIndex;\n\t}\n\n\tgetExpandedEntryCount(): number {\n\t\treturn this.expandedEntries.size;\n\t}\n\n\tprivate handlePickerInput(input: string): void {\n\t\tconst keybindings = getKeybindings();\n\t\tif (PICKER_ACTIONS.some((action) => keybindings.matches(input, action))) {\n\t\t\tthis.list?.handleInput(input);\n\t\t}\n\t}\n\n\tprivate rebuildPicker(): void {\n\t\tthis.sessionsByValue.clear();\n\t\tconst items = this.options.sessions.map((session, index) => this.toPickerItem(session, index));\n\t\tconst list = new SelectList(items, Math.min(MAX_VISIBLE_SESSIONS, Math.max(1, items.length)), {\n\t\t\tselectedPrefix: (text) => theme.fg(\"accent\", text),\n\t\t\tselectedText: (text) => text,\n\t\t\tdescription: (text) => theme.fg(\"muted\", text),\n\t\t\tscrollInfo: (text) => theme.fg(\"dim\", text),\n\t\t\tnoMatch: (text) => theme.fg(\"warning\", text.replace(\"commands\", \"sessions\")),\n\t\t});\n\t\tlist.onSelect = (item) => {\n\t\t\tconst session = this.sessionsByValue.get(item.value);\n\t\t\tif (session) void this.openSession(session);\n\t\t};\n\t\tlist.onCancel = () => this.options.done();\n\t\tthis.list = list;\n\t\tthis.clear();\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(this.topBorder);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(\n\t\t\tnew Text(\n\t\t\t\t`${theme.bold(theme.fg(\"accent\", \" Sessions\"))}${theme.fg(\"dim\", ` ${this.options.sessions.length} sessions`)}`,\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(list);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(\n\t\t\tnew TruncatedText(`${keyHint(\"tui.select.confirm\", \"view\")} ${keyHint(\"tui.select.cancel\", \"close\")}`, 0, 0),\n\t\t);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(this.bottomBorder);\n\t}\n\n\tprivate toPickerItem(session: SessionHudEntry, index: number): SelectItem {\n\t\tconst value = String(index);\n\t\tthis.sessionsByValue.set(value, session);\n\t\treturn { value, label: pickerLabel(session), description: describeSession(session) };\n\t}\n\n\tprivate async openSession(session: SessionHudEntry): Promise<void> {\n\t\tthis.mode = \"viewer\";\n\t\tthis.selectedSession = session;\n\t\tthis.snapshot = undefined;\n\t\tthis.loadingText = \"Loading session transcript...\";\n\t\tthis.resetViewerState();\n\t\tthis.options.requestRender();\n\t\ttry {\n\t\t\tthis.snapshot = await loadTranscriptSnapshot(session.path);\n\t\t\tthis.loadingText = undefined;\n\t\t} catch (error) {\n\t\t\tthis.loadingText = `Failed to read session: ${error instanceof Error ? error.message : String(error)}`;\n\t\t}\n\t\tthis.rebuildTranscript(process.stdout.columns || 80);\n\t\tthis.options.requestRender();\n\t}\n\n\tprivate resetViewerState(): void {\n\t\tthis.expandedEntries = new Set<number>();\n\t\tthis.scrollOffset = 0;\n\t\tthis.selectedEntryIndex = -1;\n\t\tthis.shouldSelectLastOnLoad = true;\n\t\tthis.renderedLines = [];\n\t\tthis.ranges = [];\n\t}\n\n\tprivate rebuildTranscript(width: number): void {\n\t\tif (!this.snapshot) return;\n\t\tconst rendered = renderTranscript(this.snapshot.entries, {\n\t\t\twidth,\n\t\t\tselectedIndex: this.selectedEntryIndex,\n\t\t\texpandedEntries: this.expandedEntries,\n\t\t\tmarkdownTheme: getMarkdownTheme(),\n\t\t});\n\t\tthis.renderedLines = rendered.lines;\n\t\tthis.ranges = rendered.ranges;\n\t\tif (this.ranges.length > 0 && this.shouldSelectLastOnLoad) {\n\t\t\tthis.shouldSelectLastOnLoad = false;\n\t\t\tthis.selectedEntryIndex = this.ranges.length - 1;\n\t\t\tthis.rebuildTranscript(width);\n\t\t}\n\t\tthis.scrollToSelected();\n\t}\n\n\tprivate renderViewer(width: number): string[] {\n\t\tthis.viewportHeight = Math.max(5, (process.stdout.rows || 32) - 8);\n\t\tthis.rebuildTranscript(width);\n\t\tconst session = this.selectedSession;\n\t\tconst title = session ? `Sessions > ${shortenPath(session.cwd) || \"unknown\"} · ${session.shortId}` : \"Sessions\";\n\t\tconst status = session\n\t\t\t? `${session.messageCount} messages · ${sessionAge(session)}${this.snapshot?.model ? ` · ${this.snapshot.model}` : \"\"}`\n\t\t\t: \"\";\n\t\tconst maxScroll = Math.max(0, this.renderedLines.length - this.viewportHeight);\n\t\tthis.scrollOffset = Math.max(0, Math.min(this.scrollOffset, maxScroll));\n\t\tconst content = this.loadingText ? [theme.fg(\"dim\", this.loadingText)] : this.renderedLines;\n\t\tconst visible = content.slice(this.scrollOffset, this.scrollOffset + this.viewportHeight);\n\t\tconst lines: string[] = [];\n\t\tlines.push(...this.topBorder.render(width));\n\t\tlines.push(renderLine(` ${theme.bold(theme.fg(\"accent\", title))}`, width));\n\t\tif (status) lines.push(renderLine(` ${theme.fg(\"dim\", status)}`, width));\n\t\tlines.push(...this.middleBorder.render(width));\n\t\tfor (const line of visible) lines.push(` ${sanitizeLine(line, width - 2)}`);\n\t\tfor (let index = visible.length; index < this.viewportHeight; index += 1) lines.push(\"\");\n\t\tconst scroll =\n\t\t\tcontent.length > this.viewportHeight\n\t\t\t\t? ` [${this.scrollOffset + 1}-${Math.min(this.scrollOffset + this.viewportHeight, content.length)}/${content.length}]`\n\t\t\t\t: \"\";\n\t\tlines.push(renderLine(` ${viewerFooter(scroll)}`, width));\n\t\tlines.push(...this.bottomBorder.render(width));\n\t\treturn lines;\n\t}\n\n\tprivate handleViewerInput(input: string): void {\n\t\tconst keybindings = getKeybindings();\n\t\tif (keybindings.matches(input, \"app.sessions.observe\")) {\n\t\t\tthis.options.done();\n\t\t\treturn;\n\t\t} else if (keybindings.matches(input, \"tui.select.cancel\")) this.backToPicker();\n\t\telse if (input === \"j\" || keybindings.matches(input, \"tui.select.down\")) this.moveSelection(1);\n\t\telse if (input === \"k\" || keybindings.matches(input, \"tui.select.up\")) this.moveSelection(-1);\n\t\telse if (keybindings.matches(input, \"tui.select.pageDown\")) this.moveSelection(5);\n\t\telse if (keybindings.matches(input, \"tui.select.pageUp\")) this.moveSelection(-5);\n\t\telse if (input === \"g\") this.jumpTo(0);\n\t\telse if (input === \"G\") this.jumpTo(this.ranges.length - 1);\n\t\telse if (keybindings.matches(input, \"tui.select.confirm\")) this.toggleExpanded();\n\t\tthis.options.requestRender();\n\t}\n\n\tprivate backToPicker(): void {\n\t\tthis.mode = \"picker\";\n\t\tthis.rebuildPicker();\n\t}\n\n\tprivate moveSelection(delta: number): void {\n\t\tif (this.ranges.length === 0) return;\n\t\tthis.selectedEntryIndex = Math.max(0, Math.min(this.selectedEntryIndex + delta, this.ranges.length - 1));\n\t\tthis.scrollToSelected();\n\t}\n\n\tprivate jumpTo(index: number): void {\n\t\tif (this.ranges.length === 0) return;\n\t\tthis.selectedEntryIndex = Math.max(0, Math.min(index, this.ranges.length - 1));\n\t\tthis.scrollToSelected();\n\t}\n\n\tprivate toggleExpanded(): void {\n\t\tif (this.ranges.length === 0) return;\n\t\tif (this.expandedEntries.has(this.selectedEntryIndex)) this.expandedEntries.delete(this.selectedEntryIndex);\n\t\telse this.expandedEntries.add(this.selectedEntryIndex);\n\t}\n\n\tprivate scrollToSelected(): void {\n\t\tconst selected = this.ranges[this.selectedEntryIndex];\n\t\tif (!selected) return;\n\t\tconst bottom = selected.lineStart + selected.lineCount;\n\t\tif (selected.lineStart < this.scrollOffset) this.scrollOffset = Math.max(0, selected.lineStart - 1);\n\t\tif (bottom > this.scrollOffset + this.viewportHeight)\n\t\t\tthis.scrollOffset = Math.max(0, bottom - this.viewportHeight + 1);\n\t}\n}\n"]}
@@ -0,0 +1,10 @@
1
+ import type { SessionHudEntry } from "./types.ts";
2
+ interface PathLike {
3
+ resolve(path: string): string;
4
+ relative(from: string, to: string): string;
5
+ isAbsolute(path: string): boolean;
6
+ }
7
+ export declare function resolveSessionHudRoot(currentSessionDir: string, defaultSessionsRoot?: string, pathImpl?: PathLike): string;
8
+ export declare function scanSessionHudEntries(root: string, currentSessionFile?: string): Promise<readonly SessionHudEntry[]>;
9
+ export {};
10
+ //# sourceMappingURL=scanner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../../../../src/core/extensions/builtin/session-observer/scanner.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,UAAU,QAAQ;IACjB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CAClC;AAED,wBAAgB,qBAAqB,CACpC,iBAAiB,EAAE,MAAM,EACzB,mBAAmB,GAAE,MAAyB,EAC9C,QAAQ,GAAE,QAA4C,GACpD,MAAM,CAQR;AAsGD,wBAAsB,qBAAqB,CAC1C,IAAI,EAAE,MAAM,EACZ,kBAAkB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,SAAS,eAAe,EAAE,CAAC,CAarC","sourcesContent":["import type { Stats } from \"node:fs\";\nimport { readdir, readFile, stat } from \"node:fs/promises\";\nimport { basename, isAbsolute, join, relative, resolve } from \"node:path\";\nimport { getSessionsDir } from \"../../../../config.ts\";\nimport type { FileEntry, SessionHeader } from \"../../../session-manager.ts\";\nimport { parseSessionEntries } from \"../../../session-manager.ts\";\nimport { compactWhitespace, getTextContent } from \"./text.ts\";\nimport type { SessionHudEntry } from \"./types.ts\";\n\ninterface PathLike {\n\tresolve(path: string): string;\n\trelative(from: string, to: string): string;\n\tisAbsolute(path: string): boolean;\n}\n\nexport function resolveSessionHudRoot(\n\tcurrentSessionDir: string,\n\tdefaultSessionsRoot: string = getSessionsDir(),\n\tpathImpl: PathLike = { resolve, relative, isAbsolute },\n): string {\n\tconst defaultRoot = pathImpl.resolve(defaultSessionsRoot);\n\tif (!currentSessionDir) return defaultRoot;\n\tconst current = pathImpl.resolve(currentSessionDir);\n\tif (current === defaultRoot) return defaultRoot;\n\tconst rel = pathImpl.relative(defaultRoot, current);\n\tif (rel && !rel.startsWith(\"..\") && !pathImpl.isAbsolute(rel)) return defaultRoot;\n\treturn current;\n}\n\nfunction hasErrorCode(error: unknown, code: string): boolean {\n\treturn error instanceof Error && \"code\" in error && error.code === code;\n}\n\nasync function readDirIfExists(path: string): Promise<readonly string[]> {\n\ttry {\n\t\treturn await readdir(path);\n\t} catch (error) {\n\t\tif (hasErrorCode(error, \"ENOENT\")) return [];\n\t\tthrow error;\n\t}\n}\n\nasync function statIfFile(path: string): Promise<Stats | undefined> {\n\ttry {\n\t\tconst fileStat = await stat(path);\n\t\treturn fileStat.isFile() ? fileStat : undefined;\n\t} catch (error) {\n\t\tif (hasErrorCode(error, \"ENOENT\")) return undefined;\n\t\tthrow error;\n\t}\n}\n\nasync function collectFilesInDir(dir: string): Promise<readonly string[]> {\n\tconst names = await readDirIfExists(dir);\n\tconst files: string[] = [];\n\tfor (const name of names) {\n\t\tif (!name.endsWith(\".jsonl\")) continue;\n\t\tconst file = join(dir, name);\n\t\tif (await statIfFile(file)) files.push(file);\n\t}\n\treturn files;\n}\n\nasync function discoverSessionFiles(root: string): Promise<readonly string[]> {\n\tconst names = await readDirIfExists(root);\n\tconst files: string[] = [...(await collectFilesInDir(root))];\n\tfor (const name of names) {\n\t\tif (name.endsWith(\".jsonl\")) continue;\n\t\tconst dir = join(root, name);\n\t\ttry {\n\t\t\tconst dirStat = await stat(dir);\n\t\t\tif (dirStat.isDirectory()) files.push(...(await collectFilesInDir(dir)));\n\t\t} catch (error) {\n\t\t\tif (!hasErrorCode(error, \"ENOENT\")) throw error;\n\t\t}\n\t}\n\treturn files;\n}\n\nfunction firstHeader(entries: readonly FileEntry[], filePath: string): SessionHeader | undefined {\n\tconst header = entries[0];\n\tif (header?.type === \"session\") return header;\n\treturn { type: \"session\", id: basename(filePath, \".jsonl\"), timestamp: new Date(0).toISOString(), cwd: \"\" };\n}\n\nfunction lastUserText(entries: readonly FileEntry[]): string {\n\tfor (let index = entries.length - 1; index >= 0; index -= 1) {\n\t\tconst entry = entries[index];\n\t\tif (entry?.type !== \"message\") continue;\n\t\tconst message = entry.message;\n\t\tif (message.role !== \"user\") continue;\n\t\tconst text = compactWhitespace(getTextContent(message.content));\n\t\tif (text) return text;\n\t}\n\treturn \"(no user prompt)\";\n}\n\nfunction latestMessageTimestamp(entries: readonly FileEntry[], fallback: number): number {\n\tfor (let index = entries.length - 1; index >= 0; index -= 1) {\n\t\tconst entry = entries[index];\n\t\tif (!entry || entry.type === \"session\") continue;\n\t\tconst timestamp = Date.parse(entry.timestamp);\n\t\tif (Number.isFinite(timestamp)) return timestamp;\n\t}\n\treturn fallback;\n}\n\nasync function summarizeSession(\n\tfilePath: string,\n\tcurrentSessionFile: string | undefined,\n): Promise<SessionHudEntry | undefined> {\n\tconst [content, fileStat] = await Promise.all([readFile(filePath, \"utf-8\"), stat(filePath)]);\n\tconst entries = parseSessionEntries(content);\n\tconst header = firstHeader(entries, filePath);\n\tif (!header) return undefined;\n\tconst messageCount = entries.filter((entry) => entry.type === \"message\").length;\n\treturn {\n\t\tid: header.id,\n\t\tshortId: header.id.length <= 8 ? header.id : header.id.slice(0, 8),\n\t\tpath: filePath,\n\t\tcwd: header.cwd,\n\t\tcreatedAt: Date.parse(header.timestamp),\n\t\tmodifiedAt: latestMessageTimestamp(entries, fileStat.mtime.getTime()),\n\t\tmessageCount,\n\t\tlastUserText: lastUserText(entries),\n\t\tisCurrent: currentSessionFile === filePath,\n\t};\n}\n\nexport async function scanSessionHudEntries(\n\troot: string,\n\tcurrentSessionFile?: string,\n): Promise<readonly SessionHudEntry[]> {\n\tconst files = await discoverSessionFiles(root);\n\tconst sessions: SessionHudEntry[] = [];\n\tfor (const file of files) {\n\t\ttry {\n\t\t\tconst session = await summarizeSession(file, currentSessionFile);\n\t\t\tif (session) sessions.push(session);\n\t\t} catch (error) {\n\t\t\tif (!hasErrorCode(error, \"ENOENT\")) throw error;\n\t\t}\n\t}\n\tsessions.sort((left, right) => right.modifiedAt - left.modifiedAt);\n\treturn sessions;\n}\n"]}