@agi-fans/dsh-tui 0.22.0 → 0.23.0

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 (279) hide show
  1. package/LICENSE +26 -0
  2. package/lib/CHANGELOG.md +199 -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 +97 -32
  38. package/lib/commands/export.js.map +1 -1
  39. package/lib/commands/files.js +2 -8
  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 +197 -120
  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 +111 -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 +112 -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 +1264 -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 +32 -13
  208. package/lib/session/session-controller.js +444 -98
  209. package/lib/session/session-controller.js.map +1 -1
  210. package/lib/session/subagent-roster.d.ts +1 -0
  211. package/lib/session/subagent-roster.js +16 -5
  212. package/lib/session/subagent-roster.js.map +1 -1
  213. package/lib/session/subagent-tracker.d.ts +2 -0
  214. package/lib/session/subagent-tracker.js +18 -0
  215. package/lib/session/subagent-tracker.js.map +1 -1
  216. package/lib/session/tui-settings.d.ts +9 -5
  217. package/lib/session/tui-settings.js +3 -1
  218. package/lib/session/tui-settings.js.map +1 -1
  219. package/lib/session/workflow-projection.d.ts +2 -1
  220. package/lib/session/workflow-projection.js +34 -4
  221. package/lib/session/workflow-projection.js.map +1 -1
  222. package/lib/views/autocomplete.d.ts +10 -7
  223. package/lib/views/autocomplete.js +44 -37
  224. package/lib/views/autocomplete.js.map +1 -1
  225. package/lib/views/context-overview.d.ts +4 -0
  226. package/lib/views/context-overview.js +43 -0
  227. package/lib/views/context-overview.js.map +1 -0
  228. package/lib/views/copy-targets.d.ts +6 -2
  229. package/lib/views/copy-targets.js +19 -14
  230. package/lib/views/copy-targets.js.map +1 -1
  231. package/lib/views/document-reader.d.ts +10 -1
  232. package/lib/views/document-reader.js +59 -5
  233. package/lib/views/document-reader.js.map +1 -1
  234. package/lib/views/event-views.js +29 -11
  235. package/lib/views/event-views.js.map +1 -1
  236. package/lib/views/file-actions.js +21 -0
  237. package/lib/views/file-actions.js.map +1 -1
  238. package/lib/views/file-browser.d.ts +98 -0
  239. package/lib/views/file-browser.js +120 -0
  240. package/lib/views/file-browser.js.map +1 -0
  241. package/lib/views/history-search.js +4 -4
  242. package/lib/views/history-search.js.map +1 -1
  243. package/lib/views/hotkey-format.d.ts +3 -2
  244. package/lib/views/hotkey-format.js +4 -3
  245. package/lib/views/hotkey-format.js.map +1 -1
  246. package/lib/views/hotkeys.js +10 -12
  247. package/lib/views/hotkeys.js.map +1 -1
  248. package/lib/views/list-view.js +16 -11
  249. package/lib/views/list-view.js.map +1 -1
  250. package/lib/views/prompt-selector.d.ts +23 -1
  251. package/lib/views/prompt-selector.js +123 -60
  252. package/lib/views/prompt-selector.js.map +1 -1
  253. package/lib/views/settings-list.d.ts +6 -1
  254. package/lib/views/settings-list.js +69 -46
  255. package/lib/views/settings-list.js.map +1 -1
  256. package/lib/views/tool-actions.d.ts +16 -0
  257. package/lib/views/tool-actions.js +29 -0
  258. package/lib/views/tool-actions.js.map +1 -0
  259. package/lib/views/trajectory-timeline.d.ts +5 -6
  260. package/lib/views/trajectory-timeline.js +25 -15
  261. package/lib/views/trajectory-timeline.js.map +1 -1
  262. package/lib/views/trajectory.d.ts +23 -2
  263. package/lib/views/trajectory.js +404 -186
  264. package/lib/views/trajectory.js.map +1 -1
  265. package/lib/views/transcript-render.d.ts +39 -15
  266. package/lib/views/transcript-render.js +406 -208
  267. package/lib/views/transcript-render.js.map +1 -1
  268. package/lib/views/transcript-types.d.ts +3 -2
  269. package/lib/views/transcript-types.js.map +1 -1
  270. package/lib/views/workflow.d.ts +16 -0
  271. package/lib/views/workflow.js +42 -0
  272. package/lib/views/workflow.js.map +1 -0
  273. package/package.json +61 -58
  274. package/lib/chrome/welcome-tips.d.ts +0 -23
  275. package/lib/chrome/welcome-tips.js +0 -24
  276. package/lib/chrome/welcome-tips.js.map +0 -1
  277. package/lib/session/steering.d.ts +0 -3
  278. package/lib/session/steering.js +0 -9
  279. package/lib/session/steering.js.map +0 -1
@@ -1,10 +1,12 @@
1
1
  /** Interactive credential commands backed by the Harness credential and settings seams. */
2
- import { spawn } from 'node:child_process';
3
- import process from 'node:process';
4
2
  import { AuthorizationError, } from '@deepseek-ai/dsh-authorization';
5
3
  import { credentialRef, } from '@deepseek-ai/dsh-credentials';
4
+ import { resolveUiLanguage, uiText } from "../locale/index.js";
5
+ import { createPromptBrowser } from "../runtime/prompt-browser.js";
6
6
  import { createAuthorizationInteraction } from "../session/authorization-interaction.js";
7
- import { APP_VERSION } from "../session/package-metadata.js";
7
+ import { logoutDeepSeekAccount } from "./auth-account.js";
8
+ import { openExternal } from "./open-external.js";
9
+ import { openAccountSettings, bindAccountNotices } from "../runtime/account-settings.js";
8
10
  import { registerCommands } from "./registration.js";
9
11
  import { authorizationFlows, catalogForRecord, catalogProvidersWithoutFlows, fallbackDeepSeekEntry, flowChoiceValue, isAccountFlow, isDeepSeek, isPiAiDeepSeekFlow, loginAuthorizationFlows, logoutableProviders, parseFlowChoice, profileApiKeyEnv, providerListId, } from "./auth-providers.js";
10
12
  import { askPrompt } from "../prompt-intents.js";
@@ -87,7 +89,7 @@ async function configuredFlowRecords(ctx) {
87
89
  async function logoutableChoices(ctx) {
88
90
  const providers = logoutableProviders(ctx);
89
91
  const records = await configuredFlowRecords(ctx);
90
- const account = accountClient(ctx);
92
+ const account = ctx.get('deepseekAccount');
91
93
  const accountState = account === undefined ? undefined : await account.getState().catch(() => undefined);
92
94
  const accountChoice = accountState?.status === 'credential-stored' ? [DEEPSEEK_ACCOUNT_CHOICE] : [];
93
95
  const actionable = [];
@@ -128,11 +130,9 @@ function credentialOwnershipBadge(info) {
128
130
  return { label: 'environment', tone: 'muted' };
129
131
  return { label: 'configured externally', tone: 'muted' };
130
132
  }
131
- async function pickLoginChoice(ctx, invocation) {
133
+ async function pickLoginChoice(ctx, invocation, prompt) {
132
134
  const entries = catalogProvidersWithoutFlows(ctx);
133
135
  const flows = loginAuthorizationFlows(ctx);
134
- if (entries.length === 0 && flows.length === 0)
135
- return CUSTOM_PROVIDER_VALUE;
136
136
  const entryRows = await Promise.all(entries.map(async (entry) => {
137
137
  const ref = profileApiKeyEnv(ctx, entry);
138
138
  const info = ref === undefined ? UNCONFIGURED : await ctx.credentials.describe(credentialRef(ref));
@@ -151,7 +151,7 @@ async function pickLoginChoice(ctx, invocation) {
151
151
  const accountState = account?.getState === undefined ? undefined : await account.getState().catch(() => undefined);
152
152
  const includeAccount = account?.getState !== undefined;
153
153
  const values = loginChoiceValues(entries, orderedFlows, true, includeAccount);
154
- const raw = await ctx.tui.prompt({
154
+ const raw = await prompt({ localize: true,
155
155
  presentation: 'card',
156
156
  filterable: true,
157
157
  title: 'Provider',
@@ -193,8 +193,7 @@ async function pickLoginChoice(ctx, invocation) {
193
193
  ],
194
194
  allowCustom: false,
195
195
  notify: false,
196
- signal: invocation.signal,
197
- });
196
+ signal: invocation.signal, });
198
197
  if (raw === null)
199
198
  return undefined;
200
199
  if (raw.trim().toLowerCase() === 'custom' || selected(raw, values) === CUSTOM_PROVIDER_VALUE) {
@@ -217,7 +216,7 @@ async function pickAuthorizationMethod(ctx, invocation, flow) {
217
216
  if (flow.methods.length === 1)
218
217
  return flow.methods[0]?.id;
219
218
  const values = flow.methods.map(method => method.id);
220
- const raw = await ctx.tui.prompt({
219
+ const raw = await ctx.tui.prompt({ localize: true,
221
220
  title: `Login to ${flow.label}`,
222
221
  question: 'Choose a sign-in method',
223
222
  options: flow.methods.map(method => ({
@@ -272,59 +271,62 @@ async function loginAuthorization(ctx, invocation, flow) {
272
271
  async function resetDeepSeekCredentialRoute(ctx) {
273
272
  await ctx.settings.mutate(DEEPSEEK_SETTINGS, [{ op: 'unset', path: ['apiKeyEnv'] }]);
274
273
  }
275
- /** Best-effort browser launch; callers always retain the full URL as a fallback. */
276
- function openExternal(url) {
277
- const command = process.platform === 'darwin'
278
- ? { file: 'open', args: [url] }
279
- : process.platform === 'win32'
280
- ? { file: 'cmd.exe', args: ['/d', '/s', '/c', 'start', '', url] }
281
- : { file: 'xdg-open', args: [url] };
282
- try {
283
- const child = spawn(command.file, command.args, { detached: true, stdio: 'ignore' });
284
- child.once('error', () => undefined);
285
- child.unref();
286
- }
287
- catch {
288
- // Headless and remote terminals still receive the visible URL.
289
- }
290
- }
291
274
  /** Best-effort browser launch for the DeepSeek key dashboard. */
292
275
  export function openDeepSeekDashboard() {
293
276
  openExternal(DEEPSEEK_API_KEYS_URL);
294
277
  }
295
- async function loginDeepSeek(ctx, invocation, config) {
278
+ async function loginDeepSeek(ctx, invocation, config, prompt) {
296
279
  const current = await ctx.credentials.describe(DEEPSEEK_API_KEY);
297
280
  if (current.configured && !current.writable) {
298
281
  return { kind: 'error', text: unmanagedCredentialMessage(DEEPSEEK_API_KEY, current, 'replace') };
299
282
  }
300
283
  if (config.openDashboard !== false)
301
284
  openDeepSeekDashboard();
302
- const raw = await ctx.tui.prompt(askPrompt({
303
- title: 'Login to DeepSeek',
304
- question: current.configured ? 'Paste a new DeepSeek API key' : 'Paste your DeepSeek API key',
305
- detail: `Create or copy a key at ${DEEPSEEK_API_KEYS_URL}. It will be validated and stored locally as ${DEEPSEEK_API_KEY}.`,
306
- secret: true,
307
- submitLabel: 'validate',
308
- signal: invocation.signal,
309
- }));
310
- if (raw === null)
311
- return { kind: 'success' };
312
- let apiKey;
313
- try {
314
- apiKey = normalizeDeepSeekApiKey(raw);
315
- await validateDeepSeekApiKey(apiKey, invocation.signal);
316
- }
317
- catch (error) {
318
- if (invocation.signal.aborted)
285
+ let stored = false;
286
+ while (!invocation.signal.aborted && !stored) {
287
+ const apiKey = await readLoginField(prompt, askPrompt({ localize: ['title', 'question', 'actions'],
288
+ title: 'Login to DeepSeek',
289
+ question: current.configured ? 'Paste a new DeepSeek API key' : 'Paste your DeepSeek API key',
290
+ detail: `Create or copy a key at ${DEEPSEEK_API_KEYS_URL}. It will be validated and stored locally as ${DEEPSEEK_API_KEY}.`,
291
+ secret: true, submitLabel: 'validate', signal: invocation.signal,
292
+ }), normalizeDeepSeekApiKey);
293
+ if (apiKey === null)
319
294
  return { kind: 'success' };
320
- return { kind: 'error', text: error instanceof Error ? error.message : 'DeepSeek login failed.' };
321
- }
322
- try {
323
- await ctx.credentials.set(DEEPSEEK_API_KEY, apiKey);
324
- }
325
- catch {
326
- return { kind: 'error', text: 'The API key is valid, but it could not be saved to the Harness credential store.' };
295
+ let validated = false;
296
+ while (!invocation.signal.aborted) {
297
+ try {
298
+ if (!validated)
299
+ await validateDeepSeekApiKey(apiKey, invocation.signal);
300
+ validated = true;
301
+ invocation.signal.throwIfAborted();
302
+ await ctx.credentials.set(DEEPSEEK_API_KEY, apiKey);
303
+ stored = true;
304
+ break;
305
+ }
306
+ catch (error) {
307
+ if (invocation.signal.aborted)
308
+ return { kind: 'success' };
309
+ const diagnostic = validated
310
+ ? 'The API key is valid, but it could not be saved to the Harness credential store.'
311
+ : error instanceof Error ? error.message : 'DeepSeek login failed.';
312
+ const action = await prompt({ localize: ['title', 'question', 'options'],
313
+ title: 'Login to DeepSeek', question: validated ? 'Could not save the key' : 'Could not validate the key',
314
+ detail: diagnostic, allowCustom: false, notify: false, signal: invocation.signal,
315
+ options: [
316
+ { label: validated ? 'Retry saving' : 'Retry validation', value: 'retry' },
317
+ { label: 'Enter another key', value: 'edit' },
318
+ { label: 'Back', value: 'back' },
319
+ ],
320
+ });
321
+ if (action === null || action === 'back')
322
+ return { kind: 'success' };
323
+ if (action !== 'retry')
324
+ break;
325
+ }
326
+ }
327
327
  }
328
+ if (invocation.signal.aborted)
329
+ return { kind: 'success' };
328
330
  // A profile may still pin the old product-owned reference. Reset it so the
329
331
  // upstream default resolves the key that was just stored.
330
332
  if (profileApiKeyEnv(ctx, fallbackDeepSeekEntry()) !== String(DEEPSEEK_API_KEY)) {
@@ -343,30 +345,23 @@ async function loginDeepSeek(ctx, invocation, config) {
343
345
  text: 'Logged in to DeepSeek. The stored key is used by the deepseek-official route on the next model request.',
344
346
  };
345
347
  }
346
- async function loginCatalog(ctx, invocation, entry) {
348
+ async function loginCatalog(ctx, invocation, entry, prompt) {
347
349
  const ref = managedProviderCredentialRef(entry.provider);
348
350
  const current = await ctx.credentials.describe(ref);
349
351
  if (current.configured && !current.writable) {
350
352
  return { kind: 'error', text: unmanagedCredentialMessage(ref, current, 'replace') };
351
353
  }
352
354
  const label = entry.displayName;
353
- const raw = await ctx.tui.prompt(askPrompt({
355
+ const apiKey = await readLoginField(prompt, askPrompt({ localize: ['title', 'question', 'actions'],
354
356
  title: `Login to ${label}`,
355
357
  question: current.configured ? `Paste a new ${label} API key` : `Paste your ${label} API key`,
356
358
  detail: `The key is stored locally and activates the ${entry.provider} route for the next model request. Use /model to select it.`,
357
359
  secret: true,
358
360
  submitLabel: 'save',
359
361
  signal: invocation.signal,
360
- }));
361
- if (raw === null)
362
+ }), value => normalizeApiKey(value, label));
363
+ if (apiKey === null)
362
364
  return { kind: 'success' };
363
- let apiKey;
364
- try {
365
- apiKey = normalizeApiKey(raw, label);
366
- }
367
- catch (error) {
368
- return { kind: 'error', text: error instanceof Error ? error.message : `${label} login failed.` };
369
- }
370
365
  try {
371
366
  await ctx.credentials.set(ref, apiKey);
372
367
  }
@@ -398,168 +393,163 @@ function reservedProviderIds(ctx) {
398
393
  .map(entry => entry.provider),
399
394
  ]);
400
395
  }
401
- async function collectCustomModels(ctx, invocation, draft) {
402
- const source = await ctx.tui.prompt({
403
- title: 'Custom models',
404
- question: 'How should omdsh learn the model ids?',
405
- options: [
406
- { label: 'Enter model ids', value: 'enter', description: 'Type one or more ids, separated by commas.' },
407
- { label: 'Fetch from endpoint', value: 'fetch', description: 'Ask the base URL for GET /models.' },
408
- ],
409
- initialValue: 'enter',
410
- allowCustom: false,
411
- notify: false,
412
- signal: invocation.signal,
413
- });
414
- if (source === null)
415
- return undefined;
416
- if (source === 'fetch') {
396
+ /** Retry a field in place, keeping only non-secret text in its editor. */
397
+ async function readLoginField(prompt, request, validate) {
398
+ let input = request.initialInput ?? '', diagnostic = '';
399
+ while (!request.signal?.aborted) {
400
+ const raw = await prompt({ ...request,
401
+ ...(request.secret ? {} : { initialInput: input }),
402
+ detail: [diagnostic, request.detail].filter(Boolean).join('\n\n'),
403
+ });
404
+ if (raw === null || request.signal?.aborted)
405
+ return null;
406
+ if (!request.secret)
407
+ input = raw;
417
408
  try {
418
- const discovered = await ctx.llm.discoverModels('llm-pi-ai', {
419
- baseURL: draft.baseURL,
420
- api: draft.api,
421
- ...(draft.apiKey === undefined ? {} : { apiKey: draft.apiKey }),
422
- }, invocation.signal);
423
- if (discovered.length === 1)
424
- return [discovered[0]?.id ?? ''];
425
- if (discovered.length > 1) {
426
- const picked = await ctx.tui.prompt({
427
- title: 'Custom models',
428
- question: 'Choose models to enable',
429
- options: discovered.map(model => ({
430
- label: model.id,
431
- value: model.id,
432
- ...(model.name === undefined || model.name === model.id ? {} : { description: model.name }),
433
- })),
434
- multiSelect: true,
435
- allowCustom: false,
436
- notify: false,
437
- filterable: true,
438
- presentation: 'card',
439
- signal: invocation.signal,
409
+ return await validate(raw);
410
+ }
411
+ catch (error) {
412
+ diagnostic = error instanceof Error ? error.message : 'Invalid value.';
413
+ }
414
+ }
415
+ return null;
416
+ }
417
+ async function collectCustomModels(ctx, invocation, draft, prompt) {
418
+ let diagnostic = '', typed = '';
419
+ while (!invocation.signal.aborted) {
420
+ const source = await prompt({ localize: true,
421
+ title: 'Custom models', question: 'How should omdsh learn the model ids?', detail: diagnostic,
422
+ options: [
423
+ { label: 'Enter model ids', value: 'enter', description: 'Type one or more ids, separated by commas.' },
424
+ { label: 'Fetch from endpoint', value: 'fetch', description: 'Ask the base URL for GET /models.' },
425
+ ],
426
+ initialValue: 'enter', allowCustom: false, notify: false, signal: invocation.signal,
427
+ });
428
+ if (source === null || invocation.signal.aborted)
429
+ return undefined;
430
+ if (source === 'fetch') {
431
+ try {
432
+ const discovered = await ctx.llm.discoverModels('llm-pi-ai', draft, invocation.signal);
433
+ if (invocation.signal.aborted)
434
+ return undefined;
435
+ if (discovered.length === 1)
436
+ return [discovered[0].id];
437
+ if (discovered.length === 0) {
438
+ diagnostic = 'No models were returned. Try again or enter model ids.';
439
+ continue;
440
+ }
441
+ const picked = await prompt({ localize: ['title', 'question', 'actions'],
442
+ title: 'Custom models', question: 'Choose models to enable',
443
+ options: discovered.map(model => ({ label: model.id, value: model.id,
444
+ ...(model.name === undefined || model.name === model.id ? {} : { description: model.name }) })),
445
+ multiSelect: true, allowCustom: false, notify: false, filterable: true,
446
+ presentation: 'card', signal: invocation.signal,
440
447
  });
441
448
  if (picked === null)
442
- return undefined;
449
+ continue;
443
450
  return parseModelIds(picked);
444
451
  }
452
+ catch (error) {
453
+ diagnostic = error instanceof Error ? error.message : 'Could not fetch models. Try again or enter model ids.';
454
+ continue;
455
+ }
445
456
  }
446
- catch (error) {
447
- if (invocation.signal.aborted)
448
- return undefined;
449
- const detail = error instanceof Error ? error.message : 'the endpoint did not list models';
450
- const fallback = await ctx.tui.prompt(askPrompt({
451
- title: 'Custom models',
452
- question: 'Enter model ids',
453
- detail: `Could not fetch models (${detail}). Type one or more ids, separated by commas.`,
454
- signal: invocation.signal,
455
- }));
456
- if (fallback === null)
457
- return undefined;
458
- return parseModelIds(fallback);
459
- }
457
+ const value = await readLoginField(prompt, askPrompt({ localize: ['title', 'question', 'actions'],
458
+ title: 'Custom models', question: 'Enter model ids',
459
+ detail: 'Separate multiple ids with commas. Example: llama3.1, qwen2.5-coder',
460
+ initialInput: typed, onCancelDraft: value => { typed = value; }, signal: invocation.signal,
461
+ }), value => parseModelIds(value).join(', '));
462
+ if (value !== null)
463
+ return parseModelIds(value);
460
464
  }
461
- const typed = await ctx.tui.prompt(askPrompt({
462
- title: 'Custom models',
463
- question: 'Enter model ids',
464
- detail: 'Separate multiple ids with commas. Example: llama3.1, qwen2.5-coder',
465
- signal: invocation.signal,
466
- }));
467
- if (typed === null)
468
- return undefined;
469
- return parseModelIds(typed);
465
+ return undefined;
470
466
  }
471
- async function loginCustom(ctx, invocation) {
472
- const idRaw = await ctx.tui.prompt(askPrompt({
473
- title: 'Custom provider',
474
- question: 'Provider id',
475
- detail: 'Lowercase letters, digits, and hyphens. This id is permanent.',
476
- signal: invocation.signal,
477
- }));
478
- if (idRaw === null)
479
- return { kind: 'success' };
480
- let id;
481
- try {
482
- id = parseProviderId(idRaw);
483
- }
484
- catch (error) {
485
- return { kind: 'error', text: error instanceof Error ? error.message : 'Invalid provider id.' };
486
- }
487
- if (reservedProviderIds(ctx).has(id)) {
488
- return { kind: 'error', text: `${id} is a catalog provider. Use /login and choose it from the list.` };
489
- }
490
- const urlRaw = await ctx.tui.prompt(askPrompt({
491
- title: 'Custom provider',
492
- question: 'Base URL',
493
- detail: 'Absolute http(s) prefix, for example http://127.0.0.1:11434/v1',
494
- signal: invocation.signal,
495
- }));
496
- if (urlRaw === null)
497
- return { kind: 'success' };
498
- let baseURL;
499
- try {
500
- baseURL = parseBaseURL(urlRaw);
501
- }
502
- catch (error) {
503
- return { kind: 'error', text: error instanceof Error ? error.message : 'Invalid base URL.' };
504
- }
505
- const api = await ctx.tui.prompt({
506
- title: 'Custom provider',
507
- question: 'API protocol',
508
- options: CUSTOM_PROTOCOLS.map(protocol => ({
509
- label: protocol,
510
- value: protocol,
511
- description: protocol === 'openai-completions'
512
- ? 'Chat Completions — Ollama, vLLM, LM Studio, and most gateways'
513
- : protocol === 'openai-responses' ? 'OpenAI Responses API' : 'Anthropic Messages API',
514
- })),
515
- initialValue: 'openai-completions',
516
- allowCustom: false,
517
- notify: false,
518
- signal: invocation.signal,
519
- });
520
- if (api === null)
521
- return { kind: 'success' };
522
- if (!CUSTOM_PROTOCOLS.includes(api)) {
523
- return { kind: 'error', text: `Unknown API protocol: ${api}` };
524
- }
525
- const keyRaw = await ctx.tui.prompt(askPrompt({
526
- title: `Login to ${id}`,
527
- question: `Paste your ${id} API key`,
528
- detail: 'Leave empty if the endpoint does not need a key.',
529
- secret: true,
530
- submitLabel: 'continue',
531
- signal: invocation.signal,
532
- }));
533
- if (keyRaw === null)
534
- return { kind: 'success' };
467
+ async function loginCustom(ctx, invocation, prompt) {
468
+ let step = 0, id = '', baseURL = '', api = 'openai-completions';
535
469
  let apiKey;
536
- if (keyRaw.trim() !== '') {
537
- try {
538
- apiKey = normalizeApiKey(keyRaw, id);
539
- }
540
- catch (error) {
541
- return { kind: 'error', text: error instanceof Error ? error.message : `${id} login failed.` };
542
- }
543
- }
544
470
  let modelIds;
545
- try {
546
- const collected = await collectCustomModels(ctx, invocation, {
547
- baseURL,
548
- api,
549
- ...(apiKey === undefined ? {} : { apiKey }),
550
- });
551
- if (collected === undefined)
552
- return { kind: 'success' };
553
- modelIds = collected.filter(item => item !== '');
554
- if (modelIds.length === 0)
555
- throw new Error('Enter at least one model id.');
556
- }
557
- catch (error) {
558
- return { kind: 'error', text: error instanceof Error ? error.message : 'Invalid model list.' };
471
+ while (!invocation.signal.aborted && modelIds === undefined) {
472
+ switch (step) {
473
+ case 0: {
474
+ const value = await readLoginField(prompt, askPrompt({ localize: ['title', 'question', 'actions'],
475
+ title: 'Custom provider', question: 'Provider id',
476
+ detail: 'Lowercase letters, digits, and hyphens. This id is permanent.',
477
+ initialInput: id, onCancelDraft: value => { id = value; }, signal: invocation.signal,
478
+ }), value => {
479
+ const parsed = parseProviderId(value);
480
+ if (reservedProviderIds(ctx).has(parsed))
481
+ throw new Error(uiText(resolveUiLanguage(ctx.tui.prefs?.().uiLanguage), '{name} is a catalog provider. Choose it from the provider list.', { name: parsed }));
482
+ return parsed;
483
+ });
484
+ if (value === null)
485
+ return undefined;
486
+ id = value;
487
+ step = 1;
488
+ break;
489
+ }
490
+ case 1: {
491
+ const value = await readLoginField(prompt, askPrompt({ localize: ['title', 'question', 'actions'],
492
+ title: 'Custom provider', question: 'Base URL',
493
+ detail: 'Absolute http(s) prefix, for example http://127.0.0.1:11434/v1',
494
+ initialInput: baseURL, onCancelDraft: value => { baseURL = value; }, signal: invocation.signal,
495
+ }), parseBaseURL);
496
+ if (value === null) {
497
+ step = 0;
498
+ break;
499
+ }
500
+ baseURL = value;
501
+ step = 2;
502
+ break;
503
+ }
504
+ case 2: {
505
+ const value = await prompt({ localize: true, title: 'Custom provider', question: 'API protocol',
506
+ options: CUSTOM_PROTOCOLS.map(protocol => ({ label: protocol, value: protocol,
507
+ description: protocol === 'openai-completions'
508
+ ? 'Chat Completions — Ollama, vLLM, LM Studio, and most gateways'
509
+ : protocol === 'openai-responses' ? 'OpenAI Responses API' : 'Anthropic Messages API' })),
510
+ initialValue: api, allowCustom: false, notify: false, signal: invocation.signal,
511
+ });
512
+ if (value === null) {
513
+ step = 1;
514
+ break;
515
+ }
516
+ if (!CUSTOM_PROTOCOLS.includes(value))
517
+ break;
518
+ api = value;
519
+ step = 3;
520
+ break;
521
+ }
522
+ case 3: {
523
+ apiKey = undefined;
524
+ const value = await readLoginField(prompt, askPrompt({ localize: ['title', 'question', 'actions'],
525
+ title: `Login to ${id}`, question: `Paste your ${id} API key`,
526
+ detail: 'Leave empty if the endpoint does not need a key.', secret: true,
527
+ submitLabel: 'continue', signal: invocation.signal,
528
+ }), value => value.trim() === '' ? '' : normalizeApiKey(value, id));
529
+ if (value === null) {
530
+ step = 2;
531
+ break;
532
+ }
533
+ apiKey = value === '' ? undefined : value;
534
+ step = 4;
535
+ break;
536
+ }
537
+ case 4:
538
+ modelIds = await collectCustomModels(ctx, invocation, { baseURL, api,
539
+ ...(apiKey === undefined ? {} : { apiKey }),
540
+ }, prompt);
541
+ if (modelIds === undefined)
542
+ step = 3;
543
+ break;
544
+ }
559
545
  }
546
+ if (invocation.signal.aborted || modelIds === undefined)
547
+ return undefined;
560
548
  const ref = managedProviderCredentialRef(id);
561
549
  if (apiKey !== undefined) {
562
550
  const current = await ctx.credentials.describe(ref);
551
+ if (invocation.signal.aborted)
552
+ return undefined;
563
553
  if (current.configured && !current.writable) {
564
554
  return { kind: 'error', text: unmanagedCredentialMessage(ref, current, 'replace') };
565
555
  }
@@ -570,6 +560,8 @@ async function loginCustom(ctx, invocation) {
570
560
  return { kind: 'error', text: 'The API key could not be saved to the Harness credential store.' };
571
561
  }
572
562
  }
563
+ if (invocation.signal.aborted)
564
+ return undefined;
573
565
  try {
574
566
  await ctx.settings.mutate(PI_AI_SETTINGS, [{
575
567
  op: 'set',
@@ -610,7 +602,7 @@ async function logoutDeepSeek(ctx, invocation) {
610
602
  if (current.source !== 'file') {
611
603
  return { kind: 'success', text: `DeepSeek is configured by ${credentialSource(current)}; the credential store cannot remove it.` };
612
604
  }
613
- const answer = await ctx.tui.prompt({
605
+ const answer = await ctx.tui.prompt({ localize: true,
614
606
  title: 'Logout from DeepSeek',
615
607
  question: 'Remove the stored DeepSeek API key?',
616
608
  detail: 'The next model request will use DEEPSEEK_API_KEY from the launch environment when one is available.',
@@ -641,43 +633,6 @@ async function logoutDeepSeek(ctx, invocation) {
641
633
  }
642
634
  return { kind: 'success', text: 'Logged out from DeepSeek. The next model request falls back to the environment when available.' };
643
635
  }
644
- async function logoutDeepSeekAccount(ctx, invocation) {
645
- const account = accountClient(ctx);
646
- if (account === undefined)
647
- return { kind: 'error', text: 'DeepSeek account integration is not mounted in this build.' };
648
- let state;
649
- try {
650
- state = await account.getState();
651
- }
652
- catch {
653
- return { kind: 'error', text: 'Could not read the shared DeepSeek account state.' };
654
- }
655
- if (state.status !== 'credential-stored') {
656
- return { kind: 'success', text: 'No DeepSeek account grant is stored.' };
657
- }
658
- const answer = await ctx.tui.prompt({
659
- title: 'Logout from DeepSeek account',
660
- question: 'Sign out of the shared DeepSeek account?',
661
- detail: 'Removes the local grant and revokes it in the background. The browser session is not signed out.',
662
- options: [
663
- { label: 'Cancel', value: 'cancel', description: 'Keep the account grant.' },
664
- { label: 'Sign out', value: 'logout', description: 'Remove the local grant and revoke it in the background.' },
665
- ],
666
- initialValue: 'cancel',
667
- allowCustom: false,
668
- notify: false,
669
- signal: invocation.signal,
670
- });
671
- if (answer !== 'logout')
672
- return { kind: 'success' };
673
- try {
674
- await account.signOut(accountClientMetadata());
675
- }
676
- catch {
677
- return { kind: 'error', text: 'Could not sign out of the DeepSeek account. Try again from DSH Web/Desktop.' };
678
- }
679
- return { kind: 'success', text: 'Signed out of the DeepSeek account. The local grant was removed; remote revocation continues in the background.' };
680
- }
681
636
  async function pickLogoutChoice(ctx, invocation) {
682
637
  const choices = await logoutableChoices(ctx);
683
638
  if (choices.length === 0)
@@ -687,7 +642,7 @@ async function pickLogoutChoice(ctx, invocation) {
687
642
  const values = choices.map(choice => choice === DEEPSEEK_ACCOUNT_CHOICE
688
643
  ? DEEPSEEK_ACCOUNT_CHOICE
689
644
  : isFlowRecordLogout(choice) ? flowChoiceValue(choice.key) : choice.provider);
690
- const raw = await ctx.tui.prompt({
645
+ const raw = await ctx.tui.prompt({ localize: true,
691
646
  presentation: 'card',
692
647
  filterable: true,
693
648
  title: 'Provider',
@@ -732,7 +687,7 @@ async function logoutRecord(ctx, invocation, record) {
732
687
  text: `The stored ${record.label} sign-in is not removable from this session. Update that credential source directly and restart omdsh.`,
733
688
  };
734
689
  }
735
- const answer = await ctx.tui.prompt({
690
+ const answer = await ctx.tui.prompt({ localize: true,
736
691
  title: `Logout from ${record.label}`,
737
692
  question: `Remove the stored ${record.label} sign-in?`,
738
693
  detail: 'The local credential record is deleted. Settings profiles and environment credentials stay in place. The issuer is not notified.',
@@ -773,7 +728,7 @@ async function logoutCatalog(ctx, invocation, entry) {
773
728
  return { kind: 'error', text: unmanagedCredentialMessage(managedRef, current, 'remove') };
774
729
  }
775
730
  const removesStoredKey = usesManaged && current.source === 'file';
776
- const answer = await ctx.tui.prompt({
731
+ const answer = await ctx.tui.prompt({ localize: true,
777
732
  title: `Logout from ${label}`,
778
733
  question: removesStoredKey
779
734
  ? `Remove the stored ${label} API key and deactivate the route?`
@@ -810,172 +765,40 @@ async function logoutCatalog(ctx, invocation, entry) {
810
765
  }
811
766
  return { kind: 'success', text: `Logged out from ${label}.` };
812
767
  }
813
- function accountClient(ctx) {
814
- return ctx.get('deepseekAccount');
815
- }
816
- function accountClientMetadata() {
817
- return {
818
- version: APP_VERSION,
819
- locale: Intl.DateTimeFormat().resolvedOptions().locale,
820
- timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60,
821
- };
822
- }
823
- function accountCallbackOrigin(ctx) {
824
- const server = ctx.get('webServer');
825
- if (server === undefined || typeof server.port !== 'number' || server.port === 0)
826
- return undefined;
827
- return `${server.protocol}//${server.host}:${server.port}`;
828
- }
829
- function accountFailure(attempt) {
830
- if (attempt?.phase === 'expired')
831
- return 'The DeepSeek sign-in expired. Run /login to try again.';
832
- if (attempt?.phase === 'failed')
833
- return `DeepSeek sign-in failed${attempt.errorCode === undefined ? '' : ` (${attempt.errorCode})`}. Run /login to try again.`;
834
- if (attempt?.phase === 'cancelled')
835
- return 'DeepSeek sign-in was cancelled.';
836
- return undefined;
837
- }
838
- async function waitForAccount(invocation, account, timeoutMs) {
839
- const deadline = Date.now() + timeoutMs;
840
- while (Date.now() < deadline) {
841
- if (invocation.signal.aborted) {
842
- const state = await account.getState().catch(() => undefined);
843
- const id = state?.attempt?.id;
844
- if (id !== undefined)
845
- await account.cancelSignIn(id).catch(() => undefined);
846
- return { kind: 'success' };
847
- }
848
- const state = await account.getState().catch(() => undefined);
849
- if (state?.status === 'credential-stored' && state.attempt?.phase === 'succeeded') {
850
- return { kind: 'success', text: 'Signed in to the DeepSeek account. Use /model to select the deepseek-account route; web search and fetch use the account token there.' };
851
- }
852
- const failure = accountFailure(state?.attempt);
853
- if (failure !== undefined)
854
- return { kind: 'error', text: failure };
855
- await new Promise(resolve => setTimeout(resolve, 400));
856
- }
857
- const state = await account.getState().catch(() => undefined);
858
- const id = state?.attempt?.id;
859
- if (id !== undefined)
860
- await account.cancelSignIn(id).catch(() => undefined);
861
- return { kind: 'error', text: 'DeepSeek sign-in timed out before the browser completed. Run /login to try again.' };
862
- }
863
- async function loginDeepSeekAccount(ctx, invocation, config) {
864
- const account = accountClient(ctx);
865
- if (account === undefined)
866
- return { kind: 'error', text: 'DeepSeek account integration is not mounted in this build.' };
867
- let state;
868
- try {
869
- state = await account.getState();
870
- }
871
- catch {
872
- return { kind: 'error', text: 'Could not read the shared DeepSeek account state.' };
873
- }
874
- if (state.status === 'credential-stored') {
875
- const choice = await ctx.tui.prompt({
876
- title: 'DeepSeek account',
877
- question: 'The account is signed in and shared with DSH.',
878
- detail: 'Use /model to select the deepseek-account route. Signing out removes the local grant; the DSH browser session is revoked in the background.',
879
- options: [
880
- { label: 'Use account route', value: 'use', description: 'Close this prompt; select the route in /model.' },
881
- { label: 'Sign out', value: 'out', description: 'Remove the local grant and revoke it in the background.' },
882
- { label: 'Cancel', value: 'cancel' },
883
- ],
884
- initialValue: 'use',
885
- allowCustom: false,
886
- notify: false,
887
- signal: invocation.signal,
888
- });
889
- if (choice === null || choice === 'cancel' || choice === 'use') {
890
- return { kind: 'success', text: 'Use /model to select the deepseek-account route.' };
891
- }
892
- try {
893
- await account.signOut(accountClientMetadata());
894
- }
895
- catch {
896
- return { kind: 'error', text: 'Could not sign out of the DeepSeek account. Try again from DSH Web/Desktop.' };
897
- }
898
- return { kind: 'success', text: 'Signed out of the DeepSeek account. The next account request requires sign-in again.' };
899
- }
900
- const server = ctx.get('webServer');
901
- if (server === undefined || ctx.tui.interactive === false) {
902
- return {
903
- kind: 'error',
904
- text: 'The loopback sign-in host is unavailable. Start omdsh with its normal terminal host, or sign in through DSH Web/Desktop and rerun /login.',
905
- };
906
- }
907
- const origin = accountCallbackOrigin(ctx);
908
- if (origin === undefined)
909
- return { kind: 'error', text: 'The loopback sign-in host did not expose a port.' };
910
- let initial;
911
- try {
912
- initial = await account.startSignIn(accountClientMetadata(), origin, 'web');
913
- }
914
- catch (error) {
915
- return { kind: 'error', text: error instanceof Error ? error.message : 'Could not start DeepSeek sign-in.' };
916
- }
917
- const attempt = initial.attempt;
918
- const failure = accountFailure(attempt);
919
- if (failure !== undefined)
920
- return { kind: 'error', text: failure };
921
- let url = attempt?.authorizeUrl;
922
- for (let i = 0; i < 40 && url === undefined; i += 1) {
923
- if (invocation.signal.aborted)
924
- break;
925
- await new Promise(resolve => setTimeout(resolve, 100));
926
- const next = await account.getState().catch(() => undefined);
927
- const nextFailure = accountFailure(next?.attempt);
928
- if (nextFailure !== undefined)
929
- return { kind: 'error', text: nextFailure };
930
- url = next?.attempt?.authorizeUrl;
931
- }
932
- if (url === undefined) {
933
- const id = attempt?.id ?? (await account.getState().catch(() => undefined))?.attempt?.id;
934
- if (id !== undefined)
935
- await account.cancelSignIn(id).catch(() => undefined);
936
- return { kind: 'error', text: 'DeepSeek did not return a browser URL for this sign-in attempt.' };
937
- }
938
- if (config.openBrowser !== false)
939
- openExternal(url);
940
- const answer = await ctx.tui.prompt({
941
- title: 'DeepSeek account',
942
- question: 'Complete sign-in in your browser, then return here.',
943
- detail: `${url}
944
-
945
- This terminal is listening on ${origin}/oauth/callback until the attempt ends.`,
946
- options: [
947
- { label: 'I completed sign-in', value: 'done' },
948
- { label: 'Cancel', value: 'cancel', description: 'Withdraw this sign-in attempt.' },
949
- ],
950
- initialValue: 'done',
951
- allowCustom: false,
952
- notify: false,
953
- signal: invocation.signal,
954
- });
955
- if (answer !== 'done') {
956
- const id = attempt?.id ?? (await account.getState().catch(() => undefined))?.attempt?.id;
957
- if (id !== undefined)
958
- await account.cancelSignIn(id).catch(() => undefined);
959
- return { kind: 'success', text: 'DeepSeek sign-in cancelled.' };
960
- }
961
- return waitForAccount(invocation, account, 120_000);
962
- }
963
768
  async function login(ctx, invocation, config) {
964
769
  if (invocation.rawInput.trim() !== '') {
965
770
  return { kind: 'error', text: 'Usage: /login (paste the key only into the protected prompt)' };
966
771
  }
967
- const choice = await pickLoginChoice(ctx, invocation);
968
- if (choice === undefined)
969
- return { kind: 'success' };
970
- if (choice === DEEPSEEK_ACCOUNT_CHOICE)
971
- return loginDeepSeekAccount(ctx, invocation, config);
972
- if (choice === CUSTOM_PROVIDER_VALUE)
973
- return loginCustom(ctx, invocation);
974
- if (isAuthorizationFlow(choice))
975
- return loginAuthorization(ctx, invocation, choice);
976
- return isDeepSeek(choice)
977
- ? loginDeepSeek(ctx, invocation, config)
978
- : loginCatalog(ctx, invocation, choice);
772
+ const abort = new AbortController();
773
+ invocation = { ...invocation, signal: AbortSignal.any([invocation.signal, abort.signal]) };
774
+ const prompt = request => ctx.tui.prompt({ dismissLabel: 'Back', ...request,
775
+ ...(request.detail === undefined ? {} : { detail: request.detail.split('\n').map(line => uiText(resolveUiLanguage(ctx.tui.prefs?.().uiLanguage), line)).join('\n') }),
776
+ signal: invocation.signal, onSuperseded: () => abort.abort(),
777
+ });
778
+ const browse = createPromptBrowser(request => prompt({ ...request, dismissLabel: 'Cancel' }));
779
+ while (!invocation.signal.aborted) {
780
+ const choice = await pickLoginChoice(ctx, invocation, browse);
781
+ if (choice === undefined || invocation.signal.aborted)
782
+ return { kind: 'success' };
783
+ if (choice === DEEPSEEK_ACCOUNT_CHOICE) {
784
+ await openAccountSettings(ctx, invocation.signal, { ...config, signIn: true });
785
+ return { kind: 'success' };
786
+ }
787
+ if (choice === CUSTOM_PROVIDER_VALUE) {
788
+ const result = await loginCustom(ctx, invocation, prompt);
789
+ if (result !== undefined)
790
+ return result;
791
+ continue;
792
+ }
793
+ if (isAuthorizationFlow(choice))
794
+ return loginAuthorization(ctx, invocation, choice);
795
+ const result = isDeepSeek(choice)
796
+ ? await loginDeepSeek(ctx, invocation, config, prompt)
797
+ : await loginCatalog(ctx, invocation, choice, prompt);
798
+ if (result.kind !== 'success' || result.text !== undefined)
799
+ return result;
800
+ }
801
+ return { kind: 'success' };
979
802
  }
980
803
  async function logout(ctx, invocation) {
981
804
  if (invocation.rawInput.trim() !== '')
@@ -992,6 +815,7 @@ async function logout(ctx, invocation) {
992
815
  : logoutCatalog(ctx, invocation, choice);
993
816
  }
994
817
  export function apply(ctx, config = {}) {
818
+ ctx.inject(['deepseekAccount'], accountCtx => { bindAccountNotices(accountCtx); });
995
819
  registerCommands(ctx, [
996
820
  {
997
821
  name: 'login',