lsd-pi 1.3.2 → 1.3.7

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 (205) hide show
  1. package/dist/cli.js +2 -1
  2. package/dist/lsd-settings-manager.d.ts +2 -0
  3. package/dist/lsd-settings-manager.js +5 -0
  4. package/dist/resource-loader.js +33 -3
  5. package/dist/resources/extensions/browser-tools/tools/codegen.js +5 -5
  6. package/dist/resources/extensions/browser-tools/tools/navigation.js +107 -178
  7. package/dist/resources/extensions/browser-tools/tools/network-mock.js +112 -167
  8. package/dist/resources/extensions/browser-tools/tools/pages.js +182 -234
  9. package/dist/resources/extensions/browser-tools/tools/refs.js +202 -461
  10. package/dist/resources/extensions/browser-tools/tools/session.js +176 -323
  11. package/dist/resources/extensions/browser-tools/tools/state-persistence.js +91 -154
  12. package/dist/resources/extensions/browser-tools/utils.js +1 -1
  13. package/dist/resources/extensions/cache-timer/index.js +3 -2
  14. package/dist/resources/extensions/slash-commands/extension-manifest.json +2 -2
  15. package/dist/resources/extensions/slash-commands/fast.js +73 -0
  16. package/dist/resources/extensions/slash-commands/index.js +2 -0
  17. package/dist/resources/extensions/slash-commands/plan.js +37 -12
  18. package/dist/resources/extensions/subagent/background-job-manager.js +13 -0
  19. package/dist/resources/extensions/subagent/in-process-runner.js +387 -0
  20. package/dist/resources/extensions/subagent/index.js +278 -626
  21. package/dist/resources/extensions/subagent/legacy-runner.js +503 -0
  22. package/dist/resources/extensions/voice/index.js +96 -36
  23. package/dist/resources/extensions/voice/push-to-talk.js +26 -0
  24. package/dist/welcome-screen.js +2 -2
  25. package/package.json +1 -1
  26. package/packages/pi-agent-core/dist/agent.d.ts +19 -0
  27. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  28. package/packages/pi-agent-core/dist/agent.js +16 -0
  29. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  30. package/packages/pi-agent-core/src/agent.ts +32 -2
  31. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts +34 -1
  32. package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
  33. package/packages/pi-ai/dist/providers/openai-codex-responses.js +32 -4
  34. package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
  35. package/packages/pi-ai/dist/providers/openai-codex-responses.test.js +127 -16
  36. package/packages/pi-ai/dist/providers/openai-codex-responses.test.js.map +1 -1
  37. package/packages/pi-ai/dist/providers/openai-responses.d.ts +8 -1
  38. package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  39. package/packages/pi-ai/dist/providers/openai-responses.fast-mode.test.d.ts +2 -0
  40. package/packages/pi-ai/dist/providers/openai-responses.fast-mode.test.d.ts.map +1 -0
  41. package/packages/pi-ai/dist/providers/openai-responses.fast-mode.test.js +67 -0
  42. package/packages/pi-ai/dist/providers/openai-responses.fast-mode.test.js.map +1 -0
  43. package/packages/pi-ai/dist/providers/openai-responses.js +21 -3
  44. package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
  45. package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  46. package/packages/pi-ai/dist/providers/simple-options.js +2 -0
  47. package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
  48. package/packages/pi-ai/dist/types.d.ts +5 -0
  49. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  50. package/packages/pi-ai/dist/types.js.map +1 -1
  51. package/packages/pi-ai/src/providers/openai-codex-responses.test.ts +143 -20
  52. package/packages/pi-ai/src/providers/openai-codex-responses.ts +47 -4
  53. package/packages/pi-ai/src/providers/openai-responses.fast-mode.test.ts +73 -0
  54. package/packages/pi-ai/src/providers/openai-responses.ts +26 -3
  55. package/packages/pi-ai/src/providers/simple-options.ts +2 -0
  56. package/packages/pi-ai/src/types.ts +5 -0
  57. package/packages/pi-coding-agent/dist/core/keybindings.d.ts +1 -1
  58. package/packages/pi-coding-agent/dist/core/keybindings.d.ts.map +1 -1
  59. package/packages/pi-coding-agent/dist/core/keybindings.js +2 -0
  60. package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
  61. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  62. package/packages/pi-coding-agent/dist/core/sdk.js +4 -2
  63. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  64. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.d.ts +2 -0
  65. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.d.ts.map +1 -0
  66. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.js +35 -0
  67. package/packages/pi-coding-agent/dist/core/settings-manager.collapse-tool-calls.test.js.map +1 -0
  68. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +12 -0
  69. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/core/settings-manager.fast-mode.test.d.ts +2 -0
  71. package/packages/pi-coding-agent/dist/core/settings-manager.fast-mode.test.d.ts.map +1 -0
  72. package/packages/pi-coding-agent/dist/core/settings-manager.fast-mode.test.js +35 -0
  73. package/packages/pi-coding-agent/dist/core/settings-manager.fast-mode.test.js.map +1 -0
  74. package/packages/pi-coding-agent/dist/core/settings-manager.js +24 -0
  75. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  76. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  78. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -1
  81. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  82. package/packages/pi-coding-agent/dist/core/tool-priority.d.ts +4 -0
  83. package/packages/pi-coding-agent/dist/core/tool-priority.d.ts.map +1 -0
  84. package/packages/pi-coding-agent/dist/core/tool-priority.js +18 -0
  85. package/packages/pi-coding-agent/dist/core/tool-priority.js.map +1 -0
  86. package/packages/pi-coding-agent/dist/core/tool-priority.test.d.ts +2 -0
  87. package/packages/pi-coding-agent/dist/core/tool-priority.test.d.ts.map +1 -0
  88. package/packages/pi-coding-agent/dist/core/tool-priority.test.js +27 -0
  89. package/packages/pi-coding-agent/dist/core/tool-priority.test.js.map +1 -0
  90. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts +5 -0
  91. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
  92. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +21 -0
  93. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +16 -1
  95. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.d.ts +2 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.d.ts.map +1 -0
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js +34 -0
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/tool-summary-line.test.js.map +1 -0
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.d.ts +45 -0
  101. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.d.ts.map +1 -0
  102. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.js +314 -0
  103. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.js.map +1 -0
  104. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.test.d.ts +2 -0
  105. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.test.d.ts.map +1 -0
  106. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.test.js +122 -0
  107. package/packages/pi-coding-agent/dist/modes/interactive/components/btw-overlay.test.js.map +1 -0
  108. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts +7 -5
  109. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.d.ts.map +1 -1
  110. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js +86 -28
  111. package/packages/pi-coding-agent/dist/modes/interactive/components/diff.js.map +1 -1
  112. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts +4 -0
  113. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +23 -10
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +8 -0
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  118. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +52 -6
  119. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +19 -0
  121. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +127 -14
  123. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  124. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts +14 -0
  125. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.d.ts.map +1 -0
  126. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js +93 -0
  127. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-summary-line.js.map +1 -0
  128. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.d.ts +2 -0
  129. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.d.ts.map +1 -0
  130. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js +328 -0
  131. package/packages/pi-coding-agent/dist/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.js.map +1 -0
  132. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  133. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +123 -0
  134. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  135. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +1 -1
  136. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -1
  137. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -1
  138. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +7 -0
  139. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -1
  140. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +4 -0
  141. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -1
  142. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -1
  143. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +9 -1
  144. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  145. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +103 -23
  146. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  147. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.d.ts.map +1 -1
  148. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js +41 -0
  149. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js.map +1 -1
  150. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +4 -4
  151. package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
  152. package/packages/pi-coding-agent/package.json +1 -1
  153. package/packages/pi-coding-agent/src/core/keybindings.ts +4 -1
  154. package/packages/pi-coding-agent/src/core/sdk.ts +4 -2
  155. package/packages/pi-coding-agent/src/core/settings-manager.collapse-tool-calls.test.ts +46 -0
  156. package/packages/pi-coding-agent/src/core/settings-manager.fast-mode.test.ts +46 -0
  157. package/packages/pi-coding-agent/src/core/settings-manager.ts +36 -0
  158. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  159. package/packages/pi-coding-agent/src/core/system-prompt.ts +6 -1
  160. package/packages/pi-coding-agent/src/core/tool-priority.test.ts +30 -0
  161. package/packages/pi-coding-agent/src/core/tool-priority.ts +17 -0
  162. package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +20 -0
  163. package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +26 -0
  164. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/tool-summary-line.test.ts +41 -0
  165. package/packages/pi-coding-agent/src/modes/interactive/components/btw-overlay.test.ts +172 -0
  166. package/packages/pi-coding-agent/src/modes/interactive/components/btw-overlay.ts +402 -0
  167. package/packages/pi-coding-agent/src/modes/interactive/components/diff.ts +105 -28
  168. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +21 -6
  169. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +63 -6
  170. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +1262 -1138
  171. package/packages/pi-coding-agent/src/modes/interactive/components/tool-summary-line.ts +120 -0
  172. package/packages/pi-coding-agent/src/modes/interactive/controllers/__tests__/chat-controller.collapsed-tool-summary.test.ts +396 -0
  173. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +530 -398
  174. package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +1 -1
  175. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +7 -0
  176. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +4 -0
  177. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +109 -23
  178. package/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +60 -1
  179. package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +4 -4
  180. package/packages/pi-tui/dist/components/editor.js +3 -3
  181. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  182. package/packages/pi-tui/src/components/editor.ts +3 -3
  183. package/pkg/dist/modes/interactive/theme/themes.js +4 -4
  184. package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
  185. package/pkg/package.json +1 -1
  186. package/src/resources/extensions/browser-tools/tools/codegen.ts +5 -5
  187. package/src/resources/extensions/browser-tools/tools/navigation.ts +118 -196
  188. package/src/resources/extensions/browser-tools/tools/network-mock.ts +114 -205
  189. package/src/resources/extensions/browser-tools/tools/pages.ts +183 -237
  190. package/src/resources/extensions/browser-tools/tools/refs.ts +193 -507
  191. package/src/resources/extensions/browser-tools/tools/session.ts +182 -321
  192. package/src/resources/extensions/browser-tools/tools/state-persistence.ts +94 -172
  193. package/src/resources/extensions/browser-tools/utils.ts +1 -1
  194. package/src/resources/extensions/cache-timer/index.ts +3 -2
  195. package/src/resources/extensions/slash-commands/extension-manifest.json +2 -2
  196. package/src/resources/extensions/slash-commands/fast.ts +89 -0
  197. package/src/resources/extensions/slash-commands/index.ts +2 -0
  198. package/src/resources/extensions/slash-commands/plan.ts +42 -12
  199. package/src/resources/extensions/subagent/background-job-manager.ts +28 -0
  200. package/src/resources/extensions/subagent/in-process-runner.ts +534 -0
  201. package/src/resources/extensions/subagent/index.ts +489 -799
  202. package/src/resources/extensions/subagent/legacy-runner.ts +607 -0
  203. package/src/resources/extensions/voice/index.ts +308 -238
  204. package/src/resources/extensions/voice/push-to-talk.ts +42 -0
  205. package/src/resources/extensions/voice/tests/push-to-talk.test.ts +109 -0
@@ -0,0 +1,503 @@
1
+ import { spawn, execFileSync } from "node:child_process";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { getAgentDir } from "@gsd/pi-coding-agent";
6
+ import { buildSubagentProcessArgs, getBundledExtensionPathsFromEnv } from "./launch-helpers.js";
7
+ import { handleSubagentPermissionRequest, isSubagentPermissionRequest } from "./approval-proxy.js";
8
+ import { resolveConfiguredSubagentModel } from "./configured-model.js";
9
+ import { normalizeSubagentModel, resolveSubagentModel } from "./model-resolution.js";
10
+ import { loadEffectivePreferences } from "../shared/preferences.js";
11
+ const liveSubagentProcesses = new Set();
12
+ export function readBudgetSubagentModelFromSettings() {
13
+ try {
14
+ const settingsPath = path.join(getAgentDir(), "settings.json");
15
+ if (!fs.existsSync(settingsPath))
16
+ return undefined;
17
+ const raw = fs.readFileSync(settingsPath, "utf-8");
18
+ const parsed = JSON.parse(raw);
19
+ return typeof parsed.budgetSubagentModel === "string"
20
+ ? normalizeSubagentModel(parsed.budgetSubagentModel)
21
+ : undefined;
22
+ }
23
+ catch {
24
+ return undefined;
25
+ }
26
+ }
27
+ export async function stopLegacySubagents() {
28
+ const active = Array.from(liveSubagentProcesses);
29
+ if (active.length === 0)
30
+ return;
31
+ for (const proc of active) {
32
+ try {
33
+ proc.kill("SIGTERM");
34
+ }
35
+ catch {
36
+ /* ignore */
37
+ }
38
+ }
39
+ await Promise.all(active.map((proc) => new Promise((resolve) => {
40
+ const done = () => resolve();
41
+ const timer = setTimeout(done, 500);
42
+ proc.once("exit", () => {
43
+ clearTimeout(timer);
44
+ resolve();
45
+ });
46
+ })));
47
+ for (const proc of active) {
48
+ if (proc.exitCode === null) {
49
+ try {
50
+ proc.kill("SIGKILL");
51
+ }
52
+ catch {
53
+ /* ignore */
54
+ }
55
+ }
56
+ }
57
+ }
58
+ function listSessionFiles(sessionDir) {
59
+ if (!fs.existsSync(sessionDir))
60
+ return [];
61
+ try {
62
+ return fs
63
+ .readdirSync(sessionDir)
64
+ .filter((name) => name.endsWith(".jsonl"))
65
+ .map((name) => path.join(sessionDir, name));
66
+ }
67
+ catch {
68
+ return [];
69
+ }
70
+ }
71
+ function detectNewSubagentSessionFile(sessionDir, before, startedAt) {
72
+ const after = listSessionFiles(sessionDir);
73
+ const created = after.filter((file) => !before.has(file));
74
+ const candidates = created.length > 0 ? created : after;
75
+ const ranked = candidates
76
+ .map((file) => {
77
+ let mtime = 0;
78
+ try {
79
+ mtime = fs.statSync(file).mtimeMs;
80
+ }
81
+ catch {
82
+ mtime = 0;
83
+ }
84
+ return { file, mtime };
85
+ })
86
+ .filter((entry) => entry.mtime >= startedAt - 5000)
87
+ .sort((a, b) => b.mtime - a.mtime);
88
+ return ranked[0]?.file;
89
+ }
90
+ function resolveSubagentCliPath(defaultCwd) {
91
+ const candidates = [process.env.GSD_BIN_PATH, process.env.LSD_BIN_PATH, process.argv[1]]
92
+ .map((value) => value?.trim())
93
+ .filter((value) => Boolean(value && value !== "undefined"));
94
+ for (const candidate of candidates) {
95
+ if (path.isAbsolute(candidate) && fs.existsSync(candidate))
96
+ return candidate;
97
+ }
98
+ const cwdCandidates = [path.join(defaultCwd, "dist", "loader.js"), path.join(defaultCwd, "scripts", "dev-cli.js")];
99
+ for (const candidate of cwdCandidates) {
100
+ if (fs.existsSync(candidate))
101
+ return candidate;
102
+ }
103
+ for (const binName of ["lsd", "gsd"]) {
104
+ try {
105
+ const resolved = execFileSync("which", [binName], { encoding: "utf-8" }).trim();
106
+ if (resolved)
107
+ return resolved;
108
+ }
109
+ catch {
110
+ /* ignore */
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+ function processSubagentEventLine(line, currentResult, emitUpdate, proc, onSessionInfo, onEventType, onParsedEvent) {
116
+ if (!line.trim())
117
+ return false;
118
+ let event;
119
+ try {
120
+ event = JSON.parse(line);
121
+ }
122
+ catch {
123
+ return false;
124
+ }
125
+ const eventType = typeof event.type === "string" ? event.type : "unknown";
126
+ onEventType?.(eventType);
127
+ onParsedEvent?.(event);
128
+ if (event.type === "subagent_session_info") {
129
+ let changed = false;
130
+ if (typeof event.sessionFile === "string" && event.sessionFile) {
131
+ if (currentResult.sessionFile !== event.sessionFile)
132
+ changed = true;
133
+ currentResult.sessionFile = event.sessionFile;
134
+ }
135
+ if (typeof event.parentSessionFile === "string" && event.parentSessionFile) {
136
+ if (currentResult.parentSessionFile !== event.parentSessionFile)
137
+ changed = true;
138
+ currentResult.parentSessionFile = event.parentSessionFile;
139
+ }
140
+ if (changed) {
141
+ onSessionInfo?.({
142
+ sessionFile: currentResult.sessionFile,
143
+ parentSessionFile: currentResult.parentSessionFile,
144
+ });
145
+ }
146
+ return false;
147
+ }
148
+ if (proc && isSubagentPermissionRequest(event)) {
149
+ void handleSubagentPermissionRequest(event, proc);
150
+ return false;
151
+ }
152
+ if ((event.type === "message_end" || event.type === "turn_end") && event.message) {
153
+ const msg = event.message;
154
+ currentResult.messages.push(msg);
155
+ if (msg.role === "assistant") {
156
+ currentResult.usage.turns++;
157
+ const usage = msg.usage;
158
+ if (usage) {
159
+ currentResult.usage.input += usage.input || 0;
160
+ currentResult.usage.output += usage.output || 0;
161
+ currentResult.usage.cacheRead += usage.cacheRead || 0;
162
+ currentResult.usage.cacheWrite += usage.cacheWrite || 0;
163
+ currentResult.usage.cost += usage.cost?.total || 0;
164
+ currentResult.usage.contextTokens = usage.totalTokens || 0;
165
+ }
166
+ if (msg.model && (!currentResult.model || msg.model.includes("/")))
167
+ currentResult.model = msg.model;
168
+ if (msg.stopReason)
169
+ currentResult.stopReason = msg.stopReason;
170
+ if (msg.errorMessage)
171
+ currentResult.errorMessage = msg.errorMessage;
172
+ }
173
+ emitUpdate();
174
+ }
175
+ if (event.type === "tool_result_end" && event.message) {
176
+ currentResult.messages.push(event.message);
177
+ emitUpdate();
178
+ }
179
+ return event.type === "agent_end";
180
+ }
181
+ function writePromptToTempFile(agentName, prompt) {
182
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
183
+ const safeName = agentName.replace(/[^\w.-]+/g, "_");
184
+ const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
185
+ fs.writeFileSync(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
186
+ return { dir: tmpDir, filePath };
187
+ }
188
+ function getFinalOutput(messages) {
189
+ for (let i = messages.length - 1; i >= 0; i--) {
190
+ const msg = messages[i];
191
+ if (msg.role === "assistant") {
192
+ for (const part of msg.content) {
193
+ if (part.type === "text")
194
+ return part.text;
195
+ }
196
+ }
197
+ }
198
+ return "";
199
+ }
200
+ export async function runLegacySingleAgent(defaultCwd, agents, agentName, task, cwd, step, modelOverride, parentModel, signal, onUpdate, makeDetails, parentSessionFile, attachableSession, onSessionInfo, onSubagentEvent, foregroundHooks) {
201
+ const agent = agents.find((a) => a.name === agentName);
202
+ if (!agent) {
203
+ const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
204
+ return {
205
+ agent: agentName,
206
+ agentSource: "unknown",
207
+ task,
208
+ exitCode: 1,
209
+ messages: [],
210
+ stderr: `Unknown agent: "${agentName}". Available agents: ${available}.`,
211
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
212
+ step,
213
+ };
214
+ }
215
+ let tmpPromptDir = null;
216
+ let tmpPromptPath = null;
217
+ const preferences = loadEffectivePreferences()?.preferences;
218
+ const settingsBudgetModel = readBudgetSubagentModelFromSettings();
219
+ const resolvedModel = resolveConfiguredSubagentModel(agent, preferences, settingsBudgetModel);
220
+ const inferredModel = resolveSubagentModel({ name: agent.name, model: resolvedModel }, { overrideModel: modelOverride, parentModel });
221
+ const currentResult = {
222
+ agent: agentName,
223
+ agentSource: agent.source,
224
+ task,
225
+ exitCode: 0,
226
+ messages: [],
227
+ stderr: "",
228
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
229
+ model: inferredModel,
230
+ step,
231
+ parentSessionFile,
232
+ };
233
+ const emitUpdate = () => {
234
+ if (onUpdate) {
235
+ onUpdate({
236
+ content: [{ type: "text", text: getFinalOutput(currentResult.messages) || "(running...)" }],
237
+ details: makeDetails([currentResult]),
238
+ });
239
+ }
240
+ };
241
+ let wasAborted = false;
242
+ let deferTempPromptCleanup = false;
243
+ let tempPromptCleanupDone = false;
244
+ const cleanupTempPromptFiles = () => {
245
+ if (tempPromptCleanupDone)
246
+ return;
247
+ tempPromptCleanupDone = true;
248
+ if (tmpPromptPath)
249
+ try {
250
+ fs.unlinkSync(tmpPromptPath);
251
+ }
252
+ catch {
253
+ /* ignore */
254
+ }
255
+ if (tmpPromptDir)
256
+ try {
257
+ fs.rmdirSync(tmpPromptDir);
258
+ }
259
+ catch {
260
+ /* ignore */
261
+ }
262
+ };
263
+ try {
264
+ if (agent.systemPrompt.trim()) {
265
+ const tmp = writePromptToTempFile(agent.name, agent.systemPrompt);
266
+ tmpPromptDir = tmp.dir;
267
+ tmpPromptPath = tmp.filePath;
268
+ }
269
+ const effectiveCwd = cwd ?? defaultCwd;
270
+ const subagentSessionDir = parentSessionFile ? path.dirname(parentSessionFile) : undefined;
271
+ const sessionFilesBefore = attachableSession && subagentSessionDir
272
+ ? new Set(listSessionFiles(subagentSessionDir))
273
+ : undefined;
274
+ const launchStartedAt = Date.now();
275
+ const args = buildSubagentProcessArgs(agent, task, tmpPromptPath, inferredModel, {
276
+ noSession: !attachableSession,
277
+ parentSessionFile: parentSessionFile,
278
+ mode: attachableSession ? "rpc" : "json",
279
+ });
280
+ const exitCode = await new Promise((resolve) => {
281
+ const bundledPaths = getBundledExtensionPathsFromEnv();
282
+ const extensionArgs = bundledPaths.flatMap((p) => ["--extension", p]);
283
+ const cliPath = resolveSubagentCliPath(effectiveCwd);
284
+ if (!cliPath) {
285
+ currentResult.stderr += "Unable to resolve LSD/GSD CLI path for subagent launch.";
286
+ resolve(1);
287
+ return;
288
+ }
289
+ const proc = spawn(process.execPath, [cliPath, ...extensionArgs, ...args], { cwd: effectiveCwd, shell: false, stdio: ["pipe", "pipe", "pipe"] });
290
+ liveSubagentProcesses.add(proc);
291
+ let buffer = "";
292
+ let completionSeen = false;
293
+ let resolved = false;
294
+ let foregroundReleased = false;
295
+ let isBusy = false;
296
+ let commandSeq = 0;
297
+ const pendingCommandResponses = new Map();
298
+ const procAbortController = new AbortController();
299
+ let resolveBackgroundResult;
300
+ let rejectBackgroundResult;
301
+ const backgroundResultPromise = new Promise((resolveBg, rejectBg) => {
302
+ resolveBackgroundResult = resolveBg;
303
+ rejectBackgroundResult = rejectBg;
304
+ });
305
+ const sendRpcCommand = async (command) => {
306
+ const id = `sa_cmd_${++commandSeq}`;
307
+ if (!proc.stdin)
308
+ throw new Error("Subagent RPC stdin is not available.");
309
+ return new Promise((resolveCmd, rejectCmd) => {
310
+ pendingCommandResponses.set(id, { resolve: resolveCmd, reject: rejectCmd });
311
+ proc.stdin.write(JSON.stringify({ id, ...command }) + "\n");
312
+ });
313
+ };
314
+ const finishForeground = (code) => {
315
+ if (resolved)
316
+ return;
317
+ resolved = true;
318
+ resolve(code);
319
+ };
320
+ const adoptToBackground = (jobId) => {
321
+ if (resolved || foregroundReleased)
322
+ return false;
323
+ foregroundReleased = true;
324
+ deferTempPromptCleanup = true;
325
+ currentResult.backgroundJobId = jobId;
326
+ finishForeground(0);
327
+ return true;
328
+ };
329
+ backgroundResultPromise.finally(() => {
330
+ if (deferTempPromptCleanup)
331
+ cleanupTempPromptFiles();
332
+ });
333
+ foregroundHooks?.onStart?.({
334
+ agentName,
335
+ task,
336
+ cwd: cwd ?? defaultCwd,
337
+ parentSessionFile,
338
+ abortController: procAbortController,
339
+ resultPromise: backgroundResultPromise,
340
+ adoptToBackground,
341
+ sendPrompt: attachableSession
342
+ ? async (text, images) => {
343
+ await sendRpcCommand({ type: "prompt", message: text, images });
344
+ }
345
+ : undefined,
346
+ sendSteer: attachableSession
347
+ ? async (text, images) => {
348
+ await sendRpcCommand({ type: "steer", message: text, images });
349
+ }
350
+ : undefined,
351
+ sendFollowUp: attachableSession
352
+ ? async (text, images) => {
353
+ await sendRpcCommand({ type: "follow_up", message: text, images });
354
+ }
355
+ : undefined,
356
+ isBusy: attachableSession ? () => isBusy : undefined,
357
+ });
358
+ proc.stdout.on("data", (data) => {
359
+ buffer += data.toString();
360
+ const lines = buffer.split("\n");
361
+ buffer = lines.pop() || "";
362
+ for (const line of lines) {
363
+ const trimmed = line.trim();
364
+ if (!trimmed)
365
+ continue;
366
+ if (attachableSession) {
367
+ try {
368
+ const parsed = JSON.parse(trimmed);
369
+ if (parsed?.type === "response" && typeof parsed.id === "string" && pendingCommandResponses.has(parsed.id)) {
370
+ const pending = pendingCommandResponses.get(parsed.id);
371
+ pendingCommandResponses.delete(parsed.id);
372
+ if (parsed.success === false) {
373
+ pending.reject(new Error(typeof parsed.error === "string" ? parsed.error : "Subagent RPC command failed."));
374
+ }
375
+ else {
376
+ pending.resolve(parsed.data);
377
+ }
378
+ continue;
379
+ }
380
+ }
381
+ catch {
382
+ // Fall through to generic event processing.
383
+ }
384
+ }
385
+ if (processSubagentEventLine(trimmed, currentResult, emitUpdate, proc, onSessionInfo, (eventType) => {
386
+ if (eventType === "agent_start")
387
+ isBusy = true;
388
+ if (eventType === "agent_end")
389
+ isBusy = false;
390
+ }, (event) => onSubagentEvent?.(event, currentResult))) {
391
+ completionSeen = true;
392
+ try {
393
+ proc.kill("SIGTERM");
394
+ }
395
+ catch {
396
+ /* ignore */
397
+ }
398
+ }
399
+ }
400
+ });
401
+ proc.stderr.on("data", (data) => {
402
+ currentResult.stderr += data.toString();
403
+ });
404
+ proc.on("close", (code) => {
405
+ liveSubagentProcesses.delete(proc);
406
+ if (buffer.trim()) {
407
+ const completedOnFlush = processSubagentEventLine(buffer, currentResult, emitUpdate, proc, onSessionInfo, (eventType) => {
408
+ if (eventType === "agent_start")
409
+ isBusy = true;
410
+ if (eventType === "agent_end")
411
+ isBusy = false;
412
+ }, (event) => onSubagentEvent?.(event, currentResult));
413
+ completionSeen = completionSeen || completedOnFlush;
414
+ }
415
+ isBusy = false;
416
+ for (const pending of pendingCommandResponses.values()) {
417
+ pending.reject(new Error("Subagent process closed before command response."));
418
+ }
419
+ pendingCommandResponses.clear();
420
+ const finalExitCode = completionSeen && (code === null || code === 143 || code === 15) ? 0 : (code ?? 0);
421
+ currentResult.exitCode = finalExitCode;
422
+ if (attachableSession && sessionFilesBefore && subagentSessionDir && !currentResult.sessionFile) {
423
+ const detected = detectNewSubagentSessionFile(subagentSessionDir, sessionFilesBefore, launchStartedAt);
424
+ if (detected)
425
+ currentResult.sessionFile = detected;
426
+ }
427
+ resolveBackgroundResult?.({
428
+ summary: getFinalOutput(currentResult.messages),
429
+ stderr: currentResult.stderr,
430
+ exitCode: finalExitCode,
431
+ model: currentResult.model,
432
+ sessionFile: currentResult.sessionFile,
433
+ parentSessionFile: currentResult.parentSessionFile,
434
+ });
435
+ foregroundHooks?.onFinish?.();
436
+ finishForeground(finalExitCode);
437
+ });
438
+ proc.on("error", (error) => {
439
+ liveSubagentProcesses.delete(proc);
440
+ isBusy = false;
441
+ for (const pending of pendingCommandResponses.values()) {
442
+ pending.reject(error instanceof Error ? error : new Error(String(error)));
443
+ }
444
+ pendingCommandResponses.clear();
445
+ rejectBackgroundResult?.(error);
446
+ foregroundHooks?.onFinish?.();
447
+ finishForeground(1);
448
+ });
449
+ if (attachableSession) {
450
+ void sendRpcCommand({ type: "prompt", message: task }).catch((error) => {
451
+ currentResult.stderr += error instanceof Error ? error.message : String(error);
452
+ try {
453
+ proc.kill("SIGTERM");
454
+ }
455
+ catch {
456
+ /* ignore */
457
+ }
458
+ });
459
+ }
460
+ const killProc = () => {
461
+ wasAborted = true;
462
+ procAbortController.abort();
463
+ proc.kill("SIGTERM");
464
+ setTimeout(() => {
465
+ if (!proc.killed)
466
+ proc.kill("SIGKILL");
467
+ }, 5000);
468
+ };
469
+ if (signal) {
470
+ if (signal.aborted)
471
+ killProc();
472
+ else
473
+ signal.addEventListener("abort", killProc, { once: true });
474
+ }
475
+ if (procAbortController.signal.aborted) {
476
+ killProc();
477
+ }
478
+ else {
479
+ procAbortController.signal.addEventListener("abort", () => {
480
+ proc.kill("SIGTERM");
481
+ setTimeout(() => {
482
+ if (!proc.killed)
483
+ proc.kill("SIGKILL");
484
+ }, 5000);
485
+ }, { once: true });
486
+ }
487
+ });
488
+ currentResult.exitCode = exitCode;
489
+ if (attachableSession && sessionFilesBefore && subagentSessionDir) {
490
+ const detected = detectNewSubagentSessionFile(subagentSessionDir, sessionFilesBefore, launchStartedAt);
491
+ if (detected) {
492
+ currentResult.sessionFile = detected;
493
+ }
494
+ }
495
+ if (wasAborted)
496
+ throw new Error("Subagent was aborted");
497
+ return currentResult;
498
+ }
499
+ finally {
500
+ if (!deferTempPromptCleanup)
501
+ cleanupTempPromptFiles();
502
+ }
503
+ }
@@ -1,10 +1,11 @@
1
1
  import { shortcutDesc } from "../shared/mod.js";
2
- import { isKeyRelease, Key, matchesKey, truncateToWidth, visibleWidth } from "@gsd/pi-tui";
2
+ import { isKeyRelease, isKittyProtocolActive, Key, matchesKey, truncateToWidth, visibleWidth } from "@gsd/pi-tui";
3
3
  import { spawn, execFileSync } from "node:child_process";
4
4
  import * as fs from "node:fs";
5
5
  import * as path from "node:path";
6
6
  import * as readline from "node:readline";
7
7
  import { linuxPython, diagnoseSounddeviceError, ensureVoiceVenv } from "./linux-ready.js";
8
+ import { handlePushToTalkInput } from "./push-to-talk.js";
8
9
  const __extensionDir = import.meta.dirname;
9
10
  const SWIFT_SRC = path.join(__extensionDir, "speech-recognizer.swift");
10
11
  const RECOGNIZER_BIN = path.join(__extensionDir, "speech-recognizer");
@@ -76,10 +77,14 @@ export default function (pi) {
76
77
  if (!IS_DARWIN && !IS_LINUX)
77
78
  return;
78
79
  let active = false;
80
+ let activationMode = null;
79
81
  let recognizerProcess = null;
80
82
  let flashOn = true;
81
83
  let flashTimer = null;
82
84
  let footerTui = null;
85
+ let closeVoiceOverlay = null;
86
+ let voiceSessionPromise = null;
87
+ let holdToTalkUnsupportedNotified = false;
83
88
  function setVoiceFooter(ctx, on) {
84
89
  if (!on) {
85
90
  stopFlash();
@@ -153,41 +158,59 @@ export default function (pi) {
153
158
  }
154
159
  footerTui = null;
155
160
  }
156
- async function toggleVoice(ctx) {
157
- if (active) {
158
- killRecognizer();
159
- active = false;
160
- setVoiceFooter(ctx, false);
161
- return;
161
+ function killRecognizer() {
162
+ if (recognizerProcess) {
163
+ recognizerProcess.kill("SIGTERM");
164
+ recognizerProcess = null;
162
165
  }
166
+ }
167
+ function prepareVoice(ctx) {
163
168
  if (IS_DARWIN) {
164
169
  if (!ensureBinary()) {
165
170
  ctx.ui.notify("Voice: failed to compile speech recognizer (need Xcode CLI tools)", "error");
166
- return;
171
+ return false;
167
172
  }
168
173
  }
169
174
  else if (IS_LINUX) {
170
175
  if (!ensureLinuxReady(ctx)) {
171
- return;
176
+ return false;
172
177
  }
173
178
  }
179
+ return true;
180
+ }
181
+ async function startVoice(ctx, mode) {
182
+ if (active)
183
+ return false;
184
+ if (!prepareVoice(ctx))
185
+ return false;
174
186
  active = true;
187
+ activationMode = mode;
175
188
  setVoiceFooter(ctx, true);
176
- await runVoiceSession(ctx);
189
+ voiceSessionPromise = runVoiceSession(ctx).finally(() => {
190
+ if (voiceSessionPromise) {
191
+ voiceSessionPromise = null;
192
+ }
193
+ });
194
+ return true;
177
195
  }
178
- pi.registerCommand("voice", {
179
- description: "Toggle voice mode",
180
- handler: async (_args, ctx) => toggleVoice(ctx),
181
- });
182
- pi.registerShortcut("ctrl+alt+v", {
183
- description: shortcutDesc("Toggle voice mode", "/voice"),
184
- handler: async (ctx) => toggleVoice(ctx),
185
- });
186
- function killRecognizer() {
187
- if (recognizerProcess) {
188
- recognizerProcess.kill("SIGTERM");
189
- recognizerProcess = null;
196
+ async function stopVoice(ctx) {
197
+ if (!active && !closeVoiceOverlay && !recognizerProcess)
198
+ return;
199
+ killRecognizer();
200
+ active = false;
201
+ activationMode = null;
202
+ setVoiceFooter(ctx, false);
203
+ const close = closeVoiceOverlay;
204
+ closeVoiceOverlay = null;
205
+ close?.();
206
+ await voiceSessionPromise;
207
+ }
208
+ async function toggleVoice(ctx) {
209
+ if (active) {
210
+ await stopVoice(ctx);
211
+ return;
190
212
  }
213
+ await startVoice(ctx, "toggle");
191
214
  }
192
215
  function startRecognizer(onPartial, onFinal, onError, onReady) {
193
216
  if (IS_LINUX) {
@@ -224,20 +247,57 @@ export default function (pi) {
224
247
  }, (text) => {
225
248
  ctx.ui.setEditorText(text);
226
249
  }, (msg) => ctx.ui.notify(`Voice: ${msg}`, "error"), () => { });
227
- ctx.ui.custom((_tui, _theme, _kb, done) => ({
228
- render() { return []; },
229
- handleInput(data) {
230
- if (isKeyRelease(data))
231
- return;
232
- if (matchesKey(data, Key.escape) || matchesKey(data, Key.enter)) {
233
- killRecognizer();
234
- active = false;
235
- setVoiceFooter(ctx, false);
236
- done();
237
- }
238
- },
239
- invalidate() { },
240
- }), { overlay: true, overlayOptions: { anchor: "bottom-center", width: "100%" } }).then(() => resolve());
250
+ ctx.ui.custom((_tui, _theme, _kb, done) => {
251
+ const close = () => done();
252
+ closeVoiceOverlay = close;
253
+ return {
254
+ render() { return []; },
255
+ handleInput(data) {
256
+ if (isKeyRelease(data))
257
+ return;
258
+ if (matchesKey(data, Key.escape) || matchesKey(data, Key.enter)) {
259
+ void stopVoice(ctx);
260
+ }
261
+ },
262
+ invalidate() { },
263
+ dispose() {
264
+ if (closeVoiceOverlay === close) {
265
+ closeVoiceOverlay = null;
266
+ }
267
+ },
268
+ };
269
+ }, { overlay: true, overlayOptions: { anchor: "bottom-center", width: "100%" } }).then(() => resolve());
241
270
  });
242
271
  }
272
+ pi.on("session_start", async (_event, ctx) => {
273
+ ctx.ui.onTerminalInput((data) => handlePushToTalkInput(data, {
274
+ active,
275
+ activationMode,
276
+ editorText: ctx.ui.getEditorText(),
277
+ holdToTalkSupported: isKittyProtocolActive(),
278
+ onUnsupported: () => {
279
+ if (holdToTalkUnsupportedNotified)
280
+ return;
281
+ holdToTalkUnsupportedNotified = true;
282
+ ctx.ui.notify("Voice: hold Space requires Kitty key-release support in this terminal — use /voice or Ctrl+Alt+V", "warning");
283
+ },
284
+ startPushToTalk: async () => {
285
+ await startVoice(ctx, "push-to-talk");
286
+ },
287
+ stopVoice: async () => {
288
+ await stopVoice(ctx);
289
+ },
290
+ }));
291
+ });
292
+ pi.on("session_shutdown", async (_event, ctx) => {
293
+ await stopVoice(ctx);
294
+ });
295
+ pi.registerCommand("voice", {
296
+ description: "Toggle voice mode",
297
+ handler: async (_args, ctx) => toggleVoice(ctx),
298
+ });
299
+ pi.registerShortcut("ctrl+alt+v", {
300
+ description: shortcutDesc("Toggle voice mode", "/voice"),
301
+ handler: async (ctx) => toggleVoice(ctx),
302
+ });
243
303
  }