lsd-pi 1.2.4 → 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 (252) 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/background-job-manager.js +11 -6
  34. package/dist/resources/extensions/subagent/background-runner.js +4 -0
  35. package/dist/resources/extensions/subagent/index.js +714 -21
  36. package/dist/resources/extensions/subagent/launch-helpers.js +19 -5
  37. package/dist/shared-paths.d.ts +4 -0
  38. package/dist/shared-paths.js +4 -0
  39. package/dist/shared-preferences.d.ts +4 -0
  40. package/dist/shared-preferences.js +4 -0
  41. package/dist/startup-model-validation.d.ts +1 -1
  42. package/dist/startup-timings.d.ts +4 -0
  43. package/dist/startup-timings.js +4 -0
  44. package/dist/update-check.d.ts +4 -0
  45. package/dist/update-check.js +4 -0
  46. package/dist/update-cmd.d.ts +4 -0
  47. package/dist/update-cmd.js +4 -0
  48. package/dist/welcome-screen.js +4 -4
  49. package/dist/wizard.d.ts +4 -0
  50. package/dist/wizard.js +4 -0
  51. package/package.json +1 -1
  52. package/packages/pi-agent-core/dist/agent.d.ts +9 -0
  53. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  54. package/packages/pi-agent-core/dist/agent.js +89 -5
  55. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  56. package/packages/pi-agent-core/dist/types.d.ts +13 -2
  57. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  58. package/packages/pi-agent-core/dist/types.js.map +1 -1
  59. package/packages/pi-agent-core/src/agent.ts +110 -4
  60. package/packages/pi-agent-core/src/types.ts +12 -3
  61. package/packages/pi-ai/dist/adaptive/classifier.d.ts +29 -0
  62. package/packages/pi-ai/dist/adaptive/classifier.d.ts.map +1 -0
  63. package/packages/pi-ai/dist/adaptive/classifier.js +72 -0
  64. package/packages/pi-ai/dist/adaptive/classifier.js.map +1 -0
  65. package/packages/pi-ai/dist/adaptive/classifier.test.d.ts +2 -0
  66. package/packages/pi-ai/dist/adaptive/classifier.test.d.ts.map +1 -0
  67. package/packages/pi-ai/dist/adaptive/classifier.test.js +32 -0
  68. package/packages/pi-ai/dist/adaptive/classifier.test.js.map +1 -0
  69. package/packages/pi-ai/dist/index.d.ts +1 -0
  70. package/packages/pi-ai/dist/index.d.ts.map +1 -1
  71. package/packages/pi-ai/dist/index.js +1 -0
  72. package/packages/pi-ai/dist/index.js.map +1 -1
  73. package/packages/pi-ai/dist/providers/amazon-bedrock.js +0 -2
  74. package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
  75. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
  76. package/packages/pi-ai/dist/providers/anthropic-shared.js +0 -2
  77. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
  78. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts +1 -1
  79. package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  80. package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  81. package/packages/pi-ai/dist/providers/google-gemini-cli.d.ts.map +1 -1
  82. package/packages/pi-ai/dist/providers/google-gemini-cli.js +0 -4
  83. package/packages/pi-ai/dist/providers/google-gemini-cli.js.map +1 -1
  84. package/packages/pi-ai/dist/providers/google-vertex.js +0 -5
  85. package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
  86. package/packages/pi-ai/dist/providers/google.js +0 -5
  87. package/packages/pi-ai/dist/providers/google.js.map +1 -1
  88. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts +1 -1
  89. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
  90. package/packages/pi-ai/dist/providers/openai-codex-responses.js +0 -2
  91. package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  92. package/packages/pi-ai/dist/providers/openai-completions.d.ts +1 -1
  93. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  94. package/packages/pi-ai/dist/providers/openai-completions.js +0 -1
  95. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  96. package/packages/pi-ai/dist/providers/openai-responses.d.ts +1 -1
  97. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  98. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  99. package/packages/pi-ai/dist/providers/openai-shared.d.ts +0 -1
  100. package/packages/pi-ai/dist/providers/openai-shared.d.ts.map +1 -1
  101. package/packages/pi-ai/dist/providers/openai-shared.js +0 -4
  102. package/packages/pi-ai/dist/providers/openai-shared.js.map +1 -1
  103. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  104. package/packages/pi-ai/dist/providers/simple-options.js +0 -1
  105. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  106. package/packages/pi-ai/dist/types.d.ts +1 -2
  107. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/types.js.map +1 -1
  109. package/packages/pi-ai/src/adaptive/classifier.test.ts +38 -0
  110. package/packages/pi-ai/src/adaptive/classifier.ts +107 -0
  111. package/packages/pi-ai/src/index.ts +1 -0
  112. package/packages/pi-ai/src/providers/amazon-bedrock.ts +0 -2
  113. package/packages/pi-ai/src/providers/anthropic-shared.ts +0 -2
  114. package/packages/pi-ai/src/providers/azure-openai-responses.ts +1 -1
  115. package/packages/pi-ai/src/providers/google-gemini-cli.ts +0 -4
  116. package/packages/pi-ai/src/providers/google-vertex.ts +0 -5
  117. package/packages/pi-ai/src/providers/google.ts +0 -5
  118. package/packages/pi-ai/src/providers/openai-codex-responses.ts +1 -3
  119. package/packages/pi-ai/src/providers/openai-completions.ts +1 -2
  120. package/packages/pi-ai/src/providers/openai-responses.ts +1 -1
  121. package/packages/pi-ai/src/providers/openai-shared.ts +0 -3
  122. package/packages/pi-ai/src/providers/simple-options.ts +0 -1
  123. package/packages/pi-ai/src/types.ts +1 -2
  124. package/packages/pi-coding-agent/dist/cli/args.js +2 -2
  125. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  126. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +7 -2
  127. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/agent-session.js +53 -20
  129. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/core/lsp/lsp.md +3 -1
  131. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/sdk.js +32 -6
  133. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/core/sdk.test.js +37 -0
  135. package/packages/pi-coding-agent/dist/core/sdk.test.js.map +1 -1
  136. package/packages/pi-coding-agent/dist/core/session-manager.d.ts +8 -0
  137. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  138. package/packages/pi-coding-agent/dist/core/session-manager.js +4 -0
  139. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  140. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +12 -7
  141. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  142. package/packages/pi-coding-agent/dist/core/settings-manager.js +20 -2
  143. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  144. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  145. package/packages/pi-coding-agent/dist/core/skills.js +4 -1
  146. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  147. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -1
  148. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  149. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  150. package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -2
  151. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  152. package/packages/pi-coding-agent/dist/core/tools/grep.js +1 -1
  153. package/packages/pi-coding-agent/dist/core/tools/grep.js.map +1 -1
  154. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
  155. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  156. package/packages/pi-coding-agent/dist/core/tools/index.js +2 -0
  157. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  158. package/packages/pi-coding-agent/dist/core/tools/pty.d.ts +10 -1
  159. package/packages/pi-coding-agent/dist/core/tools/pty.d.ts.map +1 -1
  160. package/packages/pi-coding-agent/dist/core/tools/pty.js +29 -3
  161. package/packages/pi-coding-agent/dist/core/tools/pty.js.map +1 -1
  162. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js +1 -1
  163. package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js.map +1 -1
  164. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  165. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +12 -2
  166. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  167. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +7 -2
  168. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  169. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +23 -4
  170. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  171. package/packages/pi-coding-agent/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
  172. package/packages/pi-coding-agent/dist/modes/interactive/components/thinking-selector.js +1 -2
  173. package/packages/pi-coding-agent/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  174. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  175. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +9 -0
  176. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  177. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  178. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +53 -2
  179. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  180. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts +2 -2
  181. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  182. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +10 -6
  183. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
  184. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +1 -1
  185. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  186. package/packages/pi-coding-agent/dist/modes/print-mode.d.ts.map +1 -1
  187. package/packages/pi-coding-agent/dist/modes/print-mode.js +6 -0
  188. package/packages/pi-coding-agent/dist/modes/print-mode.js.map +1 -1
  189. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  190. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +20 -0
  191. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  192. package/packages/pi-coding-agent/dist/tests/path-display.test.js +15 -0
  193. package/packages/pi-coding-agent/dist/tests/path-display.test.js.map +1 -1
  194. package/packages/pi-coding-agent/package.json +1 -1
  195. package/packages/pi-coding-agent/src/cli/args.ts +2 -2
  196. package/packages/pi-coding-agent/src/core/agent-session.ts +58 -21
  197. package/packages/pi-coding-agent/src/core/lsp/lsp.md +3 -1
  198. package/packages/pi-coding-agent/src/core/sdk.test.ts +45 -0
  199. package/packages/pi-coding-agent/src/core/sdk.ts +35 -6
  200. package/packages/pi-coding-agent/src/core/session-manager.ts +12 -0
  201. package/packages/pi-coding-agent/src/core/settings-manager.ts +32 -9
  202. package/packages/pi-coding-agent/src/core/skills.ts +4 -1
  203. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -1
  204. package/packages/pi-coding-agent/src/core/system-prompt.ts +8 -2
  205. package/packages/pi-coding-agent/src/core/tools/grep.ts +1 -1
  206. package/packages/pi-coding-agent/src/core/tools/index.ts +3 -0
  207. package/packages/pi-coding-agent/src/core/tools/pty.ts +45 -6
  208. package/packages/pi-coding-agent/src/modes/interactive/components/embedded-terminal.ts +1 -1
  209. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +10 -2
  210. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +31 -7
  211. package/packages/pi-coding-agent/src/modes/interactive/components/thinking-selector.ts +1 -2
  212. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +9 -0
  213. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +65 -3
  214. package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +11 -7
  215. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +1 -1
  216. package/packages/pi-coding-agent/src/modes/print-mode.ts +6 -0
  217. package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +29 -0
  218. package/packages/pi-coding-agent/src/tests/path-display.test.ts +17 -0
  219. package/packages/pi-tui/dist/components/loader.d.ts +5 -2
  220. package/packages/pi-tui/dist/components/loader.d.ts.map +1 -1
  221. package/packages/pi-tui/dist/components/loader.js +33 -3
  222. package/packages/pi-tui/dist/components/loader.js.map +1 -1
  223. package/packages/pi-tui/src/components/loader.ts +31 -3
  224. package/packages/rpc-client/src/index.ts +1 -1
  225. package/packages/rpc-client/src/rpc-client.ts +29 -0
  226. package/packages/rpc-client/src/rpc-types.ts +1 -1
  227. package/pkg/dist/modes/interactive/theme/theme.d.ts +2 -2
  228. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  229. package/pkg/dist/modes/interactive/theme/theme.js +10 -6
  230. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
  231. package/pkg/dist/modes/interactive/theme/themes.js +1 -1
  232. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  233. package/pkg/package.json +1 -1
  234. package/src/resources/agents/generic.md +1 -0
  235. package/src/resources/agents/scout.md +8 -1
  236. package/src/resources/agents/worker.md +1 -0
  237. package/src/resources/extensions/ask-user-questions.ts +88 -0
  238. package/src/resources/extensions/bg-shell/bg-shell-tool.ts +6 -16
  239. package/src/resources/extensions/mac-tools/index.ts +19 -34
  240. package/src/resources/extensions/memory/index.ts +22 -2
  241. package/src/resources/extensions/shared/interview-ui.ts +108 -15
  242. package/src/resources/extensions/shared/tests/ask-user-freetext.test.ts +61 -0
  243. package/src/resources/extensions/shared/tests/custom-ui-fallbacks.test.ts +46 -0
  244. package/src/resources/extensions/slash-commands/plan.ts +18 -19
  245. package/src/resources/extensions/slash-commands/tools.ts +43 -4
  246. package/src/resources/extensions/subagent/agent-switcher-component.ts +228 -0
  247. package/src/resources/extensions/subagent/agent-switcher-model.ts +160 -0
  248. package/src/resources/extensions/subagent/background-job-manager.ts +29 -6
  249. package/src/resources/extensions/subagent/background-runner.ts +8 -0
  250. package/src/resources/extensions/subagent/background-types.ts +4 -0
  251. package/src/resources/extensions/subagent/index.ts +834 -19
  252. package/src/resources/extensions/subagent/launch-helpers.ts +15 -4
@@ -32,11 +32,167 @@ import { loadEffectivePreferences } from "../shared/preferences.js";
32
32
  import { CmuxClient } from "../cmux/index.js";
33
33
  import { BackgroundJobManager } from "./background-job-manager.js";
34
34
  import { runSubagentInBackground } from "./background-runner.js";
35
+ import { showAgentSwitcher } from "./agent-switcher-component.js";
36
+ import { buildAgentSwitchTargets, } from "./agent-switcher-model.js";
35
37
  const MAX_PARALLEL_TASKS = 8;
36
38
  const MAX_CONCURRENCY = 4;
37
39
  const COLLAPSED_ITEM_COUNT = 10;
38
40
  const DEFAULT_AWAIT_SUBAGENT_TIMEOUT_SECONDS = 120;
39
41
  const liveSubagentProcesses = new Set();
42
+ const agentSessionLinksById = new Map();
43
+ const agentSessionIdsByParent = new Map();
44
+ const parentSessionByChild = new Map();
45
+ const liveRuntimeBySessionFile = new Map();
46
+ let agentSessionLinkCounter = 0;
47
+ function listSessionFiles(sessionDir) {
48
+ if (!fs.existsSync(sessionDir))
49
+ return [];
50
+ try {
51
+ return fs
52
+ .readdirSync(sessionDir)
53
+ .filter((name) => name.endsWith(".jsonl"))
54
+ .map((name) => path.join(sessionDir, name));
55
+ }
56
+ catch {
57
+ return [];
58
+ }
59
+ }
60
+ function detectNewSubagentSessionFile(sessionDir, before, startedAt) {
61
+ const after = listSessionFiles(sessionDir);
62
+ const created = after.filter((file) => !before.has(file));
63
+ const candidates = created.length > 0 ? created : after;
64
+ const ranked = candidates
65
+ .map((file) => {
66
+ let mtime = 0;
67
+ try {
68
+ mtime = fs.statSync(file).mtimeMs;
69
+ }
70
+ catch {
71
+ mtime = 0;
72
+ }
73
+ return { file, mtime };
74
+ })
75
+ .filter((entry) => entry.mtime >= startedAt - 5000)
76
+ .sort((a, b) => b.mtime - a.mtime);
77
+ return ranked[0]?.file;
78
+ }
79
+ function registerAgentSessionLink(link) {
80
+ const now = Date.now();
81
+ const id = `agent-${++agentSessionLinkCounter}`;
82
+ const full = { ...link, id, createdAt: now, updatedAt: now };
83
+ agentSessionLinksById.set(id, full);
84
+ const list = agentSessionIdsByParent.get(link.parentSessionFile) ?? [];
85
+ list.push(id);
86
+ agentSessionIdsByParent.set(link.parentSessionFile, list);
87
+ parentSessionByChild.set(link.subagentSessionFile, link.parentSessionFile);
88
+ return full;
89
+ }
90
+ function updateAgentSessionLinkState(subagentSessionFile, state) {
91
+ for (const link of agentSessionLinksById.values()) {
92
+ if (link.subagentSessionFile === subagentSessionFile) {
93
+ link.state = state;
94
+ link.updatedAt = Date.now();
95
+ return;
96
+ }
97
+ }
98
+ }
99
+ function upsertAgentSessionLink(agentName, task, parentSessionFile, subagentSessionFile, state) {
100
+ const existingParent = parentSessionByChild.get(subagentSessionFile);
101
+ if (!existingParent) {
102
+ registerAgentSessionLink({
103
+ agentName,
104
+ task,
105
+ parentSessionFile,
106
+ subagentSessionFile,
107
+ state,
108
+ });
109
+ return;
110
+ }
111
+ updateAgentSessionLinkState(subagentSessionFile, state);
112
+ }
113
+ function getAgentSessionLinksForParent(parentSessionFile) {
114
+ const ids = agentSessionIdsByParent.get(parentSessionFile) ?? [];
115
+ return ids
116
+ .map((id) => agentSessionLinksById.get(id))
117
+ .filter((entry) => Boolean(entry))
118
+ .sort((a, b) => b.updatedAt - a.updatedAt);
119
+ }
120
+ function readSessionHeader(sessionFile) {
121
+ try {
122
+ const content = fs.readFileSync(sessionFile, "utf-8");
123
+ const firstLine = content.split("\n").find((line) => line.trim().length > 0);
124
+ if (!firstLine)
125
+ return null;
126
+ const parsed = JSON.parse(firstLine);
127
+ if (!parsed || parsed.type !== "session")
128
+ return null;
129
+ return {
130
+ parentSession: typeof parsed.parentSession === "string" ? parsed.parentSession : undefined,
131
+ subagentName: typeof parsed.subagentName === "string" ? parsed.subagentName : undefined,
132
+ subagentTask: typeof parsed.subagentTask === "string" ? parsed.subagentTask : undefined,
133
+ subagentSystemPrompt: typeof parsed.subagentSystemPrompt === "string" ? parsed.subagentSystemPrompt : undefined,
134
+ subagentTools: Array.isArray(parsed.subagentTools)
135
+ ? parsed.subagentTools.filter((tool) => typeof tool === "string")
136
+ : undefined,
137
+ };
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ }
143
+ function backfillAgentSessionLinksForParent(parentSessionFile, sessionDir) {
144
+ for (const sessionFile of listSessionFiles(sessionDir)) {
145
+ if (sessionFile === parentSessionFile)
146
+ continue;
147
+ const header = readSessionHeader(sessionFile);
148
+ if (header?.parentSession !== parentSessionFile)
149
+ continue;
150
+ const existingParent = parentSessionByChild.get(sessionFile);
151
+ if (!existingParent) {
152
+ registerAgentSessionLink({
153
+ agentName: header.subagentName ?? "subagent",
154
+ task: header.subagentTask ?? "Recovered from persisted session lineage",
155
+ parentSessionFile,
156
+ subagentSessionFile: sessionFile,
157
+ state: "completed",
158
+ });
159
+ }
160
+ }
161
+ return getAgentSessionLinksForParent(parentSessionFile);
162
+ }
163
+ function formatSwitchTargetSummary(target) {
164
+ const current = target.isCurrent ? " (current)" : "";
165
+ if (target.kind === "parent") {
166
+ return `● parent — main session${current}`;
167
+ }
168
+ const icon = target.state === "running" ? "▶" : target.state === "failed" ? "✗" : "✓";
169
+ return `${icon} ${target.agentName} — ${target.taskPreview}${current}`;
170
+ }
171
+ function buildSwitchTargetsForParent(parentSessionFile, currentSessionFile, currentCwd, trackedLinks, runningJobs) {
172
+ return buildAgentSwitchTargets({
173
+ currentSessionFile,
174
+ rootParentSessionFile: parentSessionFile,
175
+ currentCwd,
176
+ trackedLinks: trackedLinks.map((link) => ({
177
+ id: link.id,
178
+ agentName: link.agentName,
179
+ task: link.task,
180
+ parentSessionFile: link.parentSessionFile,
181
+ subagentSessionFile: link.subagentSessionFile,
182
+ updatedAt: link.updatedAt,
183
+ state: link.state,
184
+ })),
185
+ runningJobs: runningJobs.map((job) => ({
186
+ id: job.id,
187
+ agentName: job.agentName,
188
+ task: job.task,
189
+ startedAt: job.startedAt,
190
+ parentSessionFile: job.parentSessionFile,
191
+ sessionFile: job.sessionFile,
192
+ cwd: job.cwd,
193
+ })),
194
+ });
195
+ }
40
196
  const AwaitSubagentParams = Type.Object({
41
197
  jobs: Type.Optional(Type.Array(Type.String(), {
42
198
  description: "Subagent job IDs to wait for. Omit to wait for the next running background subagent.",
@@ -340,7 +496,7 @@ function resolveSubagentCliPath(defaultCwd) {
340
496
  }
341
497
  return null;
342
498
  }
343
- function processSubagentEventLine(line, currentResult, emitUpdate, proc) {
499
+ function processSubagentEventLine(line, currentResult, emitUpdate, proc, onSessionInfo, onEventType, onParsedEvent) {
344
500
  if (!line.trim())
345
501
  return false;
346
502
  let event;
@@ -350,6 +506,29 @@ function processSubagentEventLine(line, currentResult, emitUpdate, proc) {
350
506
  catch {
351
507
  return false;
352
508
  }
509
+ const eventType = typeof event.type === "string" ? event.type : "unknown";
510
+ onEventType?.(eventType);
511
+ onParsedEvent?.(event);
512
+ if (event.type === "subagent_session_info") {
513
+ let changed = false;
514
+ if (typeof event.sessionFile === "string" && event.sessionFile) {
515
+ if (currentResult.sessionFile !== event.sessionFile)
516
+ changed = true;
517
+ currentResult.sessionFile = event.sessionFile;
518
+ }
519
+ if (typeof event.parentSessionFile === "string" && event.parentSessionFile) {
520
+ if (currentResult.parentSessionFile !== event.parentSessionFile)
521
+ changed = true;
522
+ currentResult.parentSessionFile = event.parentSessionFile;
523
+ }
524
+ if (changed) {
525
+ onSessionInfo?.({
526
+ sessionFile: currentResult.sessionFile,
527
+ parentSessionFile: currentResult.parentSessionFile,
528
+ });
529
+ }
530
+ return false;
531
+ }
353
532
  if (proc && isSubagentPermissionRequest(event)) {
354
533
  void handleSubagentPermissionRequest(event, proc);
355
534
  return false;
@@ -394,7 +573,7 @@ async function waitForFile(filePath, signal, timeoutMs = 30 * 60 * 1000) {
394
573
  }
395
574
  return false;
396
575
  }
397
- async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, modelOverride, parentModel, signal, onUpdate, makeDetails, foregroundHooks) {
576
+ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, modelOverride, parentModel, signal, onUpdate, makeDetails, parentSessionFile, attachableSession, onSessionInfo, onSubagentEvent, foregroundHooks) {
398
577
  const agent = agents.find((a) => a.name === agentName);
399
578
  if (!agent) {
400
579
  const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
@@ -425,6 +604,7 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
425
604
  usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
426
605
  model: inferredModel,
427
606
  step,
607
+ parentSessionFile,
428
608
  };
429
609
  const emitUpdate = () => {
430
610
  if (onUpdate) {
@@ -462,17 +642,27 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
462
642
  tmpPromptDir = tmp.dir;
463
643
  tmpPromptPath = tmp.filePath;
464
644
  }
465
- const args = buildSubagentProcessArgs(agent, task, tmpPromptPath, inferredModel);
645
+ const effectiveCwd = cwd ?? defaultCwd;
646
+ const subagentSessionDir = parentSessionFile ? path.dirname(parentSessionFile) : undefined;
647
+ const sessionFilesBefore = attachableSession && subagentSessionDir
648
+ ? new Set(listSessionFiles(subagentSessionDir))
649
+ : undefined;
650
+ const launchStartedAt = Date.now();
651
+ const args = buildSubagentProcessArgs(agent, task, tmpPromptPath, inferredModel, {
652
+ noSession: !attachableSession,
653
+ parentSessionFile: parentSessionFile,
654
+ mode: attachableSession ? "rpc" : "json",
655
+ });
466
656
  const exitCode = await new Promise((resolve) => {
467
657
  const bundledPaths = getBundledExtensionPathsFromEnv();
468
658
  const extensionArgs = bundledPaths.flatMap((p) => ["--extension", p]);
469
- const cliPath = resolveSubagentCliPath(cwd ?? defaultCwd);
659
+ const cliPath = resolveSubagentCliPath(effectiveCwd);
470
660
  if (!cliPath) {
471
661
  currentResult.stderr += "Unable to resolve LSD/GSD CLI path for subagent launch.";
472
662
  resolve(1);
473
663
  return;
474
664
  }
475
- const proc = spawn(process.execPath, [cliPath, ...extensionArgs, ...args], { cwd: cwd ?? defaultCwd, shell: false, stdio: ["pipe", "pipe", "pipe"] });
665
+ const proc = spawn(process.execPath, [cliPath, ...extensionArgs, ...args], { cwd: effectiveCwd, shell: false, stdio: ["pipe", "pipe", "pipe"] });
476
666
  // Keep stdin open so approval/classifier responses can be proxied back
477
667
  // into the child process. Closing it here can leave the subagent stuck
478
668
  // after its first turn when it requests permission for a tool call.
@@ -481,6 +671,9 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
481
671
  let completionSeen = false;
482
672
  let resolved = false;
483
673
  let foregroundReleased = false;
674
+ let isBusy = false;
675
+ let commandSeq = 0;
676
+ const pendingCommandResponses = new Map();
484
677
  const procAbortController = new AbortController();
485
678
  let resolveBackgroundResult;
486
679
  let rejectBackgroundResult;
@@ -488,6 +681,15 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
488
681
  resolveBackgroundResult = resolveBg;
489
682
  rejectBackgroundResult = rejectBg;
490
683
  });
684
+ const sendRpcCommand = async (command) => {
685
+ const id = `sa_cmd_${++commandSeq}`;
686
+ if (!proc.stdin)
687
+ throw new Error("Subagent RPC stdin is not available.");
688
+ return new Promise((resolveCmd, rejectCmd) => {
689
+ pendingCommandResponses.set(id, { resolve: resolveCmd, reject: rejectCmd });
690
+ proc.stdin.write(JSON.stringify({ id, ...command }) + "\n");
691
+ });
692
+ };
491
693
  const finishForeground = (code) => {
492
694
  if (resolved)
493
695
  return;
@@ -511,16 +713,60 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
511
713
  agentName,
512
714
  task,
513
715
  cwd: cwd ?? defaultCwd,
716
+ parentSessionFile,
514
717
  abortController: procAbortController,
515
718
  resultPromise: backgroundResultPromise,
516
719
  adoptToBackground,
720
+ sendPrompt: attachableSession
721
+ ? async (text, images) => {
722
+ await sendRpcCommand({ type: "prompt", message: text, images });
723
+ }
724
+ : undefined,
725
+ sendSteer: attachableSession
726
+ ? async (text, images) => {
727
+ await sendRpcCommand({ type: "steer", message: text, images });
728
+ }
729
+ : undefined,
730
+ sendFollowUp: attachableSession
731
+ ? async (text, images) => {
732
+ await sendRpcCommand({ type: "follow_up", message: text, images });
733
+ }
734
+ : undefined,
735
+ isBusy: attachableSession ? () => isBusy : undefined,
517
736
  });
518
737
  proc.stdout.on("data", (data) => {
519
738
  buffer += data.toString();
520
739
  const lines = buffer.split("\n");
521
740
  buffer = lines.pop() || "";
522
741
  for (const line of lines) {
523
- if (processSubagentEventLine(line, currentResult, emitUpdate, proc)) {
742
+ const trimmed = line.trim();
743
+ if (!trimmed)
744
+ continue;
745
+ if (attachableSession) {
746
+ try {
747
+ const parsed = JSON.parse(trimmed);
748
+ if (parsed?.type === "response" && typeof parsed.id === "string" && pendingCommandResponses.has(parsed.id)) {
749
+ const pending = pendingCommandResponses.get(parsed.id);
750
+ pendingCommandResponses.delete(parsed.id);
751
+ if (parsed.success === false) {
752
+ pending.reject(new Error(typeof parsed.error === "string" ? parsed.error : "Subagent RPC command failed."));
753
+ }
754
+ else {
755
+ pending.resolve(parsed.data);
756
+ }
757
+ continue;
758
+ }
759
+ }
760
+ catch {
761
+ // Fall through to generic event processing.
762
+ }
763
+ }
764
+ if (processSubagentEventLine(trimmed, currentResult, emitUpdate, proc, onSessionInfo, (eventType) => {
765
+ if (eventType === "agent_start")
766
+ isBusy = true;
767
+ if (eventType === "agent_end")
768
+ isBusy = false;
769
+ }, (event) => onSubagentEvent?.(event, currentResult))) {
524
770
  completionSeen = true;
525
771
  try {
526
772
  proc.kill("SIGTERM");
@@ -537,27 +783,65 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
537
783
  proc.on("close", (code) => {
538
784
  liveSubagentProcesses.delete(proc);
539
785
  if (buffer.trim()) {
540
- const completedOnFlush = processSubagentEventLine(buffer, currentResult, emitUpdate, proc);
786
+ const completedOnFlush = processSubagentEventLine(buffer, currentResult, emitUpdate, proc, onSessionInfo, (eventType) => {
787
+ if (eventType === "agent_start")
788
+ isBusy = true;
789
+ if (eventType === "agent_end")
790
+ isBusy = false;
791
+ }, (event) => onSubagentEvent?.(event, currentResult));
541
792
  completionSeen = completionSeen || completedOnFlush;
542
793
  }
794
+ isBusy = false;
795
+ for (const pending of pendingCommandResponses.values()) {
796
+ pending.reject(new Error("Subagent process closed before command response."));
797
+ }
798
+ pendingCommandResponses.clear();
543
799
  const finalExitCode = completionSeen && (code === null || code === 143 || code === 15) ? 0 : (code ?? 0);
544
800
  currentResult.exitCode = finalExitCode;
801
+ if (attachableSession && sessionFilesBefore && subagentSessionDir && !currentResult.sessionFile) {
802
+ const detected = detectNewSubagentSessionFile(subagentSessionDir, sessionFilesBefore, launchStartedAt);
803
+ if (detected)
804
+ currentResult.sessionFile = detected;
805
+ }
545
806
  resolveBackgroundResult?.({
546
807
  summary: getFinalOutput(currentResult.messages),
547
808
  stderr: currentResult.stderr,
548
809
  exitCode: finalExitCode,
549
810
  model: currentResult.model,
811
+ sessionFile: currentResult.sessionFile,
812
+ parentSessionFile: currentResult.parentSessionFile,
550
813
  });
551
814
  foregroundHooks?.onFinish?.();
552
815
  finishForeground(finalExitCode);
553
816
  });
554
817
  proc.on("error", (error) => {
555
818
  liveSubagentProcesses.delete(proc);
819
+ isBusy = false;
820
+ for (const pending of pendingCommandResponses.values()) {
821
+ pending.reject(error instanceof Error ? error : new Error(String(error)));
822
+ }
823
+ pendingCommandResponses.clear();
556
824
  rejectBackgroundResult?.(error);
557
825
  foregroundHooks?.onFinish?.();
558
826
  finishForeground(1);
559
827
  });
828
+ if (attachableSession) {
829
+ void sendRpcCommand({ type: "prompt", message: task }).catch((error) => {
830
+ currentResult.stderr += error instanceof Error ? error.message : String(error);
831
+ try {
832
+ proc.kill("SIGTERM");
833
+ }
834
+ catch {
835
+ /* ignore */
836
+ }
837
+ });
838
+ }
560
839
  const killProc = () => {
840
+ // If the process has been adopted to the background (e.g. via Ctrl+B or
841
+ // /agent attach_live), foregroundReleased is true and the process should
842
+ // survive the parent session's abort — don't kill it.
843
+ if (foregroundReleased)
844
+ return;
561
845
  wasAborted = true;
562
846
  procAbortController.abort();
563
847
  proc.kill("SIGTERM");
@@ -586,6 +870,12 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
586
870
  }
587
871
  });
588
872
  currentResult.exitCode = exitCode;
873
+ if (attachableSession && sessionFilesBefore && subagentSessionDir) {
874
+ const detected = detectNewSubagentSessionFile(subagentSessionDir, sessionFilesBefore, launchStartedAt);
875
+ if (detected) {
876
+ currentResult.sessionFile = detected;
877
+ }
878
+ }
589
879
  if (wasAborted)
590
880
  throw new Error("Subagent was aborted");
591
881
  return currentResult;
@@ -646,14 +936,62 @@ export default function (pi) {
646
936
  const foregroundSubagentStatusKey = "foreground-subagent";
647
937
  const foregroundSubagentHint = "Ctrl+B: move foreground subagent to background";
648
938
  let activeForegroundSubagent = null;
939
+ let activeSessionFileForUi;
940
+ const liveStreamBufferBySession = new Map();
941
+ function flushLiveStream(sessionFile) {
942
+ const buffered = liveStreamBufferBySession.get(sessionFile);
943
+ if (!buffered || !buffered.trim())
944
+ return;
945
+ liveStreamBufferBySession.set(sessionFile, "");
946
+ pi.sendMessage({
947
+ customType: "live_subagent_stream",
948
+ content: buffered,
949
+ display: true,
950
+ }, { deliverAs: "followUp" });
951
+ }
952
+ function pushLiveStreamDelta(sessionFile, delta) {
953
+ const prev = liveStreamBufferBySession.get(sessionFile) ?? "";
954
+ const next = prev + delta;
955
+ liveStreamBufferBySession.set(sessionFile, next);
956
+ if (next.length >= 120 || next.includes("\n")) {
957
+ flushLiveStream(sessionFile);
958
+ }
959
+ }
960
+ function getCurrentSessionSubagentMetadata(sessionFile) {
961
+ if (!sessionFile)
962
+ return null;
963
+ return readSessionHeader(sessionFile);
964
+ }
965
+ function applyCurrentSessionSubagentTools(ctx) {
966
+ const metadata = getCurrentSessionSubagentMetadata(ctx.sessionManager.getSessionFile());
967
+ if (metadata?.subagentTools && metadata.subagentTools.length > 0) {
968
+ ctx.setActiveTools(metadata.subagentTools);
969
+ }
970
+ }
649
971
  function getBgManager() {
650
972
  if (!bgManager)
651
973
  throw new Error("BackgroundJobManager not initialized.");
652
974
  return bgManager;
653
975
  }
654
- pi.on("session_start", async () => {
976
+ pi.on("session_start", async (_event, ctx) => {
977
+ activeSessionFileForUi = ctx.sessionManager.getSessionFile();
655
978
  bgManager = new BackgroundJobManager({
656
979
  onJobComplete: (job) => {
980
+ if (job.sessionFile && job.parentSessionFile) {
981
+ const existingParent = parentSessionByChild.get(job.sessionFile);
982
+ if (!existingParent) {
983
+ registerAgentSessionLink({
984
+ agentName: job.agentName,
985
+ task: job.task,
986
+ parentSessionFile: job.parentSessionFile,
987
+ subagentSessionFile: job.sessionFile,
988
+ state: job.status === "failed" ? "failed" : "completed",
989
+ });
990
+ }
991
+ else {
992
+ updateAgentSessionLinkState(job.sessionFile, job.status === "failed" ? "failed" : "completed");
993
+ }
994
+ }
657
995
  if (job.awaited)
658
996
  return;
659
997
  const statusEmoji = job.status === "completed" ? "✓" : job.status === "cancelled" ? "✗ cancelled" : "✗ failed";
@@ -676,22 +1014,80 @@ export default function (pi) {
676
1014
  }, { deliverAs: "followUp" });
677
1015
  },
678
1016
  });
1017
+ applyCurrentSessionSubagentTools(ctx);
679
1018
  });
680
- pi.on("session_before_switch", async () => {
681
- activeForegroundSubagent = null;
682
- if (bgManager) {
683
- for (const job of bgManager.getRunningJobs()) {
684
- bgManager.cancel(job.id);
1019
+ pi.on("session_switch", async (_event, ctx) => {
1020
+ activeSessionFileForUi = ctx.sessionManager.getSessionFile();
1021
+ applyCurrentSessionSubagentTools(ctx);
1022
+ });
1023
+ pi.on("before_agent_start", async (event, ctx) => {
1024
+ const metadata = getCurrentSessionSubagentMetadata(ctx.sessionManager.getSessionFile());
1025
+ if (!metadata?.subagentSystemPrompt)
1026
+ return;
1027
+ const subagentName = metadata.subagentName ?? "subagent";
1028
+ const taskNote = metadata.subagentTask
1029
+ ? `Original delegated task: ${metadata.subagentTask}`
1030
+ : "Continue operating as the delegated subagent for this session.";
1031
+ const antiRecursion = [
1032
+ `You are already the ${subagentName} subagent for this session.`,
1033
+ "Do not spawn or delegate to another subagent with the same name as yourself.",
1034
+ `If the user asks you to continue ${subagentName} work, do that work directly in this session.`,
1035
+ taskNote,
1036
+ "IMPORTANT: There is NO human available to answer questions in this session. Do NOT call ask_user_questions. Make all decisions autonomously based on the task and context.",
1037
+ ].join("\n");
1038
+ return {
1039
+ systemPrompt: `${event.systemPrompt}\n\n${antiRecursion}\n\n${metadata.subagentSystemPrompt}`,
1040
+ };
1041
+ });
1042
+ pi.on("input", async (event, ctx) => {
1043
+ const sessionFile = ctx.sessionManager.getSessionFile();
1044
+ if (!sessionFile)
1045
+ return;
1046
+ const runtime = liveRuntimeBySessionFile.get(sessionFile);
1047
+ if (!runtime)
1048
+ return;
1049
+ const text = event.text?.trim();
1050
+ if (!text)
1051
+ return { action: "handled" };
1052
+ const isSlashCommand = text.startsWith("/");
1053
+ if (isSlashCommand)
1054
+ return;
1055
+ try {
1056
+ if (runtime.isBusy()) {
1057
+ await runtime.sendSteer(text, event.images);
1058
+ ctx.ui.notify(`Sent steer to running subagent ${runtime.agentName}.`, "info");
685
1059
  }
1060
+ else {
1061
+ await runtime.sendPrompt(text, event.images);
1062
+ ctx.ui.notify(`Sent prompt to live subagent ${runtime.agentName}.`, "info");
1063
+ }
1064
+ return { action: "handled" };
1065
+ }
1066
+ catch (error) {
1067
+ ctx.ui.notify(`Failed to send input to live subagent ${runtime.agentName}: ${error instanceof Error ? error.message : String(error)}`, "error");
1068
+ return { action: "handled" };
686
1069
  }
687
1070
  });
1071
+ pi.on("session_before_switch", async () => {
1072
+ if (activeSessionFileForUi)
1073
+ flushLiveStream(activeSessionFileForUi);
1074
+ activeForegroundSubagent = null;
1075
+ });
688
1076
  pi.on("session_shutdown", async () => {
1077
+ if (activeSessionFileForUi)
1078
+ flushLiveStream(activeSessionFileForUi);
1079
+ activeSessionFileForUi = undefined;
689
1080
  activeForegroundSubagent = null;
690
1081
  await stopLiveSubagents();
691
1082
  if (bgManager) {
692
1083
  bgManager.shutdown();
693
1084
  bgManager = null;
694
1085
  }
1086
+ agentSessionLinksById.clear();
1087
+ agentSessionIdsByParent.clear();
1088
+ parentSessionByChild.clear();
1089
+ liveRuntimeBySessionFile.clear();
1090
+ liveStreamBufferBySession.clear();
695
1091
  });
696
1092
  // /subagents command
697
1093
  pi.registerCommand("subagents", {
@@ -795,6 +1191,191 @@ export default function (pi) {
795
1191
  ctx.ui.notify(`Available agents (${discovery.agents.length}):\n${lines.join("\n")}`, "info");
796
1192
  },
797
1193
  });
1194
+ // /agent command - switch to the parent or a tracked subagent session
1195
+ pi.registerCommand("agent", {
1196
+ description: "Switch focus to parent/subagent sessions (/agent picker, /agent <id|index|name>, /agent parent)",
1197
+ handler: async (args, ctx) => {
1198
+ const currentSessionFile = ctx.sessionManager.getSessionFile();
1199
+ if (!currentSessionFile) {
1200
+ ctx.ui.notify("Current session is in-memory only; /agent requires a persisted session file.", "warning");
1201
+ return;
1202
+ }
1203
+ const arg = args.trim();
1204
+ const parentSessionFile = parentSessionByChild.get(currentSessionFile);
1205
+ const currentParent = parentSessionFile ?? currentSessionFile;
1206
+ const currentSessionDir = path.dirname(currentParent);
1207
+ let tracked = getAgentSessionLinksForParent(currentParent).filter((entry) => fs.existsSync(entry.subagentSessionFile));
1208
+ if (tracked.length === 0) {
1209
+ tracked = backfillAgentSessionLinksForParent(currentParent, currentSessionDir)
1210
+ .filter((entry) => fs.existsSync(entry.subagentSessionFile));
1211
+ }
1212
+ const runningJobs = bgManager?.getRunningJobs() ?? [];
1213
+ const switchTargets = buildSwitchTargetsForParent(currentParent, currentSessionFile, ctx.cwd, tracked, runningJobs);
1214
+ const applySwitchTarget = async (target) => {
1215
+ if (target.selectionAction === "blocked") {
1216
+ ctx.ui.notify(target.blockedReason ?? "That target cannot be selected yet.", "warning");
1217
+ return;
1218
+ }
1219
+ if (target.selectionAction === "attach_live") {
1220
+ if (!fs.existsSync(target.sessionFile)) {
1221
+ ctx.ui.notify(`Live subagent session file is missing: ${target.sessionFile}`, "error");
1222
+ return;
1223
+ }
1224
+ const liveRuntime = liveRuntimeBySessionFile.get(target.sessionFile);
1225
+ if (!liveRuntime) {
1226
+ ctx.ui.notify("Live runtime is no longer available for this subagent. It may have completed.", "warning");
1227
+ return;
1228
+ }
1229
+ // Adopt the foreground subagent to background before switching sessions.
1230
+ // switchSession calls abort() which would fire the tool signal and SIGTERM
1231
+ // the running subagent process. Adopting to background detaches the process
1232
+ // from the foreground abort chain so it survives the session switch.
1233
+ const foreground = activeForegroundSubagent;
1234
+ if (foreground && !foreground.claimed && bgManager) {
1235
+ foreground.claimed = true;
1236
+ try {
1237
+ const jobId = bgManager.adoptRunning(foreground.agentName, foreground.task, foreground.cwd, foreground.abortController, foreground.resultPromise, {
1238
+ parentSessionFile: foreground.parentSessionFile ?? ctx.sessionManager.getSessionFile(),
1239
+ });
1240
+ const released = foreground.adoptToBackground(jobId);
1241
+ if (!released) {
1242
+ foreground.claimed = false;
1243
+ bgManager.cancel(jobId);
1244
+ }
1245
+ else {
1246
+ activeForegroundSubagent = null;
1247
+ ctx.ui.setStatus(foregroundSubagentStatusKey, undefined);
1248
+ }
1249
+ }
1250
+ catch {
1251
+ foreground.claimed = false;
1252
+ }
1253
+ }
1254
+ const switched = await ctx.switchSession(target.sessionFile);
1255
+ if (switched.cancelled) {
1256
+ ctx.ui.notify("Session switch was cancelled.", "warning");
1257
+ return;
1258
+ }
1259
+ ctx.ui.notify(`Attached to running subagent ${target.agentName}. Prompts in this session are routed live (busy => steer, idle => prompt). Use /agent parent to return.`, "info");
1260
+ return;
1261
+ }
1262
+ if (target.kind === "parent") {
1263
+ if (!fs.existsSync(target.sessionFile)) {
1264
+ ctx.ui.notify(`Parent session file not found: ${target.sessionFile}`, "error");
1265
+ return;
1266
+ }
1267
+ const switched = await ctx.switchSession(target.sessionFile);
1268
+ if (switched.cancelled) {
1269
+ ctx.ui.notify("Session switch was cancelled.", "warning");
1270
+ return;
1271
+ }
1272
+ ctx.ui.notify("Switched to parent session.", "info");
1273
+ return;
1274
+ }
1275
+ if (!fs.existsSync(target.sessionFile)) {
1276
+ ctx.ui.notify(`Subagent session file is missing: ${target.sessionFile}`, "error");
1277
+ return;
1278
+ }
1279
+ const switched = await ctx.switchSession(target.sessionFile);
1280
+ if (switched.cancelled) {
1281
+ ctx.ui.notify("Session switch was cancelled.", "warning");
1282
+ return;
1283
+ }
1284
+ updateAgentSessionLinkState(target.sessionFile, target.state === "failed" ? "failed" : "completed");
1285
+ ctx.ui.notify(`Switched to subagent ${target.agentName}. This resumes the saved subagent session; use /agent parent to return.`, "info");
1286
+ };
1287
+ if (!arg) {
1288
+ const subagentTargets = switchTargets.filter((target) => target.kind === "subagent");
1289
+ if (ctx.hasUI) {
1290
+ if (subagentTargets.length === 0 && !parentSessionFile) {
1291
+ ctx.ui.notify("No tracked subagent sessions for this parent session yet. Run a single-mode subagent first (foreground or background).", "info");
1292
+ return;
1293
+ }
1294
+ const selected = await showAgentSwitcher(ctx, switchTargets);
1295
+ if (!selected)
1296
+ return;
1297
+ await applySwitchTarget(selected);
1298
+ return;
1299
+ }
1300
+ if (subagentTargets.length === 0 && !parentSessionFile) {
1301
+ ctx.ui.notify("No tracked subagent sessions for this parent session yet. Run a single-mode subagent first (foreground or background).", "info");
1302
+ return;
1303
+ }
1304
+ const lines = ["Agent switch targets:"];
1305
+ switchTargets.forEach((target, index) => {
1306
+ lines.push(`${index + 1}. ${formatSwitchTargetSummary(target)}`);
1307
+ });
1308
+ lines.push("", "Use `/agent <index|id|name>` for explicit targeting, or `/agent parent`.");
1309
+ ctx.ui.notify(lines.join("\n"), "info");
1310
+ return;
1311
+ }
1312
+ if (arg === "parent" || arg === "main") {
1313
+ if (!parentSessionFile) {
1314
+ ctx.ui.notify("You are already in the parent/main session.", "info");
1315
+ return;
1316
+ }
1317
+ if (!fs.existsSync(parentSessionFile)) {
1318
+ ctx.ui.notify(`Parent session file not found: ${parentSessionFile}`, "error");
1319
+ return;
1320
+ }
1321
+ const switched = await ctx.switchSession(parentSessionFile);
1322
+ if (switched.cancelled) {
1323
+ ctx.ui.notify("Session switch was cancelled.", "warning");
1324
+ return;
1325
+ }
1326
+ ctx.ui.notify("Switched to parent session.", "info");
1327
+ return;
1328
+ }
1329
+ let target;
1330
+ if (/^\d+$/.test(arg)) {
1331
+ const index = Number.parseInt(arg, 10) - 1;
1332
+ target = tracked[index];
1333
+ }
1334
+ if (!target) {
1335
+ target = tracked.find((entry) => entry.id === arg);
1336
+ }
1337
+ if (!target) {
1338
+ target = tracked.find((entry) => entry.agentName === arg);
1339
+ }
1340
+ if (!target) {
1341
+ target = tracked.find((entry) => path.basename(entry.subagentSessionFile) === arg);
1342
+ }
1343
+ if (!target) {
1344
+ const runningTarget = switchTargets.find((entry) => entry.id === arg && entry.kind === "subagent");
1345
+ if (runningTarget?.state === "running") {
1346
+ ctx.ui.notify(runningTarget.blockedReason ?? "Selected subagent is still running. Live attach is not implemented yet.", "warning");
1347
+ return;
1348
+ }
1349
+ ctx.ui.notify(`Unknown subagent target: ${arg}. Run /agent to list available targets.`, "warning");
1350
+ return;
1351
+ }
1352
+ if (!fs.existsSync(target.subagentSessionFile)) {
1353
+ ctx.ui.notify(`Subagent session file is missing: ${target.subagentSessionFile}`, "error");
1354
+ return;
1355
+ }
1356
+ if (target.state === "running") {
1357
+ const liveRuntime = liveRuntimeBySessionFile.get(target.subagentSessionFile);
1358
+ if (!liveRuntime) {
1359
+ ctx.ui.notify("Live runtime is no longer available for this subagent. It may have completed.", "warning");
1360
+ return;
1361
+ }
1362
+ const switched = await ctx.switchSession(target.subagentSessionFile);
1363
+ if (switched.cancelled) {
1364
+ ctx.ui.notify("Session switch was cancelled.", "warning");
1365
+ return;
1366
+ }
1367
+ ctx.ui.notify(`Attached to running subagent ${target.agentName}. Prompts in this session are routed live (busy => steer, idle => prompt). Use /agent parent to return.`, "info");
1368
+ return;
1369
+ }
1370
+ const switched = await ctx.switchSession(target.subagentSessionFile);
1371
+ if (switched.cancelled) {
1372
+ ctx.ui.notify("Session switch was cancelled.", "warning");
1373
+ return;
1374
+ }
1375
+ updateAgentSessionLinkState(target.subagentSessionFile, target.state === "failed" ? "failed" : "completed");
1376
+ ctx.ui.notify(`Switched to subagent ${target.agentName}. This resumes the saved subagent session; use /agent parent to return.`, "info");
1377
+ },
1378
+ });
798
1379
  pi.registerShortcut(Key.ctrl("b"), {
799
1380
  description: shortcutDesc("Move foreground subagent to background", "/subagents list"),
800
1381
  handler: async (ctx) => {
@@ -809,7 +1390,9 @@ export default function (pi) {
809
1390
  running.claimed = true;
810
1391
  let jobId;
811
1392
  try {
812
- jobId = manager.adoptRunning(running.agentName, running.task, running.cwd, running.abortController, running.resultPromise);
1393
+ jobId = manager.adoptRunning(running.agentName, running.task, running.cwd, running.abortController, running.resultPromise, {
1394
+ parentSessionFile: running.parentSessionFile ?? ctx.sessionManager.getSessionFile(),
1395
+ });
813
1396
  }
814
1397
  catch (error) {
815
1398
  running.claimed = false;
@@ -873,7 +1456,7 @@ export default function (pi) {
873
1456
  "For broad review or audit requests, use scout only as a prep step; the parent model or a reviewer should make the final judgments.",
874
1457
  "Skip scout when the user already named the exact file/function to inspect or the task is obviously narrow.",
875
1458
  "Use parallel mode when tasks are independent and don't need each other's output.",
876
- "Use background: true when the user wants to keep chatting while a long-running agent works in parallel.",
1459
+ "Default to foreground (background: false) for single-mode subagents. Only set background: true when the user explicitly asks to run it in the background or to keep chatting while it runs.",
877
1460
  "If the user wants to wait for a background subagent result, use await_subagent.",
878
1461
  ],
879
1462
  parameters: SubagentParams,
@@ -884,6 +1467,7 @@ export default function (pi) {
884
1467
  const confirmProjectAgents = params.confirmProjectAgents ?? false;
885
1468
  const cmuxClient = CmuxClient.fromPreferences(loadEffectivePreferences()?.preferences);
886
1469
  const cmuxSplitsEnabled = cmuxClient.getConfig().splits;
1470
+ const invokingSessionFile = ctx.sessionManager.getSessionFile();
887
1471
  // Resolve isolation mode
888
1472
  const isolationMode = readIsolationMode();
889
1473
  const useIsolation = Boolean(params.isolated) && isolationMode !== "none";
@@ -960,7 +1544,7 @@ export default function (pi) {
960
1544
  }
961
1545
  }
962
1546
  : undefined;
963
- const result = await runSingleAgent(ctx.cwd, agents, step.agent, taskWithContext, step.cwd, i + 1, step.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, signal, chainUpdate, makeDetails("chain"));
1547
+ const result = await runSingleAgent(ctx.cwd, agents, step.agent, taskWithContext, step.cwd, i + 1, step.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, signal, chainUpdate, makeDetails("chain"), invokingSessionFile, false, undefined, undefined);
964
1548
  results.push(result);
965
1549
  const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
966
1550
  if (isError) {
@@ -1027,7 +1611,7 @@ export default function (pi) {
1027
1611
  allResults[index] = partial.details.results[0];
1028
1612
  emitParallelUpdate();
1029
1613
  }
1030
- }, makeDetails("parallel"));
1614
+ }, makeDetails("parallel"), invokingSessionFile, false, undefined);
1031
1615
  let result = await runTask();
1032
1616
  // Auto-retry failed tasks (likely API rate limit or transient error)
1033
1617
  const isFailed = result.exitCode !== 0 || (result.messages.length === 0 && !signal?.aborted);
@@ -1084,14 +1668,61 @@ export default function (pi) {
1084
1668
  const bgInferredModel = resolveSubagentModel({ name: agentForBg.name, model: bgResolvedModelCfg }, { overrideModel: params.model, parentModel: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined });
1085
1669
  let jobId;
1086
1670
  try {
1087
- jobId = runSubagentInBackground(manager, agents, params.agent, params.task, params.cwd, params.model, { defaultCwd: ctx.cwd, model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined }, async (bgSignal) => {
1671
+ jobId = runSubagentInBackground(manager, agents, params.agent, params.task, params.cwd, params.model, { defaultCwd: ctx.cwd, model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, parentSessionFile: invokingSessionFile }, async (bgSignal) => {
1672
+ let liveSessionFile;
1673
+ let liveRuntime;
1088
1674
  const result = await runSingleAgent(ctx.cwd, agents, params.agent, params.task, params.cwd, undefined, params.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, bgSignal, undefined, // no streaming updates for background jobs
1089
- makeDetails("single"));
1675
+ makeDetails("single"), invokingSessionFile, true, (info) => {
1676
+ if (!invokingSessionFile || !info.sessionFile)
1677
+ return;
1678
+ upsertAgentSessionLink(params.agent, params.task, invokingSessionFile, info.sessionFile, "running");
1679
+ liveSessionFile = info.sessionFile;
1680
+ if (liveRuntime) {
1681
+ liveRuntime.sessionFile = info.sessionFile;
1682
+ liveRuntime.parentSessionFile = info.parentSessionFile ?? invokingSessionFile;
1683
+ liveRuntimeBySessionFile.set(info.sessionFile, liveRuntime);
1684
+ }
1685
+ }, (event, partial) => {
1686
+ const sessionFile = partial.sessionFile;
1687
+ if (!sessionFile || activeSessionFileForUi !== sessionFile)
1688
+ return;
1689
+ if (event?.type === "message_update" && event.assistantMessageEvent?.type === "text_delta") {
1690
+ const delta = String(event.assistantMessageEvent.delta ?? "");
1691
+ if (delta)
1692
+ pushLiveStreamDelta(sessionFile, delta);
1693
+ }
1694
+ if (event?.type === "message_end") {
1695
+ flushLiveStream(sessionFile);
1696
+ }
1697
+ }, {
1698
+ onStart: (control) => {
1699
+ if (!control.sendPrompt || !control.sendSteer || !control.sendFollowUp || !control.isBusy)
1700
+ return;
1701
+ liveRuntime = {
1702
+ sessionFile: liveSessionFile,
1703
+ parentSessionFile: invokingSessionFile,
1704
+ agentName: params.agent,
1705
+ isBusy: control.isBusy,
1706
+ sendPrompt: control.sendPrompt,
1707
+ sendSteer: control.sendSteer,
1708
+ sendFollowUp: control.sendFollowUp,
1709
+ };
1710
+ if (liveSessionFile) {
1711
+ liveRuntimeBySessionFile.set(liveSessionFile, liveRuntime);
1712
+ }
1713
+ },
1714
+ onFinish: () => {
1715
+ if (liveSessionFile)
1716
+ liveRuntimeBySessionFile.delete(liveSessionFile);
1717
+ },
1718
+ });
1090
1719
  return {
1091
1720
  exitCode: result.exitCode,
1092
1721
  finalOutput: getFinalOutput(result.messages),
1093
1722
  stderr: result.stderr,
1094
1723
  model: result.model,
1724
+ sessionFile: result.sessionFile,
1725
+ parentSessionFile: result.parentSessionFile,
1095
1726
  };
1096
1727
  });
1097
1728
  }
@@ -1117,18 +1748,77 @@ export default function (pi) {
1117
1748
  const taskId = crypto.randomUUID();
1118
1749
  isolation = await createIsolation(effectiveCwd, taskId, isolationMode);
1119
1750
  }
1120
- const result = await runSingleAgent(ctx.cwd, agents, params.agent, params.task, isolation ? isolation.workDir : params.cwd, undefined, params.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, signal, onUpdate, makeDetails("single"), !isolation
1751
+ let liveSessionFile;
1752
+ let liveRuntime;
1753
+ const result = await runSingleAgent(ctx.cwd, agents, params.agent, params.task, isolation ? isolation.workDir : params.cwd, undefined, params.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, signal, onUpdate, makeDetails("single"), invokingSessionFile, !isolation, !isolation
1754
+ ? (info) => {
1755
+ if (!invokingSessionFile || !info.sessionFile)
1756
+ return;
1757
+ upsertAgentSessionLink(params.agent, params.task, invokingSessionFile, info.sessionFile, "running");
1758
+ liveSessionFile = info.sessionFile;
1759
+ if (liveRuntime) {
1760
+ liveRuntime.sessionFile = info.sessionFile;
1761
+ liveRuntime.parentSessionFile = info.parentSessionFile ?? invokingSessionFile;
1762
+ liveRuntimeBySessionFile.set(info.sessionFile, liveRuntime);
1763
+ }
1764
+ }
1765
+ : undefined, !isolation
1766
+ ? (event, partial) => {
1767
+ const sessionFile = partial.sessionFile;
1768
+ if (!sessionFile || activeSessionFileForUi !== sessionFile)
1769
+ return;
1770
+ if (event?.type === "message_update" && event.assistantMessageEvent?.type === "text_delta") {
1771
+ const delta = String(event.assistantMessageEvent.delta ?? "");
1772
+ if (delta)
1773
+ pushLiveStreamDelta(sessionFile, delta);
1774
+ }
1775
+ if (event?.type === "message_end") {
1776
+ flushLiveStream(sessionFile);
1777
+ }
1778
+ }
1779
+ : undefined, !isolation
1121
1780
  ? {
1122
1781
  onStart: (control) => {
1123
1782
  activeForegroundSubagent = { ...control, claimed: false };
1124
1783
  ctx.ui.setStatus(foregroundSubagentStatusKey, foregroundSubagentHint);
1784
+ if (!control.sendPrompt || !control.sendSteer || !control.sendFollowUp || !control.isBusy)
1785
+ return;
1786
+ liveRuntime = {
1787
+ sessionFile: liveSessionFile,
1788
+ parentSessionFile: invokingSessionFile,
1789
+ agentName: params.agent,
1790
+ isBusy: control.isBusy,
1791
+ sendPrompt: control.sendPrompt,
1792
+ sendSteer: control.sendSteer,
1793
+ sendFollowUp: control.sendFollowUp,
1794
+ };
1795
+ if (liveSessionFile && liveRuntime) {
1796
+ liveRuntimeBySessionFile.set(liveSessionFile, liveRuntime);
1797
+ }
1125
1798
  },
1126
1799
  onFinish: () => {
1127
1800
  activeForegroundSubagent = null;
1128
1801
  ctx.ui.setStatus(foregroundSubagentStatusKey, undefined);
1802
+ if (liveSessionFile)
1803
+ liveRuntimeBySessionFile.delete(liveSessionFile);
1129
1804
  },
1130
1805
  }
1131
1806
  : undefined);
1807
+ if (result.sessionFile && invokingSessionFile) {
1808
+ const existingParent = parentSessionByChild.get(result.sessionFile);
1809
+ if (!existingParent) {
1810
+ registerAgentSessionLink({
1811
+ agentName: result.agent,
1812
+ task: result.task,
1813
+ parentSessionFile: invokingSessionFile,
1814
+ subagentSessionFile: result.sessionFile,
1815
+ state: result.exitCode === 0 ? "completed" : "failed",
1816
+ });
1817
+ }
1818
+ else {
1819
+ updateAgentSessionLinkState(result.sessionFile, result.exitCode === 0 ? "completed" : "failed");
1820
+ }
1821
+ }
1132
1822
  if (result.backgroundJobId) {
1133
1823
  return {
1134
1824
  content: [{ type: "text", text: `Moved ${result.agent} to background as **${result.backgroundJobId}**. Use \`await_subagent\`, \`/subagents wait ${result.backgroundJobId}\`, or \`/subagents output ${result.backgroundJobId}\`.` }],
@@ -1143,10 +1833,11 @@ export default function (pi) {
1143
1833
  }
1144
1834
  }
1145
1835
  const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
1836
+ const agentSwitchHint = result.sessionFile ? "\n\nTip: run `/agent` to switch focus to this subagent session." : "";
1146
1837
  if (isError) {
1147
1838
  const errorMsg = result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
1148
1839
  return {
1149
- content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
1840
+ content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}${agentSwitchHint}` }],
1150
1841
  details: makeDetails("single")([result]),
1151
1842
  isError: true,
1152
1843
  };
@@ -1155,6 +1846,8 @@ export default function (pi) {
1155
1846
  if (mergeResult && !mergeResult.success) {
1156
1847
  outputText += `\n\n⚠ Patch merge failed: ${mergeResult.error || "unknown error"}`;
1157
1848
  }
1849
+ if (agentSwitchHint)
1850
+ outputText += agentSwitchHint;
1158
1851
  return {
1159
1852
  content: [{ type: "text", text: outputText }],
1160
1853
  details: makeDetails("single")([result]),