@codex-infinity/pi-infinity 0.52.4 → 0.60.2

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 (269) hide show
  1. package/CHANGELOG.md +387 -0
  2. package/README.md +97 -66
  3. package/dist/bun/cli.d.ts +3 -0
  4. package/dist/bun/cli.d.ts.map +1 -0
  5. package/dist/bun/cli.js +6 -0
  6. package/dist/bun/cli.js.map +1 -0
  7. package/dist/bun/register-bedrock.d.ts +2 -0
  8. package/dist/bun/register-bedrock.d.ts.map +1 -0
  9. package/dist/bun/register-bedrock.js +4 -0
  10. package/dist/bun/register-bedrock.js.map +1 -0
  11. package/dist/cli/args.d.ts +2 -0
  12. package/dist/cli/args.d.ts.map +1 -1
  13. package/dist/cli/args.js +17 -6
  14. package/dist/cli/args.js.map +1 -1
  15. package/dist/cli/initial-message.d.ts +18 -0
  16. package/dist/cli/initial-message.d.ts.map +1 -0
  17. package/dist/cli/initial-message.js +22 -0
  18. package/dist/cli/initial-message.js.map +1 -0
  19. package/dist/cli.d.ts.map +1 -1
  20. package/dist/cli.js +2 -0
  21. package/dist/cli.js.map +1 -1
  22. package/dist/core/agent-session.d.ts +42 -6
  23. package/dist/core/agent-session.d.ts.map +1 -1
  24. package/dist/core/agent-session.js +346 -72
  25. package/dist/core/agent-session.js.map +1 -1
  26. package/dist/core/auth-storage.d.ts +1 -0
  27. package/dist/core/auth-storage.d.ts.map +1 -1
  28. package/dist/core/auth-storage.js +27 -2
  29. package/dist/core/auth-storage.js.map +1 -1
  30. package/dist/core/bash-executor.d.ts +6 -7
  31. package/dist/core/bash-executor.d.ts.map +1 -1
  32. package/dist/core/bash-executor.js +8 -107
  33. package/dist/core/bash-executor.js.map +1 -1
  34. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  35. package/dist/core/compaction/branch-summarization.js +1 -0
  36. package/dist/core/compaction/branch-summarization.js.map +1 -1
  37. package/dist/core/compaction/compaction.d.ts.map +1 -1
  38. package/dist/core/compaction/compaction.js +6 -1
  39. package/dist/core/compaction/compaction.js.map +1 -1
  40. package/dist/core/compaction/utils.d.ts +3 -0
  41. package/dist/core/compaction/utils.d.ts.map +1 -1
  42. package/dist/core/compaction/utils.js +16 -1
  43. package/dist/core/compaction/utils.js.map +1 -1
  44. package/dist/core/exec.d.ts.map +1 -1
  45. package/dist/core/exec.js +7 -3
  46. package/dist/core/exec.js.map +1 -1
  47. package/dist/core/export-html/index.d.ts +5 -2
  48. package/dist/core/export-html/index.d.ts.map +1 -1
  49. package/dist/core/export-html/index.js +4 -3
  50. package/dist/core/export-html/index.js.map +1 -1
  51. package/dist/core/export-html/template.js +11 -14
  52. package/dist/core/export-html/tool-renderer.d.ts +5 -2
  53. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  54. package/dist/core/export-html/tool-renderer.js +17 -4
  55. package/dist/core/export-html/tool-renderer.js.map +1 -1
  56. package/dist/core/extensions/index.d.ts +2 -2
  57. package/dist/core/extensions/index.d.ts.map +1 -1
  58. package/dist/core/extensions/index.js +1 -1
  59. package/dist/core/extensions/index.js.map +1 -1
  60. package/dist/core/extensions/loader.d.ts.map +1 -1
  61. package/dist/core/extensions/loader.js +37 -11
  62. package/dist/core/extensions/loader.js.map +1 -1
  63. package/dist/core/extensions/runner.d.ts +8 -4
  64. package/dist/core/extensions/runner.d.ts.map +1 -1
  65. package/dist/core/extensions/runner.js +77 -8
  66. package/dist/core/extensions/runner.js.map +1 -1
  67. package/dist/core/extensions/types.d.ts +56 -4
  68. package/dist/core/extensions/types.d.ts.map +1 -1
  69. package/dist/core/extensions/types.js.map +1 -1
  70. package/dist/core/extensions/wrapper.d.ts +4 -11
  71. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  72. package/dist/core/extensions/wrapper.js +4 -78
  73. package/dist/core/extensions/wrapper.js.map +1 -1
  74. package/dist/core/footer-data-provider.d.ts +6 -1
  75. package/dist/core/footer-data-provider.d.ts.map +1 -1
  76. package/dist/core/footer-data-provider.js +83 -37
  77. package/dist/core/footer-data-provider.js.map +1 -1
  78. package/dist/core/index.d.ts +1 -1
  79. package/dist/core/index.d.ts.map +1 -1
  80. package/dist/core/index.js +1 -1
  81. package/dist/core/index.js.map +1 -1
  82. package/dist/core/keybindings.d.ts +3 -0
  83. package/dist/core/keybindings.d.ts.map +1 -1
  84. package/dist/core/keybindings.js +22 -12
  85. package/dist/core/keybindings.js.map +1 -1
  86. package/dist/core/model-registry.d.ts +11 -0
  87. package/dist/core/model-registry.d.ts.map +1 -1
  88. package/dist/core/model-registry.js +56 -16
  89. package/dist/core/model-registry.js.map +1 -1
  90. package/dist/core/model-resolver.d.ts +6 -0
  91. package/dist/core/model-resolver.d.ts.map +1 -1
  92. package/dist/core/model-resolver.js +122 -39
  93. package/dist/core/model-resolver.js.map +1 -1
  94. package/dist/core/package-manager.d.ts +19 -1
  95. package/dist/core/package-manager.d.ts.map +1 -1
  96. package/dist/core/package-manager.js +290 -57
  97. package/dist/core/package-manager.js.map +1 -1
  98. package/dist/core/resolve-config-value.d.ts.map +1 -1
  99. package/dist/core/resolve-config-value.js +43 -8
  100. package/dist/core/resolve-config-value.js.map +1 -1
  101. package/dist/core/resource-loader.d.ts.map +1 -1
  102. package/dist/core/resource-loader.js +4 -7
  103. package/dist/core/resource-loader.js.map +1 -1
  104. package/dist/core/sdk.d.ts +1 -1
  105. package/dist/core/sdk.d.ts.map +1 -1
  106. package/dist/core/sdk.js +7 -0
  107. package/dist/core/sdk.js.map +1 -1
  108. package/dist/core/session-manager.d.ts +1 -0
  109. package/dist/core/session-manager.d.ts.map +1 -1
  110. package/dist/core/session-manager.js +21 -15
  111. package/dist/core/session-manager.js.map +1 -1
  112. package/dist/core/settings-manager.d.ts +10 -0
  113. package/dist/core/settings-manager.d.ts.map +1 -1
  114. package/dist/core/settings-manager.js +59 -5
  115. package/dist/core/settings-manager.js.map +1 -1
  116. package/dist/core/skills.d.ts +3 -2
  117. package/dist/core/skills.d.ts.map +1 -1
  118. package/dist/core/skills.js +29 -8
  119. package/dist/core/skills.js.map +1 -1
  120. package/dist/core/slash-commands.d.ts.map +1 -1
  121. package/dist/core/slash-commands.js +3 -2
  122. package/dist/core/slash-commands.js.map +1 -1
  123. package/dist/core/system-prompt.d.ts +4 -0
  124. package/dist/core/system-prompt.d.ts.map +1 -1
  125. package/dist/core/system-prompt.js +43 -29
  126. package/dist/core/system-prompt.js.map +1 -1
  127. package/dist/core/tools/bash.d.ts +8 -0
  128. package/dist/core/tools/bash.d.ts.map +1 -1
  129. package/dist/core/tools/bash.js +77 -69
  130. package/dist/core/tools/bash.js.map +1 -1
  131. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  132. package/dist/core/tools/edit-diff.js +1 -0
  133. package/dist/core/tools/edit-diff.js.map +1 -1
  134. package/dist/core/tools/find.d.ts.map +1 -1
  135. package/dist/core/tools/find.js +6 -3
  136. package/dist/core/tools/find.js.map +1 -1
  137. package/dist/core/tools/index.d.ts +1 -1
  138. package/dist/core/tools/index.d.ts.map +1 -1
  139. package/dist/core/tools/index.js +1 -1
  140. package/dist/core/tools/index.js.map +1 -1
  141. package/dist/index.d.ts +3 -3
  142. package/dist/index.d.ts.map +1 -1
  143. package/dist/index.js +2 -2
  144. package/dist/index.js.map +1 -1
  145. package/dist/main.d.ts.map +1 -1
  146. package/dist/main.js +116 -36
  147. package/dist/main.js.map +1 -1
  148. package/dist/modes/interactive/components/extension-editor.d.ts +5 -2
  149. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  150. package/dist/modes/interactive/components/extension-editor.js +9 -0
  151. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  152. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  153. package/dist/modes/interactive/components/footer.js +8 -23
  154. package/dist/modes/interactive/components/footer.js.map +1 -1
  155. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  156. package/dist/modes/interactive/components/login-dialog.js +1 -1
  157. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  158. package/dist/modes/interactive/components/model-selector.d.ts +1 -1
  159. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  160. package/dist/modes/interactive/components/model-selector.js +1 -1
  161. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  162. package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
  163. package/dist/modes/interactive/components/oauth-selector.js +1 -1
  164. package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
  165. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  166. package/dist/modes/interactive/components/session-selector.js +1 -1
  167. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  168. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  169. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  170. package/dist/modes/interactive/components/settings-selector.js +15 -1
  171. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  172. package/dist/modes/interactive/components/show-images-selector.d.ts.map +1 -1
  173. package/dist/modes/interactive/components/show-images-selector.js +5 -1
  174. package/dist/modes/interactive/components/show-images-selector.js.map +1 -1
  175. package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -1
  176. package/dist/modes/interactive/components/theme-selector.js +5 -1
  177. package/dist/modes/interactive/components/theme-selector.js.map +1 -1
  178. package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  179. package/dist/modes/interactive/components/thinking-selector.js +5 -1
  180. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  181. package/dist/modes/interactive/components/tool-execution.d.ts +7 -0
  182. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  183. package/dist/modes/interactive/components/tool-execution.js +158 -7
  184. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  185. package/dist/modes/interactive/components/tree-selector.d.ts +21 -2
  186. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  187. package/dist/modes/interactive/components/tree-selector.js +127 -10
  188. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  189. package/dist/modes/interactive/components/user-message.d.ts +1 -0
  190. package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  191. package/dist/modes/interactive/components/user-message.js +12 -0
  192. package/dist/modes/interactive/components/user-message.js.map +1 -1
  193. package/dist/modes/interactive/interactive-mode.d.ts +5 -1
  194. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  195. package/dist/modes/interactive/interactive-mode.js +215 -71
  196. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  197. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  198. package/dist/modes/interactive/theme/theme.js +5 -0
  199. package/dist/modes/interactive/theme/theme.js.map +1 -1
  200. package/dist/modes/rpc/jsonl.d.ts +17 -0
  201. package/dist/modes/rpc/jsonl.d.ts.map +1 -0
  202. package/dist/modes/rpc/jsonl.js +49 -0
  203. package/dist/modes/rpc/jsonl.js.map +1 -0
  204. package/dist/modes/rpc/rpc-client.d.ts +1 -1
  205. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  206. package/dist/modes/rpc/rpc-client.js +7 -11
  207. package/dist/modes/rpc/rpc-client.js.map +1 -1
  208. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  209. package/dist/modes/rpc/rpc-mode.js +9 -11
  210. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  211. package/dist/utils/child-process.d.ts +11 -0
  212. package/dist/utils/child-process.d.ts.map +1 -0
  213. package/dist/utils/child-process.js +78 -0
  214. package/dist/utils/child-process.js.map +1 -0
  215. package/dist/utils/clipboard-image.d.ts.map +1 -1
  216. package/dist/utils/clipboard-image.js +94 -11
  217. package/dist/utils/clipboard-image.js.map +1 -1
  218. package/dist/utils/clipboard-native.d.ts +1 -0
  219. package/dist/utils/clipboard-native.d.ts.map +1 -1
  220. package/dist/utils/clipboard-native.js.map +1 -1
  221. package/dist/utils/clipboard.d.ts +1 -1
  222. package/dist/utils/clipboard.d.ts.map +1 -1
  223. package/dist/utils/clipboard.js +27 -16
  224. package/dist/utils/clipboard.js.map +1 -1
  225. package/dist/utils/exif-orientation.d.ts +5 -0
  226. package/dist/utils/exif-orientation.d.ts.map +1 -0
  227. package/dist/utils/exif-orientation.js +158 -0
  228. package/dist/utils/exif-orientation.js.map +1 -0
  229. package/dist/utils/image-convert.d.ts.map +1 -1
  230. package/dist/utils/image-convert.js +5 -1
  231. package/dist/utils/image-convert.js.map +1 -1
  232. package/dist/utils/image-resize.d.ts.map +1 -1
  233. package/dist/utils/image-resize.js +6 -1
  234. package/dist/utils/image-resize.js.map +1 -1
  235. package/dist/utils/tools-manager.d.ts.map +1 -1
  236. package/dist/utils/tools-manager.js +66 -21
  237. package/dist/utils/tools-manager.js.map +1 -1
  238. package/docs/compaction.md +2 -0
  239. package/docs/custom-provider.md +57 -9
  240. package/docs/extensions.md +125 -12
  241. package/docs/keybindings.md +11 -1
  242. package/docs/models.md +44 -2
  243. package/docs/packages.md +9 -0
  244. package/docs/providers.md +10 -1
  245. package/docs/rpc.md +44 -7
  246. package/docs/sdk.md +2 -2
  247. package/docs/settings.md +11 -0
  248. package/docs/terminal-setup.md +39 -3
  249. package/docs/tmux.md +61 -0
  250. package/docs/tree.md +9 -0
  251. package/examples/extensions/README.md +2 -0
  252. package/examples/extensions/antigravity-image-gen.ts +8 -5
  253. package/examples/extensions/built-in-tool-renderer.ts +246 -0
  254. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  255. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  256. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  257. package/examples/extensions/custom-provider-gitlab-duo/test.ts +2 -2
  258. package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
  259. package/examples/extensions/dynamic-tools.ts +74 -0
  260. package/examples/extensions/overlay-qa-tests.ts +468 -1
  261. package/examples/extensions/preset.ts +2 -3
  262. package/examples/extensions/provider-payload.ts +14 -0
  263. package/examples/extensions/sandbox/index.ts +2 -3
  264. package/examples/extensions/subagent/agents.ts +2 -3
  265. package/examples/extensions/tool-override.ts +2 -3
  266. package/examples/extensions/with-deps/index.ts +1 -5
  267. package/examples/extensions/with-deps/package-lock.json +2 -2
  268. package/examples/extensions/with-deps/package.json +1 -1
  269. package/package.json +10 -7
@@ -13,13 +13,17 @@ class TreeList {
13
13
  toolCallMap = new Map();
14
14
  multipleRoots = false;
15
15
  activePathIds = new Set();
16
+ visibleParentMap = new Map();
17
+ visibleChildrenMap = new Map();
16
18
  lastSelectedId = null;
19
+ foldedNodes = new Set();
17
20
  onSelect;
18
21
  onCancel;
19
22
  onLabelEdit;
20
- constructor(tree, currentLeafId, maxVisibleLines, initialSelectedId) {
23
+ constructor(tree, currentLeafId, maxVisibleLines, initialSelectedId, initialFilterMode) {
21
24
  this.currentLeafId = currentLeafId;
22
25
  this.maxVisibleLines = maxVisibleLines;
26
+ this.filterMode = initialFilterMode ?? "default";
23
27
  this.multipleRoots = tree.length > 1;
24
28
  this.flatNodes = this.flattenTree(tree);
25
29
  this.buildActivePath();
@@ -217,7 +221,8 @@ class TreeList {
217
221
  const isSettingsEntry = entry.type === "label" ||
218
222
  entry.type === "custom" ||
219
223
  entry.type === "model_change" ||
220
- entry.type === "thinking_level_change";
224
+ entry.type === "thinking_level_change" ||
225
+ entry.type === "session_info";
221
226
  switch (this.filterMode) {
222
227
  case "user-only":
223
228
  // Just user messages
@@ -249,6 +254,17 @@ class TreeList {
249
254
  }
250
255
  return true;
251
256
  });
257
+ // Filter out descendants of folded nodes.
258
+ if (this.foldedNodes.size > 0) {
259
+ const skipSet = new Set();
260
+ for (const flatNode of this.flatNodes) {
261
+ const { id, parentId } = flatNode.node.entry;
262
+ if (parentId != null && (this.foldedNodes.has(parentId) || skipSet.has(parentId))) {
263
+ skipSet.add(id);
264
+ }
265
+ }
266
+ this.filteredNodes = this.filteredNodes.filter((flatNode) => !skipSet.has(flatNode.node.entry.id));
267
+ }
252
268
  // Recalculate visual structure (indent, connectors, gutters) based on visible tree
253
269
  this.recalculateVisualStructure();
254
270
  // Try to preserve cursor on the same node, or find nearest visible ancestor
@@ -373,6 +389,9 @@ class TreeList {
373
389
  ]);
374
390
  }
375
391
  }
392
+ // Store visible tree maps for ancestor/descendant lookups in navigation
393
+ this.visibleParentMap = visibleParent;
394
+ this.visibleChildrenMap = visibleChildren;
376
395
  }
377
396
  /** Get searchable text content from a node */
378
397
  getSearchableText(node) {
@@ -411,6 +430,11 @@ class TreeList {
411
430
  case "branch_summary":
412
431
  parts.push("branch summary", entry.summary);
413
432
  break;
433
+ case "session_info":
434
+ parts.push("title");
435
+ if (entry.name)
436
+ parts.push(entry.name);
437
+ break;
414
438
  case "model_change":
415
439
  parts.push("model", entry.modelId);
416
440
  break;
@@ -479,6 +503,7 @@ class TreeList {
479
503
  // Build prefix char by char, placing gutters and connector at their positions
480
504
  const totalChars = displayIndent * 3;
481
505
  const prefixChars = [];
506
+ const isFolded = this.foldedNodes.has(entry.id);
482
507
  for (let i = 0; i < totalChars; i++) {
483
508
  const level = Math.floor(i / 3);
484
509
  const posInLevel = i % 3;
@@ -493,12 +518,13 @@ class TreeList {
493
518
  }
494
519
  }
495
520
  else if (connector && level === connectorPosition) {
496
- // Connector at this level
521
+ // Connector at this level, with fold indicator
497
522
  if (posInLevel === 0) {
498
523
  prefixChars.push(flatNode.isLast ? "└" : "├");
499
524
  }
500
525
  else if (posInLevel === 1) {
501
- prefixChars.push("─");
526
+ const foldable = this.isFoldable(entry.id);
527
+ prefixChars.push(isFolded ? "⊞" : foldable ? "⊟" : "─");
502
528
  }
503
529
  else {
504
530
  prefixChars.push(" ");
@@ -509,12 +535,15 @@ class TreeList {
509
535
  }
510
536
  }
511
537
  const prefix = prefixChars.join("");
538
+ // Fold marker for nodes without connectors (roots)
539
+ const showsFoldInConnector = flatNode.showConnector && !flatNode.isVirtualRootChild;
540
+ const foldMarker = isFolded && !showsFoldInConnector ? theme.fg("accent", "⊞ ") : "";
512
541
  // Active path marker - shown right before the entry text
513
542
  const isOnActivePath = this.activePathIds.has(entry.id);
514
543
  const pathMarker = isOnActivePath ? theme.fg("accent", "• ") : "";
515
544
  const label = flatNode.node.label ? theme.fg("warning", `[${flatNode.node.label}] `) : "";
516
545
  const content = this.getEntryDisplayText(flatNode.node, isSelected);
517
- let line = cursor + theme.fg("dim", prefix) + pathMarker + label + content;
546
+ let line = cursor + theme.fg("dim", prefix) + foldMarker + pathMarker + label + content;
518
547
  if (isSelected) {
519
548
  line = theme.bg("selectedBg", line);
520
549
  }
@@ -602,6 +631,11 @@ class TreeList {
602
631
  case "label":
603
632
  result = theme.fg("dim", `[label: ${entry.label ?? "(cleared)"}]`);
604
633
  break;
634
+ case "session_info":
635
+ result = entry.name
636
+ ? [theme.fg("dim", "[title: "), theme.fg("dim", entry.name), theme.fg("dim", "]")].join("")
637
+ : [theme.fg("dim", "[title: "), theme.italic(theme.fg("dim", "empty")), theme.fg("dim", "]")].join("");
638
+ break;
605
639
  default:
606
640
  result = "";
607
641
  }
@@ -703,11 +737,31 @@ class TreeList {
703
737
  else if (kb.matches(keyData, "selectDown")) {
704
738
  this.selectedIndex = this.selectedIndex === this.filteredNodes.length - 1 ? 0 : this.selectedIndex + 1;
705
739
  }
706
- else if (kb.matches(keyData, "cursorLeft")) {
740
+ else if (kb.matches(keyData, "treeFoldOrUp")) {
741
+ const currentId = this.filteredNodes[this.selectedIndex]?.node.entry.id;
742
+ if (currentId && this.isFoldable(currentId) && !this.foldedNodes.has(currentId)) {
743
+ this.foldedNodes.add(currentId);
744
+ this.applyFilter();
745
+ }
746
+ else {
747
+ this.selectedIndex = this.findBranchSegmentStart("up");
748
+ }
749
+ }
750
+ else if (kb.matches(keyData, "treeUnfoldOrDown")) {
751
+ const currentId = this.filteredNodes[this.selectedIndex]?.node.entry.id;
752
+ if (currentId && this.foldedNodes.has(currentId)) {
753
+ this.foldedNodes.delete(currentId);
754
+ this.applyFilter();
755
+ }
756
+ else {
757
+ this.selectedIndex = this.findBranchSegmentStart("down");
758
+ }
759
+ }
760
+ else if (kb.matches(keyData, "cursorLeft") || kb.matches(keyData, "selectPageUp")) {
707
761
  // Page up
708
762
  this.selectedIndex = Math.max(0, this.selectedIndex - this.maxVisibleLines);
709
763
  }
710
- else if (kb.matches(keyData, "cursorRight")) {
764
+ else if (kb.matches(keyData, "cursorRight") || kb.matches(keyData, "selectPageDown")) {
711
765
  // Page down
712
766
  this.selectedIndex = Math.min(this.filteredNodes.length - 1, this.selectedIndex + this.maxVisibleLines);
713
767
  }
@@ -720,6 +774,7 @@ class TreeList {
720
774
  else if (kb.matches(keyData, "selectCancel")) {
721
775
  if (this.searchQuery) {
722
776
  this.searchQuery = "";
777
+ this.foldedNodes.clear();
723
778
  this.applyFilter();
724
779
  }
725
780
  else {
@@ -729,26 +784,31 @@ class TreeList {
729
784
  else if (matchesKey(keyData, "ctrl+d")) {
730
785
  // Direct filter: default
731
786
  this.filterMode = "default";
787
+ this.foldedNodes.clear();
732
788
  this.applyFilter();
733
789
  }
734
790
  else if (matchesKey(keyData, "ctrl+t")) {
735
791
  // Toggle filter: no-tools ↔ default
736
792
  this.filterMode = this.filterMode === "no-tools" ? "default" : "no-tools";
793
+ this.foldedNodes.clear();
737
794
  this.applyFilter();
738
795
  }
739
796
  else if (matchesKey(keyData, "ctrl+u")) {
740
797
  // Toggle filter: user-only ↔ default
741
798
  this.filterMode = this.filterMode === "user-only" ? "default" : "user-only";
799
+ this.foldedNodes.clear();
742
800
  this.applyFilter();
743
801
  }
744
802
  else if (matchesKey(keyData, "ctrl+l")) {
745
803
  // Toggle filter: labeled-only ↔ default
746
804
  this.filterMode = this.filterMode === "labeled-only" ? "default" : "labeled-only";
805
+ this.foldedNodes.clear();
747
806
  this.applyFilter();
748
807
  }
749
808
  else if (matchesKey(keyData, "ctrl+a")) {
750
809
  // Toggle filter: all ↔ default
751
810
  this.filterMode = this.filterMode === "all" ? "default" : "all";
811
+ this.foldedNodes.clear();
752
812
  this.applyFilter();
753
813
  }
754
814
  else if (matchesKey(keyData, "shift+ctrl+o")) {
@@ -756,6 +816,7 @@ class TreeList {
756
816
  const modes = ["default", "no-tools", "user-only", "labeled-only", "all"];
757
817
  const currentIndex = modes.indexOf(this.filterMode);
758
818
  this.filterMode = modes[(currentIndex - 1 + modes.length) % modes.length];
819
+ this.foldedNodes.clear();
759
820
  this.applyFilter();
760
821
  }
761
822
  else if (matchesKey(keyData, "ctrl+o")) {
@@ -763,11 +824,13 @@ class TreeList {
763
824
  const modes = ["default", "no-tools", "user-only", "labeled-only", "all"];
764
825
  const currentIndex = modes.indexOf(this.filterMode);
765
826
  this.filterMode = modes[(currentIndex + 1) % modes.length];
827
+ this.foldedNodes.clear();
766
828
  this.applyFilter();
767
829
  }
768
830
  else if (kb.matches(keyData, "deleteCharBackward")) {
769
831
  if (this.searchQuery.length > 0) {
770
832
  this.searchQuery = this.searchQuery.slice(0, -1);
833
+ this.foldedNodes.clear();
771
834
  this.applyFilter();
772
835
  }
773
836
  }
@@ -784,10 +847,64 @@ class TreeList {
784
847
  });
785
848
  if (!hasControlChars && keyData.length > 0) {
786
849
  this.searchQuery += keyData;
850
+ this.foldedNodes.clear();
787
851
  this.applyFilter();
788
852
  }
789
853
  }
790
854
  }
855
+ /**
856
+ * Whether a node can be folded. A node is foldable if it has visible children
857
+ * and is either a root (no visible parent) or a segment start (visible parent
858
+ * has multiple visible children).
859
+ */
860
+ isFoldable(entryId) {
861
+ const children = this.visibleChildrenMap.get(entryId);
862
+ if (!children || children.length === 0)
863
+ return false;
864
+ const parentId = this.visibleParentMap.get(entryId);
865
+ if (parentId === null || parentId === undefined)
866
+ return true;
867
+ const siblings = this.visibleChildrenMap.get(parentId);
868
+ return siblings !== undefined && siblings.length > 1;
869
+ }
870
+ /**
871
+ * Find the index of the next branch segment start in the given direction.
872
+ * A segment start is the first child of a branch point.
873
+ *
874
+ * "up" walks the visible parent chain; "down" walks visible children
875
+ * (always following the first child).
876
+ */
877
+ findBranchSegmentStart(direction) {
878
+ const selectedId = this.filteredNodes[this.selectedIndex]?.node.entry.id;
879
+ if (!selectedId)
880
+ return this.selectedIndex;
881
+ const indexByEntryId = new Map(this.filteredNodes.map((node, i) => [node.node.entry.id, i]));
882
+ let currentId = selectedId;
883
+ if (direction === "down") {
884
+ while (true) {
885
+ const children = this.visibleChildrenMap.get(currentId) ?? [];
886
+ if (children.length === 0)
887
+ return indexByEntryId.get(currentId);
888
+ if (children.length > 1)
889
+ return indexByEntryId.get(children[0]);
890
+ currentId = children[0];
891
+ }
892
+ }
893
+ // direction === "up"
894
+ while (true) {
895
+ const parentId = this.visibleParentMap.get(currentId) ?? null;
896
+ if (parentId === null)
897
+ return indexByEntryId.get(currentId);
898
+ const children = this.visibleChildrenMap.get(parentId) ?? [];
899
+ if (children.length > 1) {
900
+ const segmentStart = indexByEntryId.get(currentId);
901
+ if (segmentStart < this.selectedIndex) {
902
+ return segmentStart;
903
+ }
904
+ }
905
+ currentId = parentId;
906
+ }
907
+ }
791
908
  }
792
909
  /** Component that displays the current search query */
793
910
  class SearchLine {
@@ -872,11 +989,11 @@ export class TreeSelectorComponent extends Container {
872
989
  this.labelInput.focused = value;
873
990
  }
874
991
  }
875
- constructor(tree, currentLeafId, terminalHeight, onSelect, onCancel, onLabelChange, initialSelectedId) {
992
+ constructor(tree, currentLeafId, terminalHeight, onSelect, onCancel, onLabelChange, initialSelectedId, initialFilterMode) {
876
993
  super();
877
994
  this.onLabelChangeCallback = onLabelChange;
878
995
  const maxVisibleLines = Math.max(5, Math.floor(terminalHeight / 2));
879
- this.treeList = new TreeList(tree, currentLeafId, maxVisibleLines, initialSelectedId);
996
+ this.treeList = new TreeList(tree, currentLeafId, maxVisibleLines, initialSelectedId, initialFilterMode);
880
997
  this.treeList.onSelect = onSelect;
881
998
  this.treeList.onCancel = onCancel;
882
999
  this.treeList.onLabelEdit = (entryId, currentLabel) => this.showLabelInput(entryId, currentLabel);
@@ -886,7 +1003,7 @@ export class TreeSelectorComponent extends Container {
886
1003
  this.addChild(new Spacer(1));
887
1004
  this.addChild(new DynamicBorder());
888
1005
  this.addChild(new Text(theme.bold(" Session Tree"), 1, 0));
889
- this.addChild(new TruncatedText(theme.fg("muted", " ↑/↓: move. ←/→: page. Shift+L: label. ") +
1006
+ this.addChild(new TruncatedText(theme.fg("muted", " ↑/↓: move. ←/→: page. ^←/^→ or Alt+←/Alt+→: fold/branch. Shift+L: label. ") +
890
1007
  theme.fg("muted", "^D/^T/^U/^L/^A: filters (^O/⇧^O cycle)"), 0, 0));
891
1008
  this.addChild(new SearchLine(this.treeList));
892
1009
  this.addChild(new DynamicBorder());