@zergai/ztc 0.2.6-dev.36101352252 → 0.2.7-dev.36236183737

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 (112) hide show
  1. package/README.md +43 -17
  2. package/bin/zmux-darwin-arm64 +0 -0
  3. package/bin/ztc-tui-darwin-arm64 +0 -0
  4. package/bin/zterm-darwin-arm64 +0 -0
  5. package/dist/App.js +19 -5
  6. package/dist/agent/agent.d.ts +5 -1
  7. package/dist/agent/agent.js +51 -8
  8. package/dist/agent/backends/account_pool.d.ts +5 -6
  9. package/dist/agent/backends/account_pool.js +39 -14
  10. package/dist/agent/backends/account_rotation.d.ts +3 -0
  11. package/dist/agent/backends/account_rotation.js +14 -3
  12. package/dist/agent/backends/codex_account_status.d.ts +42 -0
  13. package/dist/agent/backends/codex_account_status.js +196 -0
  14. package/dist/agent/backends/codex_app_server.d.ts +12 -2
  15. package/dist/agent/backends/codex_app_server.js +66 -6
  16. package/dist/agent/backends/index.d.ts +2 -0
  17. package/dist/agent/backends/index.js +13 -5
  18. package/dist/agent/backends/openai.d.ts +2 -2
  19. package/dist/agent/backends/openai.js +4 -3
  20. package/dist/agent/backends/openai_compatible.js +10 -10
  21. package/dist/agent/backends/openai_responses.d.ts +17 -5
  22. package/dist/agent/backends/openai_responses.js +2 -4
  23. package/dist/agent/backends/token_usage.js +1 -1
  24. package/dist/agent/backends/types.d.ts +3 -0
  25. package/dist/agent/commands/accounts.d.ts +7 -1
  26. package/dist/agent/commands/accounts.js +299 -101
  27. package/dist/agent/commands/config.js +11 -3
  28. package/dist/agent/commands/config_profile_switch.d.ts +2 -0
  29. package/dist/agent/commands/config_profile_switch.js +22 -0
  30. package/dist/agent/commands/context.js +4 -3
  31. package/dist/agent/commands/index.js +1 -2
  32. package/dist/agent/commands/input_mode.js +1 -1
  33. package/dist/agent/commands/model.js +7 -2
  34. package/dist/agent/commands/plan.d.ts +12 -0
  35. package/dist/agent/commands/plan.js +39 -28
  36. package/dist/agent/commands/profile.d.ts +1 -1
  37. package/dist/agent/commands/profile.js +34 -257
  38. package/dist/agent/commands/provider_choices.d.ts +12 -1
  39. package/dist/agent/commands/provider_choices.js +29 -2
  40. package/dist/agent/commands/provider_selection.d.ts +3 -0
  41. package/dist/agent/commands/provider_selection.js +63 -0
  42. package/dist/agent/commands/settings.d.ts +2 -0
  43. package/dist/agent/commands/settings.js +655 -50
  44. package/dist/agent/commands/types.d.ts +19 -2
  45. package/dist/agent/tools/ask_user.js +2 -2
  46. package/dist/app/agent_runner.d.ts +1 -1
  47. package/dist/app/agent_runner.js +2 -2
  48. package/dist/app/ask_user_response.d.ts +3 -0
  49. package/dist/app/ask_user_response.js +16 -0
  50. package/dist/app/command_context.js +29 -13
  51. package/dist/app/controller.d.ts +17 -2
  52. package/dist/app/controller.js +24 -1
  53. package/dist/app/selection_completion.d.ts +9 -3
  54. package/dist/app/selection_completion.js +54 -22
  55. package/dist/cli_args.js +10 -1
  56. package/dist/cli_main.js +85 -2
  57. package/dist/cli_ratatui.d.ts +22 -4
  58. package/dist/cli_ratatui.js +421 -146
  59. package/dist/config/defaults.d.ts +6 -0
  60. package/dist/config/defaults.js +20 -0
  61. package/dist/config/profiles.d.ts +9 -0
  62. package/dist/config/profiles.js +87 -0
  63. package/dist/config/types.d.ts +2 -0
  64. package/dist/config.d.ts +7 -0
  65. package/dist/config.js +283 -40
  66. package/dist/headless.js +14 -0
  67. package/dist/panes/model_catalog.d.ts +1 -1
  68. package/dist/panes/model_catalog.js +24 -2
  69. package/dist/panes/ztc_launch.js +6 -0
  70. package/dist/router_auth.js +3 -2
  71. package/dist/session.d.ts +2 -0
  72. package/dist/session.js +3 -0
  73. package/dist/types.d.ts +23 -2
  74. package/dist/ui/views/app.d.ts +3 -1
  75. package/dist/ui/views/app.js +3 -2
  76. package/dist/ui/views/completion_menu.js +6 -6
  77. package/dist/ui/views/input_area.d.ts +2 -1
  78. package/dist/ui/views/input_area.js +9 -8
  79. package/dist/ui/views/status_bar.d.ts +2 -0
  80. package/dist/ui/views/status_bar.js +15 -4
  81. package/dist/ui/widgets/color_panel.d.ts +9 -0
  82. package/dist/ui/widgets/color_panel.js +41 -26
  83. package/dist/ui/widgets/navigator_panel.d.ts +9 -0
  84. package/dist/ui/widgets/navigator_panel.js +17 -16
  85. package/dist/ui/widgets/panel_frame.d.ts +83 -3
  86. package/dist/ui/widgets/panel_frame.js +107 -9
  87. package/dist/ui/widgets/questionnaire.d.ts +9 -0
  88. package/dist/ui/widgets/questionnaire.js +214 -16
  89. package/dist/ui/widgets/resume_panel.d.ts +9 -0
  90. package/dist/ui/widgets/resume_panel.js +18 -17
  91. package/dist/ui/widgets/rewind_panel.d.ts +9 -0
  92. package/dist/ui/widgets/rewind_panel.js +15 -16
  93. package/dist/ui/widgets/select.d.ts +6 -0
  94. package/dist/ui/widgets/select.js +47 -12
  95. package/dist/ui/widgets/settings_panel.d.ts +75 -0
  96. package/dist/ui/widgets/settings_panel.js +197 -39
  97. package/dist/ui/widgets/status_line_panel.d.ts +9 -0
  98. package/dist/ui/widgets/status_line_panel.js +18 -19
  99. package/dist/ui/widgets/transcript_preview.d.ts +8 -0
  100. package/dist/ui/widgets/transcript_preview.js +10 -0
  101. package/dist/utils/context.js +2 -0
  102. package/dist/utils/fireworks.d.ts +20 -0
  103. package/dist/utils/fireworks.js +23 -0
  104. package/dist/utils/models.d.ts +2 -1
  105. package/dist/utils/models.js +19 -0
  106. package/dist/web/accounts_mirror.d.ts +7 -0
  107. package/dist/web/accounts_mirror.js +59 -7
  108. package/dist/web/mirror_stream.js +19 -5
  109. package/dist/zlm-embed.mjs +5734 -5362
  110. package/package.json +2 -2
  111. package/dist/agent/commands/provider.d.ts +0 -2
  112. package/dist/agent/commands/provider.js +0 -75
@@ -3,11 +3,17 @@
3
3
  * accounts. ztc stores only ids/labels/paths; tokens live in each account's own
4
4
  * credential dir, written by the official CLI's own login. Subcommands:
5
5
  *
6
- * /accounts list accounts grouped by provider
6
+ * /accounts open the account manager (or list in plain output)
7
7
  * /accounts add <provider> <id> [label…] [--path <dir>]
8
8
  * /accounts use <id> make an account the live one for its provider
9
9
  * /accounts login <id> run the official CLI login at the account's dir
10
10
  * /accounts remove <id> drop an account from the pool
11
11
  */
12
12
  import { Command } from './types.js';
13
+ /**
14
+ * The account list is written into the transcript, which is saved and mirrored
15
+ * to every viewer, so the ChatGPT email appears masked: enough to tell two
16
+ * logins apart, not enough to publish the address.
17
+ */
18
+ export declare function maskAccountEmail(email: string | undefined): string | undefined;
13
19
  export declare const accountsCommand: Command;
@@ -3,7 +3,7 @@
3
3
  * accounts. ztc stores only ids/labels/paths; tokens live in each account's own
4
4
  * credential dir, written by the official CLI's own login. Subcommands:
5
5
  *
6
- * /accounts list accounts grouped by provider
6
+ * /accounts open the account manager (or list in plain output)
7
7
  * /accounts add <provider> <id> [label…] [--path <dir>]
8
8
  * /accounts use <id> make an account the live one for its provider
9
9
  * /accounts login <id> run the official CLI login at the account's dir
@@ -11,9 +11,10 @@
11
11
  */
12
12
  import { spawnSync } from 'node:child_process';
13
13
  import { configStore } from '../../config.js';
14
- import { buildLoginCommand, canonicalAccountProvider, defaultAccountDir, defaultSourceFor, ensureAccountDir, isAccountProvider, probeAccountUsable, sourceDir, } from '../backends/account_pool.js';
14
+ import { buildLoginCommand, canonicalAccountProvider, defaultAccountDir, defaultSourceFor, ensureAccountDir, findCodexHomeCollision, isAccountProvider, probeAccountUsable, sourceDir, } from '../backends/account_pool.js';
15
15
  import { loginAutoRunEnabled, loginFailureReasonText, runLoginCommand } from '../backends/account_login.js';
16
16
  import { notifyAccountsChanged } from '../backends/account_events.js';
17
+ import { codexStatusCache } from '../backends/codex_account_status.js';
17
18
  import { runKimiDeviceLogin } from '../backends/kimi_code_auth.js';
18
19
  const PROVIDER_LABELS = {
19
20
  claude_code: 'Claude Code (claude CLI)',
@@ -28,7 +29,8 @@ function say(ctx, content) {
28
29
  function usage() {
29
30
  return [
30
31
  'Usage:',
31
- ' /accounts list accounts',
32
+ ' /accounts open account manager',
33
+ ' /accounts list print account status',
32
34
  ' /accounts add <provider> <id> [label…] [--path <dir>]',
33
35
  ' /accounts use <id> switch the live account for its provider',
34
36
  ' /accounts login <id> sign in an account (official CLI or device flow)',
@@ -38,7 +40,38 @@ function usage() {
38
40
  `Providers: ${Object.keys(PROVIDER_LABELS).join(', ')}`,
39
41
  ].join('\n');
40
42
  }
41
- function renderList(ctx) {
43
+ /**
44
+ * The account list is written into the transcript, which is saved and mirrored
45
+ * to every viewer, so the ChatGPT email appears masked: enough to tell two
46
+ * logins apart, not enough to publish the address.
47
+ */
48
+ export function maskAccountEmail(email) {
49
+ if (!email)
50
+ return undefined;
51
+ const at = email.lastIndexOf('@');
52
+ if (at <= 0)
53
+ return '***';
54
+ return `${email[0]}***${email.slice(at)}`;
55
+ }
56
+ async function accountStatusLabel(account) {
57
+ if (!probeAccountUsable(account))
58
+ return 'needs login';
59
+ if (account.source.kind !== 'codex_home')
60
+ return 'ready';
61
+ const status = await codexStatusCache.read(account.source.path);
62
+ if (status.state === 'reauth_required')
63
+ return 'needs login';
64
+ const windows = status.windows.map((window) => `${window.name} ${Math.round(window.remainingPercent)}% left`).join(', ');
65
+ const tokens = status.usage?.lifetimeTokens === undefined
66
+ ? '' : `${status.usage.lifetimeTokens.toLocaleString()} lifetime tokens${status.usageStale ? ' (old)' : ''}`;
67
+ if (status.state === 'unavailable')
68
+ return windows ? `account unverified; last observed ${windows}` : 'account unverified';
69
+ const email = maskAccountEmail(status.email);
70
+ if (status.state === 'quota_unavailable')
71
+ return [email, 'quota stale', windows, tokens].filter(Boolean).join('; ');
72
+ return [email, windows, tokens].filter(Boolean).join('; ') || 'ready';
73
+ }
74
+ async function renderList(ctx) {
42
75
  const accounts = configStore.listAccounts();
43
76
  if (accounts.length === 0) {
44
77
  say(ctx, `No accounts configured.\n\n${usage()}`);
@@ -57,13 +90,13 @@ function renderList(ctx) {
57
90
  lines.push('', PROVIDER_LABELS[key] ?? key);
58
91
  for (const account of group) {
59
92
  const marker = active?.id === account.id ? '*' : ' ';
60
- const usable = probeAccountUsable(account) ? 'ready' : 'needs login';
93
+ const usable = await accountStatusLabel(account);
61
94
  lines.push(` ${marker} ${account.id} — ${account.label} [${usable}]`);
62
95
  lines.push(` ${account.source.kind}: ${account.source.path}`);
63
96
  }
64
97
  }
65
98
  const rotation = configStore.getRotation();
66
- lines.push('', `* = live account for that provider. Auto-rotate: ${rotation.enabled ? `ON (${rotation.mode})` : 'off'}`);
99
+ lines.push('', `* = live account for that provider. Auto-rotate (Claude/Kimi only): ${rotation.enabled ? `ON (${rotation.mode})` : 'off'}. Codex switches only when you choose an account.`);
67
100
  say(ctx, lines.join('\n'));
68
101
  }
69
102
  function parseAdd(args) {
@@ -88,29 +121,41 @@ function parseAdd(args) {
88
121
  }
89
122
  return { provider, id, label: labelParts.join(' ') || id, path };
90
123
  }
91
- async function handleAdd(args, ctx) {
92
- const parsed = parseAdd(args);
93
- if (typeof parsed === 'string') {
94
- say(ctx, parsed);
95
- return;
96
- }
124
+ async function addAccount(parsed) {
97
125
  if (configStore.getAccount(parsed.id)) {
98
- say(ctx, `An account with id "${parsed.id}" already exists. Remove it first or pick another id.`);
99
- return;
126
+ return { error: 'duplicate_id', message: `An account with id "${parsed.id}" already exists. Remove it first or pick another id.` };
100
127
  }
101
128
  const dir = parsed.path ?? defaultAccountDir(parsed.provider, parsed.id);
102
129
  const source = defaultSourceFor(parsed.provider, dir);
130
+ const collision = findCodexHomeCollision(configStore.listAccounts(), source);
131
+ if (collision) {
132
+ return { error: 'codex_home_collision', message: `That Codex credential home is already used by "${collision.label}". Choose another path.` };
133
+ }
103
134
  ensureAccountDir(source);
104
135
  const account = { id: parsed.id, label: parsed.label, provider: parsed.provider, source };
105
136
  configStore.addAccount(account);
106
137
  await configStore.save();
138
+ return { account };
139
+ }
140
+ async function handleAdd(args, ctx) {
141
+ const parsed = parseAdd(args);
142
+ if (typeof parsed === 'string') {
143
+ say(ctx, parsed);
144
+ return;
145
+ }
146
+ const result = await addAccount(parsed);
147
+ if ('error' in result) {
148
+ say(ctx, result.message);
149
+ return;
150
+ }
151
+ const { account } = result;
107
152
  const ready = probeAccountUsable(account);
108
153
  const next = ready
109
154
  ? `Already has credentials. Use it with: /accounts use ${account.id}`
110
155
  : `Sign in with: /accounts login ${account.id}`;
111
156
  say(ctx, [
112
157
  `Added "${account.label}" (${account.id}) for ${PROVIDER_LABELS[account.provider]}.`,
113
- ` ${source.kind}: ${source.path}`,
158
+ ` ${account.source.kind}: ${account.source.path}`,
114
159
  next,
115
160
  ].join('\n'));
116
161
  }
@@ -125,6 +170,15 @@ async function handleUse(args, ctx) {
125
170
  say(ctx, `No account "${id}". See /accounts.`);
126
171
  return;
127
172
  }
173
+ if (account.source.kind === 'codex_home') {
174
+ const status = await codexStatusCache.read(account.source.path, true);
175
+ if (status.state === 'reauth_required' || status.state === 'unavailable') {
176
+ say(ctx, status.state === 'reauth_required'
177
+ ? `"${account.label}" needs a fresh ChatGPT sign-in. Run /accounts login ${account.id}, then switch again.`
178
+ : `"${account.label}" could not be verified. Refresh its status or sign in, then switch again.`);
179
+ return;
180
+ }
181
+ }
128
182
  configStore.setActiveAccount(account.provider, account.id);
129
183
  await configStore.save();
130
184
  const accountKey = canonicalAccountProvider(account.provider);
@@ -138,7 +192,7 @@ async function handleUse(args, ctx) {
138
192
  say(ctx, `Switched live ${PROVIDER_LABELS[account.provider]} account to "${account.label}".`);
139
193
  }
140
194
  else {
141
- say(ctx, `Set "${account.label}" active for ${PROVIDER_LABELS[account.provider]}. It goes live when you switch to that provider (/model or /provider).`);
195
+ say(ctx, `Set "${account.label}" active for ${PROVIDER_LABELS[account.provider]}. It goes live when you switch to that provider with /model.`);
142
196
  }
143
197
  }
144
198
  async function handleLogin(args, ctx) {
@@ -216,6 +270,15 @@ async function handleLogin(args, ctx) {
216
270
  say(ctx, `Signing in "${account.label}" on the ztc host — complete it in the browser…`);
217
271
  const attempt = await runLoginCommand(login, () => probeAccountUsable(account));
218
272
  if (attempt.outcome === 'succeeded') {
273
+ if (account.source.kind === 'codex_home') {
274
+ codexStatusCache.invalidate(account.source.path);
275
+ const status = await codexStatusCache.read(account.source.path, true);
276
+ if (status.state === 'reauth_required' || status.state === 'unavailable') {
277
+ notifyAccountsChanged();
278
+ say(ctx, `"${account.label}" saved credentials, but the ChatGPT subscription could not be verified. Run /accounts login ${account.id} again.`);
279
+ return;
280
+ }
281
+ }
219
282
  notifyAccountsChanged();
220
283
  say(ctx, `"${account.label}" is signed in. Make it live with: /accounts use ${account.id}`);
221
284
  }
@@ -235,7 +298,12 @@ async function handleLogin(args, ctx) {
235
298
  ].join('\n'));
236
299
  return;
237
300
  }
238
- const ready = probeAccountUsable(account);
301
+ let ready = probeAccountUsable(account);
302
+ if (ready && account.source.kind === 'codex_home') {
303
+ codexStatusCache.invalidate(account.source.path);
304
+ const status = await codexStatusCache.read(account.source.path, true);
305
+ ready = status.state === 'ready' || status.state === 'quota_unavailable';
306
+ }
239
307
  say(ctx, ready
240
308
  ? `"${account.label}" is signed in. Make it live with: /accounts use ${account.id}`
241
309
  : `Login exited without usable credentials in ${sourceDir(account.source)}. Try again: /accounts login ${account.id}`);
@@ -244,7 +312,8 @@ function describeRotation() {
244
312
  const r = configStore.getRotation();
245
313
  const hours = (ms) => `${Math.round((ms / 3_600_000) * 10) / 10}h`;
246
314
  return [
247
- `Auto-rotate: ${r.enabled ? 'ON' : 'off'}`,
315
+ `Auto-rotate (Claude/Kimi): ${r.enabled ? 'ON' : 'off'}`,
316
+ ' Codex: manual selection only',
248
317
  ` mode: ${r.mode}`,
249
318
  ` subagents: ${r.subagentInheritance}`,
250
319
  ` cooldowns: default ${hours(r.defaultCooldownMs)}, weekly ${hours(r.weeklyCooldownMs)}`,
@@ -303,86 +372,212 @@ async function handleRemove(args, ctx) {
303
372
  await configStore.save();
304
373
  say(ctx, `Removed "${account.label}" (${id}) from the pool. Its credential dir was left in place:\n ${sourceDir(account.source)}`);
305
374
  }
306
- // --- interactive CLI modal (real terminal only; text list elsewhere) ---
307
- function interactiveAddForm(ctx) {
308
- ctx.form.showForm({
309
- title: 'Add account',
310
- submitLabel: 'Add',
311
- fields: [
312
- {
313
- name: 'provider',
314
- label: 'Provider',
315
- type: 'select',
316
- options: Object.keys(PROVIDER_LABELS).map((id) => ({ value: id, label: PROVIDER_LABELS[id] })),
317
- defaultValue: 'claude_code',
375
+ // --- interactive terminal settings page (text list in non-interactive output) ---
376
+ function accountFamily(provider) {
377
+ if (provider.startsWith('claude_code'))
378
+ return 'Claude';
379
+ if (provider.startsWith('codex_'))
380
+ return 'Codex';
381
+ return 'Kimi';
382
+ }
383
+ function interactiveAddPage(ctx, provider) {
384
+ const selected = configStore.getProvider();
385
+ const draft = {
386
+ provider: provider ?? (isAccountProvider(selected) ? selected : 'claude_code'),
387
+ id: '', label: '', path: '',
388
+ };
389
+ const originalFamily = provider ? accountFamily(provider) : undefined;
390
+ const showAccounts = (family) => {
391
+ void runInteractive(ctx, family).catch((error) => say(ctx, `Could not open Accounts: ${String(error)}`));
392
+ };
393
+ const back = () => showAccounts(originalFamily);
394
+ let error = '';
395
+ let errorField;
396
+ let pending = false;
397
+ const showPage = (initialSetting) => {
398
+ const title = originalFamily ? `Add ${accountFamily(draft.provider)} account` : 'Add account';
399
+ ctx.settingsPanel?.showSettingsPanel({
400
+ title,
401
+ layout: 'page',
402
+ notice: error,
403
+ initialSetting,
404
+ categories: [
405
+ {
406
+ name: 'Account',
407
+ settings: [
408
+ {
409
+ name: 'provider', label: 'Provider', value: draft.provider, type: 'select',
410
+ options: Object.keys(PROVIDER_LABELS)
411
+ .map(id => ({ value: id, label: PROVIDER_LABELS[id] })),
412
+ description: 'Choose a provider for sign-in.',
413
+ },
414
+ { name: 'id', label: 'Account ID', value: draft.id, type: 'text', placeholder: 'work',
415
+ formatValue: value => value || '(required)',
416
+ description: 'Unique name used to switch.' },
417
+ { name: 'label', label: 'Label', value: draft.label, type: 'text', placeholder: 'Optional',
418
+ formatValue: value => value || '(uses ID)',
419
+ description: 'Optional name shown in Accounts.' },
420
+ { name: 'path', label: 'Credential directory', value: draft.path, type: 'text', placeholder: 'Automatic',
421
+ formatValue: value => value || 'Automatic',
422
+ description: 'Blank creates an isolated profile.' },
423
+ {
424
+ name: 'create', label: 'Create account', value: '', type: 'text',
425
+ formatValue: () => '↵',
426
+ description: 'Save now; sign in from Accounts.',
427
+ onActivate: () => {
428
+ if (pending)
429
+ return;
430
+ const id = draft.id.trim();
431
+ if (!id) {
432
+ error = 'Account ID required';
433
+ errorField = 'id';
434
+ showPage('id');
435
+ return;
436
+ }
437
+ pending = true;
438
+ void addAccount({
439
+ provider: draft.provider,
440
+ id,
441
+ label: draft.label.trim() || id,
442
+ path: draft.path.trim() || undefined,
443
+ }).then(result => {
444
+ if ('error' in result) {
445
+ error = result.error === 'duplicate_id' ? 'Account ID already exists' : 'Credential directory in use';
446
+ errorField = result.error === 'duplicate_id' ? 'id' : 'path';
447
+ showPage(errorField);
448
+ }
449
+ else {
450
+ showAccounts(accountFamily(result.account.provider));
451
+ }
452
+ }).catch((cause) => {
453
+ error = `Could not add account: ${String(cause)}`;
454
+ errorField = 'create';
455
+ showPage('create');
456
+ }).finally(() => { pending = false; });
457
+ },
458
+ },
459
+ { name: 'back', label: 'Back to accounts', value: '', type: 'text',
460
+ formatValue: () => '↵', description: 'Return without saving.', onActivate: back },
461
+ ],
462
+ },
463
+ ],
464
+ onSettingChange: (name, value) => {
465
+ const changedProvider = name === 'provider' && isAccountProvider(value);
466
+ if (changedProvider) {
467
+ draft.provider = value;
468
+ }
469
+ else if (name === 'id')
470
+ draft.id = value;
471
+ else if (name === 'label')
472
+ draft.label = value;
473
+ else if (name === 'path')
474
+ draft.path = value;
475
+ const resolvedError = Boolean(error) && (name === errorField || (changedProvider && errorField === 'path'));
476
+ if (resolvedError) {
477
+ error = '';
478
+ errorField = undefined;
479
+ }
480
+ if (changedProvider)
481
+ showPage('id');
482
+ else if (resolvedError)
483
+ showPage(name);
318
484
  },
319
- { name: 'id', label: 'Account id (e.g. work)', type: 'text' },
320
- { name: 'label', label: 'Label (optional)', type: 'text' },
321
- { name: 'path', label: 'Credential dir (optional — reuse an existing one)', type: 'text' },
322
- ],
323
- }, async (values) => {
324
- if (!values.id) {
325
- // Empty values map = cancelled (browser × or terminal ESC), or a submit
326
- // with no id. Either way nothing is added; don't frame it as an error.
327
- say(ctx, 'Add cancelled (no account id given).');
328
- return;
329
- }
330
- const args = [values.provider, values.id];
331
- if (values.label)
332
- args.push(...values.label.split(/\s+/));
333
- if (values.path)
334
- args.push('--path', values.path);
335
- await handleAdd(args, ctx);
336
- });
485
+ onClose: back,
486
+ });
487
+ };
488
+ showPage();
337
489
  }
338
- function runInteractive(ctx) {
490
+ async function runInteractive(ctx, focus) {
339
491
  const accounts = configStore.listAccounts();
340
- const options = accounts.map((account) => {
341
- const active = configStore.getActiveAccount(account.provider)?.id === account.id;
342
- const ready = probeAccountUsable(account) ? 'ready' : 'needs login';
343
- return {
344
- value: `acct:${account.id}`,
345
- label: `${active ? '● ' : ' '}${account.label} (${account.id}) — ${ready}`,
346
- description: PROVIDER_LABELS[account.provider],
492
+ const families = [
493
+ { name: 'Claude', provider: 'claude_code', matches: account => account.provider.startsWith('claude_code') },
494
+ { name: 'Codex', provider: 'codex_app_server', matches: account => account.provider.startsWith('codex_') },
495
+ { name: 'Kimi', provider: 'kimi_code', matches: account => account.provider === 'kimi_code' },
496
+ ];
497
+ const categories = await Promise.all(families.map(async (family) => {
498
+ const group = accounts.filter(family.matches);
499
+ const statuses = await Promise.all(group.map(accountStatusLabel));
500
+ const refresh = () => { void runInteractive(ctx, family.name); };
501
+ const runAction = (action) => {
502
+ void action().catch((error) => say(ctx, `Account action failed: ${String(error)}`)).finally(refresh);
347
503
  };
348
- });
349
- options.push({ value: 'add', label: '+ Add account', description: 'Register a new account' });
350
- ctx.select.showSelect({ title: 'Accounts', question: 'Pick an account or add one', type: 'select', options }, (value) => {
351
- if (value === null)
352
- return;
353
- const choice = Array.isArray(value) ? value[0] : value;
354
- if (choice === 'add') {
355
- interactiveAddForm(ctx);
356
- return;
357
- }
358
- const id = choice.startsWith('acct:') ? choice.slice('acct:'.length) : choice;
359
- ctx.select.showSelect({
360
- title: id,
361
- question: `Action for "${id}"`,
362
- type: 'select',
363
- options: [
364
- { value: 'use', label: 'Use (make live for its provider)' },
365
- { value: 'login', label: 'Login (sign in)' },
366
- { value: 'remove', label: 'Remove from pool' },
504
+ return {
505
+ name: family.name,
506
+ settings: [
507
+ {
508
+ name: `add:${family.name}`,
509
+ label: `+ Add ${family.name} account`,
510
+ value: '',
511
+ type: 'text',
512
+ description: 'Keep another account signed in for a quick manual switch.',
513
+ onActivate: () => interactiveAddPage(ctx, family.provider),
514
+ },
515
+ ...(family.name === 'Codex' ? [{
516
+ name: 'refresh:Codex',
517
+ label: 'Refresh Codex status',
518
+ value: '',
519
+ type: 'text',
520
+ description: 'Check sign-in, quota, and token activity again for every Codex account.',
521
+ onActivate: () => runAction(async () => {
522
+ await Promise.all(group.filter(account => account.source.kind === 'codex_home')
523
+ .map(account => codexStatusCache.read(account.source.path, true)));
524
+ }),
525
+ }] : []),
526
+ ...group.flatMap((account, index) => {
527
+ const active = configStore.getActiveAccount(account.provider)?.id === account.id;
528
+ const status = statuses[index];
529
+ return [
530
+ {
531
+ name: `use:${account.id}`,
532
+ label: `${active ? '● ' : ''}${account.label}`,
533
+ value: `${active ? 'In use · ' : ''}${status}`,
534
+ type: 'text',
535
+ description: `${PROVIDER_LABELS[account.provider]} · ${account.id} · ${sourceDir(account.source)}. Enter to use this account.`,
536
+ onActivate: () => runAction(() => handleUse([account.id], ctx)),
537
+ },
538
+ {
539
+ name: `login:${account.id}`,
540
+ label: `Sign in · ${account.label}`,
541
+ value: '',
542
+ type: 'text',
543
+ description: 'Refresh this account’s sign-in without signing out the others.',
544
+ onActivate: () => runAction(() => handleLogin([account.id], ctx)),
545
+ },
546
+ {
547
+ name: `remove:${account.id}`,
548
+ label: `Remove · ${account.label}`,
549
+ value: '',
550
+ type: 'text',
551
+ description: 'Forget this account. Its credential directory stays on disk.',
552
+ onActivate: () => ctx.settingsPanel?.showSettingsPanel({
553
+ title: `Remove ${account.label}?`,
554
+ categories: [{ name: 'Confirm removal', settings: [
555
+ {
556
+ name: 'keepAccount', label: 'Keep account', value: '', type: 'text',
557
+ onActivate: refresh,
558
+ },
559
+ {
560
+ name: 'confirmRemoveAccount', label: `Remove ${account.label}`, value: '', type: 'text',
561
+ description: 'The credential directory stays on disk.',
562
+ onActivate: () => runAction(() => handleRemove([account.id], ctx)),
563
+ },
564
+ ] }],
565
+ onSettingChange: () => { },
566
+ onClose: refresh,
567
+ }),
568
+ },
569
+ ];
570
+ }),
367
571
  ],
368
- }, async (action) => {
369
- if (action === null)
370
- return;
371
- const act = Array.isArray(action) ? action[0] : action;
372
- switch (act) {
373
- case 'use':
374
- await handleUse([id], ctx);
375
- return;
376
- case 'login':
377
- handleLogin([id], ctx);
378
- return;
379
- case 'remove':
380
- await handleRemove([id], ctx);
381
- return;
382
- default:
383
- say(ctx, `Unknown action "${act}".`);
384
- }
385
- });
572
+ };
573
+ }));
574
+ const provider = configStore.getProvider();
575
+ const defaultFocus = provider.startsWith('codex_') ? 'Codex'
576
+ : provider.startsWith('claude_code') ? 'Claude'
577
+ : provider === 'kimi_code' ? 'Kimi' : undefined;
578
+ ctx.settingsPanel?.showSettingsPanel({
579
+ title: 'Accounts', categories, initialCategory: focus ?? defaultFocus,
580
+ onSettingChange: () => { },
386
581
  });
387
582
  }
388
583
  export const accountsCommand = {
@@ -397,23 +592,26 @@ export const accountsCommand = {
397
592
  // auto-starts in every interactive session, so `isRunning()` is almost
398
593
  // always true — gating on it sent `/accounts` to the browser even when no
399
594
  // browser was open, so the terminal showed nothing. `hasViewers()` is true
400
- // only when a browser is really watching: then open the rich panel there;
401
- // otherwise fall back to the terminal picker (TTY) or a text list.
595
+ // only when a browser is really watching: then open Settings there;
596
+ // otherwise use the settings surface (terminal or parent mirror) or a text list.
402
597
  if (ctx.mirror?.hasViewers?.()) {
403
- ctx.mirror.publish?.({ type: 'open_accounts', data: {} });
598
+ ctx.mirror.publish?.({ type: 'settings_page', data: { title: 'Settings', filter: 'accounts' } });
404
599
  }
405
- else if (process.stdout.isTTY) {
406
- runInteractive(ctx);
600
+ else if (ctx.settingsPanel) {
601
+ await runInteractive(ctx);
407
602
  }
408
603
  else {
409
- renderList(ctx);
604
+ await renderList(ctx);
410
605
  }
411
606
  return;
412
607
  case 'list':
413
- renderList(ctx);
608
+ await renderList(ctx);
414
609
  return;
415
610
  case 'add':
416
- await handleAdd(rest, ctx);
611
+ if (rest.length === 0 && ctx.settingsPanel)
612
+ interactiveAddPage(ctx);
613
+ else
614
+ await handleAdd(rest, ctx);
417
615
  return;
418
616
  case 'use':
419
617
  case 'switch':
@@ -1,7 +1,7 @@
1
1
  import { DEFAULT_SPINNER_VERBS, formatSpinnerVerbs, parseSpinnerVerbs } from '../../utils/spinner_verbs.js';
2
2
  import { CODEX_REASONING_EFFORTS, } from '../../config/types.js';
3
3
  import { runOpenAICompatibleWizard } from './openai_compat_wizard.js';
4
- import { settingsCommand } from './settings.js';
4
+ import { openFactoryResetConfirmation, settingsCommand } from './settings.js';
5
5
  import { contextProgressBarConfig, normalizeBarColor, publishContextProgressBar, withBarColor, } from './context_progress_bar.js';
6
6
  import { compatEndpoint, DEFAULT_CONTEXT_PROGRESS_BAR } from '../../config.js';
7
7
  import { parseGoalTurnCap } from './goal.js';
@@ -116,7 +116,7 @@ const STATUS_LINE_USAGE = [
116
116
  export const configCommand = {
117
117
  name: 'config',
118
118
  description: 'Manage configuration',
119
- usage: '[show|scope|local|global|key|provider|endpoint|openai-compat|auth-mode|model|effort|display|spinner|statusline|goal-max-turns|manifests-dir|context-progress-bar|workflows] [value]',
119
+ usage: '[show|reset|scope|local|global|key|provider|endpoint|openai-compat|auth-mode|model|effort|display|spinner|statusline|goal-max-turns|manifests-dir|context-progress-bar|workflows] [value]',
120
120
  agentRunnable: true,
121
121
  category: 'config',
122
122
  handler: async (args, ctx) => {
@@ -139,6 +139,14 @@ export const configCommand = {
139
139
  return;
140
140
  }
141
141
  switch (subCmd) {
142
+ case 'reset': {
143
+ if (rest.length > 0) {
144
+ ctx.addMessage({ role: 'system', content: 'Usage: /config reset' });
145
+ return;
146
+ }
147
+ openFactoryResetConfirmation(ctx);
148
+ return;
149
+ }
142
150
  case 'scope': {
143
151
  const current = ctx.config.getConfigScope?.() || 'global';
144
152
  const requested = value.trim().toLowerCase();
@@ -498,7 +506,7 @@ Usage: /config zrouter-url <url|clear>` });
498
506
  }
499
507
  ctx.addMessage({
500
508
  role: 'system',
501
- content: `Models (${provider})\nCurrent model: ${currentModel}\n\nUsage: /model <model-name>${modelListSuffix}`
509
+ content: `Models (${provider})\nCurrent model: ${currentModel}\n\nUsage: /model <model-name> or /model <provider> [model-name]${modelListSuffix}`
502
510
  });
503
511
  return;
504
512
  }
@@ -0,0 +1,2 @@
1
+ import type { CommandContext } from './types.js';
2
+ export declare function switchConfigProfile(ctx: CommandContext, requested: string, allowNew?: boolean): Promise<void>;
@@ -0,0 +1,22 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { activeProfile, builtinProfileEmulation, listConfigProfiles, profileConfigDir, validateProfileName } from '../../config/profiles.js';
3
+ export async function switchConfigProfile(ctx, requested, allowNew = false) {
4
+ const name = validateProfileName(requested.trim());
5
+ if (!ctx.switchConfigProfile) {
6
+ throw new Error('This renderer cannot switch config profiles in place.');
7
+ }
8
+ if (!allowNew && !(await listConfigProfiles()).includes(name)) {
9
+ throw new Error(`Config profile "${name}" does not exist. Use /profile create ${name} to start a new one.`);
10
+ }
11
+ if (allowNew && (name === 'default' || builtinProfileEmulation(name))) {
12
+ throw new Error(`Config profile "${name}" is built in. Use /profile ${name} to select it.`);
13
+ }
14
+ if (allowNew && name !== 'default')
15
+ await mkdir(profileConfigDir(name), { recursive: true, mode: 0o700 });
16
+ if (name === activeProfile()) {
17
+ ctx.addMessage({ role: 'system', content: `Already using config profile ${name}.` });
18
+ return;
19
+ }
20
+ await ctx.switchConfigProfile(name);
21
+ ctx.addMessage({ role: 'system', content: `Switched to config profile ${name} in this session.` });
22
+ }
@@ -113,11 +113,12 @@ export const contextCommand = {
113
113
  // 0% would read as a cold prefix when the truth is "no telemetry".
114
114
  if (!estimated) {
115
115
  const cached = ctx.getCachedInputTokens?.();
116
- const hitRate = cacheHitRate(cached, usedTokens);
116
+ const cacheInput = ctx.getCacheInputTokens?.() ?? usedTokens;
117
+ const hitRate = cacheHitRate(cached, cacheInput);
117
118
  if (hitRate !== undefined && cached !== undefined) {
118
119
  lines.push('');
119
- lines.push('Prompt cache (this turn):');
120
- lines.push(` ${hitRate.toFixed(0)}% served from cache (${formatTokens(cached)} of ${formatTokens(usedTokens)} input tokens)`);
120
+ lines.push('Prompt cache (last model call):');
121
+ lines.push(` ${hitRate.toFixed(0)}% served from cache (${formatTokens(cached)} of ${formatTokens(cacheInput)} input tokens)`);
121
122
  }
122
123
  }
123
124
  // Approximate composition — estimate only, for orientation. The authoritative