lsd-pi 1.2.3 → 1.3.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 (254) hide show
  1. package/README.md +22 -16
  2. package/dist/app-paths.d.ts +4 -0
  3. package/dist/app-paths.js +4 -0
  4. package/dist/bedrock-auth.d.ts +4 -0
  5. package/dist/bedrock-auth.js +4 -0
  6. package/dist/bundled-extension-paths.d.ts +4 -0
  7. package/dist/bundled-extension-paths.js +4 -0
  8. package/dist/cli-theme.d.ts +2 -2
  9. package/dist/cli-theme.js +13 -14
  10. package/dist/cli.js +43 -3
  11. package/dist/codex-rotate-settings.d.ts +4 -0
  12. package/dist/codex-rotate-settings.js +4 -0
  13. package/dist/help-text.d.ts +4 -0
  14. package/dist/help-text.js +4 -0
  15. package/dist/lsd-brand.d.ts +4 -0
  16. package/dist/lsd-brand.js +4 -0
  17. package/dist/onboarding-llm.d.ts +5 -0
  18. package/dist/onboarding-llm.js +5 -0
  19. package/dist/project-sessions.d.ts +4 -0
  20. package/dist/project-sessions.js +4 -0
  21. package/dist/resources/agents/generic.md +1 -0
  22. package/dist/resources/agents/scout.md +8 -1
  23. package/dist/resources/agents/worker.md +1 -0
  24. package/dist/resources/extensions/ask-user-questions.js +70 -0
  25. package/dist/resources/extensions/bg-shell/bg-shell-tool.js +6 -16
  26. package/dist/resources/extensions/mac-tools/index.js +19 -34
  27. package/dist/resources/extensions/memory/index.js +20 -2
  28. package/dist/resources/extensions/shared/interview-ui.js +103 -20
  29. package/dist/resources/extensions/slash-commands/plan.js +18 -17
  30. package/dist/resources/extensions/slash-commands/tools.js +40 -4
  31. package/dist/resources/extensions/subagent/agent-switcher-component.js +208 -0
  32. package/dist/resources/extensions/subagent/agent-switcher-model.js +107 -0
  33. package/dist/resources/extensions/subagent/agents.js +2 -1
  34. package/dist/resources/extensions/subagent/background-job-manager.js +11 -6
  35. package/dist/resources/extensions/subagent/background-runner.js +4 -0
  36. package/dist/resources/extensions/subagent/index.js +714 -21
  37. package/dist/resources/extensions/subagent/launch-helpers.js +21 -6
  38. package/dist/shared-paths.d.ts +4 -0
  39. package/dist/shared-paths.js +4 -0
  40. package/dist/shared-preferences.d.ts +4 -0
  41. package/dist/shared-preferences.js +4 -0
  42. package/dist/startup-model-validation.d.ts +1 -1
  43. package/dist/startup-timings.d.ts +4 -0
  44. package/dist/startup-timings.js +4 -0
  45. package/dist/update-check.d.ts +4 -0
  46. package/dist/update-check.js +4 -0
  47. package/dist/update-cmd.d.ts +4 -0
  48. package/dist/update-cmd.js +4 -0
  49. package/dist/welcome-screen.js +4 -4
  50. package/dist/wizard.d.ts +4 -0
  51. package/dist/wizard.js +4 -0
  52. package/package.json +1 -1
  53. package/packages/pi-agent-core/dist/agent.d.ts +9 -0
  54. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  55. package/packages/pi-agent-core/dist/agent.js +89 -5
  56. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  57. package/packages/pi-agent-core/dist/types.d.ts +13 -2
  58. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  59. package/packages/pi-agent-core/dist/types.js.map +1 -1
  60. package/packages/pi-agent-core/src/agent.ts +110 -4
  61. package/packages/pi-agent-core/src/types.ts +12 -3
  62. package/packages/pi-ai/dist/adaptive/classifier.d.ts +29 -0
  63. package/packages/pi-ai/dist/adaptive/classifier.d.ts.map +1 -0
  64. package/packages/pi-ai/dist/adaptive/classifier.js +72 -0
  65. package/packages/pi-ai/dist/adaptive/classifier.js.map +1 -0
  66. package/packages/pi-ai/dist/adaptive/classifier.test.d.ts +2 -0
  67. package/packages/pi-ai/dist/adaptive/classifier.test.d.ts.map +1 -0
  68. package/packages/pi-ai/dist/adaptive/classifier.test.js +32 -0
  69. package/packages/pi-ai/dist/adaptive/classifier.test.js.map +1 -0
  70. package/packages/pi-ai/dist/index.d.ts +1 -0
  71. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  72. package/packages/pi-ai/dist/index.js +1 -0
  73. package/packages/pi-ai/dist/index.js.map +1 -1
  74. package/packages/pi-ai/dist/providers/amazon-bedrock.js +0 -2
  75. package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
  76. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
  77. package/packages/pi-ai/dist/providers/anthropic-shared.js +11 -3
  78. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  79. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts +1 -1
  80. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  81. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  82. package/packages/pi-ai/dist/providers/google-gemini-cli.d.ts.map +1 -1
  83. package/packages/pi-ai/dist/providers/google-gemini-cli.js +0 -4
  84. package/packages/pi-ai/dist/providers/google-gemini-cli.js.map +1 -1
  85. package/packages/pi-ai/dist/providers/google-vertex.js +0 -5
  86. package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
  87. package/packages/pi-ai/dist/providers/google.js +0 -5
  88. package/packages/pi-ai/dist/providers/google.js.map +1 -1
  89. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts +1 -1
  90. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
  91. package/packages/pi-ai/dist/providers/openai-codex-responses.js +0 -2
  92. package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  93. package/packages/pi-ai/dist/providers/openai-completions.d.ts +1 -1
  94. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  95. package/packages/pi-ai/dist/providers/openai-completions.js +0 -1
  96. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  97. package/packages/pi-ai/dist/providers/openai-responses.d.ts +1 -1
  98. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  99. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  100. package/packages/pi-ai/dist/providers/openai-shared.d.ts +0 -1
  101. package/packages/pi-ai/dist/providers/openai-shared.d.ts.map +1 -1
  102. package/packages/pi-ai/dist/providers/openai-shared.js +0 -4
  103. package/packages/pi-ai/dist/providers/openai-shared.js.map +1 -1
  104. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  105. package/packages/pi-ai/dist/providers/simple-options.js +0 -1
  106. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  107. package/packages/pi-ai/dist/types.d.ts +1 -2
  108. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  109. package/packages/pi-ai/dist/types.js.map +1 -1
  110. package/packages/pi-ai/src/adaptive/classifier.test.ts +38 -0
  111. package/packages/pi-ai/src/adaptive/classifier.ts +107 -0
  112. package/packages/pi-ai/src/index.ts +1 -0
  113. package/packages/pi-ai/src/providers/amazon-bedrock.ts +0 -2
  114. package/packages/pi-ai/src/providers/anthropic-shared.ts +12 -3
  115. package/packages/pi-ai/src/providers/azure-openai-responses.ts +1 -1
  116. package/packages/pi-ai/src/providers/google-gemini-cli.ts +0 -4
  117. package/packages/pi-ai/src/providers/google-vertex.ts +0 -5
  118. package/packages/pi-ai/src/providers/google.ts +0 -5
  119. package/packages/pi-ai/src/providers/openai-codex-responses.ts +1 -3
  120. package/packages/pi-ai/src/providers/openai-completions.ts +1 -2
  121. package/packages/pi-ai/src/providers/openai-responses.ts +1 -1
  122. package/packages/pi-ai/src/providers/openai-shared.ts +0 -3
  123. package/packages/pi-ai/src/providers/simple-options.ts +0 -1
  124. package/packages/pi-ai/src/types.ts +1 -2
  125. package/packages/pi-coding-agent/dist/cli/args.js +2 -2
  126. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +7 -2
  128. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/core/agent-session.js +62 -23
  130. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/lsp/lsp.md +3 -1
  132. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  133. package/packages/pi-coding-agent/dist/core/sdk.js +32 -6
  134. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  135. package/packages/pi-coding-agent/dist/core/sdk.test.js +37 -0
  136. package/packages/pi-coding-agent/dist/core/sdk.test.js.map +1 -1
  137. package/packages/pi-coding-agent/dist/core/session-manager.d.ts +8 -0
  138. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  139. package/packages/pi-coding-agent/dist/core/session-manager.js +4 -0
  140. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  141. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +12 -7
  142. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  143. package/packages/pi-coding-agent/dist/core/settings-manager.js +20 -2
  144. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  145. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  146. package/packages/pi-coding-agent/dist/core/skills.js +4 -1
  147. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  148. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -1
  149. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  150. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  151. package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -2
  152. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  153. package/packages/pi-coding-agent/dist/core/tools/grep.js +1 -1
  154. package/packages/pi-coding-agent/dist/core/tools/grep.js.map +1 -1
  155. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
  156. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  157. package/packages/pi-coding-agent/dist/core/tools/index.js +2 -0
  158. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  159. package/packages/pi-coding-agent/dist/core/tools/pty.d.ts +10 -1
  160. package/packages/pi-coding-agent/dist/core/tools/pty.d.ts.map +1 -1
  161. package/packages/pi-coding-agent/dist/core/tools/pty.js +29 -3
  162. package/packages/pi-coding-agent/dist/core/tools/pty.js.map +1 -1
  163. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js +1 -1
  164. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js.map +1 -1
  165. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  166. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +12 -2
  167. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  168. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +7 -2
  169. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  170. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +23 -4
  171. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  172. package/packages/pi-coding-agent/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  173. package/packages/pi-coding-agent/dist/modes/interactive/components/thinking-selector.js +1 -2
  174. package/packages/pi-coding-agent/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  175. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  176. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +9 -0
  177. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  178. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  179. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +53 -2
  180. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  181. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts +2 -2
  182. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  183. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +10 -6
  184. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
  185. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +1 -1
  186. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  187. package/packages/pi-coding-agent/dist/modes/print-mode.d.ts.map +1 -1
  188. package/packages/pi-coding-agent/dist/modes/print-mode.js +6 -0
  189. package/packages/pi-coding-agent/dist/modes/print-mode.js.map +1 -1
  190. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  191. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +20 -0
  192. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  193. package/packages/pi-coding-agent/dist/tests/path-display.test.js +15 -0
  194. package/packages/pi-coding-agent/dist/tests/path-display.test.js.map +1 -1
  195. package/packages/pi-coding-agent/package.json +1 -1
  196. package/packages/pi-coding-agent/src/cli/args.ts +2 -2
  197. package/packages/pi-coding-agent/src/core/agent-session.ts +67 -25
  198. package/packages/pi-coding-agent/src/core/lsp/lsp.md +3 -1
  199. package/packages/pi-coding-agent/src/core/sdk.test.ts +45 -0
  200. package/packages/pi-coding-agent/src/core/sdk.ts +35 -6
  201. package/packages/pi-coding-agent/src/core/session-manager.ts +12 -0
  202. package/packages/pi-coding-agent/src/core/settings-manager.ts +32 -9
  203. package/packages/pi-coding-agent/src/core/skills.ts +4 -1
  204. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -1
  205. package/packages/pi-coding-agent/src/core/system-prompt.ts +8 -2
  206. package/packages/pi-coding-agent/src/core/tools/grep.ts +1 -1
  207. package/packages/pi-coding-agent/src/core/tools/index.ts +3 -0
  208. package/packages/pi-coding-agent/src/core/tools/pty.ts +45 -6
  209. package/packages/pi-coding-agent/src/modes/interactive/components/embedded-terminal.ts +1 -1
  210. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +10 -2
  211. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +31 -7
  212. package/packages/pi-coding-agent/src/modes/interactive/components/thinking-selector.ts +1 -2
  213. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +9 -0
  214. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +65 -3
  215. package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +11 -7
  216. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +1 -1
  217. package/packages/pi-coding-agent/src/modes/print-mode.ts +6 -0
  218. package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +29 -0
  219. package/packages/pi-coding-agent/src/tests/path-display.test.ts +17 -0
  220. package/packages/pi-tui/dist/components/loader.d.ts +5 -2
  221. package/packages/pi-tui/dist/components/loader.d.ts.map +1 -1
  222. package/packages/pi-tui/dist/components/loader.js +33 -3
  223. package/packages/pi-tui/dist/components/loader.js.map +1 -1
  224. package/packages/pi-tui/src/components/loader.ts +31 -3
  225. package/packages/rpc-client/src/index.ts +1 -1
  226. package/packages/rpc-client/src/rpc-client.ts +29 -0
  227. package/packages/rpc-client/src/rpc-types.ts +1 -1
  228. package/pkg/dist/modes/interactive/theme/theme.d.ts +2 -2
  229. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  230. package/pkg/dist/modes/interactive/theme/theme.js +10 -6
  231. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
  232. package/pkg/dist/modes/interactive/theme/themes.js +1 -1
  233. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  234. package/pkg/package.json +1 -1
  235. package/src/resources/agents/generic.md +1 -0
  236. package/src/resources/agents/scout.md +8 -1
  237. package/src/resources/agents/worker.md +1 -0
  238. package/src/resources/extensions/ask-user-questions.ts +88 -0
  239. package/src/resources/extensions/bg-shell/bg-shell-tool.ts +6 -16
  240. package/src/resources/extensions/mac-tools/index.ts +19 -34
  241. package/src/resources/extensions/memory/index.ts +22 -2
  242. package/src/resources/extensions/shared/interview-ui.ts +108 -15
  243. package/src/resources/extensions/shared/tests/ask-user-freetext.test.ts +61 -0
  244. package/src/resources/extensions/shared/tests/custom-ui-fallbacks.test.ts +46 -0
  245. package/src/resources/extensions/slash-commands/plan.ts +18 -19
  246. package/src/resources/extensions/slash-commands/tools.ts +43 -4
  247. package/src/resources/extensions/subagent/agent-switcher-component.ts +228 -0
  248. package/src/resources/extensions/subagent/agent-switcher-model.ts +160 -0
  249. package/src/resources/extensions/subagent/agents.ts +2 -1
  250. package/src/resources/extensions/subagent/background-job-manager.ts +29 -6
  251. package/src/resources/extensions/subagent/background-runner.ts +8 -0
  252. package/src/resources/extensions/subagent/background-types.ts +4 -0
  253. package/src/resources/extensions/subagent/index.ts +834 -19
  254. package/src/resources/extensions/subagent/launch-helpers.ts +16 -4
@@ -0,0 +1,208 @@
1
+ import { Key, matchesKey, truncateToWidth, visibleWidth } from "@gsd/pi-tui";
2
+ const MAX_VISIBLE_ROWS = 10;
3
+ function stateIcon(theme, target) {
4
+ if (target.kind === "parent")
5
+ return theme.fg("accent", "●");
6
+ if (target.state === "running")
7
+ return theme.fg("warning", "▶");
8
+ if (target.state === "failed")
9
+ return theme.fg("error", "✗");
10
+ return theme.fg("success", "✓");
11
+ }
12
+ function stateBadge(theme, target) {
13
+ if (target.kind === "parent")
14
+ return theme.fg("muted", "parent");
15
+ if (target.state === "running")
16
+ return theme.fg("warning", "running");
17
+ if (target.state === "failed")
18
+ return theme.fg("error", "failed");
19
+ return theme.fg("success", "completed");
20
+ }
21
+ function plainLabel(target) {
22
+ if (target.kind === "parent") {
23
+ const currentSuffix = target.isCurrent ? " (current)" : "";
24
+ return `● parent — main session${currentSuffix}`;
25
+ }
26
+ const icon = target.state === "running" ? "▶" : target.state === "failed" ? "✗" : "✓";
27
+ const currentSuffix = target.isCurrent ? " (current)" : "";
28
+ return `${icon} ${target.agentName} — ${target.taskPreview}${currentSuffix}`;
29
+ }
30
+ class AgentSwitcherComponent {
31
+ tui;
32
+ theme;
33
+ targets;
34
+ done;
35
+ selectedIndex;
36
+ statusLine;
37
+ cachedWidth;
38
+ cachedLines;
39
+ constructor(tui, theme, targets, done) {
40
+ this.tui = tui;
41
+ this.theme = theme;
42
+ this.targets = targets;
43
+ this.done = done;
44
+ this.selectedIndex = this.getInitialSelectionIndex();
45
+ }
46
+ getInitialSelectionIndex() {
47
+ const firstSwitchable = this.targets.findIndex((target) => target.selectionAction !== "blocked");
48
+ return firstSwitchable >= 0 ? firstSwitchable : 0;
49
+ }
50
+ move(delta) {
51
+ if (this.targets.length === 0)
52
+ return;
53
+ const next = Math.max(0, Math.min(this.targets.length - 1, this.selectedIndex + delta));
54
+ if (next === this.selectedIndex)
55
+ return;
56
+ this.selectedIndex = next;
57
+ this.statusLine = undefined;
58
+ this.invalidate();
59
+ this.tui.requestRender();
60
+ }
61
+ handleInput(data) {
62
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
63
+ this.done(undefined);
64
+ return;
65
+ }
66
+ if (matchesKey(data, Key.up)) {
67
+ this.move(-1);
68
+ return;
69
+ }
70
+ if (matchesKey(data, Key.down)) {
71
+ this.move(1);
72
+ return;
73
+ }
74
+ if (matchesKey(data, Key.pageUp)) {
75
+ this.move(-MAX_VISIBLE_ROWS);
76
+ return;
77
+ }
78
+ if (matchesKey(data, Key.pageDown)) {
79
+ this.move(MAX_VISIBLE_ROWS);
80
+ return;
81
+ }
82
+ if (matchesKey(data, Key.enter) || matchesKey(data, Key.space)) {
83
+ const selected = this.targets[this.selectedIndex];
84
+ if (!selected) {
85
+ this.done(undefined);
86
+ return;
87
+ }
88
+ if (selected.selectionAction === "blocked") {
89
+ this.statusLine = selected.blockedReason ?? "This row cannot be selected yet.";
90
+ this.invalidate();
91
+ this.tui.requestRender();
92
+ return;
93
+ }
94
+ this.done(selected);
95
+ }
96
+ }
97
+ getVisibleWindow() {
98
+ if (this.targets.length <= MAX_VISIBLE_ROWS)
99
+ return { start: 0, end: this.targets.length };
100
+ const half = Math.floor(MAX_VISIBLE_ROWS / 2);
101
+ let start = Math.max(0, this.selectedIndex - half);
102
+ let end = Math.min(this.targets.length, start + MAX_VISIBLE_ROWS);
103
+ if (end - start < MAX_VISIBLE_ROWS) {
104
+ start = Math.max(0, end - MAX_VISIBLE_ROWS);
105
+ }
106
+ return { start, end };
107
+ }
108
+ renderRow(target, isSelected, width) {
109
+ const cursor = isSelected ? this.theme.fg("accent", "▸") : " ";
110
+ const icon = stateIcon(this.theme, target);
111
+ const baseLabel = target.kind === "parent"
112
+ ? "parent — main session"
113
+ : `${target.agentName} — ${target.taskPreview}`;
114
+ const currentTag = target.isCurrent ? this.theme.fg("accent", "(current)") : "";
115
+ const badge = stateBadge(this.theme, target);
116
+ const right = [currentTag, badge].filter(Boolean).join(" ");
117
+ const leftRaw = `${cursor} ${icon} ${baseLabel}`;
118
+ const rightWidth = right ? visibleWidth(right) + 1 : 0;
119
+ const maxLeft = Math.max(10, width - 4 - rightWidth);
120
+ const left = truncateToWidth(leftRaw, maxLeft, "…");
121
+ const spacing = Math.max(1, width - 4 - visibleWidth(left) - visibleWidth(right));
122
+ const composed = `${left}${" ".repeat(spacing)}${right}`;
123
+ if (!isSelected) {
124
+ if (target.selectionAction === "blocked")
125
+ return this.theme.fg("dim", composed);
126
+ return composed;
127
+ }
128
+ return this.theme.bold(composed);
129
+ }
130
+ box(inner, width) {
131
+ const bdr = (s) => this.theme.fg("borderMuted", s);
132
+ const iw = width - 4;
133
+ const lines = [];
134
+ lines.push(bdr("╭" + "─".repeat(width - 2) + "╮"));
135
+ for (const line of inner) {
136
+ const truncated = truncateToWidth(line, iw, "…");
137
+ const pad = Math.max(0, iw - visibleWidth(truncated));
138
+ lines.push(bdr("│") + " " + truncated + " ".repeat(pad) + " " + bdr("│"));
139
+ }
140
+ lines.push(bdr("╰" + "─".repeat(width - 2) + "╯"));
141
+ return lines;
142
+ }
143
+ render(width) {
144
+ if (this.cachedLines && this.cachedWidth === width)
145
+ return this.cachedLines;
146
+ const inner = [];
147
+ inner.push(this.theme.bold(this.theme.fg("accent", "Agent session switcher")));
148
+ inner.push(this.theme.fg("dim", "Parent + tracked subagents"));
149
+ inner.push("");
150
+ if (this.targets.length === 0) {
151
+ inner.push(this.theme.fg("dim", "No switch targets available."));
152
+ }
153
+ else {
154
+ const { start, end } = this.getVisibleWindow();
155
+ for (let i = start; i < end; i++) {
156
+ inner.push(this.renderRow(this.targets[i], i === this.selectedIndex, width));
157
+ }
158
+ if (start > 0 || end < this.targets.length) {
159
+ inner.push("");
160
+ inner.push(this.theme.fg("dim", `${this.selectedIndex + 1}/${this.targets.length}`));
161
+ }
162
+ }
163
+ inner.push("");
164
+ const actionHint = this.targets[this.selectedIndex]?.selectionAction === "attach_live"
165
+ ? "Enter attach live"
166
+ : "Enter switch";
167
+ inner.push(this.theme.fg("dim", `${actionHint} · Esc cancel`));
168
+ if (this.statusLine) {
169
+ inner.push(this.theme.fg("warning", this.statusLine));
170
+ }
171
+ this.cachedLines = this.box(inner, width);
172
+ this.cachedWidth = width;
173
+ return this.cachedLines;
174
+ }
175
+ invalidate() {
176
+ this.cachedWidth = undefined;
177
+ this.cachedLines = undefined;
178
+ }
179
+ }
180
+ export async function showAgentSwitcher(ctx, targets) {
181
+ const result = await ctx.ui.custom((tui, theme, _kb, done) => new AgentSwitcherComponent(tui, theme, targets, done), {
182
+ overlay: true,
183
+ overlayOptions: {
184
+ anchor: "center",
185
+ width: "65%",
186
+ minWidth: 60,
187
+ maxHeight: "70%",
188
+ },
189
+ });
190
+ if (result !== undefined && result !== null) {
191
+ return result;
192
+ }
193
+ if (targets.length === 0)
194
+ return undefined;
195
+ const labels = targets.map((target, index) => `${index + 1}. ${plainLabel(target)}`);
196
+ const selected = await ctx.ui.select("Switch agent session", labels);
197
+ if (!selected || Array.isArray(selected))
198
+ return undefined;
199
+ const selectedIndex = labels.indexOf(selected);
200
+ if (selectedIndex < 0)
201
+ return undefined;
202
+ const target = targets[selectedIndex];
203
+ if (target.selectionAction === "blocked") {
204
+ ctx.ui.notify(target.blockedReason ?? "That target is not selectable yet.", "warning");
205
+ return undefined;
206
+ }
207
+ return target;
208
+ }
@@ -0,0 +1,107 @@
1
+ function summarizeTask(task, max = 72) {
2
+ const trimmed = task.replace(/\s+/g, " ").trim();
3
+ if (!trimmed)
4
+ return "(no task)";
5
+ return trimmed.length > max ? `${trimmed.slice(0, max)}…` : trimmed;
6
+ }
7
+ function stateRank(state) {
8
+ if (state === "running")
9
+ return 0;
10
+ if (state === "completed")
11
+ return 1;
12
+ return 2;
13
+ }
14
+ function shouldIncludeRunningJob(job, rootParentSessionFile, currentCwd) {
15
+ if (job.parentSessionFile)
16
+ return job.parentSessionFile === rootParentSessionFile;
17
+ if (currentCwd && job.cwd)
18
+ return currentCwd === job.cwd;
19
+ return false;
20
+ }
21
+ export function buildAgentSwitchTargets(input) {
22
+ const targetsBySessionFile = new Map();
23
+ for (const link of input.trackedLinks) {
24
+ const canAttachLive = link.state === "running";
25
+ targetsBySessionFile.set(link.subagentSessionFile, {
26
+ id: link.id,
27
+ kind: "subagent",
28
+ sessionFile: link.subagentSessionFile,
29
+ parentSessionFile: link.parentSessionFile,
30
+ agentName: link.agentName,
31
+ taskPreview: summarizeTask(link.task),
32
+ state: link.state,
33
+ isCurrent: link.subagentSessionFile === input.currentSessionFile,
34
+ isLiveAttachCapable: canAttachLive,
35
+ selectionAction: link.state === "running" ? (canAttachLive ? "attach_live" : "blocked") : "switch_saved",
36
+ blockedReason: link.state === "running" && !canAttachLive
37
+ ? "Live attach is not implemented yet for running subagents."
38
+ : undefined,
39
+ sortTime: link.updatedAt,
40
+ });
41
+ }
42
+ const runningExtras = [];
43
+ for (const job of input.runningJobs) {
44
+ if (!shouldIncludeRunningJob(job, input.rootParentSessionFile, input.currentCwd))
45
+ continue;
46
+ if (job.sessionFile && targetsBySessionFile.has(job.sessionFile)) {
47
+ const existing = targetsBySessionFile.get(job.sessionFile);
48
+ existing.state = "running";
49
+ existing.isLiveAttachCapable = true;
50
+ existing.selectionAction = "attach_live";
51
+ existing.blockedReason = undefined;
52
+ existing.runningJobId = job.id;
53
+ existing.sortTime = Math.max(existing.sortTime, job.startedAt);
54
+ continue;
55
+ }
56
+ const canAttachLive = Boolean(job.sessionFile);
57
+ const syntheticSessionFile = job.sessionFile ?? `${input.rootParentSessionFile}#${job.id}`;
58
+ runningExtras.push({
59
+ id: job.id,
60
+ kind: "subagent",
61
+ sessionFile: syntheticSessionFile,
62
+ parentSessionFile: job.parentSessionFile ?? input.rootParentSessionFile,
63
+ agentName: job.agentName,
64
+ taskPreview: summarizeTask(job.task),
65
+ state: "running",
66
+ isCurrent: Boolean(job.sessionFile && job.sessionFile === input.currentSessionFile),
67
+ isLiveAttachCapable: canAttachLive,
68
+ selectionAction: canAttachLive ? "attach_live" : "blocked",
69
+ blockedReason: canAttachLive ? undefined : "Live attach is not implemented yet for running subagents.",
70
+ runningJobId: job.id,
71
+ sortTime: job.startedAt,
72
+ });
73
+ }
74
+ const parentTarget = {
75
+ id: "parent",
76
+ kind: "parent",
77
+ sessionFile: input.rootParentSessionFile,
78
+ agentName: "parent",
79
+ taskPreview: "Main session",
80
+ state: "completed",
81
+ isCurrent: input.currentSessionFile === input.rootParentSessionFile,
82
+ isLiveAttachCapable: false,
83
+ selectionAction: input.currentSessionFile === input.rootParentSessionFile ? "blocked" : "switch_saved",
84
+ blockedReason: input.currentSessionFile === input.rootParentSessionFile
85
+ ? "You are already in the parent/main session."
86
+ : undefined,
87
+ sortTime: Number.MAX_SAFE_INTEGER,
88
+ };
89
+ const targets = [
90
+ parentTarget,
91
+ ...targetsBySessionFile.values(),
92
+ ...runningExtras,
93
+ ];
94
+ targets.sort((a, b) => {
95
+ if (a.isCurrent !== b.isCurrent)
96
+ return a.isCurrent ? -1 : 1;
97
+ if (a.kind !== b.kind)
98
+ return a.kind === "parent" ? -1 : 1;
99
+ const stateDiff = stateRank(a.state) - stateRank(b.state);
100
+ if (stateDiff !== 0)
101
+ return stateDiff;
102
+ if (a.sortTime !== b.sortTime)
103
+ return b.sortTime - a.sortTime;
104
+ return a.agentName.localeCompare(b.agentName);
105
+ });
106
+ return targets;
107
+ }
@@ -51,7 +51,8 @@ function loadAgentsFromDir(dir, source) {
51
51
  const tools = frontmatter.tools
52
52
  ?.split(",")
53
53
  .map((t) => t.trim())
54
- .filter(Boolean);
54
+ .filter(Boolean)
55
+ .filter((tool, index, all) => all.indexOf(tool) === index);
55
56
  agents.push({
56
57
  name: frontmatter.name,
57
58
  description: frontmatter.description,
@@ -28,15 +28,15 @@ export class BackgroundJobManager {
28
28
  * @param runFn Async function that runs the agent and returns a result summary
29
29
  * @returns Job ID prefixed with `sa_`
30
30
  */
31
- register(agentName, task, cwd, runFn) {
31
+ register(agentName, task, cwd, runFn, metadata) {
32
32
  const abortController = new AbortController();
33
- return this.attachJob(agentName, task, cwd, abortController, runFn(abortController.signal));
33
+ return this.attachJob(agentName, task, cwd, abortController, runFn(abortController.signal), metadata);
34
34
  }
35
35
  /**
36
36
  * Adopt an already-running foreground subagent into background tracking.
37
37
  */
38
- adoptRunning(agentName, task, cwd, abortController, resultPromise) {
39
- return this.attachJob(agentName, task, cwd, abortController, resultPromise);
38
+ adoptRunning(agentName, task, cwd, abortController, resultPromise, metadata) {
39
+ return this.attachJob(agentName, task, cwd, abortController, resultPromise, metadata);
40
40
  }
41
41
  /**
42
42
  * Cancel a running job.
@@ -84,7 +84,7 @@ export class BackgroundJobManager {
84
84
  }
85
85
  }
86
86
  // ── Private ──────────────────────────────────────────────────────────────
87
- attachJob(agentName, task, cwd, abortController, resultPromise) {
87
+ attachJob(agentName, task, cwd, abortController, resultPromise, metadata) {
88
88
  const running = this.getRunningJobs();
89
89
  if (running.length >= this.maxRunning) {
90
90
  throw new Error(`Maximum concurrent background subagents reached (${this.maxRunning}). ` +
@@ -105,17 +105,22 @@ export class BackgroundJobManager {
105
105
  cwd,
106
106
  status: "running",
107
107
  startedAt: Date.now(),
108
+ model: metadata?.model,
109
+ sessionFile: metadata?.sessionFile,
110
+ parentSessionFile: metadata?.parentSessionFile,
108
111
  abortController,
109
112
  promise: undefined,
110
113
  };
111
114
  job.promise = resultPromise
112
- .then(({ summary, stderr, exitCode, model }) => {
115
+ .then(({ summary, stderr, exitCode, model, sessionFile, parentSessionFile }) => {
113
116
  job.status = exitCode === 0 ? "completed" : "failed";
114
117
  job.completedAt = Date.now();
115
118
  job.resultSummary = summary;
116
119
  job.stderr = stderr;
117
120
  job.exitCode = exitCode;
118
121
  job.model = model;
122
+ job.sessionFile = sessionFile;
123
+ job.parentSessionFile = parentSessionFile;
119
124
  this.scheduleEviction(id);
120
125
  this.deliverResult(job);
121
126
  })
@@ -30,7 +30,11 @@ runFn) {
30
30
  stderr: result.stderr,
31
31
  exitCode: result.exitCode,
32
32
  model: result.model,
33
+ sessionFile: result.sessionFile,
34
+ parentSessionFile: result.parentSessionFile,
33
35
  };
36
+ }, {
37
+ parentSessionFile: ctx.parentSessionFile,
34
38
  });
35
39
  return jobId;
36
40
  }