@undefineds.co/linx 0.3.5 → 0.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 (172) hide show
  1. package/README.md +58 -23
  2. package/dist/generated/version.js +1 -1
  3. package/dist/generated/version.js.map +1 -1
  4. package/dist/index.js +334 -162
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/account-session.js +4 -8
  7. package/dist/lib/account-session.js.map +1 -1
  8. package/dist/lib/ai-command.js +228 -178
  9. package/dist/lib/ai-command.js.map +1 -1
  10. package/dist/lib/auto-mode/archive.js +38 -7
  11. package/dist/lib/auto-mode/archive.js.map +1 -1
  12. package/dist/lib/auto-mode/auth.js.map +1 -1
  13. package/dist/lib/auto-mode/display.js +71 -45
  14. package/dist/lib/auto-mode/display.js.map +1 -1
  15. package/dist/lib/auto-mode/format.js +9 -7
  16. package/dist/lib/auto-mode/format.js.map +1 -1
  17. package/dist/lib/auto-mode/hooks/claude.js +12 -2
  18. package/dist/lib/auto-mode/hooks/claude.js.map +1 -1
  19. package/dist/lib/auto-mode/hooks/codex.js +17 -7
  20. package/dist/lib/auto-mode/hooks/codex.js.map +1 -1
  21. package/dist/lib/auto-mode/hooks/index.js +28 -8
  22. package/dist/lib/auto-mode/hooks/index.js.map +1 -1
  23. package/dist/lib/auto-mode/pod-ai.js +20 -37
  24. package/dist/lib/auto-mode/pod-ai.js.map +1 -1
  25. package/dist/lib/auto-mode/pod-approval.js +124 -195
  26. package/dist/lib/auto-mode/pod-approval.js.map +1 -1
  27. package/dist/lib/auto-mode/pod-persistence.js +169 -90
  28. package/dist/lib/auto-mode/pod-persistence.js.map +1 -1
  29. package/dist/lib/auto-mode/runner.js +683 -81
  30. package/dist/lib/auto-mode/runner.js.map +1 -1
  31. package/dist/lib/auto-mode/secretary.js +186 -41
  32. package/dist/lib/auto-mode/secretary.js.map +1 -1
  33. package/dist/lib/auto-mode-command.js +32 -32
  34. package/dist/lib/auto-mode-command.js.map +1 -1
  35. package/dist/lib/chat-api.js +242 -50
  36. package/dist/lib/chat-api.js.map +1 -1
  37. package/dist/lib/codex-plugin/bridge.js +164 -17
  38. package/dist/lib/codex-plugin/bridge.js.map +1 -1
  39. package/dist/lib/codex-plugin/codex-native-proxy.js +370 -34
  40. package/dist/lib/codex-plugin/codex-native-proxy.js.map +1 -1
  41. package/dist/lib/credentials-store.js +33 -42
  42. package/dist/lib/credentials-store.js.map +1 -1
  43. package/dist/lib/linx-cloud-errors.js +61 -0
  44. package/dist/lib/linx-cloud-errors.js.map +1 -0
  45. package/dist/lib/linx-tui-contract.js +8 -5
  46. package/dist/lib/linx-tui-contract.js.map +1 -1
  47. package/dist/lib/login-command.js +9 -2
  48. package/dist/lib/login-command.js.map +1 -1
  49. package/dist/lib/models.js +3 -20
  50. package/dist/lib/models.js.map +1 -1
  51. package/dist/lib/oidc-auth.js +143 -17
  52. package/dist/lib/oidc-auth.js.map +1 -1
  53. package/dist/lib/oidc-session-storage.js +2 -6
  54. package/dist/lib/oidc-session-storage.js.map +1 -1
  55. package/dist/lib/pi-adapter/auto-input-controller.js +988 -0
  56. package/dist/lib/pi-adapter/auto-input-controller.js.map +1 -0
  57. package/dist/lib/pi-adapter/backend-command.js +2 -0
  58. package/dist/lib/pi-adapter/backend-command.js.map +1 -0
  59. package/dist/lib/pi-adapter/backend-credentials.js +80 -0
  60. package/dist/lib/pi-adapter/backend-credentials.js.map +1 -0
  61. package/dist/lib/pi-adapter/branding.js +246 -108
  62. package/dist/lib/pi-adapter/branding.js.map +1 -1
  63. package/dist/lib/pi-adapter/control-state.js +72 -0
  64. package/dist/lib/pi-adapter/control-state.js.map +1 -0
  65. package/dist/lib/pi-adapter/interactive.js +2634 -30
  66. package/dist/lib/pi-adapter/interactive.js.map +1 -1
  67. package/dist/lib/pi-adapter/pod-approval.js +382 -210
  68. package/dist/lib/pi-adapter/pod-approval.js.map +1 -1
  69. package/dist/lib/pi-adapter/pod-mirror-mapping.js +71 -17
  70. package/dist/lib/pi-adapter/pod-mirror-mapping.js.map +1 -1
  71. package/dist/lib/pi-adapter/pod-mirror.js +531 -64
  72. package/dist/lib/pi-adapter/pod-mirror.js.map +1 -1
  73. package/dist/lib/pi-adapter/pod-native.js +81 -85
  74. package/dist/lib/pi-adapter/pod-native.js.map +1 -1
  75. package/dist/lib/pi-adapter/pod-status-output.js +54 -0
  76. package/dist/lib/pi-adapter/pod-status-output.js.map +1 -0
  77. package/dist/lib/pi-adapter/runtime.js +458 -228
  78. package/dist/lib/pi-adapter/runtime.js.map +1 -1
  79. package/dist/lib/pi-adapter/session-control.js +509 -0
  80. package/dist/lib/pi-adapter/session-control.js.map +1 -0
  81. package/dist/lib/pi-adapter/session.js +35 -22
  82. package/dist/lib/pi-adapter/session.js.map +1 -1
  83. package/dist/lib/pi-adapter/stream.js +89 -32
  84. package/dist/lib/pi-adapter/stream.js.map +1 -1
  85. package/dist/lib/pi-adapter/sync-recovery.js +89 -0
  86. package/dist/lib/pi-adapter/sync-recovery.js.map +1 -0
  87. package/dist/lib/pi-adapter/web-fetch.js +13 -14
  88. package/dist/lib/pi-adapter/web-fetch.js.map +1 -1
  89. package/dist/lib/pod-chat-store.js +254 -78
  90. package/dist/lib/pod-chat-store.js.map +1 -1
  91. package/dist/lib/pod-data-session.js +156 -35
  92. package/dist/lib/pod-data-session.js.map +1 -1
  93. package/dist/lib/solid-auth-store.js +27 -0
  94. package/dist/lib/solid-auth-store.js.map +1 -0
  95. package/dist/lib/solid-auth.js +2 -4
  96. package/dist/lib/solid-auth.js.map +1 -1
  97. package/dist/lib/solid-client-credentials-login.js +100 -0
  98. package/dist/lib/solid-client-credentials-login.js.map +1 -0
  99. package/dist/lib/solid-local-store.js +31 -0
  100. package/dist/lib/solid-local-store.js.map +1 -0
  101. package/dist/lib/symphony/archive.js +328 -18
  102. package/dist/lib/symphony/archive.js.map +1 -1
  103. package/dist/lib/symphony/pod-projection.js +2222 -0
  104. package/dist/lib/symphony/pod-projection.js.map +1 -0
  105. package/dist/lib/symphony-command.js +602 -178
  106. package/dist/lib/symphony-command.js.map +1 -1
  107. package/dist/lib/sync-checkpoint-store.js +74 -0
  108. package/dist/lib/sync-checkpoint-store.js.map +1 -0
  109. package/dist/skills/symphony/SKILL.md +665 -0
  110. package/package.json +15 -9
  111. package/vendor/agent-runtime/dist/agent-runtime.d.ts +137 -0
  112. package/vendor/agent-runtime/dist/agent-runtime.js +211 -0
  113. package/vendor/agent-runtime/dist/auto-mode.d.ts +78 -13
  114. package/vendor/agent-runtime/dist/auto-mode.js +288 -31
  115. package/vendor/agent-runtime/dist/control-plane.d.ts +28 -0
  116. package/vendor/agent-runtime/dist/control-plane.js +79 -0
  117. package/vendor/agent-runtime/dist/file-sync.d.ts +157 -0
  118. package/vendor/agent-runtime/dist/file-sync.js +314 -0
  119. package/vendor/agent-runtime/dist/index.d.ts +7 -0
  120. package/vendor/agent-runtime/dist/index.js +7 -0
  121. package/vendor/agent-runtime/dist/reconciler.d.ts +117 -0
  122. package/vendor/agent-runtime/dist/reconciler.js +361 -0
  123. package/vendor/agent-runtime/dist/symphony.d.ts +128 -8
  124. package/vendor/agent-runtime/dist/symphony.js +362 -57
  125. package/vendor/agent-runtime/dist/sync.d.ts +271 -0
  126. package/vendor/agent-runtime/dist/sync.js +550 -0
  127. package/vendor/agent-runtime/dist/thread-reconciler-controller.d.ts +58 -0
  128. package/vendor/agent-runtime/dist/thread-reconciler-controller.js +137 -0
  129. package/vendor/agent-runtime/dist/turn-controller.js +2 -2
  130. package/vendor/agent-runtime/dist/wake-scheduler.d.ts +67 -0
  131. package/vendor/agent-runtime/dist/wake-scheduler.js +194 -0
  132. package/vendor/agent-runtime/package.json +8 -1
  133. package/vendor/pi-web-access/CHANGELOG.md +387 -0
  134. package/vendor/pi-web-access/LICENSE +21 -0
  135. package/vendor/pi-web-access/README.md +352 -0
  136. package/vendor/pi-web-access/activity.ts +101 -0
  137. package/vendor/pi-web-access/banner.png +0 -0
  138. package/vendor/pi-web-access/chrome-cookies.ts +322 -0
  139. package/vendor/pi-web-access/code-search.ts +107 -0
  140. package/vendor/pi-web-access/curator-page.ts +3359 -0
  141. package/vendor/pi-web-access/curator-server.ts +605 -0
  142. package/vendor/pi-web-access/exa.ts +520 -0
  143. package/vendor/pi-web-access/extract.ts +641 -0
  144. package/vendor/pi-web-access/gemini-api.ts +112 -0
  145. package/vendor/pi-web-access/gemini-search.ts +361 -0
  146. package/vendor/pi-web-access/gemini-url-context.ts +126 -0
  147. package/vendor/pi-web-access/gemini-web-config.ts +52 -0
  148. package/vendor/pi-web-access/gemini-web.ts +396 -0
  149. package/vendor/pi-web-access/github-api.ts +196 -0
  150. package/vendor/pi-web-access/github-extract.ts +634 -0
  151. package/vendor/pi-web-access/index.ts +2346 -0
  152. package/vendor/pi-web-access/package.json +45 -0
  153. package/vendor/pi-web-access/pdf-extract.ts +192 -0
  154. package/vendor/pi-web-access/perplexity.ts +195 -0
  155. package/vendor/pi-web-access/pi-web-fetch-demo.mp4 +0 -0
  156. package/vendor/pi-web-access/rsc-extract.ts +338 -0
  157. package/vendor/pi-web-access/skills/librarian/SKILL.md +195 -0
  158. package/vendor/pi-web-access/storage.ts +72 -0
  159. package/vendor/pi-web-access/summary-review.ts +276 -0
  160. package/vendor/pi-web-access/test/gemini-web-cookie-opt-in.test.mjs +41 -0
  161. package/vendor/pi-web-access/test/pdf-extract.test.mjs +95 -0
  162. package/vendor/pi-web-access/utils.ts +44 -0
  163. package/vendor/pi-web-access/video-extract.ts +378 -0
  164. package/vendor/pi-web-access/youtube-extract.ts +310 -0
  165. package/dist/lib/pi-adapter/auth.js +0 -68
  166. package/dist/lib/pi-adapter/auth.js.map +0 -1
  167. package/dist/lib/pi-adapter/pod-tools.js +0 -140
  168. package/dist/lib/pi-adapter/pod-tools.js.map +0 -1
  169. package/dist/skills/drizzle-solid/SKILL.md +0 -340
  170. package/dist/skills/pod-storage/SKILL.md +0 -100
  171. package/dist/skills/solid-modeling/SKILL.md +0 -274
  172. package/dist/skills/xpod-componentsjs/SKILL.md +0 -284
@@ -1,33 +1,2411 @@
1
- import { InteractiveMode } from '@mariozechner/pi-coding-agent';
2
- import { FooterComponent } from '@mariozechner/pi-coding-agent';
3
- import { truncateToWidth, visibleWidth } from '@mariozechner/pi-tui';
1
+ import { InteractiveMode } from '@earendil-works/pi-coding-agent';
2
+ import { AssistantMessageComponent, FooterComponent, LoginDialogComponent } from '@earendil-works/pi-coding-agent';
3
+ import { Container, getKeybindings, Spacer, Text, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';
4
+ import { existsSync, statSync } from 'node:fs';
5
+ import { resolve } from 'node:path';
6
+ import { connectAiProviderCredential } from '../ai-command.js';
7
+ import { listArchivedAutoModeSessions, runAutoMode } from '../auto-mode/runner.js';
8
+ import { resolveAutoModeCommandRoute, } from '../../../vendor/agent-runtime/dist/auto-mode.js';
9
+ import { getAIConfigProviderCatalog, getAIConfigProviderMetadata } from '../models.js';
10
+ import { runSymphony } from '../symphony-command.js';
4
11
  import { applyLinxInteractiveBranding, requestLinxCloudLogin } from './branding.js';
12
+ import { installPodStatusOutputFilter } from './pod-status-output.js';
13
+ import { createPodBackedExtensionUiContext } from './pod-approval.js';
14
+ import { DEFAULT_SECRETARY_CHAT_ID, secretaryChatUri, secretaryThreadUri } from './pod-mirror-mapping.js';
15
+ import { getSecretaryAutoInputController } from './auto-input-controller.js';
16
+ import { createSymphonyIdeaRecord, listSymphonyIssues, listSymphonySessions, writeSymphonyIdea, } from '../symphony/archive.js';
17
+ import { listOpenSymphonyIssuesFromPod, listRecentSymphonyReportsFromPod, listRunningSymphonyWorkersFromPod, mirrorSymphonyProjectionJsonLdFromPod, persistSymphonyIdeaToPod, persistSymphonyProjectionToPod, } from '../symphony/pod-projection.js';
18
+ import { getSessionControlManager, installSessionControlRuntimeEventBridge, } from './session-control.js';
5
19
  let footerPatched = false;
6
- export function bootstrapPiInteractiveMode(runtime) {
20
+ let assistantMessagePatched = false;
21
+ let linxResumeOutputStyleRestore = null;
22
+ const BACKEND_OWNED_SLASH_COMMANDS = new Set([
23
+ 'commands',
24
+ 'models',
25
+ 'rollback',
26
+ 'status',
27
+ ]);
28
+ const SYMPHONY_STATUS_POD_TIMEOUT_MS = 1_200;
29
+ const DEFAULT_SYMPHONY_WORKER_SUPERVISOR_INTERVAL_MS = 10 * 60 * 1000;
30
+ export function bootstrapLinxInteractiveMode(runtime, options = {}) {
31
+ installLinxResumeOutputStyle();
7
32
  patchPiFooter();
33
+ patchPiAssistantMessageRendering();
8
34
  const sessionCwd = runtime?.cwd || process.cwd();
9
- const interactive = new InteractiveMode(runtime, {});
35
+ ensureInteractiveRuntimeHost(runtime);
36
+ const interactive = new InteractiveMode(runtime, options);
37
+ interactive.runtime = runtime;
38
+ interactive.__autoEnabled = runtime?.autoEnabled === true;
39
+ interactive.__linxSymphonyModeEnabled = runtime?.symphonyEnabled === true;
40
+ if (options.onSymphonyControlChange) {
41
+ ;
42
+ interactive.__linxOnSymphonyControlChange = options.onSymphonyControlChange;
43
+ }
44
+ const sessionControlManager = getSessionControlManager(interactive, runtime, sessionCwd);
45
+ runtime?.backendCommandRouter?.setSessionControl?.(sessionControlManager);
46
+ const restorePodStatusOutputFilter = installPodStatusOutputFilter();
10
47
  applyLinxInteractiveBranding(interactive);
11
48
  patchInteractiveExitMessage(interactive);
12
- // Register /cd slash command — workspace follows terminal, session stays
13
- installLinxCdCommand(interactive, sessionCwd);
14
- return {
49
+ patchInteractivePodStatusFilterCleanup(interactive, restorePodStatusOutputFilter);
50
+ installPodBackedExtensionUi(interactive, runtime, sessionControlManager);
51
+ installSymphonyAutocomplete(interactive);
52
+ // Register /cd slash command; workspace follows terminal while session stays.
53
+ installLinxGlobalCommands(interactive, runtime, sessionCwd, options);
54
+ installSymphonyCommand(interactive);
55
+ installBackendCommandRouter(interactive, runtime?.backendCommandRouter);
56
+ installSessionControlRuntimeEventBridge(interactive, runtime, sessionCwd);
57
+ installLinxSessionCommandRouter(interactive, runtime);
58
+ installLinxSessionCommandRouterAfterRebind(interactive, runtime);
59
+ if (options.restoredAuto === true && runtime?.autoEnabled === true) {
60
+ installLinxRestoredAutoStartup(interactive, runtime, sessionControlManager);
61
+ }
62
+ installLinxInteractivePostInitHooks(interactive, runtime);
63
+ const bootstrap = {
15
64
  async init() {
16
65
  await interactive.init();
17
- installLinxEscapeInterrupt(interactive);
18
66
  },
19
- async run() {
20
- await interactive.run();
67
+ async run() {
68
+ await bootstrap.init();
69
+ await withLinxResumeOutputStyle(() => interactive.run());
70
+ },
71
+ requestLogin(reason = 'manual') {
72
+ requestLinxCloudLogin(interactive, reason);
73
+ },
74
+ async requestBackendCredential(details) {
75
+ return promptForBackendCredential(interactive, details);
76
+ },
77
+ __unsafeInteractiveForTests: interactive,
78
+ stop() {
79
+ interactive.stop();
80
+ },
81
+ };
82
+ return bootstrap;
83
+ }
84
+ function installLinxInteractivePostInitHooks(interactive, runtime) {
85
+ if (!interactive || interactive.__linxInteractivePostInitHooksInstalled) {
86
+ return;
87
+ }
88
+ const originalInit = interactive.init?.bind(interactive);
89
+ if (typeof originalInit !== 'function') {
90
+ return;
91
+ }
92
+ interactive.init = async function patchedLinxInteractivePostInit(...args) {
93
+ if (this.__linxInteractiveInitCompleted === true) {
94
+ installLinxSessionCommandRouter(this, runtime);
95
+ installLinxInputCommandRouter(this, runtime);
96
+ installLinxFinalSubmitCommandRouter(this, runtime);
97
+ installLinxEscapeInterrupt(this);
98
+ return undefined;
99
+ }
100
+ const result = await originalInit(...args);
101
+ this.__linxInteractiveInitCompleted = true;
102
+ installLinxSessionCommandRouter(this, runtime);
103
+ installLinxInputCommandRouter(this, runtime);
104
+ installLinxFinalSubmitCommandRouter(this, runtime);
105
+ installLinxEscapeInterrupt(this);
106
+ return result;
107
+ };
108
+ interactive.__linxInteractivePostInitHooksInstalled = true;
109
+ }
110
+ /** @deprecated Use bootstrapLinxInteractiveMode. */
111
+ export const bootstrapPiInteractiveMode = bootstrapLinxInteractiveMode;
112
+ export function installLinxRestoredAutoStartup(interactive, runtime, sessionControl = getSessionControlManager(interactive, runtime)) {
113
+ if (!interactive || interactive.__linxRestoredAutoStartupInstalled) {
114
+ return;
115
+ }
116
+ const originalInit = interactive.init?.bind(interactive);
117
+ if (typeof originalInit !== 'function') {
118
+ return;
119
+ }
120
+ interactive.init = async function patchedLinxRestoredAutoInit(...args) {
121
+ const result = await originalInit(...args);
122
+ if (this.__autoEnabled === true && runtime?.autoEnabled === true) {
123
+ const controller = getSecretaryAutoInputController(this, runtime, sessionControl);
124
+ controller.start({ scheduleImmediately: true });
125
+ interactive.showStatus?.([
126
+ 'Auto restored from the previous session.',
127
+ '托管中 · Secretary 自动输入 · Ctrl+C 接管 · /auto off',
128
+ ].join('\n'));
129
+ interactive.ui?.requestRender?.();
130
+ }
131
+ return result;
132
+ };
133
+ interactive.__linxRestoredAutoStartupInstalled = true;
134
+ }
135
+ function ensureInteractiveRuntimeHost(runtime) {
136
+ if (!runtime || typeof runtime !== 'object') {
137
+ return;
138
+ }
139
+ if (typeof runtime.setBeforeSessionInvalidate !== 'function') {
140
+ runtime.setBeforeSessionInvalidate = (callback) => {
141
+ runtime.__linxBeforeSessionInvalidate = callback;
142
+ };
143
+ }
144
+ if (typeof runtime.setRebindSession !== 'function') {
145
+ runtime.setRebindSession = (callback) => {
146
+ runtime.__linxRebindSession = callback;
147
+ };
148
+ }
149
+ }
150
+ function patchInteractivePodStatusFilterCleanup(interactive, restore) {
151
+ const originalStop = interactive.stop?.bind(interactive);
152
+ if (typeof originalStop !== 'function') {
153
+ return;
154
+ }
155
+ interactive.stop = function patchedStopWithPodStatusCleanup(...args) {
156
+ try {
157
+ originalStop(...args);
158
+ }
159
+ finally {
160
+ restore();
161
+ }
162
+ };
163
+ }
164
+ export function installPodBackedExtensionUi(interactive, runtime, sessionControl = getSessionControlManager(interactive, runtime)) {
165
+ if (interactive.__linxPodBackedExtensionUiInstalled) {
166
+ return;
167
+ }
168
+ const originalCreate = interactive.createExtensionUIContext?.bind(interactive);
169
+ if (typeof originalCreate !== 'function') {
170
+ return;
171
+ }
172
+ interactive.createExtensionUIContext = function patchedCreateExtensionUIContext(...args) {
173
+ const baseUi = originalCreate(...args);
174
+ if (!baseUi || typeof baseUi !== 'object') {
175
+ return baseUi;
176
+ }
177
+ return createPodBackedExtensionUiContext(baseUi, {
178
+ cwd: interactive?.session?.cwd ?? runtime?.cwd ?? process.cwd(),
179
+ sessionId: () => interactive?.sessionManager?.getSessionId?.()
180
+ ?? interactive?.session?.sessionManager?.getSessionId?.()
181
+ ?? interactive?.session?.sessionId,
182
+ sessionControl,
183
+ onWarning(error) {
184
+ const message = error instanceof Error ? error.message : String(error);
185
+ interactive.showWarning?.(`Pod approval sync unavailable: ${message}`);
186
+ },
187
+ });
188
+ };
189
+ interactive.__linxPodBackedExtensionUiInstalled = true;
190
+ }
191
+ export function installBackendCommandRouter(interactive, router) {
192
+ if (!router) {
193
+ return;
194
+ }
195
+ interactive.__linxHandleProjectedBackendCommand = async (text) => {
196
+ const command = text.trim();
197
+ if (!shouldRouteToBackendCommand(command)) {
198
+ return false;
199
+ }
200
+ let routed;
201
+ try {
202
+ routed = await router.execute(command);
203
+ }
204
+ catch (error) {
205
+ const message = error instanceof Error ? error.message : String(error);
206
+ interactive.showError?.(`${router.backend} command failed: ${message}`);
207
+ return true;
208
+ }
209
+ if (!routed.handled) {
210
+ return false;
211
+ }
212
+ if (routed.message) {
213
+ interactive.showStatus?.(routed.message);
214
+ }
215
+ interactive.ui?.requestRender?.();
216
+ return true;
217
+ };
218
+ installProjectedCommandRouter(interactive);
219
+ const originalSetup = interactive.setupEditorSubmitHandler?.bind(interactive);
220
+ if (typeof originalSetup !== 'function') {
221
+ return;
222
+ }
223
+ interactive.setupEditorSubmitHandler = function patchedBackendCommandSetupEditorSubmitHandler(...args) {
224
+ const result = originalSetup(...args);
225
+ const originalSubmit = this.defaultEditor?.onSubmit?.bind(this.defaultEditor);
226
+ if (typeof originalSubmit !== 'function') {
227
+ return result;
228
+ }
229
+ this.defaultEditor.onSubmit = async (text) => {
230
+ const command = text.trim();
231
+ if (!shouldRouteToBackendCommand(command)) {
232
+ await originalSubmit(text);
233
+ return;
234
+ }
235
+ let routed;
236
+ try {
237
+ routed = await router.execute(command);
238
+ }
239
+ catch (error) {
240
+ const message = error instanceof Error ? error.message : String(error);
241
+ this.showError?.(`${router.backend} command failed: ${message}`);
242
+ return;
243
+ }
244
+ if (!routed.handled) {
245
+ await originalSubmit(text);
246
+ return;
247
+ }
248
+ if (routed.clearInput !== false) {
249
+ this.editor?.setText?.('');
250
+ }
251
+ if (routed.message) {
252
+ this.showStatus?.(routed.message);
253
+ }
254
+ this.ui?.requestRender?.();
255
+ };
256
+ return result;
257
+ };
258
+ }
259
+ function shouldRouteToBackendCommand(command) {
260
+ if (!command.startsWith('/')) {
261
+ return false;
262
+ }
263
+ const name = command.slice(1).split(/\s+/, 1)[0]?.toLowerCase();
264
+ if (!name) {
265
+ return false;
266
+ }
267
+ return BACKEND_OWNED_SLASH_COMMANDS.has(name);
268
+ }
269
+ export function installLinxGlobalCommands(interactive, runtime, sessionCwd, options = {}) {
270
+ installLinxCwdStartupNotice(interactive, sessionCwd);
271
+ installLinxAutoEditorIndicator(interactive);
272
+ if (options.onAutoControlChange) {
273
+ interactive.__linxOnAutoControlChange = options.onAutoControlChange;
274
+ }
275
+ installLinxGlobalCommandHandler(interactive, runtime);
276
+ }
277
+ export function installLinxAutoEditorIndicator(interactive) {
278
+ if (!interactive || interactive.__linxAutoEditorIndicatorInstalled) {
279
+ return;
280
+ }
281
+ decorateLinxAutoEditorRender(interactive.defaultEditor, interactive);
282
+ if (interactive.editor && interactive.editor !== interactive.defaultEditor) {
283
+ decorateLinxAutoEditorRender(interactive.editor, interactive);
284
+ }
285
+ const originalSetCustomEditorComponent = interactive.setCustomEditorComponent?.bind(interactive);
286
+ if (typeof originalSetCustomEditorComponent === 'function') {
287
+ interactive.setCustomEditorComponent = function patchedSetCustomEditorComponent(...args) {
288
+ const result = originalSetCustomEditorComponent(...args);
289
+ decorateLinxAutoEditorRender(this.defaultEditor, this);
290
+ if (this.editor && this.editor !== this.defaultEditor) {
291
+ decorateLinxAutoEditorRender(this.editor, this);
292
+ }
293
+ return result;
294
+ };
295
+ }
296
+ interactive.__linxAutoEditorIndicatorInstalled = true;
297
+ }
298
+ function decorateLinxAutoEditorRender(editor, interactive) {
299
+ if (!editor || editor.__linxAutoEditorIndicatorRenderInstalled || typeof editor.render !== 'function') {
300
+ return;
301
+ }
302
+ const originalRender = editor.render.bind(editor);
303
+ editor.render = function linxAutoEditorIndicatorRender(width) {
304
+ const lines = originalRender(width);
305
+ if (interactive.__autoEnabled !== true) {
306
+ return lines;
307
+ }
308
+ return decorateLinxAutoEditorLines(lines, width);
309
+ };
310
+ editor.__linxAutoEditorIndicatorRenderInstalled = true;
311
+ }
312
+ function decorateLinxAutoEditorLines(lines, width) {
313
+ const rendered = Array.isArray(lines) ? [...lines] : [];
314
+ const indicator = buildLinxAutoEditorIndicatorLine(width);
315
+ if (rendered.length === 0) {
316
+ return [indicator];
317
+ }
318
+ rendered[0] = indicator;
319
+ return rendered;
320
+ }
321
+ export function buildLinxAutoEditorIndicatorLine(width) {
322
+ if (width <= 0) {
323
+ return '';
324
+ }
325
+ const label = ' 托管中 · Secretary 自动输入 · Ctrl+C 接管 · /auto off ';
326
+ const fitted = truncateToWidth(label, width);
327
+ const padded = fitted + ' '.repeat(Math.max(0, width - visibleWidth(fitted)));
328
+ return `\x1b[1m\x1b[38;5;230m\x1b[48;5;58m${padded}\x1b[0m`;
329
+ }
330
+ function installLinxGlobalCommandHandler(interactive, runtime) {
331
+ if (interactive.__linxGlobalCommandHandlerInstalled) {
332
+ return;
333
+ }
334
+ const originalSetup = interactive.setupEditorSubmitHandler?.bind(interactive);
335
+ if (typeof originalSetup !== 'function') {
336
+ return;
337
+ }
338
+ interactive.setupEditorSubmitHandler = function patchedLinxGlobalSetupEditorSubmitHandler(...args) {
339
+ const result = originalSetup(...args);
340
+ const originalSubmit = this.defaultEditor?.onSubmit?.bind(this.defaultEditor);
341
+ if (typeof originalSubmit !== 'function') {
342
+ return result;
343
+ }
344
+ this.defaultEditor.onSubmit = async (text) => {
345
+ const command = parseLinxGlobalCommand(text.trim());
346
+ if (!command) {
347
+ recordSubmittedUserMessage(this, runtime, text);
348
+ await originalSubmit(text);
349
+ return;
350
+ }
351
+ this.editor?.setText?.('');
352
+ await handleLinxGlobalCommand(this, runtime, command);
353
+ };
354
+ return result;
355
+ };
356
+ interactive.__linxGlobalCommandHandlerInstalled = true;
357
+ interactive.__linxHandleProjectedGlobalCommand = async (text) => {
358
+ const command = parseLinxGlobalCommand(text.trim());
359
+ if (!command) {
360
+ return false;
361
+ }
362
+ await handleLinxGlobalCommand(interactive, runtime, command);
363
+ if (command.action === 'peer-command') {
364
+ return 'peer-command';
365
+ }
366
+ return true;
367
+ };
368
+ installProjectedCommandRouter(interactive);
369
+ }
370
+ export function installLinxInputCommandRouter(interactive, runtime) {
371
+ if (!interactive || interactive.__linxInputCommandRouterInstalled) {
372
+ return;
373
+ }
374
+ const originalGetUserInput = interactive.getUserInput?.bind(interactive);
375
+ if (typeof originalGetUserInput !== 'function') {
376
+ return;
377
+ }
378
+ interactive.getUserInput = async function patchedLinxGetUserInput(...args) {
379
+ while (true) {
380
+ const input = await originalGetUserInput(...args);
381
+ if (typeof input !== 'string') {
382
+ return input;
383
+ }
384
+ const command = parseLinxGlobalCommand(input.trim());
385
+ if (!command) {
386
+ return input;
387
+ }
388
+ this.editor?.setText?.('');
389
+ await handleLinxGlobalCommand(this, runtime, command);
390
+ }
391
+ };
392
+ interactive.__linxInputCommandRouterInstalled = true;
393
+ }
394
+ export function installLinxFinalSubmitCommandRouter(interactive, runtime) {
395
+ if (!interactive) {
396
+ return;
397
+ }
398
+ const wrapEditor = (editor) => {
399
+ if (!editor || typeof editor.onSubmit !== 'function') {
400
+ return;
401
+ }
402
+ if (editor.onSubmit.__linxFinalSubmitCommandRouterWrapped === true) {
403
+ return;
404
+ }
405
+ const originalSubmit = editor.onSubmit.bind(editor);
406
+ const wrappedSubmit = async (text) => {
407
+ const command = parseLinxGlobalCommand(String(text ?? '').trim());
408
+ if (!command) {
409
+ await originalSubmit(text);
410
+ return;
411
+ }
412
+ interactive.editor?.setText?.('');
413
+ await handleLinxGlobalCommand(interactive, runtime, command);
414
+ };
415
+ wrappedSubmit.__linxFinalSubmitCommandRouterWrapped = true;
416
+ editor.onSubmit = wrappedSubmit;
417
+ };
418
+ wrapEditor(interactive.defaultEditor);
419
+ if (interactive.editor !== interactive.defaultEditor) {
420
+ wrapEditor(interactive.editor);
421
+ }
422
+ const originalSetCustomEditorComponent = interactive.setCustomEditorComponent?.bind(interactive);
423
+ if (typeof originalSetCustomEditorComponent === 'function'
424
+ && interactive.__linxFinalSubmitSetCustomEditorComponentPatched !== true) {
425
+ interactive.setCustomEditorComponent = function patchedLinxFinalSubmitSetCustomEditorComponent(...args) {
426
+ const result = originalSetCustomEditorComponent(...args);
427
+ wrapEditor(this.defaultEditor);
428
+ if (this.editor !== this.defaultEditor) {
429
+ wrapEditor(this.editor);
430
+ }
431
+ return result;
432
+ };
433
+ interactive.__linxFinalSubmitSetCustomEditorComponentPatched = true;
434
+ }
435
+ interactive.__linxFinalSubmitCommandRouterInstalled = true;
436
+ }
437
+ export function installLinxSessionCommandRouter(interactive, runtime) {
438
+ const session = interactive?.session ?? runtime?.session;
439
+ if (!session || typeof session !== 'object' || session.__linxSessionCommandRouterInstalled === true) {
440
+ return;
441
+ }
442
+ const originalPrompt = typeof session.prompt === 'function'
443
+ ? session.prompt.bind(session)
444
+ : undefined;
445
+ const originalSendUserMessage = typeof session.sendUserMessage === 'function'
446
+ ? session.sendUserMessage.bind(session)
447
+ : undefined;
448
+ if (!originalPrompt && !originalSendUserMessage) {
449
+ return;
450
+ }
451
+ if (originalPrompt) {
452
+ session.__linxPromptWithoutCommandRouting = originalPrompt;
453
+ session.prompt = async (text, ...args) => {
454
+ if (await maybeHandleLinxSessionCommand(interactive, runtime, text)) {
455
+ return undefined;
456
+ }
457
+ return originalPrompt(text, ...args);
458
+ };
459
+ }
460
+ if (originalSendUserMessage) {
461
+ session.__linxSendUserMessageWithoutCommandRouting = originalSendUserMessage;
462
+ session.sendUserMessage = async (text, ...args) => {
463
+ if (await maybeHandleLinxSessionCommand(interactive, runtime, text)) {
464
+ return undefined;
465
+ }
466
+ return originalSendUserMessage(text, ...args);
467
+ };
468
+ }
469
+ session.__linxSessionCommandRouterInstalled = true;
470
+ }
471
+ function installLinxSessionCommandRouterAfterRebind(interactive, runtime) {
472
+ if (!interactive || interactive.__linxSessionCommandRouterAfterRebindInstalled === true) {
473
+ return;
474
+ }
475
+ const originalRebind = interactive.rebindCurrentSession?.bind(interactive);
476
+ if (typeof originalRebind !== 'function') {
477
+ return;
478
+ }
479
+ interactive.rebindCurrentSession = async function patchedLinxRebindCurrentSession(...args) {
480
+ const result = await originalRebind(...args);
481
+ installLinxSessionCommandRouter(this, runtime);
482
+ return result;
483
+ };
484
+ interactive.__linxSessionCommandRouterAfterRebindInstalled = true;
485
+ }
486
+ async function maybeHandleLinxSessionCommand(interactive, runtime, text) {
487
+ if (typeof text !== 'string') {
488
+ return false;
489
+ }
490
+ const command = parseLinxGlobalCommand(text.trim());
491
+ if (!command) {
492
+ return false;
493
+ }
494
+ interactive.editor?.setText?.('');
495
+ await handleLinxGlobalCommand(interactive, runtime, command);
496
+ return true;
497
+ }
498
+ function recordSubmittedUserMessage(interactive, runtime, text) {
499
+ const input = text.trim();
500
+ if (!input || input.startsWith('/')) {
501
+ return;
502
+ }
503
+ try {
504
+ getSessionControlManager(interactive, runtime).recordUserMessage({ text: input });
505
+ }
506
+ catch (error) {
507
+ const message = error instanceof Error ? error.message : String(error);
508
+ interactive.showWarning?.(`Thread reconciliation unavailable: ${message}`);
509
+ }
510
+ }
511
+ function parseLinxGlobalCommand(input) {
512
+ const autoModeRoute = resolveAutoModeCommandRoute(input);
513
+ if (autoModeRoute?.kind === 'control-command') {
514
+ return { action: 'auto', route: autoModeRoute };
515
+ }
516
+ if (autoModeRoute?.kind === 'peer-command') {
517
+ return { action: 'peer-command', route: autoModeRoute };
518
+ }
519
+ if (input === '/cd') {
520
+ return { action: 'cd' };
521
+ }
522
+ if (input.startsWith('/cd ')) {
523
+ return { action: 'cd', target: input.slice('/cd'.length).trim() };
524
+ }
525
+ if (input === '/ai connect') {
526
+ return { action: 'ai-connect' };
527
+ }
528
+ if (input.startsWith('/ai connect ')) {
529
+ return { action: 'ai-connect', ...parseInteractiveAiConnectArgs(input.slice('/ai connect'.length).trim()) };
530
+ }
531
+ if (input === '/rewind') {
532
+ return { action: 'rewind-select' };
533
+ }
534
+ if (input.startsWith('/rewind ')) {
535
+ const turns = parseRewindTurnCount(input.slice('/rewind'.length).trim());
536
+ return { action: 'rewind-turns', turns: turns ?? 0 };
537
+ }
538
+ return null;
539
+ }
540
+ function parseRewindTurnCount(input) {
541
+ if (!/^\d+$/.test(input)) {
542
+ return null;
543
+ }
544
+ const value = Number.parseInt(input, 10);
545
+ return Number.isSafeInteger(value) && value > 0 ? value : null;
546
+ }
547
+ function parseInteractiveAiConnectArgs(input) {
548
+ const tokens = splitInteractiveCommandArgs(input);
549
+ let provider;
550
+ let baseUrl;
551
+ let model;
552
+ for (let index = 0; index < tokens.length; index += 1) {
553
+ const token = tokens[index];
554
+ if (!token) {
555
+ continue;
556
+ }
557
+ if (token === '--base-url') {
558
+ baseUrl = tokens[index + 1];
559
+ index += 1;
560
+ continue;
561
+ }
562
+ if (token.startsWith('--base-url=')) {
563
+ baseUrl = token.slice('--base-url='.length);
564
+ continue;
565
+ }
566
+ if (token === '--model') {
567
+ model = tokens[index + 1];
568
+ index += 1;
569
+ continue;
570
+ }
571
+ if (token.startsWith('--model=')) {
572
+ model = token.slice('--model='.length);
573
+ continue;
574
+ }
575
+ if (!token.startsWith('-') && !provider) {
576
+ provider = token;
577
+ }
578
+ }
579
+ return {
580
+ ...(provider?.trim() ? { provider: provider.trim() } : {}),
581
+ ...(baseUrl?.trim() ? { baseUrl: baseUrl.trim() } : {}),
582
+ ...(model?.trim() ? { model: model.trim() } : {}),
583
+ };
584
+ }
585
+ function splitInteractiveCommandArgs(input) {
586
+ const tokens = [];
587
+ let current = '';
588
+ let quote = null;
589
+ let escaping = false;
590
+ for (const char of input) {
591
+ if (escaping) {
592
+ current += char;
593
+ escaping = false;
594
+ continue;
595
+ }
596
+ if (char === '\\') {
597
+ escaping = true;
598
+ continue;
599
+ }
600
+ if (quote) {
601
+ if (char === quote) {
602
+ quote = null;
603
+ }
604
+ else {
605
+ current += char;
606
+ }
607
+ continue;
608
+ }
609
+ if (char === '"' || char === "'") {
610
+ quote = char;
611
+ continue;
612
+ }
613
+ if (/\s/.test(char)) {
614
+ if (current) {
615
+ tokens.push(current);
616
+ current = '';
617
+ }
618
+ continue;
619
+ }
620
+ current += char;
621
+ }
622
+ if (escaping) {
623
+ current += '\\';
624
+ }
625
+ if (current) {
626
+ tokens.push(current);
627
+ }
628
+ return tokens;
629
+ }
630
+ async function handleLinxGlobalCommand(interactive, runtime, command) {
631
+ if (command.action === 'auto') {
632
+ const auto = command.route.auto;
633
+ const enabled = auto?.action === 'set' ? auto.enabled : undefined;
634
+ const initialInput = auto?.action === 'set' ? auto.initialInput : undefined;
635
+ await handleInteractiveAutoCommand(interactive, runtime, enabled, {
636
+ scheduleImmediately: initialInput === undefined,
637
+ });
638
+ if (initialInput) {
639
+ const controller = getSecretaryAutoInputController(interactive, runtime, getSessionControlManager(interactive, runtime));
640
+ await controller.submit(initialInput, { reason: 'auto-on' });
641
+ }
642
+ return;
643
+ }
644
+ if (command.action === 'peer-command') {
645
+ await handleInteractivePeerCommand(interactive, runtime, command.route);
646
+ return;
647
+ }
648
+ if (command.action === 'ai-connect') {
649
+ await handleInteractiveAiConnectCommand(interactive, runtime, command);
650
+ return;
651
+ }
652
+ if (command.action === 'rewind-select') {
653
+ await handleInteractiveRewindSelector(interactive, runtime);
654
+ return;
655
+ }
656
+ if (command.action === 'rewind-turns') {
657
+ await handleInteractiveRewindTurnsCommand(interactive, runtime, command.turns);
658
+ return;
659
+ }
660
+ await changeInteractiveCwd(interactive, runtime, command.target);
661
+ }
662
+ async function handleInteractiveRewindSelector(interactive, runtime) {
663
+ const session = resolveInteractiveSession(interactive, runtime);
664
+ const sessionManager = resolveInteractiveSessionManager(interactive, runtime);
665
+ if (!sessionManager) {
666
+ interactive.showError?.('Cannot rewind: no active LinX session history.');
667
+ interactive.ui?.requestRender?.();
668
+ return;
669
+ }
670
+ if (typeof interactive.showSelector !== 'function') {
671
+ await handleInteractiveRewindTurnsCommand(interactive, runtime, 1);
672
+ return;
673
+ }
674
+ const userMessages = collectRewindUserMessages(session, sessionManager);
675
+ if (userMessages.length === 0) {
676
+ interactive.showStatus?.('Nothing to rewind: no user turns in the active branch.');
677
+ interactive.ui?.requestRender?.();
678
+ return;
679
+ }
680
+ const initialSelectedId = userMessages[userMessages.length - 1]?.id;
681
+ interactive.showSelector((done) => {
682
+ const selector = new LinxRewindMessageSelectorComponent(userMessages, async (entryId) => {
683
+ try {
684
+ await rewindSessionManagerBeforeUserEntry(interactive, runtime, session, sessionManager, entryId);
685
+ done();
686
+ }
687
+ catch (error) {
688
+ done();
689
+ interactive.showError?.(error instanceof Error ? error.message : String(error));
690
+ }
691
+ }, () => {
692
+ done();
693
+ interactive.ui?.requestRender?.();
694
+ }, initialSelectedId);
695
+ return { component: selector, focus: selector.getMessageList() };
696
+ });
697
+ }
698
+ async function handleInteractiveRewindTurnsCommand(interactive, runtime, turns) {
699
+ if (!Number.isSafeInteger(turns) || turns <= 0) {
700
+ interactive.showStatus?.('Usage: /rewind [turns] where turns is a positive integer.');
701
+ interactive.ui?.requestRender?.();
702
+ return;
703
+ }
704
+ const session = resolveInteractiveSession(interactive, runtime);
705
+ const sessionManager = resolveInteractiveSessionManager(interactive, runtime);
706
+ if (!sessionManager) {
707
+ interactive.showError?.('Cannot rewind: no active LinX session history.');
708
+ interactive.ui?.requestRender?.();
709
+ return;
710
+ }
711
+ await stopActiveSessionWorkForRewind(session);
712
+ resetPendingAutoInputForRewind(interactive, runtime);
713
+ const previousState = captureRewindSessionState(sessionManager);
714
+ const previousBranch = getActiveSessionBranch(sessionManager);
715
+ const result = rewindSessionManagerByTurns(sessionManager, turns);
716
+ if (result.rewound === 0) {
717
+ interactive.showStatus?.('Nothing to rewind: no user turns in the active branch.');
718
+ interactive.ui?.requestRender?.();
719
+ return;
720
+ }
721
+ const cleanResult = materializeCleanRewindSession(sessionManager, result.targetLeafId, previousState);
722
+ syncAgentStateFromSessionManager(session, sessionManager);
723
+ await syncRewindProjection(interactive, runtime, {
724
+ previousState,
725
+ cleanResult,
726
+ abandonedEntries: collectAbandonedRewindEntries(previousBranch, result.targetLeafId),
727
+ });
728
+ const remainingMessages = Array.isArray(session?.agent?.state?.messages)
729
+ ? session.agent.state.messages.length
730
+ : undefined;
731
+ const target = describeRewindTarget(result.targetLeafId, cleanResult);
732
+ const suffix = remainingMessages === undefined ? '' : ` Active context now has ${remainingMessages} message${remainingMessages === 1 ? '' : 's'}.`;
733
+ interactive.showStatus?.(`Rewound ${result.rewound} turn${result.rewound === 1 ? '' : 's'} to ${target}.${suffix}`);
734
+ interactive.ui?.requestRender?.();
735
+ }
736
+ async function rewindSessionManagerBeforeUserEntry(interactive, runtime, session, sessionManager, entryId) {
737
+ const entry = typeof sessionManager?.getEntry === 'function'
738
+ ? sessionManager.getEntry(entryId)
739
+ : getActiveSessionBranch(sessionManager).find((candidate) => candidate?.id === entryId);
740
+ if (!entry || entry.type !== 'message' || entry.message?.role !== 'user') {
741
+ throw new Error('Cannot rewind: selected message is not a user turn in the active branch.');
742
+ }
743
+ const previousState = captureRewindSessionState(sessionManager);
744
+ const previousBranch = getActiveSessionBranch(sessionManager);
745
+ await stopActiveSessionWorkForRewind(session);
746
+ resetPendingAutoInputForRewind(interactive, runtime);
747
+ const targetLeafId = typeof entry.parentId === 'string' && entry.parentId ? entry.parentId : null;
748
+ moveSessionManagerLeaf(sessionManager, targetLeafId);
749
+ const cleanResult = materializeCleanRewindSession(sessionManager, targetLeafId, previousState);
750
+ syncAgentStateFromSessionManager(session, sessionManager);
751
+ await syncRewindProjection(interactive, runtime, {
752
+ previousState,
753
+ cleanResult,
754
+ abandonedEntries: collectAbandonedRewindEntries(previousBranch, targetLeafId),
755
+ });
756
+ const remainingMessages = Array.isArray(session?.agent?.state?.messages)
757
+ ? session.agent.state.messages.length
758
+ : undefined;
759
+ const target = describeRewindTarget(targetLeafId, cleanResult);
760
+ const suffix = remainingMessages === undefined ? '' : ` Active context now has ${remainingMessages} message${remainingMessages === 1 ? '' : 's'}.`;
761
+ interactive.showStatus?.(`Rewound to before selected message at ${target}.${suffix}`);
762
+ interactive.ui?.requestRender?.();
763
+ }
764
+ function resolveInteractiveSession(interactive, runtime) {
765
+ return interactive?.session ?? runtime?.session;
766
+ }
767
+ function resolveInteractiveSessionManager(interactive, runtime) {
768
+ return interactive?.session?.sessionManager
769
+ ?? interactive?.sessionManager
770
+ ?? runtime?.session?.sessionManager
771
+ ?? runtime?.sessionManager;
772
+ }
773
+ async function stopActiveSessionWorkForRewind(session) {
774
+ if (!session) {
775
+ return;
776
+ }
777
+ const shouldWait = session.isStreaming === true || session.isBashRunning === true;
778
+ try {
779
+ if (session.isBashRunning === true && typeof session.abortBash === 'function') {
780
+ session.abortBash();
781
+ }
782
+ if (session.isStreaming === true && typeof session.abort === 'function') {
783
+ session.abort();
784
+ }
785
+ }
786
+ catch {
787
+ // Rewind should still repair the active branch even if abort reporting fails.
788
+ }
789
+ if (!shouldWait || typeof session.agent?.waitForIdle !== 'function') {
790
+ return;
791
+ }
792
+ await Promise.race([
793
+ Promise.resolve(session.agent.waitForIdle()).catch(() => undefined),
794
+ new Promise((resolve) => setTimeout(resolve, 1_500)),
795
+ ]);
796
+ }
797
+ function resetPendingAutoInputForRewind(interactive, runtime) {
798
+ if (interactive?.__autoEnabled !== true || !interactive?.__linxAutoInputController) {
799
+ return;
800
+ }
801
+ try {
802
+ interactive.__linxAutoInputController.stop();
803
+ interactive.__linxAutoInputController.start({ scheduleImmediately: false });
804
+ }
805
+ catch (error) {
806
+ const message = error instanceof Error ? error.message : String(error);
807
+ interactive.showWarning?.(`Auto input reset after rewind failed: ${message}`);
808
+ }
809
+ if (runtime && typeof runtime === 'object') {
810
+ runtime.autoEnabled = true;
811
+ }
812
+ }
813
+ function rewindSessionManagerByTurns(sessionManager, turns) {
814
+ let rewound = 0;
815
+ let targetLeafId = resolveSessionManagerLeafId(sessionManager);
816
+ for (; rewound < turns; rewound += 1) {
817
+ const branch = getActiveSessionBranch(sessionManager);
818
+ const latestUserEntry = findLatestUserMessageEntry(branch);
819
+ if (!latestUserEntry) {
820
+ break;
821
+ }
822
+ targetLeafId = typeof latestUserEntry.parentId === 'string' && latestUserEntry.parentId
823
+ ? latestUserEntry.parentId
824
+ : null;
825
+ moveSessionManagerLeaf(sessionManager, targetLeafId);
826
+ }
827
+ return { rewound, targetLeafId };
828
+ }
829
+ function resolveSessionManagerLeafId(sessionManager) {
830
+ return typeof sessionManager?.getLeafId === 'function'
831
+ ? sessionManager.getLeafId()
832
+ : null;
833
+ }
834
+ function getActiveSessionBranch(sessionManager) {
835
+ const hasLeafApi = typeof sessionManager?.getLeafId === 'function';
836
+ const leafId = hasLeafApi ? sessionManager.getLeafId() : undefined;
837
+ if (hasLeafApi && leafId === null) {
838
+ return [];
839
+ }
840
+ if (typeof sessionManager?.getBranch === 'function') {
841
+ const branch = hasLeafApi ? sessionManager.getBranch(leafId) : sessionManager.getBranch();
842
+ return Array.isArray(branch) ? branch : [];
843
+ }
844
+ const entries = typeof sessionManager?.getEntries === 'function' ? sessionManager.getEntries() : [];
845
+ return Array.isArray(entries) ? entries : [];
846
+ }
847
+ function findLatestUserMessageEntry(branch) {
848
+ for (let index = branch.length - 1; index >= 0; index -= 1) {
849
+ const entry = branch[index];
850
+ if (entry?.type === 'message' && entry.message?.role === 'user') {
851
+ return entry;
852
+ }
853
+ }
854
+ return null;
855
+ }
856
+ function moveSessionManagerLeaf(sessionManager, leafId) {
857
+ if (leafId) {
858
+ sessionManager.branch?.(leafId);
859
+ return;
860
+ }
861
+ sessionManager.resetLeaf?.();
862
+ }
863
+ function captureRewindSessionState(sessionManager) {
864
+ return {
865
+ id: normalizeRewindString(sessionManager?.getSessionId?.()),
866
+ file: normalizeRewindString(sessionManager?.getSessionFile?.()),
867
+ createdAt: resolveRewindSessionCreatedAt(sessionManager),
868
+ };
869
+ }
870
+ function materializeCleanRewindSession(sessionManager, targetLeafId, previousState) {
871
+ const beforeId = previousState.id;
872
+ let materialized = false;
873
+ let warning;
874
+ try {
875
+ if (targetLeafId && typeof sessionManager?.createBranchedSession === 'function') {
876
+ sessionManager.createBranchedSession(targetLeafId);
877
+ materialized = true;
878
+ }
879
+ else if (!targetLeafId && typeof sessionManager?.newSession === 'function') {
880
+ sessionManager.newSession(previousState.file ? { parentSession: previousState.file } : undefined);
881
+ materialized = true;
882
+ }
883
+ }
884
+ catch (error) {
885
+ warning = error instanceof Error ? error.message : String(error);
886
+ }
887
+ const id = normalizeRewindString(sessionManager?.getSessionId?.());
888
+ const file = normalizeRewindString(sessionManager?.getSessionFile?.());
889
+ return {
890
+ materialized,
891
+ sessionChanged: Boolean(beforeId && id && beforeId !== id),
892
+ id,
893
+ file,
894
+ ...(warning ? { warning } : {}),
895
+ };
896
+ }
897
+ function describeRewindTarget(targetLeafId, cleanResult) {
898
+ const target = targetLeafId ? `leaf ${targetLeafId}` : 'session root';
899
+ if (!cleanResult.materialized) {
900
+ return target;
901
+ }
902
+ if (cleanResult.sessionChanged && cleanResult.id) {
903
+ return `${target} in clean session ${cleanResult.id}`;
904
+ }
905
+ return `${target} in clean session`;
906
+ }
907
+ function collectAbandonedRewindEntries(previousBranch, targetLeafId) {
908
+ if (!Array.isArray(previousBranch) || previousBranch.length === 0) {
909
+ return [];
910
+ }
911
+ if (!targetLeafId) {
912
+ return previousBranch;
913
+ }
914
+ const targetIndex = previousBranch.findIndex((entry) => entry?.id === targetLeafId);
915
+ return targetIndex >= 0 ? previousBranch.slice(targetIndex + 1) : previousBranch;
916
+ }
917
+ async function syncRewindProjection(interactive, runtime, input) {
918
+ if (input.cleanResult.warning) {
919
+ interactive.showWarning?.(`Clean rewind history materialization skipped: ${input.cleanResult.warning}`);
920
+ }
921
+ const mirror = runtime?.__linxPodMirror ?? interactive?.__linxPodMirror;
922
+ if (!mirror || typeof mirror.syncRewindProjection !== 'function') {
923
+ return;
924
+ }
925
+ try {
926
+ await mirror.syncRewindProjection({
927
+ previousSessionId: input.previousState.id,
928
+ previousSessionFile: input.previousState.file,
929
+ previousCreatedAt: input.previousState.createdAt,
930
+ cleanSessionId: input.cleanResult.id,
931
+ cleanSessionFile: input.cleanResult.file,
932
+ abandonedEntries: input.abandonedEntries,
933
+ });
934
+ }
935
+ catch (error) {
936
+ const message = error instanceof Error ? error.message : String(error);
937
+ interactive.showWarning?.(`Pod rewind projection unavailable: ${message}`);
938
+ }
939
+ }
940
+ function resolveRewindSessionCreatedAt(sessionManager) {
941
+ const headerTimestamp = normalizeRewindString(sessionManager?.getHeader?.()?.timestamp);
942
+ const headerDate = toValidRewindDate(headerTimestamp);
943
+ if (headerDate) {
944
+ return headerDate;
945
+ }
946
+ const entries = Array.isArray(sessionManager?.getEntries?.()) ? sessionManager.getEntries() : [];
947
+ for (const entry of entries) {
948
+ const timestamp = normalizeRewindString(entry?.timestamp);
949
+ const date = toValidRewindDate(timestamp);
950
+ if (date) {
951
+ return date;
952
+ }
953
+ }
954
+ const sessionId = normalizeRewindString(sessionManager?.getSessionId?.());
955
+ return sessionId ? parseRewindDateFromSessionId(sessionId) ?? undefined : undefined;
956
+ }
957
+ function parseRewindDateFromSessionId(sessionId) {
958
+ const prefix = sessionId.replace(/-/g, '').slice(0, 12);
959
+ if (!/^[\da-f]{12}$/i.test(prefix)) {
960
+ return null;
961
+ }
962
+ const millis = Number.parseInt(prefix, 16);
963
+ if (!Number.isFinite(millis) || millis <= 0) {
964
+ return null;
965
+ }
966
+ return toValidRewindDate(millis);
967
+ }
968
+ function toValidRewindDate(value) {
969
+ if (value instanceof Date) {
970
+ return Number.isNaN(value.getTime()) ? null : value;
971
+ }
972
+ if (typeof value === 'number' || typeof value === 'string') {
973
+ const date = new Date(value);
974
+ return Number.isNaN(date.getTime()) ? null : date;
975
+ }
976
+ return null;
977
+ }
978
+ function normalizeRewindString(value) {
979
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
980
+ }
981
+ function syncAgentStateFromSessionManager(session, sessionManager) {
982
+ const context = sessionManager.buildSessionContext?.();
983
+ if (!context || !session?.agent?.state) {
984
+ return;
985
+ }
986
+ if (Array.isArray(context.messages)) {
987
+ session.agent.state.messages = context.messages;
988
+ }
989
+ }
990
+ function collectRewindUserMessages(_session, sessionManager) {
991
+ return getActiveSessionBranch(sessionManager)
992
+ .filter((entry) => entry?.type === 'message' && entry.message?.role === 'user')
993
+ .map((entry) => ({
994
+ id: String(entry.id),
995
+ text: extractRewindMessageText(entry.message?.content) || '(empty user message)',
996
+ }));
997
+ }
998
+ function extractRewindMessageText(content) {
999
+ if (typeof content === 'string') {
1000
+ return content;
1001
+ }
1002
+ if (!Array.isArray(content)) {
1003
+ return '';
1004
+ }
1005
+ return content
1006
+ .filter((part) => typeof part === 'object' && part !== null && part.type === 'text')
1007
+ .map((part) => typeof part.text === 'string' ? part.text : '')
1008
+ .join('');
1009
+ }
1010
+ class LinxRewindMessageList {
1011
+ messages;
1012
+ selectedIndex;
1013
+ onSelect;
1014
+ onCancel;
1015
+ maxVisible = 10;
1016
+ constructor(messages, initialSelectedId) {
1017
+ this.messages = messages;
1018
+ const initialIndex = initialSelectedId
1019
+ ? messages.findIndex((message) => message.id === initialSelectedId)
1020
+ : -1;
1021
+ this.selectedIndex = initialIndex >= 0 ? initialIndex : Math.max(0, messages.length - 1);
1022
+ }
1023
+ invalidate() {
1024
+ // No cached render state.
1025
+ }
1026
+ render(width) {
1027
+ const lines = [];
1028
+ if (this.messages.length === 0) {
1029
+ return [' No user messages found'];
1030
+ }
1031
+ const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.messages.length - this.maxVisible));
1032
+ const endIndex = Math.min(startIndex + this.maxVisible, this.messages.length);
1033
+ for (let index = startIndex; index < endIndex; index += 1) {
1034
+ const message = this.messages[index];
1035
+ const isSelected = index === this.selectedIndex;
1036
+ const cursor = isSelected ? '> ' : ' ';
1037
+ const normalized = message.text.replace(/\n/g, ' ').trim();
1038
+ lines.push(`${cursor}${truncateToWidth(normalized, Math.max(1, width - 2))}`);
1039
+ lines.push(` Rewind before message ${index + 1} of ${this.messages.length}`);
1040
+ lines.push('');
1041
+ }
1042
+ if (startIndex > 0 || endIndex < this.messages.length) {
1043
+ lines.push(` (${this.selectedIndex + 1}/${this.messages.length})`);
1044
+ }
1045
+ return lines;
1046
+ }
1047
+ handleInput(keyData) {
1048
+ const keybindings = getKeybindings();
1049
+ if (keybindings.matches(keyData, 'tui.select.up')) {
1050
+ this.selectedIndex = this.selectedIndex === 0 ? this.messages.length - 1 : this.selectedIndex - 1;
1051
+ return;
1052
+ }
1053
+ if (keybindings.matches(keyData, 'tui.select.down')) {
1054
+ this.selectedIndex = this.selectedIndex === this.messages.length - 1 ? 0 : this.selectedIndex + 1;
1055
+ return;
1056
+ }
1057
+ if (keybindings.matches(keyData, 'tui.select.confirm')) {
1058
+ const selected = this.messages[this.selectedIndex];
1059
+ if (selected) {
1060
+ this.onSelect?.(selected.id);
1061
+ }
1062
+ return;
1063
+ }
1064
+ if (keybindings.matches(keyData, 'tui.select.cancel')) {
1065
+ this.onCancel?.();
1066
+ }
1067
+ }
1068
+ }
1069
+ class LinxRewindMessageSelectorComponent extends Container {
1070
+ messageList;
1071
+ constructor(messages, onSelect, onCancel, initialSelectedId) {
1072
+ super();
1073
+ this.addChild(new Spacer(1));
1074
+ this.addChild(new Text('Rewind to Message', 1, 0));
1075
+ this.addChild(new Text('Select the first user message to remove from the active branch.', 1, 0));
1076
+ this.addChild(new Text('The selected message and everything after it stay in history but leave the active context.', 1, 0));
1077
+ this.addChild(new Spacer(1));
1078
+ this.messageList = new LinxRewindMessageList(messages, initialSelectedId);
1079
+ this.messageList.onSelect = onSelect;
1080
+ this.messageList.onCancel = onCancel;
1081
+ this.addChild(this.messageList);
1082
+ this.addChild(new Spacer(1));
1083
+ if (messages.length === 0) {
1084
+ setTimeout(() => onCancel(), 100);
1085
+ }
1086
+ }
1087
+ getMessageList() {
1088
+ return this.messageList;
1089
+ }
1090
+ }
1091
+ function installProjectedCommandRouter(interactive) {
1092
+ interactive.__linxHandleProjectedCommand = async (text) => {
1093
+ const command = text.trim();
1094
+ if (!command.startsWith('/')) {
1095
+ return false;
1096
+ }
1097
+ if (typeof interactive.__linxHandleProjectedGlobalCommand === 'function') {
1098
+ const handled = await interactive.__linxHandleProjectedGlobalCommand(command);
1099
+ if (handled === 'peer-command') {
1100
+ return 'peer-command';
1101
+ }
1102
+ if (handled === true) {
1103
+ return true;
1104
+ }
1105
+ }
1106
+ if (typeof interactive.__linxHandleProjectedBackendCommand === 'function') {
1107
+ const handled = await interactive.__linxHandleProjectedBackendCommand(command);
1108
+ if (handled === true) {
1109
+ return true;
1110
+ }
1111
+ }
1112
+ return false;
1113
+ };
1114
+ }
1115
+ async function handleInteractivePeerCommand(interactive, runtime, route) {
1116
+ const goalMode = route.secretaryBehavior?.goalMode;
1117
+ if (goalMode !== undefined) {
1118
+ applyInteractiveGoalMode(interactive, runtime, goalMode);
1119
+ interactive.showStatus?.(`Peer command routed; Secretary goal supervision mirror is ${goalMode ? 'active' : 'paused'}.`);
1120
+ }
1121
+ else {
1122
+ interactive.showStatus?.('Peer command routed to current chat peer.');
1123
+ }
1124
+ await submitProjectedBackendInput(interactive, route.text);
1125
+ interactive.ui?.requestRender?.();
1126
+ }
1127
+ function applyInteractiveGoalMode(interactive, runtime, enabled) {
1128
+ interactive.__linxGoalModeEnabled = enabled;
1129
+ if (enabled) {
1130
+ interactive.__linxGoalModeSupervisorLastAt = Date.now();
1131
+ }
1132
+ else {
1133
+ delete interactive.__linxGoalModeSupervisorLastAt;
1134
+ }
1135
+ if (runtime && typeof runtime === 'object') {
1136
+ runtime.goalMode = enabled;
1137
+ if (enabled) {
1138
+ runtime.goalModeSupervisorLastAt = interactive.__linxGoalModeSupervisorLastAt;
1139
+ }
1140
+ else {
1141
+ delete runtime.goalModeSupervisorLastAt;
1142
+ }
1143
+ }
1144
+ }
1145
+ async function submitProjectedBackendInput(interactive, text) {
1146
+ const session = interactive?.session;
1147
+ const sendUserMessage = typeof session?.__linxSendUserMessageWithoutCommandRouting === 'function'
1148
+ ? session.__linxSendUserMessageWithoutCommandRouting
1149
+ : session?.sendUserMessage;
1150
+ if (typeof sendUserMessage === 'function') {
1151
+ await sendUserMessage(text, session.isStreaming ? { deliverAs: 'followUp' } : undefined);
1152
+ return;
1153
+ }
1154
+ const prompt = typeof session?.__linxPromptWithoutCommandRouting === 'function'
1155
+ ? session.__linxPromptWithoutCommandRouting
1156
+ : session?.prompt;
1157
+ if (typeof prompt === 'function') {
1158
+ await prompt(text, session.isStreaming ? { streamingBehavior: 'followUp' } : undefined);
1159
+ return;
1160
+ }
1161
+ throw new Error('Active LinX session cannot accept peer goal input');
1162
+ }
1163
+ async function handleInteractiveAutoCommand(interactive, runtime, enabled, options = {}) {
1164
+ if (enabled === undefined) {
1165
+ const active = interactive.__autoEnabled === true;
1166
+ interactive.showStatus?.(formatAutoModeChangeStatus(active));
1167
+ interactive.ui?.requestRender?.();
1168
+ return;
1169
+ }
1170
+ const control = getSessionControlManager(interactive, runtime);
1171
+ control.setAutoEnabled(enabled);
1172
+ interactive.__autoEnabled = enabled;
1173
+ if (runtime && typeof runtime === 'object') {
1174
+ runtime.autoEnabled = enabled;
1175
+ }
1176
+ const controller = getSecretaryAutoInputController(interactive, runtime, control);
1177
+ if (enabled) {
1178
+ controller.start({ scheduleImmediately: options.scheduleImmediately !== false });
1179
+ }
1180
+ else {
1181
+ controller.stop();
1182
+ }
1183
+ interactive.showStatus?.(formatAutoModeChangeStatus(enabled));
1184
+ interactive.ui?.requestRender?.();
1185
+ await interactive.__linxOnAutoControlChange?.(enabled);
1186
+ }
1187
+ function formatAutoModeChangeStatus(enabled) {
1188
+ return enabled
1189
+ ? [
1190
+ 'Auto is on.',
1191
+ 'Auto on: Secretary drives the current session input loop.',
1192
+ 'What changed: backend prompts and blocked approval/input requests go to Secretary first; Secretary answers in-policy and asks you only when blocked.',
1193
+ 'User-visible state: the input bar shows托管中; Ctrl+C or /auto off hands control back to you.',
1194
+ 'Backend approval policy is unchanged.',
1195
+ ].join('\n')
1196
+ : [
1197
+ 'Auto is off.',
1198
+ 'Auto off: you drive the current session directly.',
1199
+ 'What changed: backend prompts, approvals, and free-form input return to the local TUI unless another explicit control path handles them.',
1200
+ 'Auto only controls input ownership; it does not change whether the current chat peer is Secretary or worker/backend.',
1201
+ 'Use /auto on to hand control back to Secretary.',
1202
+ ].join('\n');
1203
+ }
1204
+ async function changeInteractiveCwd(interactive, runtime, target) {
1205
+ if (!target) {
1206
+ interactive.showStatus?.(`Current workspace: ${resolveInteractiveCwd(interactive, runtime)}`);
1207
+ interactive.ui?.requestRender?.();
1208
+ return;
1209
+ }
1210
+ const nextCwd = resolve(resolveInteractiveCwd(interactive, runtime), target);
1211
+ if (!existsSync(nextCwd)) {
1212
+ interactive.showError?.(`Workspace not found: ${nextCwd}`);
1213
+ interactive.ui?.requestRender?.();
1214
+ return;
1215
+ }
1216
+ if (!statSync(nextCwd).isDirectory()) {
1217
+ interactive.showError?.(`Workspace is not a directory: ${nextCwd}`);
1218
+ interactive.ui?.requestRender?.();
1219
+ return;
1220
+ }
1221
+ process.chdir(nextCwd);
1222
+ setRuntimeCwd(interactive, runtime, nextCwd);
1223
+ await runtime?.backendCommandRouter?.setCwd?.(nextCwd);
1224
+ interactive.showStatus?.(`Workspace changed to ${nextCwd}. Session history stays in the current thread.`);
1225
+ interactive.ui?.requestRender?.();
1226
+ }
1227
+ function resolveInteractiveCwd(interactive, runtime) {
1228
+ const candidates = [
1229
+ interactive?.session?.cwd,
1230
+ runtime?.cwd,
1231
+ interactive?.sessionManager?.getCwd?.(),
1232
+ interactive?.session?.sessionManager?.getCwd?.(),
1233
+ process.cwd(),
1234
+ ];
1235
+ for (const candidate of candidates) {
1236
+ if (typeof candidate === 'string' && candidate.trim()) {
1237
+ return candidate.trim();
1238
+ }
1239
+ }
1240
+ return process.cwd();
1241
+ }
1242
+ function setRuntimeCwd(interactive, runtime, cwd) {
1243
+ if (interactive?.session && typeof interactive.session === 'object') {
1244
+ interactive.session.cwd = cwd;
1245
+ }
1246
+ if (runtime && typeof runtime === 'object') {
1247
+ runtime.cwd = cwd;
1248
+ }
1249
+ }
1250
+ export function installSymphonyCommand(interactive) {
1251
+ if (interactive.__linxSymphonyCommandInstalled) {
1252
+ return;
1253
+ }
1254
+ const originalSetup = interactive.setupEditorSubmitHandler?.bind(interactive);
1255
+ if (typeof originalSetup !== 'function') {
1256
+ return;
1257
+ }
1258
+ interactive.setupEditorSubmitHandler = function patchedSymphonySetupEditorSubmitHandler(...args) {
1259
+ const result = originalSetup(...args);
1260
+ const originalSubmit = this.defaultEditor?.onSubmit?.bind(this.defaultEditor);
1261
+ if (typeof originalSubmit !== 'function') {
1262
+ return result;
1263
+ }
1264
+ this.defaultEditor.onSubmit = async (text) => {
1265
+ const input = text.trim();
1266
+ const command = parseSymphonyCommand(input);
1267
+ if (command) {
1268
+ this.editor?.setText?.('');
1269
+ await handleSymphonyCommand(this, command);
1270
+ return;
1271
+ }
1272
+ if (this.__linxSymphonyModeEnabled && shouldProjectSymphonyInput(input)) {
1273
+ const source = await resolveSymphonySourceContext(this);
1274
+ const idea = await captureSymphonyIdeaIfNeeded(input, source);
1275
+ getSessionControlManager(this, this.runtime).recordUserMessage({ text: input });
1276
+ if (shouldDispatchSymphonyWorkerInput(input)) {
1277
+ await dispatchSymphonyWorkerFromInteractive(this, input, source);
1278
+ return;
1279
+ }
1280
+ await originalSubmit(buildSymphonyDelegationPrompt(input, {
1281
+ persistentMode: true,
1282
+ ...(source ? { source } : {}),
1283
+ ...(idea ? { idea } : {}),
1284
+ }));
1285
+ return;
1286
+ }
1287
+ await originalSubmit(text);
1288
+ };
1289
+ return result;
1290
+ };
1291
+ interactive.__linxSymphonyCommandInstalled = true;
1292
+ }
1293
+ export function installSymphonyAutocomplete(interactive) {
1294
+ installLinxCommandAutocomplete(interactive);
1295
+ }
1296
+ export function installLinxCommandAutocomplete(interactive) {
1297
+ if (interactive.__linxCommandAutocompleteInstalled || interactive.__linxSymphonyAutocompleteInstalled) {
1298
+ return;
1299
+ }
1300
+ const setupName = typeof interactive.setupAutocompleteProvider === 'function'
1301
+ ? 'setupAutocompleteProvider'
1302
+ : 'setupAutocomplete';
1303
+ const originalSetup = interactive[setupName]?.bind(interactive);
1304
+ if (typeof originalSetup !== 'function') {
1305
+ return;
1306
+ }
1307
+ interactive[setupName] = function patchedLinxSetupAutocompleteProvider(...args) {
1308
+ const result = originalSetup(...args);
1309
+ installLinxAutocompleteCommands(this.autocompleteProvider);
1310
+ return result;
1311
+ };
1312
+ interactive.__linxCommandAutocompleteInstalled = true;
1313
+ interactive.__linxSymphonyAutocompleteInstalled = true;
1314
+ }
1315
+ function installLinxAutocompleteCommands(provider) {
1316
+ if (!Array.isArray(provider?.commands)) {
1317
+ return;
1318
+ }
1319
+ for (const command of LINX_INTERACTIVE_SLASH_COMMANDS) {
1320
+ if (!provider.commands.some((existing) => getAutocompleteCommandName(existing) === command.name)) {
1321
+ provider.commands.push(command);
1322
+ }
1323
+ }
1324
+ }
1325
+ const LINX_INTERACTIVE_SLASH_COMMANDS = [
1326
+ {
1327
+ name: 'auto',
1328
+ argumentHint: 'on|off|status',
1329
+ description: 'toggle AI Secretary driving for this session',
1330
+ getArgumentCompletions: (prefix) => completeStaticArguments(prefix, [
1331
+ { value: 'on', description: 'Secretary drives the session and asks when blocked' },
1332
+ { value: 'off', description: 'User drives the session directly' },
1333
+ { value: 'status', description: 'Show whether Secretary driving is enabled' },
1334
+ ]),
1335
+ },
1336
+ {
1337
+ name: 'cd',
1338
+ argumentHint: '<dir>',
1339
+ description: 'change workspace for this LinX session',
1340
+ },
1341
+ {
1342
+ name: 'goal',
1343
+ argumentHint: '<peer-command>',
1344
+ description: 'send a goal command to the current chat peer',
1345
+ },
1346
+ {
1347
+ name: 'rewind',
1348
+ description: 'select a user message and rewind the active branch before it',
1349
+ },
1350
+ {
1351
+ name: 'ai',
1352
+ argumentHint: 'connect <provider>',
1353
+ description: 'connect AI provider credentials to LinX Pod settings',
1354
+ getArgumentCompletions: completeAiArguments,
1355
+ },
1356
+ {
1357
+ name: 'symphony',
1358
+ argumentHint: 'on|off|status',
1359
+ description: 'switch chat peer between Secretary and backend worker',
1360
+ getArgumentCompletions: (prefix) => completeStaticArguments(prefix, [
1361
+ { value: 'on', description: 'Chat with Secretary using Symphony orchestration skills' },
1362
+ { value: 'off', description: 'Chat directly with the current worker/backend peer' },
1363
+ { value: 'status', description: 'Show Symphony state and source conversation' },
1364
+ ]),
1365
+ },
1366
+ ];
1367
+ function completeStaticArguments(prefix, options) {
1368
+ const normalized = prefix.trimStart().toLowerCase();
1369
+ const matches = options.filter((option) => option.value.startsWith(normalized));
1370
+ if (matches.length === 0) {
1371
+ return null;
1372
+ }
1373
+ return matches.map((option) => ({
1374
+ value: option.value,
1375
+ label: option.value,
1376
+ description: option.description,
1377
+ }));
1378
+ }
1379
+ function completeAiArguments(prefix) {
1380
+ const input = prefix.trimStart().toLowerCase();
1381
+ if (!input || 'connect'.startsWith(input)) {
1382
+ return [{
1383
+ value: 'connect ',
1384
+ label: 'connect',
1385
+ description: 'Connect an AI provider key to LinX Pod AI settings',
1386
+ }];
1387
+ }
1388
+ const connectPrefix = 'connect ';
1389
+ if (!input.startsWith(connectPrefix)) {
1390
+ return null;
1391
+ }
1392
+ const providerPrefix = input.slice(connectPrefix.length);
1393
+ const providers = getAiConnectCompletionProviders();
1394
+ const matches = providers.filter((provider) => provider.startsWith(providerPrefix));
1395
+ if (matches.length === 0) {
1396
+ return null;
1397
+ }
1398
+ return matches.map((provider) => ({
1399
+ value: `connect ${provider}`,
1400
+ label: provider,
1401
+ description: `Connect ${provider} credentials`,
1402
+ }));
1403
+ }
1404
+ function getAiConnectCompletionProviders() {
1405
+ const providerIds = [];
1406
+ const aliases = [];
1407
+ for (const entry of getAIConfigProviderCatalog()) {
1408
+ providerIds.push(entry.id);
1409
+ aliases.push(...(entry.aliases ?? []));
1410
+ }
1411
+ return Array.from(new Set([...providerIds, ...aliases]));
1412
+ }
1413
+ function getAutocompleteCommandName(command) {
1414
+ if (!command || typeof command !== 'object') {
1415
+ return undefined;
1416
+ }
1417
+ const value = 'name' in command
1418
+ ? command.name
1419
+ : 'value' in command
1420
+ ? command.value
1421
+ : undefined;
1422
+ return typeof value === 'string' ? value : undefined;
1423
+ }
1424
+ function parseSymphonyCommand(input) {
1425
+ if (input !== '/symphony' && !input.startsWith('/symphony ')) {
1426
+ return null;
1427
+ }
1428
+ const args = input === '/symphony' ? '' : input.slice('/symphony'.length).trim();
1429
+ if (!args || args.toLowerCase() === 'on' || args.toLowerCase() === 'enable') {
1430
+ return { action: 'enable' };
1431
+ }
1432
+ const normalized = args.toLowerCase();
1433
+ if (normalized === 'off' || normalized === 'disable' || normalized === 'exit') {
1434
+ return { action: 'disable' };
1435
+ }
1436
+ if (normalized === 'status') {
1437
+ return { action: 'status' };
1438
+ }
1439
+ return { action: 'usage', input: args };
1440
+ }
1441
+ async function handleSymphonyCommand(interactive, command) {
1442
+ if (command.action === 'enable') {
1443
+ interactive.__linxSymphonyModeEnabled = true;
1444
+ interactive.__linxSymphonyModeGeneration = (Number(interactive.__linxSymphonyModeGeneration) || 0) + 1;
1445
+ if (interactive.runtime && typeof interactive.runtime === 'object') {
1446
+ interactive.runtime.symphonyEnabled = true;
1447
+ }
1448
+ interactive.showStatus?.(formatSymphonyModeChangeStatus(true));
1449
+ interactive.ui?.requestRender?.();
1450
+ await interactive.__linxOnSymphonyControlChange?.(true);
1451
+ return;
1452
+ }
1453
+ if (command.action === 'disable') {
1454
+ interactive.__linxSymphonyModeEnabled = false;
1455
+ interactive.__linxSymphonyModeGeneration = (Number(interactive.__linxSymphonyModeGeneration) || 0) + 1;
1456
+ abortInteractiveSymphonyDispatches(interactive);
1457
+ if (interactive.runtime && typeof interactive.runtime === 'object') {
1458
+ interactive.runtime.symphonyEnabled = false;
1459
+ }
1460
+ interactive.showStatus?.(formatSymphonyModeChangeStatus(false));
1461
+ interactive.ui?.requestRender?.();
1462
+ await interactive.__linxOnSymphonyControlChange?.(false);
1463
+ return;
1464
+ }
1465
+ if (command.action === 'status') {
1466
+ interactive.showStatus?.(await formatSymphonyStatus(interactive));
1467
+ interactive.ui?.requestRender?.();
1468
+ return;
1469
+ }
1470
+ interactive.showStatus?.(formatSymphonyUsage(command.input));
1471
+ interactive.ui?.requestRender?.();
1472
+ }
1473
+ function formatSymphonyModeChangeStatus(enabled) {
1474
+ return enabled
1475
+ ? [
1476
+ 'Symphony on: you are now chatting with Secretary.',
1477
+ 'What changed: following normal messages enter the Secretary control lane before worker/backend routing.',
1478
+ 'Skills: issue triage, existing Issue lookup, create/update/ask decision, task split, worker dispatch, status/report tracking.',
1479
+ 'Ordinary chat stays ordinary Message; only trackable work becomes Issue/Task/Delivery/Session.',
1480
+ 'Use /symphony status to inspect workers, /symphony off to chat with the current worker/backend peer.',
1481
+ ].join('\n')
1482
+ : [
1483
+ 'Symphony off: you are now chatting with the current worker/backend peer.',
1484
+ 'What changed: following messages bypass Secretary Symphony triage and dispatch.',
1485
+ 'Current Symphony dispatches started from this TUI were cancelled; archived workers remain inspectable with /symphony status.',
1486
+ 'Use /symphony on to chat with Secretary again.',
1487
+ ].join('\n');
1488
+ }
1489
+ function formatSymphonyUsage(input) {
1490
+ return [
1491
+ `Unsupported /symphony argument: ${input}`,
1492
+ 'Use /symphony on to chat with Secretary, /symphony off to chat with the worker/backend peer, or /symphony status to inspect workers.',
1493
+ 'After enabling Symphony, send the objective as a normal chat message to Secretary; Secretary will decide whether it is an Issue, update existing work, split tasks, and dispatch workers.',
1494
+ ].join('\n');
1495
+ }
1496
+ function shouldProjectSymphonyInput(input) {
1497
+ return Boolean(input)
1498
+ && !input.startsWith('/')
1499
+ && !input.startsWith('!');
1500
+ }
1501
+ function shouldDispatchSymphonyWorkerInput(input) {
1502
+ const normalized = input.trim().toLowerCase();
1503
+ if (!normalized) {
1504
+ return false;
1505
+ }
1506
+ return /\b(delegate|dispatch|assign|worker|agent|task)\b/u.test(normalized)
1507
+ || /(派工|派活|派发|委派|分派|交给.*(worker|agent|codex|claude|codebuddy|ai)|让.*(worker|agent|codex|claude|codebuddy|ai).*做|发一个任务|派出一个任务)/u.test(input);
1508
+ }
1509
+ async function dispatchSymphonyWorkerFromInteractive(interactive, objective, source) {
1510
+ const backend = resolveSymphonyWorkerBackend(interactive, objective);
1511
+ const agentRuntime = resolveSymphonyControlAgentRuntime(interactive);
1512
+ const workerModel = resolveSymphonyWorkerModel(interactive, objective, backend);
1513
+ const workerCredentialSource = resolveSymphonyWorkerCredentialSource(interactive, backend);
1514
+ const workerGoalMode = interactive.__autoEnabled === true;
1515
+ const workerSupervisorIntervalMs = workerGoalMode ? resolveSymphonyWorkerSupervisorIntervalMs(interactive) : undefined;
1516
+ const cwd = resolveInteractiveCwd(interactive, interactive.runtime);
1517
+ const dispatchGeneration = Number(interactive.__linxSymphonyModeGeneration) || 0;
1518
+ const dispatches = Array.isArray(interactive.__linxSymphonyDispatches)
1519
+ ? interactive.__linxSymphonyDispatches
1520
+ : [];
1521
+ interactive.__linxSymphonyDispatches = dispatches;
1522
+ const controller = new AbortController();
1523
+ const controllers = getInteractiveSymphonyDispatchControllers(interactive);
1524
+ controllers.add(controller);
1525
+ interactive.showStatus?.([
1526
+ 'Symphony dispatch started.',
1527
+ `Worker backend: ${backend}`,
1528
+ `Worker credentials: ${workerCredentialSource}`,
1529
+ ...(agentRuntime ? [`Control runtime: ${formatSymphonyControlRuntime(agentRuntime)}`] : []),
1530
+ ...(workerModel ? [`Worker model: ${workerModel}`] : []),
1531
+ workerGoalMode
1532
+ ? `Worker goal: on · supervisor interval=${formatSymphonySupervisorInterval(workerSupervisorIntervalMs)}`
1533
+ : 'Worker goal: off',
1534
+ 'Status: creating Issue / Task / Delivery and starting a quiet worker session.',
1535
+ 'Use /symphony status to inspect running workers and reports.',
1536
+ ].join('\n'));
1537
+ interactive.ui?.requestRender?.();
1538
+ const run = typeof interactive.__linxRunSymphony === 'function'
1539
+ ? interactive.__linxRunSymphony
1540
+ : runSymphony;
1541
+ const dispatchArgs = {
1542
+ objective: [objective],
1543
+ backend,
1544
+ auto: interactive.__autoEnabled === true,
1545
+ cwd,
1546
+ plain: true,
1547
+ print: false,
1548
+ quietProjectionErrors: true,
1549
+ quietWorkers: true,
1550
+ credentialSource: workerCredentialSource,
1551
+ agentRuntime,
1552
+ workerModel,
1553
+ workerGoalMode,
1554
+ workerSupervisorIntervalMs,
1555
+ signal: controller.signal,
1556
+ ...(source?.chat ? { chat: source.chat } : {}),
1557
+ ...(source?.thread ? { thread: source.thread } : {}),
1558
+ target: {
1559
+ source: 'active-session',
1560
+ backend,
1561
+ agent: `${backend}-worker`,
1562
+ label: `${backend} worker`,
1563
+ ...(source?.chat ? { chat: source.chat } : {}),
1564
+ ...(source?.thread ? { thread: source.thread } : {}),
1565
+ },
1566
+ };
1567
+ const runtime = createInteractiveSymphonyRuntime(interactive);
1568
+ const dispatch = run(dispatchArgs, runtime)
1569
+ .then((plan) => {
1570
+ if (!isCurrentSymphonyDispatch(interactive, dispatchGeneration)) {
1571
+ return;
1572
+ }
1573
+ interactive.showStatus?.(formatSymphonyDispatchResult(plan));
1574
+ })
1575
+ .catch((error) => {
1576
+ if (!isCurrentSymphonyDispatch(interactive, dispatchGeneration)) {
1577
+ return;
1578
+ }
1579
+ if (isSymphonyAbortError(error)) {
1580
+ interactive.showStatus?.('Symphony dispatch cancelled.');
1581
+ return;
1582
+ }
1583
+ const message = error instanceof Error ? error.message : String(error);
1584
+ interactive.showError?.(`Symphony dispatch failed: ${message}`);
1585
+ })
1586
+ .finally(() => {
1587
+ controllers.delete(controller);
1588
+ if (!isCurrentSymphonyDispatch(interactive, dispatchGeneration)) {
1589
+ return;
1590
+ }
1591
+ interactive.ui?.requestRender?.();
1592
+ });
1593
+ dispatches.push(dispatch);
1594
+ await Promise.resolve();
1595
+ }
1596
+ function getInteractiveSymphonyDispatchControllers(interactive) {
1597
+ if (!(interactive.__linxSymphonyDispatchControllers instanceof Set)) {
1598
+ interactive.__linxSymphonyDispatchControllers = new Set();
1599
+ }
1600
+ return interactive.__linxSymphonyDispatchControllers;
1601
+ }
1602
+ function abortInteractiveSymphonyDispatches(interactive) {
1603
+ const controllers = getInteractiveSymphonyDispatchControllers(interactive);
1604
+ for (const controller of controllers) {
1605
+ if (!controller.signal.aborted) {
1606
+ controller.abort(new Error('Symphony dispatch aborted by /symphony off'));
1607
+ }
1608
+ }
1609
+ controllers.clear();
1610
+ }
1611
+ function isSymphonyAbortError(error) {
1612
+ return error instanceof Error
1613
+ && (error.name === 'AbortError' || error.message.toLowerCase().includes('aborted'));
1614
+ }
1615
+ function isCurrentSymphonyDispatch(interactive, generation) {
1616
+ return interactive.__linxSymphonyModeEnabled === true
1617
+ && (Number(interactive.__linxSymphonyModeGeneration) || 0) === generation;
1618
+ }
1619
+ function createInteractiveSymphonyRuntime(interactive) {
1620
+ const projectionRuntime = interactive?.__linxSymphonyPodProjectionRuntime;
1621
+ if (!projectionRuntime) {
1622
+ return undefined;
1623
+ }
1624
+ return {
1625
+ runAutoMode,
1626
+ listAutoModeSessions: listArchivedAutoModeSessions,
1627
+ persistSymphonyProjectionToPod(plan, options) {
1628
+ return persistSymphonyProjectionToPod(plan, {
1629
+ ...options,
1630
+ runtime: projectionRuntime,
1631
+ });
21
1632
  },
22
- requestLogin(reason = 'manual') {
23
- requestLinxCloudLogin(interactive, reason);
1633
+ listOpenSymphonyIssuesFromPod() {
1634
+ return listOpenSymphonyIssuesFromPod({ runtime: projectionRuntime });
24
1635
  },
25
- stop() {
26
- interactive.stop();
1636
+ mirrorSymphonyProjectionJsonLdFromPod(result) {
1637
+ return mirrorSymphonyProjectionJsonLdFromPod(result, { runtime: projectionRuntime });
27
1638
  },
28
1639
  };
29
1640
  }
30
- function installLinxCdCommand(interactive, sessionCwd) {
1641
+ function resolveSymphonyWorkerBackend(interactive, objective) {
1642
+ const candidates = [
1643
+ interactive?.__linxSymphonyWorkerBackend,
1644
+ interactive?.runtime?.symphonyWorkerBackend,
1645
+ extractSymphonyWorkerBackendFromText(objective),
1646
+ interactive?.runtime?.runtimeBackend,
1647
+ interactive?.runtime?.workerBackend,
1648
+ interactive?.runtime?.backendCommandRouter?.backend,
1649
+ interactive?.runtime?.backendSessionRef?.backend,
1650
+ ];
1651
+ for (const candidate of candidates) {
1652
+ if (candidate === 'cc') {
1653
+ return 'claude';
1654
+ }
1655
+ if (isSymphonyWorkerBackend(candidate)) {
1656
+ return candidate;
1657
+ }
1658
+ }
1659
+ return 'codex';
1660
+ }
1661
+ function isSymphonyWorkerBackend(value) {
1662
+ return value === 'linx' || value === 'codex' || value === 'claude' || value === 'codebuddy';
1663
+ }
1664
+ function resolveSymphonyWorkerCredentialSource(interactive, backend) {
1665
+ const configured = normalizeSymphonyCredentialSource(interactive?.__linxSymphonyWorkerCredentialSource, interactive?.runtime?.symphonyWorkerCredentialSource, interactive?.runtime?.workerCredentialSource);
1666
+ if (configured) {
1667
+ return configured;
1668
+ }
1669
+ return backend === 'linx' ? 'cloud' : 'local';
1670
+ }
1671
+ function normalizeSymphonyCredentialSource(...values) {
1672
+ for (const value of values) {
1673
+ if (value === 'local' || value === 'cloud') {
1674
+ return value;
1675
+ }
1676
+ if (typeof value === 'string') {
1677
+ const normalized = value.trim().toLowerCase();
1678
+ if (normalized === 'local' || normalized === 'cloud') {
1679
+ return normalized;
1680
+ }
1681
+ }
1682
+ }
1683
+ return undefined;
1684
+ }
1685
+ function extractSymphonyWorkerBackendFromText(input) {
1686
+ const normalized = input?.trim().toLowerCase();
1687
+ if (!normalized) {
1688
+ return undefined;
1689
+ }
1690
+ if (/\b(?:linx|pi)\s*(?:runtime|backend|worker|agent)\b/u.test(normalized)
1691
+ || /\b(?:runtime|backend|worker|agent)\s*(?:=|:|:|是|用|使用|设为|指定为)\s*(?:linx|pi)\b/u.test(normalized)
1692
+ || /(用|使用|让|派)\s*(linx|pi)\s*(runtime|后端|worker|agent|模型)?/u.test(normalized)) {
1693
+ return 'linx';
1694
+ }
1695
+ if (/\b(?:claude|cc)\s*(?:code\s*)?(?:runtime|backend|worker|agent)\b/u.test(normalized)
1696
+ || /\b(?:runtime|backend|worker|agent)\s*(?:=|:|:|是|用|使用|设为|指定为)\s*(?:claude|cc)\b/u.test(normalized)
1697
+ || /(用|使用|让|派)\s*(?:claude|cc)\s*(?:code|runtime|后端|worker|agent|模型)?/u.test(normalized)) {
1698
+ return 'claude';
1699
+ }
1700
+ if (/\bcodex\s*(?:runtime|backend|worker|agent)?\b/u.test(normalized)) {
1701
+ return 'codex';
1702
+ }
1703
+ if (/\b(?:claude|cc)\s*(?:code|runtime|backend|worker|agent)?\b/u.test(normalized)) {
1704
+ return 'claude';
1705
+ }
1706
+ if (/\bcodebuddy\s*(?:runtime|backend|worker|agent)?\b/u.test(normalized)) {
1707
+ return 'codebuddy';
1708
+ }
1709
+ return undefined;
1710
+ }
1711
+ function resolveSymphonyControlAgentRuntime(interactive) {
1712
+ const configured = normalizeSymphonyAgentRuntimeConfig(interactive?.__linxAgentRuntime, interactive?.__linxAgentRuntimeConfig, interactive?.runtime?.agentRuntime, interactive?.runtime?.agentRuntimeConfig);
1713
+ const model = configured?.model ?? normalizeSymphonyConfigString(interactive?.session?.model?.id, interactive?.runtime?.model);
1714
+ if (!configured && !model) {
1715
+ return undefined;
1716
+ }
1717
+ return {
1718
+ backend: configured?.backend ?? 'linx',
1719
+ credentialSource: configured?.credentialSource ?? 'cloud',
1720
+ ...configured,
1721
+ ...(model ? { model } : {}),
1722
+ };
1723
+ }
1724
+ function normalizeSymphonyAgentRuntimeConfig(...values) {
1725
+ for (const value of values) {
1726
+ if (!isRecord(value)) {
1727
+ continue;
1728
+ }
1729
+ const metadata = isRecord(value.metadata) ? { ...value.metadata } : undefined;
1730
+ const resolved = {
1731
+ ...(normalizeSymphonyConfigString(value.backend) ? { backend: normalizeSymphonyConfigString(value.backend) } : {}),
1732
+ ...(normalizeSymphonyConfigString(value.model) ? { model: normalizeSymphonyConfigString(value.model) } : {}),
1733
+ ...(normalizeSymphonyConfigString(value.credentialSource) ? { credentialSource: normalizeSymphonyConfigString(value.credentialSource) } : {}),
1734
+ ...(normalizeSymphonyConfigString(value.runtime) ? { runtime: normalizeSymphonyConfigString(value.runtime) } : {}),
1735
+ ...(normalizeSymphonyConfigString(value.transport) ? { transport: normalizeSymphonyConfigString(value.transport) } : {}),
1736
+ ...(normalizeSymphonyConfigString(value.endpoint) ? { endpoint: normalizeSymphonyConfigString(value.endpoint) } : {}),
1737
+ ...(metadata ? { metadata } : {}),
1738
+ };
1739
+ if (Object.keys(resolved).length > 0) {
1740
+ return resolved;
1741
+ }
1742
+ }
1743
+ return undefined;
1744
+ }
1745
+ function formatSymphonyControlRuntime(runtime) {
1746
+ return [
1747
+ runtime.backend ?? 'linx',
1748
+ runtime.model,
1749
+ runtime.credentialSource ? `credentials=${runtime.credentialSource}` : undefined,
1750
+ ].filter(Boolean).join(' · ');
1751
+ }
1752
+ function resolveSymphonyWorkerModel(interactive, objective, backend) {
1753
+ const configured = normalizeSymphonyConfigString(interactive?.__linxSymphonyWorkerModel, interactive?.runtime?.symphonyWorkerModel, interactive?.runtime?.workerModel, extractSymphonyWorkerModelFromText(objective));
1754
+ if (backend === 'claude' && configured && isProviderRoutedModel(configured)) {
1755
+ return 'opus';
1756
+ }
1757
+ return configured;
1758
+ }
1759
+ function resolveSymphonyWorkerSupervisorIntervalMs(interactive) {
1760
+ const value = Number(interactive?.__linxSymphonyWorkerSupervisorIntervalMs
1761
+ ?? interactive?.runtime?.symphonyWorkerSupervisorIntervalMs
1762
+ ?? DEFAULT_SYMPHONY_WORKER_SUPERVISOR_INTERVAL_MS);
1763
+ if (!Number.isFinite(value) || value <= 0) {
1764
+ return DEFAULT_SYMPHONY_WORKER_SUPERVISOR_INTERVAL_MS;
1765
+ }
1766
+ return Math.trunc(value);
1767
+ }
1768
+ function formatSymphonySupervisorInterval(value) {
1769
+ const intervalMs = Number(value);
1770
+ if (!Number.isFinite(intervalMs) || intervalMs <= 0) {
1771
+ return `${DEFAULT_SYMPHONY_WORKER_SUPERVISOR_INTERVAL_MS / 60_000}m`;
1772
+ }
1773
+ if (intervalMs % 60_000 === 0) {
1774
+ return `${intervalMs / 60_000}m`;
1775
+ }
1776
+ if (intervalMs % 1000 === 0) {
1777
+ return `${intervalMs / 1000}s`;
1778
+ }
1779
+ return `${intervalMs}ms`;
1780
+ }
1781
+ function normalizeSymphonyConfigString(...values) {
1782
+ for (const value of values) {
1783
+ const normalized = typeof value === 'string' ? value.trim() : '';
1784
+ if (normalized) {
1785
+ return normalized;
1786
+ }
1787
+ }
1788
+ return undefined;
1789
+ }
1790
+ function extractSymphonyWorkerModelFromText(input) {
1791
+ const patterns = [
1792
+ /(?:worker|agent|模型|model)\s*(?:=|:|:|是|用|使用|设为|指定为)\s*([A-Za-z0-9][A-Za-z0-9._/-]{1,80})/iu,
1793
+ /(?:用|使用|让|派)\s*([A-Za-z0-9][A-Za-z0-9._/-]{1,80})\s*(?:作为)?\s*(?:worker|agent|模型|model)/iu,
1794
+ /\b((?:deepseek|gpt|claude|qwen|gemini)[A-Za-z0-9._/-]{1,80})\s*(?:worker|agent)?/iu,
1795
+ ];
1796
+ for (const pattern of patterns) {
1797
+ const match = input.match(pattern);
1798
+ const normalized = normalizeSymphonyModelToken(match?.[1]);
1799
+ if (normalized) {
1800
+ return normalized;
1801
+ }
1802
+ }
1803
+ return undefined;
1804
+ }
1805
+ function normalizeSymphonyModelToken(value) {
1806
+ const normalized = typeof value === 'string'
1807
+ ? value.trim().replace(/[,。,.、;;::!?!?))\]}】]+$/u, '')
1808
+ : '';
1809
+ return normalized || undefined;
1810
+ }
1811
+ function isProviderRoutedModel(model) {
1812
+ return /(?:deepseek|qwen|gemini|kimi|moonshot|mistral|grok|glm|minimax)/iu.test(model);
1813
+ }
1814
+ function formatSymphonyDispatchResult(plan) {
1815
+ const worker = plan.workers[0];
1816
+ const session = worker?.session ?? plan.session;
1817
+ const delivery = worker?.delivery ?? plan.delivery;
1818
+ const lines = [
1819
+ plan.issue.status === 'resolved' && delivery.status === 'completed'
1820
+ ? 'Symphony dispatch completed.'
1821
+ : 'Symphony dispatch recorded.',
1822
+ `Issue: ${plan.issue.title} (${formatSymphonyResourceTail(plan.issue.uri) ?? plan.issue.uri})`,
1823
+ `Task: ${formatSymphonyResourceTail(worker?.task ?? plan.task) ?? worker?.task ?? plan.task}`,
1824
+ `Delivery: ${delivery.status}${delivery.autoModeSessionId ? ` · runtime=${delivery.autoModeSessionId}` : ''}`,
1825
+ `Worker session: ${session.status}${session.autoModeSessionId ? ` · runtime=${session.autoModeSessionId}` : ''}`,
1826
+ 'Use /symphony status to inspect the Pod-projected worker report.',
1827
+ ];
1828
+ if (session.error) {
1829
+ lines.push(`Error: ${session.error}`);
1830
+ }
1831
+ return lines.join('\n');
1832
+ }
1833
+ async function captureSymphonyIdeaIfNeeded(input, source) {
1834
+ if (!shouldCaptureSymphonyIdeaInput(input)) {
1835
+ return undefined;
1836
+ }
1837
+ try {
1838
+ const affectedArea = inferSymphonyIdeaAffectedArea(input);
1839
+ const captureInput = {
1840
+ input,
1841
+ commitment: 'thought',
1842
+ status: 'captured',
1843
+ currentUnderstanding: input.trim(),
1844
+ nextStep: 'Bind this Idea against existing control records before promoting it to work.',
1845
+ ...(source?.chat ? { chat: source.chat } : {}),
1846
+ ...(source?.thread ? { thread: source.thread } : {}),
1847
+ ...(affectedArea ? { affectedArea } : {}),
1848
+ };
1849
+ const idea = createSymphonyIdeaRecord(captureInput);
1850
+ const persisted = await persistSymphonyIdeaToPod(idea)
1851
+ .catch(() => null);
1852
+ if (!persisted) {
1853
+ writeSymphonyIdea(idea);
1854
+ }
1855
+ return {
1856
+ uri: idea.uri,
1857
+ summary: idea.summary,
1858
+ status: idea.status,
1859
+ commitment: idea.commitment,
1860
+ };
1861
+ }
1862
+ catch {
1863
+ return undefined;
1864
+ }
1865
+ }
1866
+ function shouldCaptureSymphonyIdeaInput(input) {
1867
+ const normalized = input.trim();
1868
+ if (normalized.length < 12) {
1869
+ return false;
1870
+ }
1871
+ return /\b(idea|maybe|perhaps|could we|should we|what if|proposal|direction)\b/iu.test(normalized)
1872
+ || /(我觉得|感觉|也许|可能|考虑|想法|方向|要不要|能不能|是不是|是否|应该)/u.test(normalized);
1873
+ }
1874
+ function inferSymphonyIdeaAffectedArea(input) {
1875
+ const normalized = input.toLowerCase();
1876
+ if (/symphony|secretary|auto|approval|grant|pod|xpod|skill|worker|agent/u.test(normalized)) {
1877
+ return normalized.match(/symphony|secretary|auto|approval|grant|pod|xpod|skill|worker|agent/u)?.[0];
1878
+ }
1879
+ if (/(建模|模型|数据|同步|权限|审批|托管|多端|工作流|指标|质检)/u.test(input)) {
1880
+ return input.match(/建模|模型|数据|同步|权限|审批|托管|多端|工作流|指标|质检/u)?.[0];
1881
+ }
1882
+ return undefined;
1883
+ }
1884
+ function buildSymphonyDelegationPrompt(objective, options) {
1885
+ const modeLine = options.persistentMode
1886
+ ? 'Symphony is on: the user is chatting with Secretary in this LinX TUI session.'
1887
+ : 'This is a chat-driven Symphony request from the LinX TUI.';
1888
+ const sourceLines = options.source
1889
+ ? [
1890
+ '',
1891
+ 'Source conversation resources:',
1892
+ `Chat: ${options.source.chat}`,
1893
+ `Thread: ${options.source.thread}`,
1894
+ ...(options.source.sessionId ? [`Runtime session: ${options.source.sessionId}`] : []),
1895
+ ]
1896
+ : [];
1897
+ const ideaLines = options.idea
1898
+ ? [
1899
+ '',
1900
+ 'Captured Idea:',
1901
+ `Idea: ${options.idea.uri}`,
1902
+ `Summary: ${options.idea.summary}`,
1903
+ `Commitment: ${options.idea.commitment}`,
1904
+ `Status: ${options.idea.status}`,
1905
+ ]
1906
+ : [];
1907
+ return [
1908
+ 'AI Secretary Symphony request.',
1909
+ modeLine,
1910
+ ...sourceLines,
1911
+ ...ideaLines,
1912
+ '',
1913
+ 'User objective:',
1914
+ objective.trim(),
1915
+ '',
1916
+ 'Act as AI Secretary with Symphony skills enabled: issue triage, existing issue lookup, create/update/ask decision, task split, worker dispatch, and status/report tracking.',
1917
+ 'Decide whether this objective should be delegated to backend workers through Symphony.',
1918
+ 'Do not create an Issue for ordinary chat. Create or update an Issue only when the objective is a trackable work item.',
1919
+ 'If this is an uncommitted fragment, keep or merge it as an Idea first; do not dispatch a worker until promotion gates are met.',
1920
+ 'Before creating a new Issue, compare against existing open Issues. Update the existing Issue when it is clearly the same work item, and ask the user only when new-vs-existing is ambiguous.',
1921
+ 'Every delegation must target a Chat resource. Use a personal AI contact chat when assigning to one worker, or a group chat when the work belongs in a shared room.',
1922
+ 'Use the Source conversation resources only as provenance unless they are also the correct target chat.',
1923
+ 'If delegation is appropriate, create or update the normal LinX work context, derive issue/task acceptance criteria from the objective and source context, and project the task to the selected backend worker.',
1924
+ 'Ask the user only when acceptance, authority, credentials, or target selection cannot be safely inferred.',
1925
+ 'If delegation is not appropriate, explain the reason and continue in this conversation.',
1926
+ 'Keep the user-facing answer concise and show the next observable step.',
1927
+ ].join('\n');
1928
+ }
1929
+ async function formatSymphonyStatus(interactive) {
1930
+ const enabled = interactive.__linxSymphonyModeEnabled === true;
1931
+ const [source, workersRead, issuesRead, reportsRead] = await Promise.all([
1932
+ resolveSymphonySourceContext(interactive),
1933
+ listRunningSymphonyWorkers(interactive),
1934
+ listOpenSymphonyIssues(interactive),
1935
+ listRecentSymphonyReports(interactive),
1936
+ ]);
1937
+ const workers = workersRead.items;
1938
+ const issues = issuesRead.items;
1939
+ const reports = reportsRead.items;
1940
+ const projectionErrors = Array.from(new Set([
1941
+ workersRead.error,
1942
+ issuesRead.error,
1943
+ reportsRead.error,
1944
+ ].filter((item) => Boolean(item))));
1945
+ const projectionSources = new Set([workersRead.source, issuesRead.source, reportsRead.source]);
1946
+ const lines = [
1947
+ `Symphony is ${enabled ? 'on' : 'off'}.`,
1948
+ `Current chat peer: ${enabled ? 'Secretary' : 'worker/backend peer'}.`,
1949
+ `Open issues: ${issues.length}`,
1950
+ `Running workers: ${workers.length}`,
1951
+ `Recent reports: ${reports.length}`,
1952
+ projectionErrors.length > 0
1953
+ ? `Pod projection: unavailable (${formatSymphonyStatusError(projectionErrors[0])})`
1954
+ : projectionSources.has('pod')
1955
+ ? 'Pod projection: active.'
1956
+ : 'Pod projection: local archive only.',
1957
+ 'Skills: issue triage, existing issue lookup, create/update/ask decision, task split, worker dispatch, status/report tracking.',
1958
+ 'Delegation target: AI Secretary must choose a Chat resource before dispatch.',
1959
+ 'Allowed targets: personal AI contact chat or group chat.',
1960
+ 'Thread role: concrete work timeline under the selected Chat.',
1961
+ 'Session role: backend runtime lifecycle only.',
1962
+ ];
1963
+ if (projectionErrors.length > 0) {
1964
+ lines.push('Fallback: showing local Symphony archive while Pod projection is unavailable.');
1965
+ }
1966
+ for (const issue of issues.slice(0, 5)) {
1967
+ lines.push(` - ${formatSymphonyIssueStatus(issue)}`);
1968
+ }
1969
+ if (issues.length > 5) {
1970
+ lines.push(` ... ${issues.length - 5} more open issue(s)`);
1971
+ }
1972
+ for (const worker of workers.slice(0, 5)) {
1973
+ lines.push(` - ${formatSymphonyWorkerStatus(worker)}`);
1974
+ }
1975
+ if (workers.length > 5) {
1976
+ lines.push(` ... ${workers.length - 5} more running worker(s)`);
1977
+ }
1978
+ for (const report of reports.slice(0, 5)) {
1979
+ lines.push(` - ${formatSymphonyReportStatus(report)}`);
1980
+ }
1981
+ if (reports.length > 5) {
1982
+ lines.push(` ... ${reports.length - 5} more recent report(s)`);
1983
+ }
1984
+ if (source) {
1985
+ lines.push('Source conversation:', ` Chat: ${source.chat}`, ` Thread: ${source.thread}`, ...(source.sessionId ? [` Runtime session: ${source.sessionId}`] : []));
1986
+ }
1987
+ else {
1988
+ lines.push('Source conversation: unavailable until LinX has WebID and session id.');
1989
+ }
1990
+ lines.push('Commands: /symphony on chat with Secretary, /symphony status inspect workers, /symphony off chat with worker/backend.');
1991
+ return lines.join('\n');
1992
+ }
1993
+ function formatSymphonyStatusError(message) {
1994
+ return message.replace(/\s+/gu, ' ').trim().slice(0, 180);
1995
+ }
1996
+ function resolveSymphonyStatusPodTimeoutMs(interactive) {
1997
+ const value = Number(interactive?.__linxSymphonyStatusPodTimeoutMs);
1998
+ return Number.isFinite(value) && value > 0 ? value : SYMPHONY_STATUS_POD_TIMEOUT_MS;
1999
+ }
2000
+ async function withSymphonyStatusTimeout(interactive, label, task) {
2001
+ const timeoutMs = resolveSymphonyStatusPodTimeoutMs(interactive);
2002
+ let timer = null;
2003
+ try {
2004
+ return await Promise.race([
2005
+ task,
2006
+ new Promise((_, reject) => {
2007
+ timer = setTimeout(() => {
2008
+ reject(new Error(`${label} timed out after ${timeoutMs}ms`));
2009
+ }, timeoutMs);
2010
+ }),
2011
+ ]);
2012
+ }
2013
+ finally {
2014
+ if (timer) {
2015
+ clearTimeout(timer);
2016
+ }
2017
+ }
2018
+ }
2019
+ async function listOpenSymphonyIssues(interactive) {
2020
+ const projectionRuntime = interactive?.__linxSymphonyPodProjectionRuntime;
2021
+ let projectionError;
2022
+ try {
2023
+ if (projectionRuntime?.issueResource) {
2024
+ const podIssues = await withSymphonyStatusTimeout(interactive, 'Symphony Pod issue status', listOpenSymphonyIssuesFromPod({ runtime: projectionRuntime }));
2025
+ if (podIssues) {
2026
+ return { items: podIssues, source: 'pod' };
2027
+ }
2028
+ }
2029
+ }
2030
+ catch (error) {
2031
+ projectionError = error instanceof Error ? error.message : String(error);
2032
+ // Fall back to local no-Pod archive below.
2033
+ }
2034
+ try {
2035
+ const issues = typeof interactive?.__linxListSymphonyIssues === 'function'
2036
+ ? interactive.__linxListSymphonyIssues()
2037
+ : listSymphonyIssues();
2038
+ return {
2039
+ items: issues.filter((issue) => issue.status !== 'closed' && issue.status !== 'resolved'),
2040
+ source: 'local',
2041
+ ...(projectionError ? { error: projectionError } : {}),
2042
+ };
2043
+ }
2044
+ catch {
2045
+ return { items: [], source: 'none', ...(projectionError ? { error: projectionError } : {}) };
2046
+ }
2047
+ }
2048
+ async function listRunningSymphonyWorkers(interactive) {
2049
+ const projectionRuntime = interactive?.__linxSymphonyPodProjectionRuntime;
2050
+ let projectionError;
2051
+ try {
2052
+ if (projectionRuntime?.sessionResource) {
2053
+ const podWorkers = await withSymphonyStatusTimeout(interactive, 'Symphony Pod worker status', listRunningSymphonyWorkersFromPod({ runtime: projectionRuntime }));
2054
+ if (podWorkers) {
2055
+ return { items: podWorkers, source: 'pod' };
2056
+ }
2057
+ }
2058
+ }
2059
+ catch (error) {
2060
+ projectionError = error instanceof Error ? error.message : String(error);
2061
+ }
2062
+ try {
2063
+ if (typeof interactive?.__linxListSymphonySessions === 'function') {
2064
+ const sessions = interactive.__linxListSymphonySessions();
2065
+ return {
2066
+ items: sessions.filter((session) => session.status === 'running'),
2067
+ source: 'local',
2068
+ ...(projectionError ? { error: projectionError } : {}),
2069
+ };
2070
+ }
2071
+ return {
2072
+ items: listSymphonySessions()
2073
+ .filter((session) => session.status === 'running'),
2074
+ source: 'local',
2075
+ ...(projectionError ? { error: projectionError } : {}),
2076
+ };
2077
+ }
2078
+ catch {
2079
+ return { items: [], source: 'none', ...(projectionError ? { error: projectionError } : {}) };
2080
+ }
2081
+ }
2082
+ async function listRecentSymphonyReports(interactive) {
2083
+ const projectionRuntime = interactive?.__linxSymphonyPodProjectionRuntime;
2084
+ let projectionError;
2085
+ try {
2086
+ if (projectionRuntime?.deliveryResource) {
2087
+ const podReports = await withSymphonyStatusTimeout(interactive, 'Symphony Pod report status', listRecentSymphonyReportsFromPod({
2088
+ runtime: projectionRuntime,
2089
+ limit: 5,
2090
+ }));
2091
+ if (podReports) {
2092
+ return { items: podReports, source: 'pod' };
2093
+ }
2094
+ }
2095
+ }
2096
+ catch (error) {
2097
+ projectionError = error instanceof Error ? error.message : String(error);
2098
+ // Fall back to local no-Pod archive below.
2099
+ }
2100
+ try {
2101
+ const sessions = typeof interactive?.__linxListSymphonySessions === 'function'
2102
+ ? interactive.__linxListSymphonySessions()
2103
+ : listSymphonySessions();
2104
+ return {
2105
+ items: sessions
2106
+ .filter((session) => session.status === 'completed' || session.status === 'failed')
2107
+ .slice(0, 5),
2108
+ source: 'local',
2109
+ ...(projectionError ? { error: projectionError } : {}),
2110
+ };
2111
+ }
2112
+ catch {
2113
+ return { items: [], source: 'none', ...(projectionError ? { error: projectionError } : {}) };
2114
+ }
2115
+ }
2116
+ function formatSymphonyWorkerStatus(session) {
2117
+ const target = session.target?.label
2118
+ ?? session.target?.agent
2119
+ ?? session.target?.chat
2120
+ ?? session.backend;
2121
+ const suffix = [
2122
+ session.autoModeSessionId ? `runtime=${session.autoModeSessionId}` : undefined,
2123
+ session.target?.chat ? `chat=${session.target.chat}` : undefined,
2124
+ session.cwd ? `cwd=${session.cwd}` : undefined,
2125
+ ].filter(Boolean).join(' · ');
2126
+ return `${session.backend}/${session.mode} -> ${target}${suffix ? ` (${suffix})` : ''}`;
2127
+ }
2128
+ function formatSymphonyReportStatus(report) {
2129
+ const status = report.status;
2130
+ const reportRecord = report;
2131
+ const target = reportRecord.agent
2132
+ ?? reportRecord.target?.label
2133
+ ?? reportRecord.target?.agent
2134
+ ?? report.backend;
2135
+ const title = 'summary' in report && report.summary
2136
+ ? report.summary
2137
+ : 'title' in report && report.title
2138
+ ? report.title
2139
+ : 'task' in report && report.task
2140
+ ? formatSymphonyResourceTail(report.task)
2141
+ : undefined;
2142
+ const suffix = [
2143
+ report.autoModeSessionId ? `runtime=${report.autoModeSessionId}` : undefined,
2144
+ 'thread' in report && report.thread ? `thread=${report.thread}` : undefined,
2145
+ 'completedAt' in report && report.completedAt ? `completed=${report.completedAt}` : undefined,
2146
+ report.error ? `error=${report.error}` : undefined,
2147
+ ].filter(Boolean).join(' · ');
2148
+ return `${status} ${report.backend} -> ${target}${title ? `: ${title}` : ''}${suffix ? ` (${suffix})` : ''}`;
2149
+ }
2150
+ function formatSymphonyIssueStatus(issue) {
2151
+ const taskCount = issue.tasks?.length ?? 0;
2152
+ const suffix = [
2153
+ formatSymphonyResourceTail(issue.uri),
2154
+ taskCount > 0 ? `${taskCount} task${taskCount === 1 ? '' : 's'}` : undefined,
2155
+ issue.thread ? `thread=${issue.thread}` : undefined,
2156
+ ].filter(Boolean).join(' · ');
2157
+ return `${issue.status} ${issue.title}${suffix ? ` (${suffix})` : ''}`;
2158
+ }
2159
+ function formatSymphonyResourceTail(uri) {
2160
+ if (!uri) {
2161
+ return undefined;
2162
+ }
2163
+ return uri.trim().match(/[:/#]([^:/#]+)$/u)?.[1] ?? uri;
2164
+ }
2165
+ async function resolveSymphonySourceContext(interactive) {
2166
+ const sessionId = interactive?.sessionManager?.getSessionId?.()
2167
+ ?? interactive?.session?.sessionManager?.getSessionId?.()
2168
+ ?? interactive?.session?.sessionId;
2169
+ const webId = await resolveSymphonyWebId(interactive);
2170
+ if (typeof sessionId !== 'string' || !sessionId.trim() || !webId) {
2171
+ return undefined;
2172
+ }
2173
+ const trimmedSessionId = sessionId.trim();
2174
+ return {
2175
+ chat: secretaryChatUri(webId),
2176
+ thread: secretaryThreadUri(webId, trimmedSessionId, DEFAULT_SECRETARY_CHAT_ID),
2177
+ sessionId: trimmedSessionId,
2178
+ };
2179
+ }
2180
+ async function resolveSymphonyWebId(interactive) {
2181
+ const candidates = [
2182
+ interactive?.podSession?.webId,
2183
+ interactive?.runtime?.podSession?.webId,
2184
+ interactive?.session?.podSession?.webId,
2185
+ interactive?.session?.runtime?.podSession?.webId,
2186
+ interactive?.session?.state?.webId,
2187
+ interactive?.state?.webId,
2188
+ ];
2189
+ for (const candidate of candidates) {
2190
+ if (typeof candidate === 'string' && candidate.trim()) {
2191
+ return candidate.trim();
2192
+ }
2193
+ }
2194
+ const podSession = await interactive?.runtime?.getPodDataSession?.().catch(() => null);
2195
+ if (typeof podSession?.webId === 'string' && podSession.webId.trim()) {
2196
+ interactive.runtime.podSession = podSession;
2197
+ return podSession.webId.trim();
2198
+ }
2199
+ return undefined;
2200
+ }
2201
+ async function promptForBackendCredential(interactive, details) {
2202
+ const reason = details.reason ?? 'missing';
2203
+ const repairLabel = formatBackendCredentialRepairReason(reason);
2204
+ interactive.showStatus?.(`AI Secretary detected ${repairLabel} ${details.providerLabel} credentials before this backend can answer. ` +
2205
+ 'Enter them here; LinX will save them to your Pod AI settings and retry the message.');
2206
+ if (canRenderPiLoginDialog(interactive)) {
2207
+ return promptForApiCredentialWithPiDialog(interactive, {
2208
+ title: `Connect ${details.providerLabel}`,
2209
+ providerId: details.providerId,
2210
+ providerLabel: details.providerLabel,
2211
+ providerIdPrompt: details.providerIdPrompt,
2212
+ apiKeyPrompt: details.apiKeyPrompt,
2213
+ baseUrlPrompt: details.baseUrlPrompt,
2214
+ progress: [
2215
+ `AI Secretary detected ${repairLabel} credentials.`,
2216
+ 'LinX will save this with `linx ai connect` semantics into your Pod AI settings.',
2217
+ ],
2218
+ errorPrefix: `Failed to collect ${details.providerLabel} credentials`,
2219
+ });
2220
+ }
2221
+ return promptForBackendCredentialWithExtensionInput(interactive, details, repairLabel);
2222
+ }
2223
+ async function handleInteractiveAiConnectCommand(interactive, runtime, command) {
2224
+ const providerId = command.provider?.trim();
2225
+ if (!providerId) {
2226
+ interactive.showStatus?.('Usage: /ai connect <provider> [--base-url <url>] [--model <model>] - connect an AI provider key to LinX Pod AI settings.');
2227
+ interactive.ui?.requestRender?.();
2228
+ return;
2229
+ }
2230
+ const metadata = getAIConfigProviderMetadata(providerId);
2231
+ const providerLabel = metadata.displayName ?? metadata.id;
2232
+ const credential = canRenderPiLoginDialog(interactive)
2233
+ ? await promptForApiCredentialWithPiDialog(interactive, {
2234
+ title: `Connect ${providerLabel}`,
2235
+ providerId: metadata.id,
2236
+ providerLabel,
2237
+ apiKeyPrompt: `${providerLabel} API key`,
2238
+ baseUrlPrompt: command.baseUrl ? undefined : 'API base URL',
2239
+ progress: [
2240
+ `Connect ${providerLabel} with LinX AI connect.`,
2241
+ 'LinX will save this provider key to your Pod AI settings, not Pi auth.json.',
2242
+ ...(command.model ? [`Default model: ${command.model}`] : []),
2243
+ ],
2244
+ errorPrefix: `Failed to connect ${providerLabel}`,
2245
+ })
2246
+ : await promptForApiCredentialWithExtensionInput(interactive, {
2247
+ providerId: metadata.id,
2248
+ providerLabel,
2249
+ apiKeyPrompt: `${providerLabel} API key`,
2250
+ baseUrlPrompt: command.baseUrl ? undefined : 'API base URL',
2251
+ repairLabel: 'connect',
2252
+ });
2253
+ const apiKey = credential?.apiKey?.trim();
2254
+ if (!apiKey) {
2255
+ interactive.showStatus?.(`${providerLabel} AI connect cancelled.`);
2256
+ interactive.ui?.requestRender?.();
2257
+ return;
2258
+ }
2259
+ try {
2260
+ const saveCredential = resolveInteractiveAiConnectCredentialSaver(interactive, runtime);
2261
+ const credentialProviderId = credential?.providerId?.trim();
2262
+ const credentialBaseUrl = credential?.baseUrl?.trim() || command.baseUrl?.trim();
2263
+ const model = command.model?.trim();
2264
+ const result = await saveCredential({
2265
+ provider: credentialProviderId || metadata.id,
2266
+ apiKey,
2267
+ ...(credentialBaseUrl ? { baseUrl: credentialBaseUrl } : {}),
2268
+ ...(model ? { model } : {}),
2269
+ });
2270
+ interactive.showStatus?.(`Connected AI provider ${result.providerId} to LinX Pod AI settings. api-key: ${result.maskedApiKey}`);
2271
+ interactive.session?.modelRegistry?.refresh?.();
2272
+ await interactive.updateAvailableProviderCount?.();
2273
+ }
2274
+ catch (error) {
2275
+ const message = error instanceof Error ? error.message : String(error);
2276
+ interactive.showError?.(`LinX AI connect failed: ${message}`);
2277
+ }
2278
+ finally {
2279
+ interactive.ui?.requestRender?.();
2280
+ }
2281
+ }
2282
+ function resolveInteractiveAiConnectCredentialSaver(interactive, runtime) {
2283
+ const candidates = [
2284
+ runtime?.connectAiProviderCredential,
2285
+ interactive?.__linxConnectAiProviderCredential,
2286
+ interactive?.runtime?.connectAiProviderCredential,
2287
+ ];
2288
+ for (const candidate of candidates) {
2289
+ if (typeof candidate === 'function') {
2290
+ return candidate;
2291
+ }
2292
+ }
2293
+ return connectAiProviderCredential;
2294
+ }
2295
+ async function promptForApiCredentialWithPiDialog(interactive, details) {
2296
+ const dialog = new LoginDialogComponent(interactive.ui, details.providerId, () => undefined, details.providerLabel, details.title);
2297
+ const restoreEditor = () => {
2298
+ interactive.editorContainer.clear();
2299
+ interactive.editorContainer.addChild(interactive.editor);
2300
+ interactive.ui?.setFocus?.(interactive.editor);
2301
+ interactive.ui?.requestRender?.();
2302
+ };
2303
+ interactive.editorContainer.clear();
2304
+ interactive.editorContainer.addChild(dialog);
2305
+ interactive.ui?.setFocus?.(dialog);
2306
+ interactive.ui?.requestRender?.();
2307
+ try {
2308
+ for (const line of details.progress ?? []) {
2309
+ dialog.showProgress(line);
2310
+ }
2311
+ let providerId = details.providerId;
2312
+ if (details.providerIdPrompt) {
2313
+ const providerIdValue = await dialog.showPrompt(`Enter ${details.providerIdPrompt}:`, details.providerId);
2314
+ providerId = typeof providerIdValue === 'string' && providerIdValue.trim()
2315
+ ? providerIdValue.trim()
2316
+ : details.providerId;
2317
+ }
2318
+ const apiKeyValue = await dialog.showPrompt(`Enter ${details.apiKeyPrompt}:`);
2319
+ const apiKey = typeof apiKeyValue === 'string' ? apiKeyValue.trim() : '';
2320
+ if (!apiKey) {
2321
+ return null;
2322
+ }
2323
+ let baseUrl;
2324
+ if (details.baseUrlPrompt) {
2325
+ const baseUrlValue = await dialog.showPrompt(`Enter ${details.baseUrlPrompt} (optional):`);
2326
+ baseUrl = typeof baseUrlValue === 'string' && baseUrlValue.trim()
2327
+ ? baseUrlValue.trim()
2328
+ : undefined;
2329
+ }
2330
+ return { providerId, apiKey, ...(baseUrl ? { baseUrl } : {}) };
2331
+ }
2332
+ catch (error) {
2333
+ const message = error instanceof Error ? error.message : String(error);
2334
+ if (message !== 'Login cancelled') {
2335
+ interactive.showError?.(`${details.errorPrefix}: ${message}`);
2336
+ }
2337
+ return null;
2338
+ }
2339
+ finally {
2340
+ restoreEditor();
2341
+ }
2342
+ }
2343
+ function canRenderPiLoginDialog(interactive) {
2344
+ return Boolean(interactive?.isInitialized === true
2345
+ && interactive?.ui
2346
+ && interactive?.editor
2347
+ && typeof interactive?.editorContainer?.clear === 'function'
2348
+ && typeof interactive?.editorContainer?.addChild === 'function'
2349
+ && typeof interactive?.ui?.setFocus === 'function'
2350
+ && typeof interactive?.ui?.requestRender === 'function');
2351
+ }
2352
+ async function promptForBackendCredentialWithExtensionInput(interactive, details, repairLabel) {
2353
+ return promptForApiCredentialWithExtensionInput(interactive, {
2354
+ providerId: details.providerId,
2355
+ providerLabel: details.providerLabel,
2356
+ providerIdPrompt: details.providerIdPrompt,
2357
+ apiKeyPrompt: details.apiKeyPrompt,
2358
+ baseUrlPrompt: details.baseUrlPrompt,
2359
+ repairLabel,
2360
+ });
2361
+ }
2362
+ async function promptForApiCredentialWithExtensionInput(interactive, details) {
2363
+ const repairLabel = details.repairLabel;
2364
+ const apiKeyTitle = [
2365
+ `${details.providerLabel} ${repairLabel} credential`,
2366
+ `Paste an ${details.apiKeyPrompt}; LinX will save it to your Pod AI settings.`,
2367
+ 'Press Escape to cancel.',
2368
+ ].join('\n');
2369
+ if (typeof interactive.showExtensionInput !== 'function') {
2370
+ interactive.showError?.(`This terminal cannot collect ${details.providerLabel} credentials inside the TUI. Run \`linx ai connect ${details.providerId}\` first.`);
2371
+ return null;
2372
+ }
2373
+ let providerId = details.providerId;
2374
+ if (details.providerIdPrompt) {
2375
+ const providerIdTitle = [
2376
+ `${details.providerLabel} ${repairLabel} provider`,
2377
+ 'Enter the provider id to store under /settings/providers/{provider}.ttl.',
2378
+ `Default: ${details.providerId}`,
2379
+ 'Press Escape to cancel.',
2380
+ ].join('\n');
2381
+ const providerIdValue = await interactive.showExtensionInput(providerIdTitle, details.providerIdPrompt);
2382
+ providerId = typeof providerIdValue === 'string' && providerIdValue.trim()
2383
+ ? providerIdValue.trim()
2384
+ : details.providerId;
2385
+ }
2386
+ const apiKeyValue = await interactive.showExtensionInput(apiKeyTitle, details.apiKeyPrompt);
2387
+ const apiKey = typeof apiKeyValue === 'string' ? apiKeyValue.trim() : '';
2388
+ if (!apiKey) {
2389
+ return null;
2390
+ }
2391
+ let baseUrl;
2392
+ if (details.baseUrlPrompt) {
2393
+ const baseUrlTitle = [
2394
+ `${details.providerLabel} ${repairLabel} base URL`,
2395
+ 'Optional. Leave empty to use the shared provider default.',
2396
+ 'Press Escape to cancel.',
2397
+ ].join('\n');
2398
+ const baseUrlValue = await interactive.showExtensionInput(baseUrlTitle, details.baseUrlPrompt);
2399
+ baseUrl = typeof baseUrlValue === 'string' && baseUrlValue.trim()
2400
+ ? baseUrlValue.trim()
2401
+ : undefined;
2402
+ }
2403
+ return { providerId, apiKey, ...(baseUrl ? { baseUrl } : {}) };
2404
+ }
2405
+ function formatBackendCredentialRepairReason(reason) {
2406
+ return reason === 'invalid' ? 'invalid' : 'missing';
2407
+ }
2408
+ function installLinxCwdStartupNotice(interactive, sessionCwd) {
31
2409
  const originalInit = interactive.init?.bind(interactive);
32
2410
  if (typeof originalInit !== 'function')
33
2411
  return;
@@ -49,31 +2427,77 @@ export function installLinxEscapeInterrupt(interactive) {
49
2427
  if (!editor || editor.__linxEscapeInterruptInstalled) {
50
2428
  return;
51
2429
  }
52
- let currentOnEscape = typeof editor.onEscape === 'function'
2430
+ const initialOnEscape = typeof editor.onEscape === 'function'
53
2431
  ? editor.onEscape
54
2432
  : undefined;
2433
+ let currentOnEscape = isLinxEscapeInterruptWrapper(initialOnEscape)
2434
+ ? undefined
2435
+ : initialOnEscape;
2436
+ const linxEscapeInterrupt = function linxEscapeInterrupt() {
2437
+ const session = interactive?.session;
2438
+ if (handBackAutoControlOnInterrupt(interactive)) {
2439
+ return;
2440
+ }
2441
+ if (session?.isBashRunning && typeof session.abortBash === 'function') {
2442
+ void session.abortBash();
2443
+ return;
2444
+ }
2445
+ if (isLinxSessionRunning(interactive) && typeof session?.abort === 'function') {
2446
+ void session.abort();
2447
+ return;
2448
+ }
2449
+ currentOnEscape?.call(editor);
2450
+ };
2451
+ Object.defineProperty(linxEscapeInterrupt, '__linxEscapeInterruptWrapper', {
2452
+ value: true,
2453
+ });
55
2454
  Object.defineProperty(editor, 'onEscape', {
56
2455
  configurable: true,
57
2456
  get() {
58
- return function linxEscapeInterrupt() {
59
- const session = interactive?.session;
60
- if (session?.isBashRunning && typeof session.abortBash === 'function') {
61
- void session.abortBash();
62
- return;
63
- }
64
- if (isLinxSessionRunning(interactive) && typeof session?.abort === 'function') {
65
- void session.abort();
66
- return;
67
- }
68
- currentOnEscape?.call(editor);
69
- };
2457
+ return linxEscapeInterrupt;
70
2458
  },
71
2459
  set(next) {
2460
+ if (isLinxEscapeInterruptWrapper(next)) {
2461
+ return;
2462
+ }
72
2463
  currentOnEscape = typeof next === 'function' ? next : undefined;
73
2464
  },
74
2465
  });
2466
+ installLinxClearInterrupt(interactive, editor);
75
2467
  editor.__linxEscapeInterruptInstalled = true;
76
2468
  }
2469
+ function isLinxEscapeInterruptWrapper(value) {
2470
+ return typeof value === 'function'
2471
+ && value.__linxEscapeInterruptWrapper === true;
2472
+ }
2473
+ function installLinxClearInterrupt(interactive, editor) {
2474
+ const handlers = editor?.actionHandlers;
2475
+ if (!(handlers instanceof Map) || editor.__linxClearInterruptInstalled) {
2476
+ return;
2477
+ }
2478
+ const originalClear = handlers.get('app.clear');
2479
+ handlers.set('app.clear', () => {
2480
+ if (handBackAutoControlOnInterrupt(interactive)) {
2481
+ return;
2482
+ }
2483
+ originalClear?.call(editor);
2484
+ });
2485
+ editor.__linxClearInterruptInstalled = true;
2486
+ }
2487
+ function handBackAutoControlOnInterrupt(interactive) {
2488
+ if (interactive?.__autoEnabled !== true) {
2489
+ return false;
2490
+ }
2491
+ const session = interactive?.session;
2492
+ if (session?.isBashRunning && typeof session.abortBash === 'function') {
2493
+ void session.abortBash();
2494
+ }
2495
+ else if (isLinxSessionRunning(interactive) && typeof session?.abort === 'function') {
2496
+ void session.abort();
2497
+ }
2498
+ void handleInteractiveAutoCommand(interactive, interactive?.runtime, false);
2499
+ return true;
2500
+ }
77
2501
  function isLinxSessionRunning(interactive) {
78
2502
  return interactive?.session?.isStreaming === true
79
2503
  || Boolean(interactive?.loadingAnimation)
@@ -123,10 +2547,160 @@ export function buildLinxExitMessage(interactive) {
123
2547
  lines.push(`Token usage: ${usageParts.join(' · ')}`);
124
2548
  }
125
2549
  if (typeof sessionId === 'string' && sessionId.trim()) {
126
- lines.push(`Resume: linx resume ${sessionId}`);
2550
+ lines.push(`Resume: linx --session ${sessionId}`);
127
2551
  }
128
2552
  return lines.join('\n');
129
2553
  }
2554
+ export function installLinxResumeOutputStyle() {
2555
+ if (linxResumeOutputStyleRestore) {
2556
+ return linxResumeOutputStyleRestore;
2557
+ }
2558
+ const originalWrite = process.stdout.write;
2559
+ const originalErrorWrite = process.stderr.write;
2560
+ const stdoutFilter = createPiResumeOutputFilter();
2561
+ const stderrFilter = createPiResumeOutputFilter();
2562
+ const patchedStdoutWrite = function patchedPersistentLinxStdoutWrite(chunk, encodingOrCallback, callback) {
2563
+ return writeWithPiResumeFilter(process.stdout, originalWrite, stdoutFilter, chunk, encodingOrCallback, callback);
2564
+ };
2565
+ const patchedStderrWrite = function patchedPersistentLinxStderrWrite(chunk, encodingOrCallback, callback) {
2566
+ return writeWithPiResumeFilter(process.stderr, originalErrorWrite, stderrFilter, chunk, encodingOrCallback, callback);
2567
+ };
2568
+ process.stdout.write = patchedStdoutWrite;
2569
+ process.stderr.write = patchedStderrWrite;
2570
+ linxResumeOutputStyleRestore = () => {
2571
+ flushPiResumeOutputFilter(process.stdout, originalWrite, stdoutFilter);
2572
+ flushPiResumeOutputFilter(process.stderr, originalErrorWrite, stderrFilter);
2573
+ if (process.stdout.write === patchedStdoutWrite) {
2574
+ process.stdout.write = originalWrite;
2575
+ }
2576
+ if (process.stderr.write === patchedStderrWrite) {
2577
+ process.stderr.write = originalErrorWrite;
2578
+ }
2579
+ linxResumeOutputStyleRestore = null;
2580
+ };
2581
+ return linxResumeOutputStyleRestore;
2582
+ }
2583
+ export async function withLinxResumeOutputStyle(run) {
2584
+ const originalWrite = process.stdout.write;
2585
+ const originalErrorWrite = process.stderr.write;
2586
+ const stdoutFilter = createPiResumeOutputFilter();
2587
+ const stderrFilter = createPiResumeOutputFilter();
2588
+ process.stdout.write = function patchedLinxStdoutWrite(chunk, encodingOrCallback, callback) {
2589
+ return writeWithPiResumeFilter(process.stdout, originalWrite, stdoutFilter, chunk, encodingOrCallback, callback);
2590
+ };
2591
+ process.stderr.write = function patchedLinxStderrWrite(chunk, encodingOrCallback, callback) {
2592
+ return writeWithPiResumeFilter(process.stderr, originalErrorWrite, stderrFilter, chunk, encodingOrCallback, callback);
2593
+ };
2594
+ try {
2595
+ const result = await run();
2596
+ await new Promise((resolve) => setImmediate(resolve));
2597
+ return result;
2598
+ }
2599
+ finally {
2600
+ flushPiResumeOutputFilter(process.stdout, originalWrite, stdoutFilter);
2601
+ flushPiResumeOutputFilter(process.stderr, originalErrorWrite, stderrFilter);
2602
+ process.stdout.write = originalWrite;
2603
+ process.stderr.write = originalErrorWrite;
2604
+ }
2605
+ }
2606
+ /** @deprecated Use withLinxResumeOutputStyle. */
2607
+ export const withSuppressedPiResumeOutput = withLinxResumeOutputStyle;
2608
+ function createPiResumeOutputFilter() {
2609
+ return { pending: '', suppressing: false };
2610
+ }
2611
+ function writeWithPiResumeFilter(stream, originalWrite, filter, chunk, encodingOrCallback, callback) {
2612
+ const text = typeof chunk === 'string'
2613
+ ? chunk
2614
+ : Buffer.isBuffer(chunk) || chunk instanceof Uint8Array
2615
+ ? Buffer.from(chunk).toString('utf8')
2616
+ : '';
2617
+ if (!text) {
2618
+ return originalWrite.call(stream, chunk, encodingOrCallback, callback);
2619
+ }
2620
+ const output = filterPiResumeOutputText(text, filter);
2621
+ if (!output) {
2622
+ const done = typeof encodingOrCallback === 'function' ? encodingOrCallback : callback;
2623
+ done?.();
2624
+ return true;
2625
+ }
2626
+ if (output === text && !filter.pending) {
2627
+ return originalWrite.call(stream, chunk, encodingOrCallback, callback);
2628
+ }
2629
+ return originalWrite.call(stream, output, encodingOrCallback, callback);
2630
+ }
2631
+ function flushPiResumeOutputFilter(stream, originalWrite, filter) {
2632
+ const pending = filter.pending;
2633
+ filter.pending = '';
2634
+ if (filter.suppressing) {
2635
+ filter.suppressing = false;
2636
+ return;
2637
+ }
2638
+ if (!pending || isPotentialPiResumeOutput(pending)) {
2639
+ return;
2640
+ }
2641
+ originalWrite.call(stream, pending);
2642
+ }
2643
+ function filterPiResumeOutputText(text, filter) {
2644
+ let input = filter.pending + text;
2645
+ filter.pending = '';
2646
+ let output = '';
2647
+ while (input) {
2648
+ const newlineIndex = input.indexOf('\n');
2649
+ if (newlineIndex >= 0) {
2650
+ const line = input.slice(0, newlineIndex + 1);
2651
+ if (filter.suppressing) {
2652
+ filter.suppressing = false;
2653
+ }
2654
+ else if (!isPiResumeOutput(line)) {
2655
+ output += line;
2656
+ }
2657
+ input = input.slice(newlineIndex + 1);
2658
+ continue;
2659
+ }
2660
+ if (filter.suppressing) {
2661
+ return output;
2662
+ }
2663
+ if (isPiResumeOutput(input)) {
2664
+ filter.suppressing = true;
2665
+ return output;
2666
+ }
2667
+ if (isPotentialPiResumeOutput(input)) {
2668
+ filter.pending = input;
2669
+ return output;
2670
+ }
2671
+ output += input;
2672
+ return output;
2673
+ }
2674
+ return output;
2675
+ }
2676
+ function isPiResumeOutput(text) {
2677
+ if (!text) {
2678
+ return false;
2679
+ }
2680
+ const plain = stripAnsi(text);
2681
+ return /To resume this session:\s*pi\s+--session(?:-dir|\s)/u.test(plain)
2682
+ || /To resume this session:\s*pi\s+/u.test(plain);
2683
+ }
2684
+ function isPotentialPiResumeOutput(text) {
2685
+ const plain = stripAnsi(text).trimStart();
2686
+ if (!plain || plain.length >= 512) {
2687
+ return false;
2688
+ }
2689
+ const marker = 'To resume this session:';
2690
+ if (marker.startsWith(plain)) {
2691
+ return true;
2692
+ }
2693
+ if (!plain.startsWith(marker)) {
2694
+ return false;
2695
+ }
2696
+ const commandPrefix = plain.slice(marker.length).trimStart();
2697
+ return !commandPrefix
2698
+ || 'pi --session-dir'.startsWith(commandPrefix)
2699
+ || 'pi --session'.startsWith(commandPrefix);
2700
+ }
2701
+ function stripAnsi(text) {
2702
+ return text.replace(/\x1b\[[0-9;]*m/gu, '');
2703
+ }
130
2704
  function patchPiFooter() {
131
2705
  if (footerPatched) {
132
2706
  return;
@@ -143,6 +2717,36 @@ function patchPiFooter() {
143
2717
  };
144
2718
  footerPatched = true;
145
2719
  }
2720
+ export function patchPiAssistantMessageRendering() {
2721
+ if (assistantMessagePatched) {
2722
+ return;
2723
+ }
2724
+ const originalUpdateContent = AssistantMessageComponent.prototype.updateContent;
2725
+ AssistantMessageComponent.prototype.updateContent = function patchedUpdateContent(message) {
2726
+ const sanitizedMessage = stripLinxHiddenAssistantContent(message);
2727
+ return originalUpdateContent.call(this, sanitizedMessage);
2728
+ };
2729
+ assistantMessagePatched = true;
2730
+ }
2731
+ function stripLinxHiddenAssistantContent(message) {
2732
+ if (!isRecord(message) || !Array.isArray(message.content)) {
2733
+ return message;
2734
+ }
2735
+ const content = message.content.filter((part) => !isLinxHiddenAssistantContentPart(part));
2736
+ if (content.length === message.content.length) {
2737
+ return message;
2738
+ }
2739
+ return {
2740
+ ...message,
2741
+ content,
2742
+ };
2743
+ }
2744
+ function isLinxHiddenAssistantContentPart(part) {
2745
+ return isRecord(part) && part.type === 'thinking';
2746
+ }
2747
+ function isRecord(value) {
2748
+ return typeof value === 'object' && value !== null;
2749
+ }
146
2750
  function buildLinxFooterStatusLine(session, width, autoCompactEnabled) {
147
2751
  const usage = calculateSessionUsage(session);
148
2752
  const state = session?.state ?? {};