@armadra/agent 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/CHANGELOG.zh-CN.md +32 -0
  3. package/dist/agent/session-settings.d.ts +4 -1
  4. package/dist/agent/session-settings.js +7 -2
  5. package/dist/ai/apis/chatgpt-backend.d.ts +7 -2
  6. package/dist/ai/apis/chatgpt-backend.js +12 -4
  7. package/dist/ai/providers/discovered-cache.d.ts +59 -0
  8. package/dist/ai/providers/discovered-cache.js +146 -0
  9. package/dist/ai/providers/model-visibility.d.ts +33 -0
  10. package/dist/ai/providers/model-visibility.js +70 -0
  11. package/dist/ai/providers/registry.d.ts +2 -0
  12. package/dist/ai/providers/registry.js +13 -7
  13. package/dist/ai/types.d.ts +5 -0
  14. package/dist/auth/chatgpt/backend-client.d.ts +11 -2
  15. package/dist/auth/chatgpt/backend-client.js +38 -6
  16. package/dist/auth/chatgpt/cli.d.ts +4 -1
  17. package/dist/auth/chatgpt/cli.js +40 -6
  18. package/dist/auth/chatgpt/follow.d.ts +16 -0
  19. package/dist/auth/chatgpt/follow.js +51 -0
  20. package/dist/auth/chatgpt/presets.d.ts +8 -0
  21. package/dist/auth/chatgpt/presets.js +12 -0
  22. package/dist/auth/testing/fake-oauth.d.ts +4 -1
  23. package/dist/auth/testing/fake-oauth.js +24 -1
  24. package/dist/bundle/ama.cjs +1663 -799
  25. package/dist/cli/compose-providers.d.ts +2 -0
  26. package/dist/cli/compose-providers.js +9 -0
  27. package/dist/cli/startup-steps.js +4 -1
  28. package/dist/cli/subcommands/config-set.js +19 -11
  29. package/dist/cli/subcommands/models-discover.d.ts +4 -1
  30. package/dist/cli/subcommands/models-discover.js +51 -3
  31. package/dist/cli/subcommands/models-enable.d.ts +13 -0
  32. package/dist/cli/subcommands/models-enable.js +145 -0
  33. package/dist/cli/subcommands/models.d.ts +2 -0
  34. package/dist/cli/subcommands/models.js +10 -2
  35. package/dist/config/json-schema.js +2 -1
  36. package/dist/config/key-docs.js +7 -1
  37. package/dist/config/schema-w5.js +9 -2
  38. package/dist/config/schema-w6.d.ts +1 -1
  39. package/dist/config/schema-w6.js +2 -1
  40. package/dist/config/settings-registry.d.ts +3 -0
  41. package/dist/config/settings-registry.js +1 -0
  42. package/dist/config/types-w5.d.ts +7 -0
  43. package/dist/config/types-w5.js +2 -0
  44. package/dist/config/types-w6.d.ts +5 -0
  45. package/dist/i18n/catalog.d.ts +50 -3
  46. package/dist/i18n/messages/auth.d.ts +16 -2
  47. package/dist/i18n/messages/auth.js +34 -2
  48. package/dist/i18n/messages/cli-help.js +4 -0
  49. package/dist/i18n/messages/config-keys.d.ts +6 -0
  50. package/dist/i18n/messages/config-keys.js +8 -2
  51. package/dist/i18n/messages/config.d.ts +8 -0
  52. package/dist/i18n/messages/config.js +2 -0
  53. package/dist/i18n/messages/errors.d.ts +1 -1
  54. package/dist/i18n/messages/interactive-startup.d.ts +0 -4
  55. package/dist/i18n/messages/interactive-startup.js +0 -4
  56. package/dist/i18n/messages/interactive.d.ts +0 -4
  57. package/dist/i18n/messages/panels.d.ts +43 -0
  58. package/dist/i18n/messages/panels.js +43 -0
  59. package/dist/i18n/messages/settings.d.ts +7 -0
  60. package/dist/i18n/messages/settings.js +7 -0
  61. package/dist/i18n/messages/subcommands-config.d.ts +2 -0
  62. package/dist/i18n/messages/subcommands-config.js +2 -0
  63. package/dist/i18n/messages/subcommands-models.d.ts +34 -0
  64. package/dist/i18n/messages/subcommands-models.js +34 -0
  65. package/dist/i18n/messages/subcommands.d.ts +28 -0
  66. package/dist/i18n/messages/subcommands.js +3 -0
  67. package/dist/modes/interactive/commands.d.ts +3 -0
  68. package/dist/modes/interactive/commands.js +18 -11
  69. package/dist/modes/interactive/config-ui.js +1 -1
  70. package/dist/modes/interactive/interactive-mode.js +2 -0
  71. package/dist/modes/interactive/model-items.d.ts +45 -0
  72. package/dist/modes/interactive/model-items.js +134 -0
  73. package/dist/modes/interactive/model-picker.d.ts +57 -0
  74. package/dist/modes/interactive/model-picker.js +191 -0
  75. package/dist/modes/interactive/startup-ui.d.ts +3 -9
  76. package/dist/modes/interactive/startup-ui.js +8 -60
  77. package/dist/tui/components/select-list.d.ts +2 -0
  78. package/dist/tui/components/select-list.js +7 -0
  79. package/docs/en/providers.md +9 -3
  80. package/docs/en/tui.md +8 -1
  81. package/docs/providers.md +27 -6
  82. package/docs/tui.md +8 -1
  83. package/package.json +1 -1
@@ -125,6 +125,9 @@ export const en = {
125
125
  setDone: (key, value, scope) => `${key} = ${value} (${scope})`,
126
126
  unsetDone: (key, value, scope) => `${key} reset (${scope}); now ${value}`,
127
127
  stillOverridden: (key, source) => `${key} is still overridden by ${source}; the saved value applies once that is removed`,
128
+ /** `ama config set` when a higher layer hides the write: first line = what was written. */
129
+ writtenTo: (key, value, scope) => `${key} = ${value} (written to ${scope})`,
130
+ overriddenNow: (source, effective) => `Still overridden by ${source} (${effective}); the effective value is ${effective}`,
128
131
  slashUsage: "Usage: /config [key=value | key value]",
129
132
  errors: {
130
133
  unknownKey: (key) => `${key} isn't a /config setting. Run /config to see what's available.`,
@@ -153,6 +156,7 @@ export const en = {
153
156
  agents: "agents.<id>: ama config edit",
154
157
  hooks: "hooks.json · /hooks",
155
158
  auth: "ama auth",
159
+ modelsEnabled: "/model (Space) · ama models enable / disable",
156
160
  },
157
161
  cli: {
158
162
  usage: `Usage: ama config get <key> [--json]
@@ -289,6 +293,8 @@ export const zh = {
289
293
  setDone: (key, value, scope) => `${key} = ${value}(${scope})`,
290
294
  unsetDone: (key, value, scope) => `已恢复 ${key}(${scope}),现为 ${value}`,
291
295
  stillOverridden: (key, source) => `${key} 仍被 ${source} 覆盖,撤掉后写入的值才生效`,
296
+ writtenTo: (key, value, scope) => `${key} = ${value}(已写入${scope})`,
297
+ overriddenNow: (source, effective) => `当前仍被 ${source}(${effective})覆盖,生效值为 ${effective}`,
292
298
  slashUsage: "用法:/config [key=value | key value]",
293
299
  errors: {
294
300
  unknownKey: (key) => `${key} 不是可设置项,用 /config 查看`,
@@ -317,6 +323,7 @@ export const zh = {
317
323
  agents: "agents.<id>:ama config edit",
318
324
  hooks: "hooks.json · /hooks",
319
325
  auth: "ama auth",
326
+ modelsEnabled: "/model(Space)· ama models enable / disable",
320
327
  },
321
328
  cli: {
322
329
  usage: `用法:ama config get <key> [--json]
@@ -55,6 +55,7 @@ export declare const en: {
55
55
  probeHeader: (count: number, order: string, requests: number, extra: number, limit: number) => string;
56
56
  unavailable: string;
57
57
  probeStopped: (reason: string) => string;
58
+ cached: (path: string) => string;
58
59
  };
59
60
  };
60
61
  export declare const zh: {
@@ -107,5 +108,6 @@ export declare const zh: {
107
108
  probeHeader: (count: number, order: string, requests: number, extra: number, limit: number) => string;
108
109
  unavailable: string;
109
110
  probeStopped: (reason: string) => string;
111
+ cached: (path: string) => string;
110
112
  };
111
113
  };
@@ -64,6 +64,7 @@ export const en = {
64
64
  `${extra > 0 ? `; ${extra} more over --limit ${limit} are not probed` : ""}\n`,
65
65
  unavailable: "unavailable (all three protocols failed)",
66
66
  probeStopped: (reason) => `ama: probing stopped early (${reason})\n`,
67
+ cached: (path) => `Model list cached at ${path}; /model shows these models\n`,
67
68
  },
68
69
  };
69
70
  export const zh = {
@@ -123,5 +124,6 @@ export const zh = {
123
124
  `${extra > 0 ? `;另有 ${extra} 个超出 --limit ${limit},未探测` : ""}\n`,
124
125
  unavailable: "不可用(三种协议均失败)",
125
126
  probeStopped: (reason) => `ama: 探测提前停止(${reason})\n`,
127
+ cached: (path) => `模型列表已缓存到 ${path},/model 里会列出这些模型\n`,
126
128
  },
127
129
  };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * 消息目录:subcommands 里 `ama models enable | disable | list --enabled`(`modelsEnabled`)。由
3
+ * messages/subcommands.ts 引用。
4
+ */
5
+ export declare const en: {
6
+ modelsEnabled: {
7
+ badRef: (ref: string) => string;
8
+ unknownModel: (ref: string) => string;
9
+ added: (refs: string, path: string) => string;
10
+ alreadyListed: (refs: string) => string;
11
+ removed: (refs: string, path: string) => string;
12
+ notListed: (refs: string) => string;
13
+ cleared: (path: string) => string;
14
+ listHeader: (path: string) => string;
15
+ listUnset: string;
16
+ notInTable: string;
17
+ nothing: string;
18
+ };
19
+ };
20
+ export declare const zh: {
21
+ modelsEnabled: {
22
+ badRef: (ref: string) => string;
23
+ unknownModel: (ref: string) => string;
24
+ added: (refs: string, path: string) => string;
25
+ alreadyListed: (refs: string) => string;
26
+ removed: (refs: string, path: string) => string;
27
+ notListed: (refs: string) => string;
28
+ cleared: (path: string) => string;
29
+ listHeader: (path: string) => string;
30
+ listUnset: string;
31
+ notInTable: string;
32
+ nothing: string;
33
+ };
34
+ };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * 消息目录:subcommands 里 `ama models enable | disable | list --enabled`(`modelsEnabled`)。由
3
+ * messages/subcommands.ts 引用。
4
+ */
5
+ export const en = {
6
+ modelsEnabled: {
7
+ badRef: (ref) => `ama: ${ref} is not provider/model[@channel] or provider/*\n`,
8
+ unknownModel: (ref) => `ama: warning: ${ref} is not in the model table; written anyway (relays accept any id)\n`,
9
+ added: (refs, path) => `Added to models.enabled: ${refs} → ${path}\n`,
10
+ alreadyListed: (refs) => `Already in models.enabled: ${refs}\n`,
11
+ removed: (refs, path) => `Removed from models.enabled: ${refs} → ${path}\n`,
12
+ notListed: (refs) => `Not in models.enabled: ${refs}\n`,
13
+ cleared: (path) => `models.enabled is empty and was removed; /model shows every configured model again → ${path}\n`,
14
+ listHeader: (path) => `models.enabled (${path}):\n`,
15
+ listUnset: "models.enabled is not set; /model shows the models of configured providers:\n",
16
+ notInTable: "not in the model table",
17
+ nothing: " (none)\n",
18
+ },
19
+ };
20
+ export const zh = {
21
+ modelsEnabled: {
22
+ badRef: (ref) => `ama: ${ref} 不是 provider/model[@channel] 或 provider/*\n`,
23
+ unknownModel: (ref) => `ama: 警告:${ref} 不在模型表里,照样写入(中转站接受任意 id)\n`,
24
+ added: (refs, path) => `已加入 models.enabled:${refs} → ${path}\n`,
25
+ alreadyListed: (refs) => `已在 models.enabled 里:${refs}\n`,
26
+ removed: (refs, path) => `已移出 models.enabled:${refs} → ${path}\n`,
27
+ notListed: (refs) => `不在 models.enabled 里:${refs}\n`,
28
+ cleared: (path) => `models.enabled 已清空并删除,/model 恢复显示全部已配置的模型 → ${path}\n`,
29
+ listHeader: (path) => `models.enabled(${path}):\n`,
30
+ listUnset: "未设置 models.enabled;/model 显示已配置供应商的模型:\n",
31
+ notInTable: "不在模型表里",
32
+ nothing: " (无)\n",
33
+ },
34
+ };
@@ -225,6 +225,19 @@ export declare const en: {
225
225
  noHits: (all: boolean) => "No matches\n" | "No matches (only sessions of this directory; --all searches all)\n";
226
226
  truncated: (limit: number) => string;
227
227
  };
228
+ modelsEnabled: {
229
+ badRef: (ref: string) => string;
230
+ unknownModel: (ref: string) => string;
231
+ added: (refs: string, path: string) => string;
232
+ alreadyListed: (refs: string) => string;
233
+ removed: (refs: string, path: string) => string;
234
+ notListed: (refs: string) => string;
235
+ cleared: (path: string) => string;
236
+ listHeader: (path: string) => string;
237
+ listUnset: string;
238
+ notInTable: string;
239
+ nothing: string;
240
+ };
228
241
  configShow: {
229
242
  usage: string;
230
243
  unknownAction: (action: string) => string;
@@ -274,6 +287,7 @@ export declare const en: {
274
287
  probeHeader: (count: number, order: string, requests: number, extra: number, limit: number) => string;
275
288
  unavailable: string;
276
289
  probeStopped: (reason: string) => string;
290
+ cached: (path: string) => string;
277
291
  };
278
292
  };
279
293
  export declare const zh: {
@@ -495,6 +509,19 @@ export declare const zh: {
495
509
  noHits: (all: boolean) => "没有命中\n" | "没有命中(只搜了当前目录的会话,--all 搜全部)\n";
496
510
  truncated: (limit: number) => string;
497
511
  };
512
+ modelsEnabled: {
513
+ badRef: (ref: string) => string;
514
+ unknownModel: (ref: string) => string;
515
+ added: (refs: string, path: string) => string;
516
+ alreadyListed: (refs: string) => string;
517
+ removed: (refs: string, path: string) => string;
518
+ notListed: (refs: string) => string;
519
+ cleared: (path: string) => string;
520
+ listHeader: (path: string) => string;
521
+ listUnset: string;
522
+ notInTable: string;
523
+ nothing: string;
524
+ };
498
525
  configShow: {
499
526
  usage: string;
500
527
  unknownAction: (action: string) => string;
@@ -544,5 +571,6 @@ export declare const zh: {
544
571
  probeHeader: (count: number, order: string, requests: number, extra: number, limit: number) => string;
545
572
  unavailable: string;
546
573
  probeStopped: (reason: string) => string;
574
+ cached: (path: string) => string;
547
575
  };
548
576
  };
@@ -7,8 +7,10 @@
7
7
  */
8
8
  import { plural } from "../format.js";
9
9
  import * as configShow from "./subcommands-config.js";
10
+ import * as modelsEnabled from "./subcommands-models.js";
10
11
  export const en = {
11
12
  ...configShow.en,
13
+ ...modelsEnabled.en,
12
14
  /** 多个子命令共用。 */
13
15
  common: {
14
16
  warning: (text) => `ama: warning: ${text}\n`,
@@ -242,6 +244,7 @@ export const en = {
242
244
  };
243
245
  export const zh = {
244
246
  ...configShow.zh,
247
+ ...modelsEnabled.zh,
245
248
  common: {
246
249
  warning: (text) => `ama: 警告:${text}\n`,
247
250
  cancelled: "ama: 已取消\n",
@@ -15,6 +15,7 @@
15
15
  * 直接进子 Agent 视图(W6-A)。钩子没装时回落 commands-core(line 模式同一套文本)。
16
16
  */
17
17
  import type { AgentSession, RewindDraftText } from "../../agent/types.js";
18
+ import type { ProviderRegistryApi } from "../../ai/types.js";
18
19
  import type { SwitchRequest } from "../../cli/compose-session.js";
19
20
  import type { Runtime } from "../../cli/runtime.js";
20
21
  import type { PermissionMode } from "../../permissions/types.js";
@@ -67,6 +68,8 @@ export interface CommandUi {
67
68
  memoryPanel?(args: string): void | Promise<void>;
68
69
  /** [W6-C0] 设置面板(W6-S;`/config`,`/config key=value` 直接设一项)。 */
69
70
  configPanel?(args: string): void | Promise<void>;
71
+ /** `/model` 选择器(model-picker.ts:Tab 视图、Space 清单);没有时回落为 `pick` + `modelItems`。 */
72
+ pickModel?(providers: ProviderRegistryApi, current: string | undefined): Promise<string | undefined>;
70
73
  }
71
74
  /**
72
75
  * [W6-C0] 面板类命令 → `CommandUi` 钩子。钩子存在就调用并返回 true;不存在返回 false(回落 commands-core)。
@@ -156,19 +156,26 @@ async function handlePick(what, ui) {
156
156
  case "model": {
157
157
  const current = session.state.model;
158
158
  const ref = current === undefined ? undefined : `${current.provider}/${current.id}`;
159
- const picked = await ui.pick({
160
- title: m.modelTitle,
161
- items: await modelItems(ui.env === undefined
162
- ? ui.runtime.providers
163
- : hideFakeProvider(ui.runtime.providers, ui.env)),
164
- filterable: true,
165
- showCount: true,
166
- ...(ref !== undefined ? { selected: ref, currentValue: ref } : {}),
167
- });
159
+ const providers = ui.env === undefined
160
+ ? ui.runtime.providers
161
+ : hideFakeProvider(ui.runtime.providers, ui.env);
162
+ const picked = ui.pickModel !== undefined
163
+ ? await ui.pickModel(providers, ref)
164
+ : (await ui.pick({
165
+ title: m.modelTitle,
166
+ items: await modelItems(providers, {
167
+ current: ref,
168
+ enabled: ui.runtime.config.models?.enabled,
169
+ hints: false,
170
+ }),
171
+ filterable: true,
172
+ showCount: true,
173
+ ...(ref !== undefined ? { selected: ref, currentValue: ref } : {}),
174
+ }))?.value;
168
175
  if (picked === undefined)
169
176
  return;
170
- await session.setModel(picked.value);
171
- ui.notice("info", m.modelSet(picked.value));
177
+ await session.setModel(picked);
178
+ ui.notice("info", m.modelSet(picked));
172
179
  return;
173
180
  }
174
181
  case "session": {
@@ -232,7 +232,7 @@ export class ConfigUi {
232
232
  items = thinkingItems(true);
233
233
  else if (spec.kind === "model")
234
234
  items = [
235
- ...(await modelItems(hideFakeProvider(this.deps.runtime.providers, this.deps.context.io.env))),
235
+ ...(await modelItems(hideFakeProvider(this.deps.runtime.providers, this.deps.context.io.env), { current, enabled: this.deps.runtime.config.models?.enabled, hints: false })),
236
236
  { value: "", label: s.defaultChoice },
237
237
  ];
238
238
  else
@@ -41,6 +41,7 @@ import { configUiFor } from "./config-ui.js";
41
41
  import { createKeyDispatch } from "./key-dispatch.js";
42
42
  import { MessageView, exitSummaryLines } from "./message-view.js";
43
43
  import { openPicker } from "./pickers.js";
44
+ import { modelPickerFor } from "./model-picker.js";
44
45
  import { createRewindFlow } from "./rewind-flow.js";
45
46
  import { openTraceView } from "./trace-view.js";
46
47
  import { StartupHeader } from "./startup-header.js";
@@ -285,6 +286,7 @@ export function runInteractiveMode(runtime, context, options = {}) {
285
286
  return next;
286
287
  },
287
288
  pick: (spec) => openPicker(pickerHost, spec),
289
+ pickModel: modelPickerFor(pickerHost, runtime, context),
288
290
  confirmMode,
289
291
  rewind: () => rewind.open(),
290
292
  setDraft,
@@ -0,0 +1,45 @@
1
+ /**
2
+ * 模型选择器的列表项(`/model`、启动选择器、`/config` 的模型项共用)。
3
+ *
4
+ * - 缺省视图「已配置」:只列有 key、OAuth 已登录或本地的供应商;设置了 `models.enabled` 时只列清单内的
5
+ * (`provider/*` 整个供应商)。「全部」视图(`/model` 里 Tab)另列没配置的供应商,组标题标「未配置 key」。
6
+ * - 当前会话的模型不在视图里时置顶一行,组标题「当前」。
7
+ * - 多渠道供应商每个模型一行(首选渠道),说明里列出其它渠道;`channels`(筛选文本含 `@`)时再列出
8
+ * `model@渠道` 行。清单里逐字写了 `provider/model@渠道` 的渠道行总是列出。
9
+ * - 模型表为空的供应商(ChatGPT 订阅,未缓存发现结果)给一行提示(value 以 `HINT_PREFIX` 开头,不是模型)。
10
+ *
11
+ * 先 `loadModelCatalog`(解析 key,异步)再 `catalogItems`(同步):选择器切视图、改清单时不必重查 key。
12
+ */
13
+ import type { Model, ProviderData, ProviderRegistryApi } from "../../ai/types.js";
14
+ import { type ProviderAccess } from "../../ai/providers/model-visibility.js";
15
+ import type { SelectItem } from "../../tui.js";
16
+ /** 提示行的 value 前缀(`ama:hint:<供应商>`);选中时不是模型。 */
17
+ export declare const HINT_PREFIX = "ama:hint:";
18
+ export declare function isHintValue(value: string): boolean;
19
+ export interface CatalogProvider {
20
+ provider: ProviderData;
21
+ access: ProviderAccess;
22
+ }
23
+ export interface ModelCatalog {
24
+ providers: readonly CatalogProvider[];
25
+ }
26
+ export interface ModelItemOptions {
27
+ /** `configured`(缺省)或 `all`。 */
28
+ view?: "configured" | "all";
29
+ /** `models.enabled`;undefined / 空表示不限。 */
30
+ enabled?: readonly string[] | undefined;
31
+ /** 当前模型 `provider/model[@channel]`。 */
32
+ current?: string | undefined;
33
+ /** 列出 `model@渠道` 行。 */
34
+ channels?: boolean;
35
+ /** 列提示行(缺省 true;启动选择器与 `/config` 关掉)。 */
36
+ hints?: boolean;
37
+ }
38
+ /** 选择器里模型的说明:名称(与 id 不同时)、上下文、`img`(收图片)。 */
39
+ export declare function modelDescription(model: Model): string | undefined;
40
+ export declare function loadModelCatalog(registry: ProviderRegistryApi): Promise<ModelCatalog>;
41
+ export declare function accessLabel(access: ProviderAccess): string;
42
+ /** 目录 → 列表项(规则见文件头)。 */
43
+ export declare function catalogItems(catalog: ModelCatalog, options?: ModelItemOptions): SelectItem[];
44
+ /** 一步到位:解析 key 再生成列表项。 */
45
+ export declare function modelItems(registry: ProviderRegistryApi, options?: ModelItemOptions): Promise<SelectItem[]>;
@@ -0,0 +1,134 @@
1
+ /**
2
+ * 模型选择器的列表项(`/model`、启动选择器、`/config` 的模型项共用)。
3
+ *
4
+ * - 缺省视图「已配置」:只列有 key、OAuth 已登录或本地的供应商;设置了 `models.enabled` 时只列清单内的
5
+ * (`provider/*` 整个供应商)。「全部」视图(`/model` 里 Tab)另列没配置的供应商,组标题标「未配置 key」。
6
+ * - 当前会话的模型不在视图里时置顶一行,组标题「当前」。
7
+ * - 多渠道供应商每个模型一行(首选渠道),说明里列出其它渠道;`channels`(筛选文本含 `@`)时再列出
8
+ * `model@渠道` 行。清单里逐字写了 `provider/model@渠道` 的渠道行总是列出。
9
+ * - 模型表为空的供应商(ChatGPT 订阅,未缓存发现结果)给一行提示(value 以 `HINT_PREFIX` 开头,不是模型)。
10
+ *
11
+ * 先 `loadModelCatalog`(解析 key,异步)再 `catalogItems`(同步):选择器切视图、改清单时不必重查 key。
12
+ */
13
+ import { accessReady, enabledMatch, providerAccess, } from "../../ai/providers/model-visibility.js";
14
+ import { CHATGPT_PROVIDER_ID } from "../../auth/chatgpt/presets.js";
15
+ import { msg } from "../../i18n/index.js";
16
+ /** 提示行的 value 前缀(`ama:hint:<供应商>`);选中时不是模型。 */
17
+ export const HINT_PREFIX = "ama:hint:";
18
+ export function isHintValue(value) {
19
+ return value.startsWith(HINT_PREFIX);
20
+ }
21
+ /** 选择器里模型的说明:名称(与 id 不同时)、上下文、`img`(收图片)。 */
22
+ export function modelDescription(model) {
23
+ const ctx = model.contextWindow;
24
+ const parts = [
25
+ model.name !== "" && model.name !== model.id ? model.name : undefined,
26
+ ctx === undefined
27
+ ? undefined
28
+ : ctx >= 1_000_000
29
+ ? `${Math.round(ctx / 100_000) / 10}M`
30
+ : `${Math.round(ctx / 1000)}k`,
31
+ model.input.includes("image") ? "img" : undefined,
32
+ ].filter((x) => x !== undefined);
33
+ return parts.length > 0 ? parts.join(" · ") : undefined;
34
+ }
35
+ export async function loadModelCatalog(registry) {
36
+ const providers = [];
37
+ for (const provider of registry.list())
38
+ providers.push({ provider, access: await providerAccess(registry, provider) });
39
+ return { providers };
40
+ }
41
+ export function accessLabel(access) {
42
+ const m = msg().panels.model;
43
+ return access === "none" ? m.noKey : m[access];
44
+ }
45
+ /** 模型的其它渠道(首选渠道之外)。 */
46
+ function otherChannels(model) {
47
+ return (model.channels ?? []).filter((c) => c !== "" && c !== model.channel);
48
+ }
49
+ function row(model, group, channel) {
50
+ const suffix = channel === undefined ? "" : `@${channel}`;
51
+ const item = {
52
+ value: `${model.provider}/${model.id}${suffix}`,
53
+ label: `${model.id}${suffix}`,
54
+ group,
55
+ };
56
+ const others = channel === undefined ? otherChannels(model) : [];
57
+ const parts = [
58
+ modelDescription(model),
59
+ others.length > 0
60
+ ? msg().panels.model.otherChannels(others.map((c) => `@${c}`).join(" "))
61
+ : undefined,
62
+ ].filter((x) => x !== undefined);
63
+ if (parts.length > 0)
64
+ item.description = parts.join(" · ");
65
+ return item;
66
+ }
67
+ function hintRow(entry, group) {
68
+ const { provider, access } = entry;
69
+ if (provider.models.length > 0 || !provider.requiresApiKey)
70
+ return undefined;
71
+ const m = msg().panels.model;
72
+ if (access === "key" || access === "oauth")
73
+ return { value: `${HINT_PREFIX}${provider.id}`, label: m.discoverHint(provider.id), group };
74
+ if (provider.id === CHATGPT_PROVIDER_ID)
75
+ return { value: `${HINT_PREFIX}${provider.id}`, label: m.loginHint(provider.id), group };
76
+ return undefined;
77
+ }
78
+ /** 目录 → 列表项(规则见文件头)。 */
79
+ export function catalogItems(catalog, options = {}) {
80
+ const all = options.view === "all";
81
+ const list = options.enabled !== undefined && options.enabled.length > 0 ? options.enabled : undefined;
82
+ const ready = [];
83
+ const rest = [];
84
+ for (const entry of catalog.providers) {
85
+ const { provider, access } = entry;
86
+ const isReady = accessReady(access);
87
+ const group = `${provider.id} · ${accessLabel(access)}`;
88
+ const items = [];
89
+ const listedProvider = list?.includes(`${provider.id}/*`) === true;
90
+ for (const model of provider.models) {
91
+ const main = row(model, group);
92
+ const listed = list !== undefined && enabledMatch(list, main.value) !== undefined;
93
+ const shown = all || (list !== undefined ? listed : isReady);
94
+ if (shown) {
95
+ if (all && listed)
96
+ main.badge = msg().panels.model.listed;
97
+ if (all && listed)
98
+ main.badgeColor = "dim";
99
+ items.push(main);
100
+ }
101
+ for (const channel of otherChannels(model)) {
102
+ const extra = row(model, group, channel);
103
+ const exact = list?.includes(extra.value) === true;
104
+ if (exact || (shown && options.channels === true))
105
+ items.push(extra);
106
+ }
107
+ }
108
+ const hint = options.hints === false ? undefined : hintRow(entry, group);
109
+ if (hint !== undefined && (all || (list !== undefined ? listedProvider : isReady)))
110
+ items.push(hint);
111
+ (isReady ? ready : rest).push(...items);
112
+ }
113
+ return pinCurrent(catalog, [...ready, ...rest], options.current);
114
+ }
115
+ /** 当前模型不在列表里时置顶一行。 */
116
+ function pinCurrent(catalog, items, current) {
117
+ if (current === undefined || current === "" || items.some((i) => i.value === current))
118
+ return items;
119
+ const slash = current.indexOf("/");
120
+ const at = current.lastIndexOf("@");
121
+ const base = at > slash ? current.slice(0, at) : current;
122
+ const model = catalog.providers
123
+ .find((p) => p.provider.id === current.slice(0, slash))
124
+ ?.provider.models.find((m) => `${m.provider}/${m.id}` === base);
125
+ const item = { value: current, label: current, group: msg().panels.model.current };
126
+ const description = model === undefined ? undefined : modelDescription(model);
127
+ if (description !== undefined)
128
+ item.description = description;
129
+ return [item, ...items];
130
+ }
131
+ /** 一步到位:解析 key 再生成列表项。 */
132
+ export async function modelItems(registry, options = {}) {
133
+ return catalogItems(await loadModelCatalog(registry), options);
134
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * 交互模式的 `/model` 选择器:居中覆盖层,列表项见 model-items.ts。
3
+ *
4
+ * - `Tab` 在「已配置 / 全部」两个视图间切换(过滤文本保留);全部视图里没配置的供应商标「未配置 key」,
5
+ * 选中它的模型不切换,选择器底部提示 `ama auth set` / `ama providers add`;
6
+ * - `Space` 把高亮的模型加入 / 移出用户级 `models.enabled`(经 config/edit.ts 写回);只经 `provider/*`
7
+ * 列入的不能单独移出,提示改用 `ama models disable`;
8
+ * - 筛选文本含 `@` 时列出各模型的 `@渠道` 行;
9
+ * - 已在用清单时,从全部视图选中清单外的模型会先把它加入清单再切换;
10
+ * - 提示行(模型表为空的供应商)选中时只在底部显示命令。
11
+ */
12
+ import type { ProviderRegistryApi } from "../../ai/types.js";
13
+ import type { ModeContext } from "../../cli/deps.js";
14
+ import type { Runtime } from "../../cli/runtime.js";
15
+ import { SelectList, type Component, type Focusable } from "../../tui.js";
16
+ import { type ModelCatalog } from "./model-items.js";
17
+ import type { PickerHost } from "./pickers.js";
18
+ export interface ModelPickerOptions {
19
+ title: string;
20
+ providers: ProviderRegistryApi;
21
+ /** 当前模型 `provider/model[@channel]`。 */
22
+ current?: string | undefined;
23
+ /** 打开时的 `models.enabled`。 */
24
+ enabled?: readonly string[] | undefined;
25
+ /** 写回用户级 `models.enabled`(undefined 删除该键);抛错时错误显示在选择器底部。 */
26
+ saveEnabled(next: string[] | undefined): void;
27
+ }
28
+ type View = "configured" | "all";
29
+ /** 选择器本体(可单独测试):`done` 收到选中的模型引用或 undefined(取消)。 */
30
+ export declare class ModelPickerView implements Component, Focusable {
31
+ private readonly host;
32
+ private readonly catalog;
33
+ private readonly options;
34
+ private readonly done;
35
+ private view;
36
+ private enabled;
37
+ private notice;
38
+ private channels;
39
+ readonly list: SelectList;
40
+ constructor(host: PickerHost, catalog: ModelCatalog, options: ModelPickerOptions, done: (ref: string | undefined) => void);
41
+ get focused(): boolean;
42
+ set focused(value: boolean);
43
+ get currentView(): View;
44
+ private rebuild;
45
+ handleInput(data: string): void;
46
+ private providerOf;
47
+ private save;
48
+ private toggle;
49
+ private select;
50
+ render(width: number): string[];
51
+ invalidate(): void;
52
+ }
53
+ /** 打开 `/model` 选择器;返回选中的模型引用,取消返回 undefined。 */
54
+ export declare function openModelPicker(host: PickerHost, options: ModelPickerOptions): Promise<string | undefined>;
55
+ /** 交互模式装配:清单读 `runtime.config`,写用户级 config.json 并换上新配置。 */
56
+ export declare function modelPickerFor(host: PickerHost, runtime: Runtime, context: ModeContext): (providers: ProviderRegistryApi, current: string | undefined) => Promise<string | undefined>;
57
+ export {};