@agi-fans/dsh-tui 0.22.0 → 0.23.1

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 (282) hide show
  1. package/LICENSE +26 -0
  2. package/lib/CHANGELOG.md +219 -1
  3. package/lib/chrome/box.d.ts +1 -18
  4. package/lib/chrome/box.js +2 -45
  5. package/lib/chrome/box.js.map +1 -1
  6. package/lib/chrome/code-highlight.js +4 -4
  7. package/lib/chrome/code-highlight.js.map +1 -1
  8. package/lib/chrome/command-output.d.ts +0 -1
  9. package/lib/chrome/command-output.js +11 -5
  10. package/lib/chrome/command-output.js.map +1 -1
  11. package/lib/chrome/goal-bar.js +1 -1
  12. package/lib/chrome/goal-bar.js.map +1 -1
  13. package/lib/chrome/main-screen-renderer.js +4 -3
  14. package/lib/chrome/main-screen-renderer.js.map +1 -1
  15. package/lib/chrome/markdown.d.ts +3 -3
  16. package/lib/chrome/markdown.js +85 -36
  17. package/lib/chrome/markdown.js.map +1 -1
  18. package/lib/chrome/md-style.d.ts +5 -2
  19. package/lib/chrome/md-style.js +13 -4
  20. package/lib/chrome/md-style.js.map +1 -1
  21. package/lib/chrome/renderer.d.ts +37 -1
  22. package/lib/chrome/renderer.js.map +1 -1
  23. package/lib/chrome/status-line.d.ts +3 -3
  24. package/lib/chrome/status-line.js +5 -5
  25. package/lib/chrome/status-line.js.map +1 -1
  26. package/lib/chrome/theme.d.ts +8 -3
  27. package/lib/chrome/theme.js +51 -153
  28. package/lib/chrome/theme.js.map +1 -1
  29. package/lib/commands/auth-account.d.ts +9 -0
  30. package/lib/commands/auth-account.js +202 -0
  31. package/lib/commands/auth-account.js.map +1 -0
  32. package/lib/commands/auth.js +238 -414
  33. package/lib/commands/auth.js.map +1 -1
  34. package/lib/commands/diff.js +22 -16
  35. package/lib/commands/diff.js.map +1 -1
  36. package/lib/commands/export.d.ts +0 -1
  37. package/lib/commands/export.js +99 -32
  38. package/lib/commands/export.js.map +1 -1
  39. package/lib/commands/files.js +11 -9
  40. package/lib/commands/files.js.map +1 -1
  41. package/lib/commands/integrations.d.ts +12 -1
  42. package/lib/commands/integrations.js +120 -23
  43. package/lib/commands/integrations.js.map +1 -1
  44. package/lib/commands/jobs-browser.d.ts +3 -0
  45. package/lib/commands/jobs-browser.js +152 -0
  46. package/lib/commands/jobs-browser.js.map +1 -0
  47. package/lib/commands/jobs.d.ts +3 -14
  48. package/lib/commands/jobs.js +26 -18
  49. package/lib/commands/jobs.js.map +1 -1
  50. package/lib/commands/model.d.ts +3 -0
  51. package/lib/commands/model.js +212 -128
  52. package/lib/commands/model.js.map +1 -1
  53. package/lib/commands/open-external.d.ts +2 -0
  54. package/lib/commands/open-external.js +19 -0
  55. package/lib/commands/open-external.js.map +1 -0
  56. package/lib/commands/permission.js +65 -54
  57. package/lib/commands/permission.js.map +1 -1
  58. package/lib/commands/plugins.js +381 -139
  59. package/lib/commands/plugins.js.map +1 -1
  60. package/lib/commands/schedule-catalog.d.ts +12 -0
  61. package/lib/commands/schedule-catalog.js +41 -0
  62. package/lib/commands/schedule-catalog.js.map +1 -0
  63. package/lib/commands/schedule-history.d.ts +4 -0
  64. package/lib/commands/schedule-history.js +62 -0
  65. package/lib/commands/schedule-history.js.map +1 -0
  66. package/lib/commands/schedule.d.ts +12 -15
  67. package/lib/commands/schedule.js +220 -187
  68. package/lib/commands/schedule.js.map +1 -1
  69. package/lib/commands/session-configuration.d.ts +3 -0
  70. package/lib/commands/session-configuration.js +62 -28
  71. package/lib/commands/session-configuration.js.map +1 -1
  72. package/lib/commands/session.js +116 -66
  73. package/lib/commands/session.js.map +1 -1
  74. package/lib/commands/terminal.js +14 -8
  75. package/lib/commands/terminal.js.map +1 -1
  76. package/lib/definition.d.ts +115 -15
  77. package/lib/index.d.ts +1 -0
  78. package/lib/index.js +1 -0
  79. package/lib/index.js.map +1 -1
  80. package/lib/input/actions.d.ts +1 -1
  81. package/lib/input/actions.js +5 -2
  82. package/lib/input/actions.js.map +1 -1
  83. package/lib/input/composer-files.d.ts +2 -0
  84. package/lib/input/composer-files.js +13 -3
  85. package/lib/input/composer-files.js.map +1 -1
  86. package/lib/input/image-paste.d.ts +5 -12
  87. package/lib/input/image-paste.js +32 -28
  88. package/lib/input/image-paste.js.map +1 -1
  89. package/lib/input/key-test.d.ts +5 -0
  90. package/lib/input/key-test.js +19 -0
  91. package/lib/input/key-test.js.map +1 -0
  92. package/lib/input/keys.d.ts +2 -0
  93. package/lib/input/keys.js +4 -1
  94. package/lib/input/keys.js.map +1 -1
  95. package/lib/locale/index.d.ts +6 -0
  96. package/lib/locale/index.js +32 -0
  97. package/lib/locale/index.js.map +1 -0
  98. package/lib/locale/prompt.d.ts +4 -0
  99. package/lib/locale/prompt.js +24 -0
  100. package/lib/locale/prompt.js.map +1 -0
  101. package/lib/locale/zh.d.ts +1114 -0
  102. package/lib/locale/zh.js +1115 -0
  103. package/lib/locale/zh.js.map +1 -0
  104. package/lib/prompt-intents.d.ts +4 -0
  105. package/lib/prompt-intents.js +4 -0
  106. package/lib/prompt-intents.js.map +1 -1
  107. package/lib/runtime/account-settings.d.ts +18 -0
  108. package/lib/runtime/account-settings.js +241 -0
  109. package/lib/runtime/account-settings.js.map +1 -0
  110. package/lib/runtime/agent-profile.d.ts +3 -3
  111. package/lib/runtime/agent-profile.js +4 -4
  112. package/lib/runtime/agent-profile.js.map +1 -1
  113. package/lib/runtime/btw-policy.js +2 -1
  114. package/lib/runtime/btw-policy.js.map +1 -1
  115. package/lib/runtime/btw.js +1 -2
  116. package/lib/runtime/btw.js.map +1 -1
  117. package/lib/runtime/composer-images.d.ts +2 -2
  118. package/lib/runtime/composer-images.js +4 -4
  119. package/lib/runtime/composer-images.js.map +1 -1
  120. package/lib/runtime/composer-shell.js +27 -1
  121. package/lib/runtime/composer-shell.js.map +1 -1
  122. package/lib/runtime/file-browser.d.ts +19 -0
  123. package/lib/runtime/file-browser.js +291 -0
  124. package/lib/runtime/file-browser.js.map +1 -0
  125. package/lib/runtime/file-review.d.ts +6 -9
  126. package/lib/runtime/file-review.js +91 -7
  127. package/lib/runtime/file-review.js.map +1 -1
  128. package/lib/runtime/help-reader.d.ts +12 -0
  129. package/lib/runtime/help-reader.js +50 -0
  130. package/lib/runtime/help-reader.js.map +1 -0
  131. package/lib/runtime/human-interaction.js +1 -1
  132. package/lib/runtime/human-interaction.js.map +1 -1
  133. package/lib/runtime/model-catalog-editor.d.ts +12 -0
  134. package/lib/runtime/model-catalog-editor.js +215 -0
  135. package/lib/runtime/model-catalog-editor.js.map +1 -0
  136. package/lib/runtime/model-settings.d.ts +11 -0
  137. package/lib/runtime/model-settings.js +404 -0
  138. package/lib/runtime/model-settings.js.map +1 -0
  139. package/lib/runtime/plugin-settings.d.ts +4 -1
  140. package/lib/runtime/plugin-settings.js +164 -88
  141. package/lib/runtime/plugin-settings.js.map +1 -1
  142. package/lib/runtime/prompt-browser.d.ts +9 -0
  143. package/lib/runtime/prompt-browser.js +19 -0
  144. package/lib/runtime/prompt-browser.js.map +1 -0
  145. package/lib/runtime/provider-local.d.ts +27 -27
  146. package/lib/runtime/provider-local.js +1288 -308
  147. package/lib/runtime/provider-local.js.map +1 -1
  148. package/lib/runtime/reference-reader.d.ts +15 -0
  149. package/lib/runtime/reference-reader.js +78 -0
  150. package/lib/runtime/reference-reader.js.map +1 -0
  151. package/lib/runtime/runner.js +38 -29
  152. package/lib/runtime/runner.js.map +1 -1
  153. package/lib/runtime/session-runtime.js +1 -1
  154. package/lib/runtime/session-runtime.js.map +1 -1
  155. package/lib/runtime/settings-draft.d.ts +21 -0
  156. package/lib/runtime/settings-draft.js +166 -0
  157. package/lib/runtime/settings-draft.js.map +1 -0
  158. package/lib/runtime/settings-preferences.js +1 -1
  159. package/lib/runtime/settings-preferences.js.map +1 -1
  160. package/lib/runtime/startup-notices.d.ts +0 -1
  161. package/lib/runtime/startup-notices.js +35 -6
  162. package/lib/runtime/startup-notices.js.map +1 -1
  163. package/lib/runtime/subagent-model-selection.js +2 -1
  164. package/lib/runtime/subagent-model-selection.js.map +1 -1
  165. package/lib/runtime/subagent-settings.d.ts +14 -0
  166. package/lib/runtime/subagent-settings.js +288 -0
  167. package/lib/runtime/subagent-settings.js.map +1 -0
  168. package/lib/runtime/terminal-notifications.d.ts +3 -0
  169. package/lib/runtime/terminal-notifications.js +13 -8
  170. package/lib/runtime/terminal-notifications.js.map +1 -1
  171. package/lib/runtime/terminal-probe.d.ts +2 -0
  172. package/lib/runtime/terminal-probe.js +9 -0
  173. package/lib/runtime/terminal-probe.js.map +1 -1
  174. package/lib/runtime/tool-presentation.js.map +1 -1
  175. package/lib/runtime/tool-reader.d.ts +5 -0
  176. package/lib/runtime/tool-reader.js +27 -0
  177. package/lib/runtime/tool-reader.js.map +1 -0
  178. package/lib/runtime/web-settings.d.ts +14 -0
  179. package/lib/runtime/web-settings.js +339 -0
  180. package/lib/runtime/web-settings.js.map +1 -0
  181. package/lib/session/command-attachments.d.ts +17 -0
  182. package/lib/session/command-attachments.js +50 -0
  183. package/lib/session/command-attachments.js.map +1 -0
  184. package/lib/session/context-diagnostics.d.ts +16 -0
  185. package/lib/session/context-diagnostics.js +15 -0
  186. package/lib/session/context-diagnostics.js.map +1 -1
  187. package/lib/session/external-agents.d.ts +3 -1
  188. package/lib/session/external-agents.js +35 -10
  189. package/lib/session/external-agents.js.map +1 -1
  190. package/lib/session/generation-continuation.d.ts +11 -0
  191. package/lib/session/generation-continuation.js +34 -0
  192. package/lib/session/generation-continuation.js.map +1 -0
  193. package/lib/session/goal-controls.d.ts +5 -0
  194. package/lib/session/goal-controls.js +81 -0
  195. package/lib/session/goal-controls.js.map +1 -0
  196. package/lib/session/interaction-adapter.d.ts +7 -3
  197. package/lib/session/interaction-adapter.js +121 -49
  198. package/lib/session/interaction-adapter.js.map +1 -1
  199. package/lib/session/message-queue.d.ts +2 -0
  200. package/lib/session/message-queue.js +102 -17
  201. package/lib/session/message-queue.js.map +1 -1
  202. package/lib/session/pending-inputs.d.ts +30 -0
  203. package/lib/session/pending-inputs.js +183 -0
  204. package/lib/session/pending-inputs.js.map +1 -0
  205. package/lib/session/session-configuration.js +1 -1
  206. package/lib/session/session-configuration.js.map +1 -1
  207. package/lib/session/session-controller.d.ts +33 -20
  208. package/lib/session/session-controller.js +500 -129
  209. package/lib/session/session-controller.js.map +1 -1
  210. package/lib/session/session-library.d.ts +3 -1
  211. package/lib/session/session-library.js +15 -0
  212. package/lib/session/session-library.js.map +1 -1
  213. package/lib/session/subagent-roster.d.ts +1 -0
  214. package/lib/session/subagent-roster.js +16 -5
  215. package/lib/session/subagent-roster.js.map +1 -1
  216. package/lib/session/subagent-tracker.d.ts +2 -0
  217. package/lib/session/subagent-tracker.js +18 -0
  218. package/lib/session/subagent-tracker.js.map +1 -1
  219. package/lib/session/tui-settings.d.ts +9 -5
  220. package/lib/session/tui-settings.js +3 -1
  221. package/lib/session/tui-settings.js.map +1 -1
  222. package/lib/session/workflow-projection.d.ts +2 -1
  223. package/lib/session/workflow-projection.js +34 -4
  224. package/lib/session/workflow-projection.js.map +1 -1
  225. package/lib/views/autocomplete.d.ts +12 -12
  226. package/lib/views/autocomplete.js +54 -45
  227. package/lib/views/autocomplete.js.map +1 -1
  228. package/lib/views/context-overview.d.ts +4 -0
  229. package/lib/views/context-overview.js +43 -0
  230. package/lib/views/context-overview.js.map +1 -0
  231. package/lib/views/copy-targets.d.ts +6 -2
  232. package/lib/views/copy-targets.js +19 -14
  233. package/lib/views/copy-targets.js.map +1 -1
  234. package/lib/views/document-reader.d.ts +10 -1
  235. package/lib/views/document-reader.js +63 -6
  236. package/lib/views/document-reader.js.map +1 -1
  237. package/lib/views/event-views.js +29 -11
  238. package/lib/views/event-views.js.map +1 -1
  239. package/lib/views/file-actions.js +21 -0
  240. package/lib/views/file-actions.js.map +1 -1
  241. package/lib/views/file-browser.d.ts +98 -0
  242. package/lib/views/file-browser.js +120 -0
  243. package/lib/views/file-browser.js.map +1 -0
  244. package/lib/views/history-search.js +4 -4
  245. package/lib/views/history-search.js.map +1 -1
  246. package/lib/views/hotkey-format.d.ts +3 -2
  247. package/lib/views/hotkey-format.js +4 -3
  248. package/lib/views/hotkey-format.js.map +1 -1
  249. package/lib/views/hotkeys.js +10 -12
  250. package/lib/views/hotkeys.js.map +1 -1
  251. package/lib/views/list-view.js +16 -11
  252. package/lib/views/list-view.js.map +1 -1
  253. package/lib/views/prompt-selector.d.ts +23 -1
  254. package/lib/views/prompt-selector.js +123 -60
  255. package/lib/views/prompt-selector.js.map +1 -1
  256. package/lib/views/settings-list.d.ts +6 -1
  257. package/lib/views/settings-list.js +69 -46
  258. package/lib/views/settings-list.js.map +1 -1
  259. package/lib/views/tool-actions.d.ts +16 -0
  260. package/lib/views/tool-actions.js +29 -0
  261. package/lib/views/tool-actions.js.map +1 -0
  262. package/lib/views/trajectory-timeline.d.ts +5 -6
  263. package/lib/views/trajectory-timeline.js +25 -15
  264. package/lib/views/trajectory-timeline.js.map +1 -1
  265. package/lib/views/trajectory.d.ts +23 -2
  266. package/lib/views/trajectory.js +404 -186
  267. package/lib/views/trajectory.js.map +1 -1
  268. package/lib/views/transcript-render.d.ts +39 -15
  269. package/lib/views/transcript-render.js +406 -208
  270. package/lib/views/transcript-render.js.map +1 -1
  271. package/lib/views/transcript-types.d.ts +3 -2
  272. package/lib/views/transcript-types.js.map +1 -1
  273. package/lib/views/workflow.d.ts +16 -0
  274. package/lib/views/workflow.js +42 -0
  275. package/lib/views/workflow.js.map +1 -0
  276. package/package.json +61 -58
  277. package/lib/chrome/welcome-tips.d.ts +0 -23
  278. package/lib/chrome/welcome-tips.js +0 -24
  279. package/lib/chrome/welcome-tips.js.map +0 -1
  280. package/lib/session/steering.d.ts +0 -3
  281. package/lib/session/steering.js +0 -9
  282. package/lib/session/steering.js.map +0 -1
@@ -1,9 +1,13 @@
1
1
  /** Native profile management over the published Harness transaction service. */
2
2
  import { randomUUID } from 'node:crypto';
3
+ import { startCreator } from "./session-configuration.js";
3
4
  import { registerCommands } from "./registration.js";
4
5
  import { askPrompt, confirmPrompt } from "../prompt-intents.js";
5
- import { editSettingsEntry, nativeIntegrationId, showIntegrationStatus } from "../runtime/plugin-settings.js";
6
- import { externalAgentGuide } from "../session/external-agents.js";
6
+ import { editSettingsEntry, nativeIntegrationId, pluginSettingsEntries, showIntegrationStatus } from "../runtime/plugin-settings.js";
7
+ import { externalAgentGuide, externalAgentSummary } from "../session/external-agents.js";
8
+ import { readReference } from "../runtime/reference-reader.js";
9
+ import { createPromptBrowser } from "../runtime/prompt-browser.js";
10
+ import { resolveUiLanguage, uiText } from "../locale/index.js";
7
11
  export const name = 'omdsh-command-plugins';
8
12
  export const inject = ['commands', 'pluginManager', 'tui'];
9
13
  // Removing the command's own presentation or session owner during its awaited
@@ -19,19 +23,53 @@ export function pluginReadOnly(row) {
19
23
  return 'terminal-required';
20
24
  return row.readOnlyReason;
21
25
  }
22
- export function changeText(result) {
26
+ function changeSummary(result) {
23
27
  const selection = result.application === 'cancelled' || result.application === 'failed' || result.enabled === undefined
24
28
  ? '' : result.enabled ? ' · enabled' : ' · disabled';
25
29
  const restart = result.application === 'restart-required' && result.restartReason !== undefined
26
30
  ? ` · restart required (${result.restartReason})` : '';
31
+ return `${result.target}: ${result.application}${selection}${restart}`;
32
+ }
33
+ export function changeText(result) {
27
34
  return [
28
- `${result.target}: ${result.application}${selection}${restart}`,
35
+ changeSummary(result),
29
36
  ...(result.error === undefined ? [] : [result.error.diagnostic ?? result.error.code]),
30
37
  ...(result.error?.incompatible ?? []).map(item => `${item.name}@${item.version}: DSH ${item.runtimeVersion} rejects ${JSON.stringify(item.requirements)}`),
31
38
  ...(result.warnings ?? []),
32
39
  ...(result.packageResult === undefined ? [] : [result.packageResult.output, `Full installation log: ${result.packageResult.logPath}`]),
33
40
  ].filter(Boolean).join('\n');
34
41
  }
42
+ /** Keep the observed outcome beside the operation, with full logs on demand. */
43
+ function changeFeedback(result) {
44
+ if (result === undefined)
45
+ return {};
46
+ return {
47
+ detail: changeSummary(result),
48
+ actions: [{ key: 'Alt+D', label: 'details', valuePrefix: 'result:', scope: 'list' }],
49
+ };
50
+ }
51
+ async function readChangeDetails(ctx, result, signal) {
52
+ if (result === undefined || signal.aborted)
53
+ return;
54
+ const text = changeText(result);
55
+ if (ctx.tui.interactive === false) {
56
+ ctx.tui.commandOutput('/plugins', text);
57
+ return;
58
+ }
59
+ let position;
60
+ while (!signal.aborted) {
61
+ const action = await ctx.tui.prompt({ localize: ['options'], title: result.target, question: '',
62
+ presentation: 'document', documentSource: { text }, allowCustom: false, notify: false, signal,
63
+ ...(position === undefined ? {} : { documentPosition: position }),
64
+ onDocumentPosition: value => { position = value; },
65
+ options: [{ label: 'Back', value: 'back' },
66
+ ...(ctx.tui.copyText === undefined ? [] : [{ label: 'Copy details', value: 'copy' }])],
67
+ });
68
+ if (signal.aborted || action !== 'copy')
69
+ return;
70
+ await ctx.tui.copyText?.(text, result.target);
71
+ }
72
+ }
35
73
  function bundleStatus(row) {
36
74
  if (row.availability === 'missing')
37
75
  return row.installed ? 'Package unavailable' : 'Not installed';
@@ -56,7 +94,7 @@ async function install(ctx, spec, signal, approvedBuilds, saveExact = false) {
56
94
  const operation = ctx.pluginManager.installBundle(spec, { requestId, saveExact, ...(approvedBuilds === undefined ? {} : { approvedBuilds }) });
57
95
  // The installer owns cancellation through restoration, even after the
58
96
  // prompt closes. Never abandon its promise or report cancellation early.
59
- const prompt = ctx.tui.prompt({
97
+ const prompt = ctx.tui.prompt({ localize: true,
60
98
  title: 'Install plugin', question: spec, presentation: 'progress',
61
99
  detail, refreshDocument: () => detail, notify: false,
62
100
  allowCustom: false, signal: AbortSignal.any([signal, promptAbort.signal]),
@@ -76,10 +114,10 @@ async function install(ctx, spec, signal, approvedBuilds, saveExact = false) {
76
114
  async function confirmInstall(ctx, spec, signal, saveExact = false) {
77
115
  const inspected = await ctx.pluginManager.inspect(spec, undefined, signal);
78
116
  if (inspected.status === 'refused') {
79
- ctx.tui.notice(inspected.reason, { level: 'error' });
117
+ await readReference(ctx.tui, spec, inspected.reason, signal);
80
118
  return;
81
119
  }
82
- const consent = await confirmPrompt(request => ctx.tui.prompt(request), {
120
+ const consent = await confirmPrompt(request => ctx.tui.prompt(request), { localize: ['title', 'options', 'actions'],
83
121
  title: 'Install bundle',
84
122
  question: inspected.name ?? spec,
85
123
  detail: `${inspected.version ?? ''} ${inspected.description ?? ''}\nInstalled Host code runs with your user permissions and affects all sessions in this profile.`,
@@ -92,11 +130,10 @@ async function confirmInstall(ctx, spec, signal, saveExact = false) {
92
130
  return;
93
131
  let result = await install(ctx, spec, signal, undefined, saveExact);
94
132
  if (result.pendingBuilds?.length && !signal.aborted) {
95
- ctx.tui.commandOutput('/plugins', changeText(result));
96
- const approved = await confirmPrompt(request => ctx.tui.prompt(request), {
133
+ const approved = await confirmPrompt(request => ctx.tui.prompt(request), { localize: ['title', 'options', 'actions'],
97
134
  title: 'Dependency scripts',
98
135
  question: 'Allow these scripts and retry?',
99
- detail: `${result.pendingBuilds.join('\n')}\nPermission persists by package name in this profile. Scripts run with your user permissions.`,
136
+ detail: `${changeSummary(result)}\n\n${result.pendingBuilds.join('\n')}\nPermission persists by package name in this profile. Scripts run with your user permissions.`,
100
137
  acceptLabel: 'Allow scripts and retry',
101
138
  cancelLabel: null,
102
139
  signal,
@@ -106,173 +143,291 @@ async function confirmInstall(ctx, spec, signal, saveExact = false) {
106
143
  }
107
144
  return result;
108
145
  }
109
- /**
110
- * Browse the plugin indexes the profile configures and install an entry.
111
- *
112
- * The Host service owns fetching, validating and annotating an index; this only
113
- * presents it, which is the split the Harness client makes too. With no source
114
- * configured the answer is that sentence rather than an empty list, because the
115
- * next step is a setting rather than a choice.
116
- */
117
146
  /** Index text in the reader's language when the index offers one. */
118
147
  function marketText(text) {
119
148
  return typeof text === 'string' ? text : text.en;
120
149
  }
121
- async function discover(ctx, signal) {
150
+ async function discover(ctx, signal, onSuperseded) {
151
+ const prompt = (request) => ctx.tui.prompt({ dismissLabel: 'Back', ...request, signal: request.signal ?? signal, onSuperseded });
122
152
  const market = ctx.get('pluginMarket');
153
+ const t = (text) => uiText(resolveUiLanguage(ctx.tui.prefs?.().uiLanguage), text);
123
154
  if (market === undefined) {
124
- ctx.tui.notice('The plugin market is not mounted in this profile.', { level: 'error' });
155
+ await readReference(ctx.tui, t('Discover plugins'), t('The plugin market is not mounted in this profile.'), signal);
125
156
  return;
126
157
  }
127
- while (!signal.aborted) {
128
- // `listIndexes` is the public read: the configured sources without fetching them.
158
+ const searchPrompt = (query) => prompt(askPrompt({ localize: ['title', 'question', 'actions'],
159
+ title: 'Discover plugins', question: 'Search the plugin indexes (empty lists everything)', initialInput: query, signal }));
160
+ const initialQuery = await searchPrompt('');
161
+ if (initialQuery === null || signal.aborted)
162
+ return;
163
+ let query = initialQuery;
164
+ let entries = [];
165
+ const pages = new Map();
166
+ const failures = new Map();
167
+ let diagnostic = '', result;
168
+ const load = async (mode = 'first') => {
129
169
  const sources = market.listIndexes();
130
170
  const usable = sources.filter(source => source.error === undefined);
131
- if (usable.length === 0) {
132
- ctx.tui.commandOutput('/plugins discover', sources.length === 0
133
- ? 'No plugin index sources are configured. Add one in Settings to browse a market.'
134
- : 'No usable plugin index source:\n\n' + sources.map(source => `${source.url} · ${source.error}`).join('\n'));
135
- return;
136
- }
137
- const text = await ctx.tui.prompt(askPrompt({ title: 'Discover plugins', question: 'Search the plugin indexes (empty lists everything)', signal }));
138
- if (text === null || signal.aborted)
139
- return;
171
+ for (const url of pages.keys())
172
+ if (!usable.some(source => source.url === url))
173
+ pages.delete(url);
174
+ for (const url of failures.keys())
175
+ if (!usable.some(source => source.url === url))
176
+ failures.delete(url);
177
+ const requests = usable.flatMap(source => {
178
+ const previous = pages.get(source.url);
179
+ const failure = failures.get(source.url);
180
+ const offset = mode === 'more' ? previous?.nextOffset
181
+ : mode === 'retry' ? failure?.offset : 0;
182
+ return offset == null ? [] : [{ url: source.url, offset, previous }];
183
+ });
184
+ // The published search API has no per-request abort. Dismissal abandons
185
+ // this read, and only the winning result may update the browser state.
186
+ const dismissed = new AbortController();
187
+ const readSignal = AbortSignal.any([signal, dismissed.signal]);
188
+ const progressEnd = new AbortController();
189
+ let progress;
190
+ let stop;
191
+ const stopped = new Promise(resolve => { stop = () => resolve(null); });
192
+ readSignal.addEventListener('abort', stop, { once: true });
193
+ if (readSignal.aborted)
194
+ stop();
195
+ const timer = setTimeout(() => {
196
+ if (readSignal.aborted)
197
+ return;
198
+ progress = prompt({ localize: true, title: 'Discover plugins', question: 'Loading plugins…',
199
+ detail: t('Esc returns to the results.'), presentation: 'progress', allowCustom: false, notify: false,
200
+ signal: AbortSignal.any([readSignal, progressEnd.signal]),
201
+ }).then(() => { if (!progressEnd.signal.aborted)
202
+ dismissed.abort(); }, () => dismissed.abort());
203
+ }, 150);
140
204
  try {
141
- // Search every source, so the reader does not have to know which index
142
- // happens to carry what they are looking for.
143
- const pages = await Promise.all(usable.map(source => market.search(source.url, {
144
- ...(text.trim() === '' ? {} : { text: text.trim() }),
145
- sort: { kind: 'relevance' }, offset: 0, limit: 20,
146
- })));
147
- const entries = pages.flatMap(page => page.entries.map(entry => ({ page, entry })));
148
- if (entries.length === 0) {
149
- ctx.tui.commandOutput('/plugins discover', `No plugin matches "${text.trim()}".`);
205
+ const operation = Promise.allSettled(requests.map(async ({ url, offset, previous }) => {
206
+ const queryOptions = { ...(query.trim() === '' ? {} : { text: query.trim() }), sort: { kind: 'relevance' }, limit: 20 };
207
+ let page = await market.search(url, { ...queryOptions, offset,
208
+ ...(mode === 'refresh' || (mode === 'retry' && offset === 0) ? { refresh: true } : {}),
209
+ ...(offset > 0 && previous !== undefined ? { fetchedAt: previous.fetchedAt } : {}),
210
+ });
211
+ if (readSignal.aborted)
212
+ return page;
213
+ if (offset > 0 && previous !== undefined) {
214
+ if (page.fetchedAt === previous.fetchedAt)
215
+ page = { ...page, entries: [...previous.entries, ...page.entries] };
216
+ else
217
+ page = await market.search(url, { ...queryOptions, offset: 0, fetchedAt: page.fetchedAt });
218
+ }
219
+ return page;
220
+ }));
221
+ const settled = await Promise.race([operation, stopped]);
222
+ if (signal.aborted)
223
+ return;
224
+ if (settled === null || readSignal.aborted) {
225
+ diagnostic = t('Loading cancelled. Refresh to try again.');
150
226
  return;
151
227
  }
152
- const chosen = await ctx.tui.prompt({ ...list, title: 'Discover · results', question: `${entries.length} matching plugins`,
153
- options: entries.map(({ page, entry }, at) => {
154
- // The version that install is offered for is the one the index defaults
155
- // to; its compatibility is what decides whether the offer is honest.
156
- const version = entry.versions.find(item => item.version === entry.defaultVersion) ?? entry.versions[0];
157
- return {
158
- label: entry.name,
159
- value: String(at),
160
- description: [
161
- entry.defaultVersion ?? '',
162
- marketText(page.title),
163
- entry.installation.status === 'installed' ? 'installed' : '',
164
- version?.compatibility.compatible === false ? 'incompatible' : '',
165
- ].filter(part => part !== '').join(' · '),
166
- ...(entry.description === undefined ? {} : { preview: marketText(entry.description) }),
167
- };
168
- }), signal });
169
- const picked = chosen === null ? undefined : entries[Number(chosen)];
170
- if (picked === undefined)
171
- continue;
172
- const version = picked.entry.versions.find(item => item.version === picked.entry.defaultVersion) ?? picked.entry.versions[0];
173
- if (version === undefined) {
174
- ctx.tui.notice(`${picked.entry.name} lists no version.`, { level: 'error' });
228
+ settled.forEach((response, index) => {
229
+ const request = requests[index];
230
+ if (response.status === 'fulfilled') {
231
+ pages.set(request.url, response.value);
232
+ failures.delete(request.url);
233
+ }
234
+ else
235
+ failures.set(request.url, { offset: request.offset,
236
+ message: response.reason instanceof Error ? response.reason.message : String(response.reason) });
237
+ });
238
+ entries = [...pages.values()].flatMap(page => page.entries.map(entry => ({ page, entry })));
239
+ diagnostic = sources.length === 0 ? t('No plugin index sources are configured. Add one in Settings to browse a market.')
240
+ : [...sources.filter(source => source.error !== undefined).map(source => `${source.url} · ${source.error}`),
241
+ ...[...failures].map(([url, failure]) => `${url} · ${failure.message}`)].join('\n');
242
+ }
243
+ finally {
244
+ clearTimeout(timer);
245
+ progressEnd.abort();
246
+ await progress;
247
+ readSignal.removeEventListener('abort', stop);
248
+ }
249
+ };
250
+ const browse = createPromptBrowser(prompt);
251
+ await load();
252
+ while (!signal.aborted) {
253
+ const feedback = changeFeedback(result);
254
+ const total = [...pages.values()].reduce((sum, page) => sum + page.total, 0);
255
+ const count = pages.size === 0 ? '' : t('Showing {loaded} of {total} plugins').replace('{loaded}', String(entries.length)).replace('{total}', String(total));
256
+ const chosen = await browse({ localize: ['title', 'actions'], ...list,
257
+ title: 'Discover plugins', question: query,
258
+ detail: [count, diagnostic, feedback.detail].filter(Boolean).join('\n'), emptyText: t('No matching plugins.'),
259
+ options: entries.map(({ page, entry }) => {
260
+ const version = entry.versions.find(item => item.version === entry.defaultVersion) ?? entry.versions[0];
261
+ return {
262
+ label: entry.name, value: JSON.stringify([page.url, entry.name]),
263
+ description: [entry.defaultVersion ?? '', marketText(page.title),
264
+ entry.installation.status === 'installed' ? t('Installed') : '',
265
+ version?.compatibility.compatible === false ? t('Incompatible') : '',
266
+ ].filter(Boolean).join(' · '),
267
+ ...(entry.description === undefined ? {} : { preview: marketText(entry.description) }),
268
+ };
269
+ }),
270
+ actions: [
271
+ { key: 'Alt+S', label: 'search', valuePrefix: 'search:', scope: 'list' },
272
+ { key: 'Alt+R', label: 'refresh', valuePrefix: 'refresh:', scope: 'list' },
273
+ ...([...pages.values()].some(page => page.nextOffset !== null) ? [{ key: 'Alt+N', label: 'load more', valuePrefix: 'more:', scope: 'list' }] : []),
274
+ ...(failures.size > 0 ? [{ key: 'Alt+T', label: 'retry', valuePrefix: 'retry:', scope: 'list' }] : []),
275
+ ...(feedback.actions ?? []),
276
+ ], signal, }, query);
277
+ if (chosen === null || signal.aborted)
278
+ return;
279
+ if (chosen === 'result:') {
280
+ await readChangeDetails(ctx, result, signal);
281
+ continue;
282
+ }
283
+ if (chosen === 'search:') {
284
+ const next = await searchPrompt(query);
285
+ if (next === null || signal.aborted)
175
286
  continue;
287
+ if (next !== query) {
288
+ entries = [];
289
+ pages.clear();
290
+ failures.clear();
291
+ result = undefined;
176
292
  }
177
- if (version.compatibility.compatible === false) {
178
- ctx.tui.notice(`${picked.entry.name} ${version.version} requires ${Object.entries(version.compatibility.requirements).map(([key, value]) => `${key} ${value}`).join(', ')}.`, { level: 'error' });
179
- continue;
293
+ query = next;
294
+ await load();
295
+ continue;
296
+ }
297
+ if (chosen === 'refresh:') {
298
+ await load('refresh');
299
+ continue;
300
+ }
301
+ if (chosen === 'more:') {
302
+ await load('more');
303
+ continue;
304
+ }
305
+ if (chosen === 'retry:') {
306
+ await load('retry');
307
+ continue;
308
+ }
309
+ const picked = entries.find(({ page, entry }) => JSON.stringify([page.url, entry.name]) === chosen);
310
+ if (picked === undefined)
311
+ continue;
312
+ const version = picked.entry.versions.find(item => item.version === picked.entry.defaultVersion) ?? picked.entry.versions[0];
313
+ if (version === undefined) {
314
+ diagnostic = `${picked.entry.name} lists no version.`;
315
+ continue;
316
+ }
317
+ if (version.compatibility.compatible === false) {
318
+ diagnostic = `${picked.entry.name} ${version.version} requires ${Object.entries(version.compatibility.requirements).map(([key, value]) => `${key} ${value}`).join(', ')}.`;
319
+ continue;
320
+ }
321
+ try {
322
+ const change = await confirmInstall(ctx, version.install.spec, signal, true);
323
+ if (change !== undefined) {
324
+ result = change;
325
+ await load();
180
326
  }
181
- const result = await confirmInstall(ctx, version.install.spec, signal, true);
182
- if (result !== undefined)
183
- ctx.tui.commandOutput('/plugins', changeText(result));
184
- return;
185
327
  }
186
328
  catch (error) {
187
- if (signal.aborted)
188
- return;
189
- ctx.tui.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
329
+ diagnostic = error instanceof Error ? error.message : String(error);
330
+ }
331
+ }
332
+ }
333
+ async function inspectPluginEntry(ctx, row, signal) {
334
+ const language = resolveUiLanguage(ctx.tui.prefs?.().uiLanguage);
335
+ const t = (text) => uiText(language, text);
336
+ const description = row.meta?.description;
337
+ const introduction = typeof description === 'string' ? description : description?.[language] ?? description?.en;
338
+ const reason = pluginReadOnly(row);
339
+ const text = [
340
+ `${t('Module')} ${row.moduleName}`,
341
+ `${t('Plugin entry')} ${row.patchId ?? row.entryId}`,
342
+ `${t('Status')} ${t(row.enabled ? 'Enabled' : 'Disabled')}`,
343
+ `${t('Load state')} ${row.fiberPhase ?? t('Not mounted')}`,
344
+ ...(introduction === undefined ? [] : ['', introduction]),
345
+ ...(reason === undefined ? [] : ['', `${t('Read-only')} ${reason === 'terminal-required' ? t('Required by the terminal.') : reason}`]),
346
+ ...(row.diagnostic === undefined ? [] : ['', t('Diagnostic'), row.diagnostic]),
347
+ ...(row.meta?.error === undefined ? [] : ['', t('Diagnostic'), row.meta.error]),
348
+ ].join('\n');
349
+ let position;
350
+ while (!signal.aborted) {
351
+ const action = await ctx.tui.prompt({ localize: ['options'], title: row.moduleName, question: '',
352
+ presentation: 'document', documentSource: { text }, allowCustom: false, notify: false, signal,
353
+ ...(position === undefined ? {} : { documentPosition: position }),
354
+ onDocumentPosition: value => { position = value; },
355
+ options: [{ label: 'Back', value: 'back' },
356
+ ...(ctx.tui.copyText === undefined ? [] : [{ label: 'Copy details', value: 'copy' }]),
357
+ ...(reason === undefined ? [{ label: row.enabled ? 'Disable' : 'Enable', value: 'toggle' }] : [])] });
358
+ if (signal.aborted || action === null || action === 'back')
190
359
  return;
360
+ if (action === 'copy')
361
+ await ctx.tui.copyText?.(text, t('Plugin entry'));
362
+ if (action === 'toggle' && reason === undefined) {
363
+ const confirmed = await confirmPrompt(request => ctx.tui.prompt(request), { localize: ['title', 'options'],
364
+ title: 'Plugin entry', question: row.moduleName, acceptLabel: row.enabled ? 'Disable' : 'Enable', cancelLabel: null, signal });
365
+ if (confirmed && !signal.aborted) {
366
+ const current = (await ctx.pluginManager.listPlugins()).find(item => item.entryId === row.entryId);
367
+ if (signal.aborted || current === undefined || current.enabled !== row.enabled || pluginReadOnly(current) !== undefined)
368
+ return;
369
+ return ctx.pluginManager.setPluginEnabled(row.entryId, !row.enabled);
370
+ }
191
371
  }
192
372
  }
193
373
  }
194
- async function manage(ctx, invocation, signal) {
195
- if (invocation.rawInput.trim() !== '')
196
- return { kind: 'error', text: 'Usage: /plugins' };
197
- if (invocation.agent.status === 'running')
198
- return { kind: 'error', text: 'Wait for the current turn to finish before managing plugins.' };
374
+ async function browseBundles(ctx, signal, configurationCtx) {
375
+ const browse = createPromptBrowser(request => ctx.tui.prompt(request));
376
+ let result;
199
377
  while (!signal.aborted) {
200
- const action = await ctx.tui.prompt({ ...list, title: 'Plugins', question: 'Manage this profile',
201
- detail: 'Changes affect every session using this profile. Package replacements may require a restart.',
202
- options: [
203
- { label: 'Plugin entries', value: 'entries' },
204
- { label: 'Bundles', value: 'bundles' },
205
- { label: 'Discover', value: 'discover', description: 'Browse the plugin indexes this profile configures and install an entry.' },
206
- { label: 'Install bundle', value: 'install' },
207
- ], signal });
208
- if (action === null)
209
- break;
210
- if (action === 'discover') {
211
- await discover(ctx, signal);
378
+ const rows = await ctx.pluginManager.listBundles();
379
+ const selected = await browse({ localize: true, ...list, ...changeFeedback(result), title: 'Bundles', question: 'Choose a bundle',
380
+ options: rows.map(row => ({ label: row.name, value: row.name,
381
+ description: `${row.version ?? row.installTarget?.version ?? ''} · ${bundleStatus(row)}${row.error === undefined ? '' : ` · ${row.error.diagnostic ?? row.error.code}`}` })), signal });
382
+ if (selected === null || signal.aborted)
383
+ return;
384
+ if (selected === 'result:') {
385
+ await readChangeDetails(ctx, result, signal);
212
386
  continue;
213
387
  }
214
- let result;
215
- if (action === 'entries') {
216
- const rows = await ctx.pluginManager.listPlugins();
217
- const id = await ctx.tui.prompt({ ...list, title: 'Plugin entries', question: 'Choose an entry to inspect or toggle',
218
- options: rows.map(row => ({ label: row.patchId ?? String(row.entryId), value: String(row.entryId),
219
- description: `${row.enabled ? 'Enabled' : 'Disabled'} · ${row.fiberPhase ?? 'not mounted'} · ${row.moduleName}${pluginReadOnly(row) === undefined ? '' : ` · ${pluginReadOnly(row)}`}` })), signal });
220
- const row = rows.find(row => row.entryId === id);
221
- if (row === undefined)
222
- continue;
223
- const reason = pluginReadOnly(row);
224
- if (reason !== undefined) {
225
- ctx.tui.notice(`${row.moduleName}: read-only (${reason}).`);
226
- continue;
227
- }
228
- const choice = await confirmPrompt(request => ctx.tui.prompt(request), {
229
- title: 'Plugin entry',
230
- question: row.moduleName,
231
- acceptLabel: row.enabled ? 'Disable' : 'Enable',
232
- cancelLabel: null,
233
- signal,
234
- });
235
- if (choice && !signal.aborted)
236
- result = await ctx.pluginManager.setPluginEnabled(row.entryId, !row.enabled);
237
- }
238
- else if (action === 'bundles') {
239
- const rows = await ctx.pluginManager.listBundles();
240
- const selected = await ctx.tui.prompt({ ...list, title: 'Bundles', question: 'Choose a bundle',
241
- options: rows.map(row => ({ label: row.name, value: row.name,
242
- description: `${row.version ?? row.installTarget?.version ?? ''} · ${bundleStatus(row)}${row.error === undefined ? '' : ` · ${row.error.diagnostic ?? row.error.code}`}` })), signal });
243
- const row = rows.find(row => row.name === selected);
244
- if (row === undefined)
245
- continue;
246
- if (row.readOnlyReason !== undefined) {
247
- ctx.tui.notice(`${row.name}: read-only (${row.readOnlyReason}).`);
248
- continue;
249
- }
388
+ if (!rows.some(row => row.name === selected))
389
+ continue;
390
+ let choice = null;
391
+ while (!signal.aborted) {
392
+ const row = (await ctx.pluginManager.listBundles()).find(row => row.name === selected);
393
+ if (row === undefined || signal.aborted)
394
+ break;
250
395
  const guide = externalAgentGuide(row.name);
251
396
  const integration = nativeIntegrationId(row.name);
252
397
  const ownership = row.availability === 'installation'
253
398
  ? `Included with omdsh. ${integration === undefined ? 'Use Enable or Disable to manage this bundle.' : 'Open Integration settings to enable or disable it.'} It updates with omdsh and cannot be uninstalled separately.`
254
399
  : row.installed ? 'Installed in this profile. Uninstall unloads the bundle and removes its package from this profile.' : undefined;
255
- const detail = [ownership, guide ?? row.description].filter(Boolean).join('\n\n');
256
- const choice = await ctx.tui.prompt({ title: row.name, question: 'Manage bundle', ...(detail === '' ? {} : { detail }),
257
- options: [
400
+ const language = resolveUiLanguage(ctx.tui.prefs?.().uiLanguage);
401
+ const detail = [ownership, externalAgentSummary(row.name, language) ?? row.description].filter(Boolean).join('\n\n');
402
+ choice = await ctx.tui.prompt({ localize: true, title: row.name, question: 'Manage bundle', ...changeFeedback(result),
403
+ detail: [detail, row.readOnlyReason, changeFeedback(result).detail].filter(Boolean).join('\n\n'),
404
+ ...(choice === null ? {} : { initialValue: choice }),
405
+ options: row.readOnlyReason === undefined ? [
258
406
  ...(guide === undefined || row.availability !== 'missing' && integration !== undefined ? [] : [{ label: 'Check profile status', value: 'check' }]),
259
407
  ...(integration === undefined || row.availability === 'missing' ? [] : [{ label: 'Integration settings', value: 'settings' }]),
260
408
  ...(row.installTarget === undefined || row.availability === 'installation' ? [] : [{ label: row.availability === 'missing' ? 'Install' : 'Reinstall', value: 'install' }]),
261
409
  ...(integration !== undefined || row.availability === 'missing' || row.error !== undefined && !row.enabled ? [] : [{ label: row.enabled ? 'Disable' : 'Enable', value: 'toggle' }]),
262
410
  ...(row.removable ? [{ label: row.installed ? 'Uninstall' : 'Remove from profile', value: 'remove' }] : []),
263
- ], allowCustom: false, notify: false, signal });
264
- if (signal.aborted)
411
+ ...(guide === undefined || row.availability !== 'missing' ? [] : [{ label: 'Usage notes', value: 'guide' }]),
412
+ ] : [], allowCustom: false, notify: false, signal });
413
+ if (signal.aborted || choice === null)
265
414
  break;
415
+ if (choice === 'result:') {
416
+ await readChangeDetails(ctx, result, signal);
417
+ continue;
418
+ }
266
419
  if (choice === 'check' && guide !== undefined)
267
420
  await showIntegrationStatus(ctx, row.name, signal);
421
+ if (choice === 'guide' && guide !== undefined)
422
+ await readReference(ctx.tui, `${row.name} · ${uiText(language, 'Usage notes')}`, guide, signal);
268
423
  if (choice === 'settings' && integration !== undefined)
269
- await editSettingsEntry(ctx, integration, signal);
424
+ await editSettingsEntry(configurationCtx ?? ctx, integration, signal);
270
425
  if (choice === 'install' && row.installTarget !== undefined)
271
- result = await confirmInstall(ctx, row.installTarget.spec, signal, true);
426
+ result = await confirmInstall(ctx, row.installTarget.spec, signal, true) ?? result;
272
427
  if (choice === 'toggle')
273
428
  result = await ctx.pluginManager.setBundleEnabled(row.name, !row.enabled);
274
429
  if (choice === 'remove' && row.removable) {
275
- const consent = await confirmPrompt(request => ctx.tui.prompt(request), {
430
+ const consent = await confirmPrompt(request => ctx.tui.prompt(request), { localize: ['title', 'options', 'actions'],
276
431
  title: row.installed ? 'Uninstall bundle' : 'Remove bundle from profile',
277
432
  question: row.name,
278
433
  detail: row.installed
@@ -286,22 +441,109 @@ async function manage(ctx, invocation, signal) {
286
441
  result = await ctx.pluginManager.removeBundle(row.name);
287
442
  }
288
443
  }
444
+ }
445
+ }
446
+ async function manage(ctx, invocation, signal, configurationCtx) {
447
+ if (invocation.rawInput.trim() !== '')
448
+ return { kind: 'error', text: 'Usage: /plugins' };
449
+ if (invocation.agent.status === 'running')
450
+ return { kind: 'error', text: 'Wait for the current turn to finish before managing plugins.' };
451
+ const navigation = new AbortController();
452
+ signal = AbortSignal.any([signal, navigation.signal]);
453
+ const onSuperseded = () => navigation.abort();
454
+ const browse = createPromptBrowser(request => ctx.tui.prompt({ ...request, onSuperseded }));
455
+ let result;
456
+ while (!signal.aborted) {
457
+ const runtime = ctx.get('omdshSession');
458
+ const creator = runtime === undefined ? undefined : (await runtime.agentPresets()).find(preset => preset.id === 'cordis');
459
+ if (signal.aborted)
460
+ break;
461
+ const action = await browse({ localize: true, ...list, ...changeFeedback(result), title: 'Plugins', question: 'Manage this profile',
462
+ detail: [uiText(resolveUiLanguage(ctx.tui.prefs?.().uiLanguage), 'Changes affect every session using this profile. Package replacements may require a restart.'), changeFeedback(result).detail].filter(Boolean).join('\n\n'),
463
+ options: [
464
+ ...(configurationCtx === undefined ? [] : [{ label: 'Configure plugins', value: 'configure', description: 'Shell, subagents, web providers and installed plugin settings.' }]),
465
+ ...(runtime === undefined ? [] : [{ label: 'Let the agent create a plugin', value: 'creator', disabled: creator === undefined || creator.broken !== undefined, description: creator === undefined ? 'Creator is unavailable in this profile.' : creator.broken ?? 'Start Creator without changing your default Agent preset.' }]),
466
+ { label: 'Plugin entries', value: 'entries' },
467
+ { label: 'Bundles', value: 'bundles' },
468
+ { label: 'Discover', value: 'discover', description: 'Browse the plugin indexes this profile configures and install an entry.' },
469
+ { label: 'Install bundle', value: 'install' },
470
+ ], signal });
471
+ if (action === null)
472
+ break;
473
+ if (action === 'result:') {
474
+ await readChangeDetails(ctx, result, signal);
475
+ continue;
476
+ }
477
+ if (action === 'creator')
478
+ return startCreator(ctx, { ...invocation, signal });
479
+ if (action === 'configure') {
480
+ if (configurationCtx === undefined)
481
+ continue;
482
+ while (!signal.aborted) {
483
+ const entries = pluginSettingsEntries(configurationCtx).filter(entry => ['tools', 'agent', 'integrations', 'advanced'].includes(entry.section ?? 'advanced'));
484
+ const id = await browse({ localize: true, ...list, title: 'Plugin settings', question: 'Choose a plugin to configure',
485
+ options: entries.map(entry => ({ label: entry.label, value: entry.id, description: entry.summary ?? entry.description })), signal });
486
+ if (id === null || signal.aborted)
487
+ break;
488
+ if (entries.some(entry => entry.id === id))
489
+ await editSettingsEntry(configurationCtx, id, signal);
490
+ }
491
+ continue;
492
+ }
493
+ if (action === 'discover') {
494
+ await discover(ctx, signal, onSuperseded);
495
+ continue;
496
+ }
497
+ if (action === 'entries') {
498
+ let entryResult;
499
+ while (!signal.aborted) {
500
+ const rows = await ctx.pluginManager.listPlugins();
501
+ if (signal.aborted)
502
+ break;
503
+ const id = await browse({ localize: true, ...list, ...changeFeedback(entryResult), title: 'Plugin entries', question: 'Choose an entry to inspect',
504
+ options: rows.map(row => ({ label: row.patchId ?? String(row.entryId), value: String(row.entryId),
505
+ description: `${row.enabled ? 'Enabled' : 'Disabled'} · ${row.fiberPhase ?? 'not mounted'} · ${row.moduleName}${pluginReadOnly(row) === undefined ? '' : ` · ${pluginReadOnly(row)}`}` })), signal });
506
+ if (id === 'result:') {
507
+ await readChangeDetails(ctx, entryResult, signal);
508
+ continue;
509
+ }
510
+ const row = rows.find(row => row.entryId === id);
511
+ if (row === undefined || signal.aborted)
512
+ break;
513
+ const change = await inspectPluginEntry(ctx, row, signal);
514
+ if (change !== undefined)
515
+ entryResult = change;
516
+ }
517
+ }
518
+ else if (action === 'bundles') {
519
+ await browseBundles(ctx, signal, configurationCtx);
520
+ }
289
521
  else if (action === 'install') {
290
- const spec = await ctx.tui.prompt(askPrompt({ title: 'Install bundle', question: 'Package name, version, Git URL, or absolute path', signal }));
522
+ const spec = await ctx.tui.prompt(askPrompt({ localize: ['title', 'question', 'actions'], title: 'Install bundle', question: 'Package name, version, Git URL, or absolute path', signal }));
291
523
  if (spec === null || spec.trim() === '' || signal.aborted)
292
524
  continue;
293
- result = await confirmInstall(ctx, spec.trim(), signal);
525
+ result = await confirmInstall(ctx, spec.trim(), signal) ?? result;
294
526
  }
295
- if (result !== undefined)
296
- ctx.tui.commandOutput('/plugins', changeText(result));
297
527
  }
298
528
  return { kind: 'success' };
299
529
  }
300
530
  export function apply(ctx) {
301
531
  const closing = new AbortController();
532
+ const configurations = new Map();
533
+ // Configuration is optional. Each editor receives the services declared by its scope.
534
+ for (const services of [['settings'], ['settings', 'credentials']]) {
535
+ const key = services.join(',');
536
+ ctx.inject(services, configurationCtx => {
537
+ const lifetime = new AbortController();
538
+ configurations.set(key, { ctx: configurationCtx, signal: lifetime.signal });
539
+ configurationCtx.effect(() => () => { configurations.delete(key); lifetime.abort(); });
540
+ });
541
+ }
302
542
  registerCommands(ctx, [{ name: 'plugins', description: 'Manage profile plugins and bundles', handler: async (invocation) => {
543
+ const configuration = configurations.get('settings,credentials') ?? configurations.get('settings');
544
+ const signal = AbortSignal.any([invocation.signal, closing.signal, ...(configuration === undefined ? [] : [configuration.signal])]);
303
545
  try {
304
- return await manage(ctx, invocation, AbortSignal.any([invocation.signal, closing.signal]));
546
+ return await manage(ctx, invocation, signal, configuration?.ctx);
305
547
  }
306
548
  catch (error) {
307
549
  return { kind: 'error', text: error instanceof Error ? error.message : String(error) };