@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.
- package/README.md +43 -17
- package/bin/zmux-darwin-arm64 +0 -0
- package/bin/ztc-tui-darwin-arm64 +0 -0
- package/bin/zterm-darwin-arm64 +0 -0
- package/dist/App.js +19 -5
- package/dist/agent/agent.d.ts +5 -1
- package/dist/agent/agent.js +51 -8
- package/dist/agent/backends/account_pool.d.ts +5 -6
- package/dist/agent/backends/account_pool.js +39 -14
- package/dist/agent/backends/account_rotation.d.ts +3 -0
- package/dist/agent/backends/account_rotation.js +14 -3
- package/dist/agent/backends/codex_account_status.d.ts +42 -0
- package/dist/agent/backends/codex_account_status.js +196 -0
- package/dist/agent/backends/codex_app_server.d.ts +12 -2
- package/dist/agent/backends/codex_app_server.js +66 -6
- package/dist/agent/backends/index.d.ts +2 -0
- package/dist/agent/backends/index.js +13 -5
- package/dist/agent/backends/openai.d.ts +2 -2
- package/dist/agent/backends/openai.js +4 -3
- package/dist/agent/backends/openai_compatible.js +10 -10
- package/dist/agent/backends/openai_responses.d.ts +17 -5
- package/dist/agent/backends/openai_responses.js +2 -4
- package/dist/agent/backends/token_usage.js +1 -1
- package/dist/agent/backends/types.d.ts +3 -0
- package/dist/agent/commands/accounts.d.ts +7 -1
- package/dist/agent/commands/accounts.js +299 -101
- package/dist/agent/commands/config.js +11 -3
- package/dist/agent/commands/config_profile_switch.d.ts +2 -0
- package/dist/agent/commands/config_profile_switch.js +22 -0
- package/dist/agent/commands/context.js +4 -3
- package/dist/agent/commands/index.js +1 -2
- package/dist/agent/commands/input_mode.js +1 -1
- package/dist/agent/commands/model.js +7 -2
- package/dist/agent/commands/plan.d.ts +12 -0
- package/dist/agent/commands/plan.js +39 -28
- package/dist/agent/commands/profile.d.ts +1 -1
- package/dist/agent/commands/profile.js +34 -257
- package/dist/agent/commands/provider_choices.d.ts +12 -1
- package/dist/agent/commands/provider_choices.js +29 -2
- package/dist/agent/commands/provider_selection.d.ts +3 -0
- package/dist/agent/commands/provider_selection.js +63 -0
- package/dist/agent/commands/settings.d.ts +2 -0
- package/dist/agent/commands/settings.js +655 -50
- package/dist/agent/commands/types.d.ts +19 -2
- package/dist/agent/tools/ask_user.js +2 -2
- package/dist/app/agent_runner.d.ts +1 -1
- package/dist/app/agent_runner.js +2 -2
- package/dist/app/ask_user_response.d.ts +3 -0
- package/dist/app/ask_user_response.js +16 -0
- package/dist/app/command_context.js +29 -13
- package/dist/app/controller.d.ts +17 -2
- package/dist/app/controller.js +24 -1
- package/dist/app/selection_completion.d.ts +9 -3
- package/dist/app/selection_completion.js +54 -22
- package/dist/cli_args.js +10 -1
- package/dist/cli_main.js +85 -2
- package/dist/cli_ratatui.d.ts +22 -4
- package/dist/cli_ratatui.js +421 -146
- package/dist/config/defaults.d.ts +6 -0
- package/dist/config/defaults.js +20 -0
- package/dist/config/profiles.d.ts +9 -0
- package/dist/config/profiles.js +87 -0
- package/dist/config/types.d.ts +2 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +283 -40
- package/dist/headless.js +14 -0
- package/dist/panes/model_catalog.d.ts +1 -1
- package/dist/panes/model_catalog.js +24 -2
- package/dist/panes/ztc_launch.js +6 -0
- package/dist/router_auth.js +3 -2
- package/dist/session.d.ts +2 -0
- package/dist/session.js +3 -0
- package/dist/types.d.ts +23 -2
- package/dist/ui/views/app.d.ts +3 -1
- package/dist/ui/views/app.js +3 -2
- package/dist/ui/views/completion_menu.js +6 -6
- package/dist/ui/views/input_area.d.ts +2 -1
- package/dist/ui/views/input_area.js +9 -8
- package/dist/ui/views/status_bar.d.ts +2 -0
- package/dist/ui/views/status_bar.js +15 -4
- package/dist/ui/widgets/color_panel.d.ts +9 -0
- package/dist/ui/widgets/color_panel.js +41 -26
- package/dist/ui/widgets/navigator_panel.d.ts +9 -0
- package/dist/ui/widgets/navigator_panel.js +17 -16
- package/dist/ui/widgets/panel_frame.d.ts +83 -3
- package/dist/ui/widgets/panel_frame.js +107 -9
- package/dist/ui/widgets/questionnaire.d.ts +9 -0
- package/dist/ui/widgets/questionnaire.js +214 -16
- package/dist/ui/widgets/resume_panel.d.ts +9 -0
- package/dist/ui/widgets/resume_panel.js +18 -17
- package/dist/ui/widgets/rewind_panel.d.ts +9 -0
- package/dist/ui/widgets/rewind_panel.js +15 -16
- package/dist/ui/widgets/select.d.ts +6 -0
- package/dist/ui/widgets/select.js +47 -12
- package/dist/ui/widgets/settings_panel.d.ts +75 -0
- package/dist/ui/widgets/settings_panel.js +197 -39
- package/dist/ui/widgets/status_line_panel.d.ts +9 -0
- package/dist/ui/widgets/status_line_panel.js +18 -19
- package/dist/ui/widgets/transcript_preview.d.ts +8 -0
- package/dist/ui/widgets/transcript_preview.js +10 -0
- package/dist/utils/context.js +2 -0
- package/dist/utils/fireworks.d.ts +20 -0
- package/dist/utils/fireworks.js +23 -0
- package/dist/utils/models.d.ts +2 -1
- package/dist/utils/models.js +19 -0
- package/dist/web/accounts_mirror.d.ts +7 -0
- package/dist/web/accounts_mirror.js +59 -7
- package/dist/web/mirror_stream.js +19 -5
- package/dist/zlm-embed.mjs +5734 -5362
- package/package.json +2 -2
- package/dist/agent/commands/provider.d.ts +0 -2
- package/dist/agent/commands/provider.js +0 -75
|
@@ -14,12 +14,11 @@
|
|
|
14
14
|
* reordering hard to land, so the pickup is explicit and you can see the row
|
|
15
15
|
* travel.
|
|
16
16
|
*/
|
|
17
|
-
import { box, text } from '../core/index.js';
|
|
18
17
|
import { displayWidth, padEndDisplay } from '../core/text_width.js';
|
|
19
18
|
import { TERMINAL_THEME } from '../views/theme.js';
|
|
20
19
|
import { fuzzyMatch } from '../../utils/fuzzy_match.js';
|
|
21
20
|
import { cycleStatusLineVariant, resolveStatusLineVariants, statusLineItemGroups, statusLineItemOptions, STATUS_LINE_VARIANT_LABELS, } from '../status_line_config.js';
|
|
22
|
-
import { bodyBand, bottomBorder, clipText, dropLastGrapheme, fitSegments, framedLine, isInsideBand, joinColumns, lineText, planPanelSplit, sideBySideBands, stackedBands, topBorder, } from './panel_frame.js';
|
|
21
|
+
import { bodyBand, bottomBorder, clipText, dropLastGrapheme, fitSegments, framedLine, isInsideBand, joinColumns, lineText, planPanelSplit, sideBySideBands, stackedBands, topBorder, buildWidgetFrameView, widgetFrameGeometry, } from './panel_frame.js';
|
|
23
22
|
/** The order pane is the point of this page, so it keeps a workable width. */
|
|
24
23
|
const ORDER_PANE_MIN_WIDTH = 34;
|
|
25
24
|
/** Below this the panes stack instead of splitting. */
|
|
@@ -34,6 +33,15 @@ function sideLabel(side) {
|
|
|
34
33
|
export class StatusLinePanelWidget {
|
|
35
34
|
host;
|
|
36
35
|
active = null;
|
|
36
|
+
/**
|
|
37
|
+
* Column the painted frame starts on, in surface coordinates.
|
|
38
|
+
*
|
|
39
|
+
* Zero unless the terminal is wide enough to trigger the shared cap, in which
|
|
40
|
+
* case it is that centring margin. The wheel bands add it back: a notch lands
|
|
41
|
+
* in terminal columns, so without it a capped page would scroll from a pane
|
|
42
|
+
* that is no longer under the pointer.
|
|
43
|
+
*/
|
|
44
|
+
frameInset = 0;
|
|
37
45
|
draft = {
|
|
38
46
|
left: [],
|
|
39
47
|
right: [],
|
|
@@ -620,17 +628,7 @@ export class StatusLinePanelWidget {
|
|
|
620
628
|
const lines = this.buildSurfaceLines(cols, maxRows);
|
|
621
629
|
return {
|
|
622
630
|
content: lines.map(lineText).join('\n'),
|
|
623
|
-
view:
|
|
624
|
-
flexDirection: 'row',
|
|
625
|
-
height: 1,
|
|
626
|
-
flexShrink: 0,
|
|
627
|
-
backgroundColor: line.backgroundColor,
|
|
628
|
-
})), {
|
|
629
|
-
flexDirection: 'column',
|
|
630
|
-
height: lines.length,
|
|
631
|
-
flexShrink: 0,
|
|
632
|
-
backgroundColor: TERMINAL_THEME.bg,
|
|
633
|
-
}),
|
|
631
|
+
view: buildWidgetFrameView(lines, this.frameInset),
|
|
634
632
|
lineCount: lines.length,
|
|
635
633
|
};
|
|
636
634
|
}
|
|
@@ -643,9 +641,10 @@ export class StatusLinePanelWidget {
|
|
|
643
641
|
if (maxRows !== undefined && maxRows <= 0)
|
|
644
642
|
return [];
|
|
645
643
|
const termCols = Math.max(1, Math.floor(cols || process.stdout.columns || 80));
|
|
646
|
-
// The host embeds the surface in a `paddingX: 1` column, so
|
|
647
|
-
//
|
|
648
|
-
const width =
|
|
644
|
+
// The host embeds the surface in a `paddingX: 1` column, so the frame asks
|
|
645
|
+
// for `termCols - 2` and the shared cap centres whatever is left over.
|
|
646
|
+
const { width, inset } = widgetFrameGeometry(termCols);
|
|
647
|
+
this.frameInset = inset;
|
|
649
648
|
const height = Math.max(1, Math.floor(maxRows ?? 20));
|
|
650
649
|
const innerWidth = Math.max(0, width - 4);
|
|
651
650
|
const lines = [topBorder(width, 'Status line', `${this.sideItems(this.side).length} on ${this.side}`)];
|
|
@@ -690,7 +689,7 @@ export class StatusLinePanelWidget {
|
|
|
690
689
|
...padRow(order.rows[index], split.previewWidth),
|
|
691
690
|
], width));
|
|
692
691
|
}
|
|
693
|
-
const bands = sideBySideBands(sideBandTop, lines.length - 1, split.listWidth, split.previewWidth);
|
|
692
|
+
const bands = sideBySideBands(sideBandTop, lines.length - 1, split.listWidth, split.previewWidth, this.frameInset);
|
|
694
693
|
this.fieldBand = bands.list;
|
|
695
694
|
this.lineBand = bands.preview;
|
|
696
695
|
}
|
|
@@ -718,7 +717,7 @@ export class StatusLinePanelWidget {
|
|
|
718
717
|
// Stacked, the panes share the width and are told apart by rows. The
|
|
719
718
|
// field pane's top is the heading row it opened with (or the first row it
|
|
720
719
|
// drew when the frame had no room for headings).
|
|
721
|
-
const bands = stackedBands({ top: stackedFieldBottom - fieldCapacity, bottom: stackedFieldBottom }, { top: stackedLineTop, bottom: lines.length - 1 }, innerWidth);
|
|
720
|
+
const bands = stackedBands({ top: stackedFieldBottom - fieldCapacity, bottom: stackedFieldBottom }, { top: stackedLineTop, bottom: lines.length - 1 }, innerWidth, this.frameInset);
|
|
722
721
|
this.fieldBand = bands.list;
|
|
723
722
|
this.lineBand = bands.preview;
|
|
724
723
|
}
|
|
@@ -735,7 +734,7 @@ export class StatusLinePanelWidget {
|
|
|
735
734
|
lines.push(framedLine(windowed.rows[index]?.segments ?? [], width));
|
|
736
735
|
}
|
|
737
736
|
// Too short for two panes, so only the one on screen is a wheel target.
|
|
738
|
-
const band = bodyBand(singleTop, lines.length - 1, innerWidth);
|
|
737
|
+
const band = bodyBand(singleTop, lines.length - 1, innerWidth, this.frameInset);
|
|
739
738
|
this.fieldBand = kind === 'fields' ? band : null;
|
|
740
739
|
this.lineBand = kind === 'line' ? band : null;
|
|
741
740
|
}
|
|
@@ -124,6 +124,14 @@ export interface TranscriptHeaderInput {
|
|
|
124
124
|
focused: boolean;
|
|
125
125
|
/** Heading before the separator, e.g. `PREVIEW`. */
|
|
126
126
|
heading?: string;
|
|
127
|
+
/**
|
|
128
|
+
* Columns the heading has to fit in.
|
|
129
|
+
*
|
|
130
|
+
* Without it an over-long heading reports four facts and lets the frame cut
|
|
131
|
+
* the last one in half. With it the heading keeps the title and the first
|
|
132
|
+
* fact whole, and the caller clips words it can afford to lose.
|
|
133
|
+
*/
|
|
134
|
+
maxWidth?: number;
|
|
127
135
|
}
|
|
128
136
|
/**
|
|
129
137
|
* Pane heading: marker, title, and how much of the transcript is on screen.
|
|
@@ -263,5 +263,15 @@ export function transcriptHeaderText(input) {
|
|
|
263
263
|
const last = Math.min(total, input.scroll + rows);
|
|
264
264
|
parts.push(`${first}-${last}/${total}`);
|
|
265
265
|
}
|
|
266
|
+
// A narrow pane gets the title and the first fact, not four items clipped
|
|
267
|
+
// mid-word: the title says which session this is, and the first fact says how
|
|
268
|
+
// big it is. The caller drops whatever still does not fit, but it never has
|
|
269
|
+
// to cut a fact in half — each one is dropped whole.
|
|
270
|
+
if (input.maxWidth !== undefined && displayWidth(`${marker} ${heading} · ${parts.join(' · ')}`) > input.maxWidth) {
|
|
271
|
+
const kept = [parts[0]];
|
|
272
|
+
if (parts.length > 1)
|
|
273
|
+
kept.push(parts[1]);
|
|
274
|
+
return `${marker} ${heading} · ${kept.join(' · ')}`;
|
|
275
|
+
}
|
|
266
276
|
return `${marker} ${heading} · ${parts.join(' · ')}`;
|
|
267
277
|
}
|
package/dist/utils/context.js
CHANGED
|
@@ -44,6 +44,8 @@ export const MODEL_CONTEXT_WINDOWS = {
|
|
|
44
44
|
// this bare key catches all gemini-* ids. Previously had no entry at all -> 128K.
|
|
45
45
|
'gemini': 1000000,
|
|
46
46
|
// GPT models
|
|
47
|
+
'gpt-6-sol': 1050000,
|
|
48
|
+
'gpt-6-luna': 1050000,
|
|
47
49
|
'gpt-4': 8192,
|
|
48
50
|
'gpt-4-32k': 32768,
|
|
49
51
|
'gpt-4-turbo': 128000,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fireworks' curated pay-per-token Serverless chat/vision catalog.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors zapps/zergrouter/server/lib/fireworks-models.ts. Router is the source
|
|
5
|
+
* of truth and its /v1/models endpoint wins when discovery succeeds; this local
|
|
6
|
+
* copy only keeps the picker populated and gives the funded default a name when
|
|
7
|
+
* discovery fails or returns nothing. Keep the slugs aligned with Router.
|
|
8
|
+
*/
|
|
9
|
+
export interface FireworksCatalogModel {
|
|
10
|
+
slug: string;
|
|
11
|
+
name: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const FIREWORKS_CHAT_MODELS: readonly FireworksCatalogModel[];
|
|
14
|
+
/**
|
|
15
|
+
* ZergRouter funds exactly this slug on platform credits
|
|
16
|
+
* (ZROUTER_FUNDED_DEEPSEEK_PROVIDER === 'fireworks' in fly.prod.toml), so it is
|
|
17
|
+
* the default a Router-only user can select without a personal Fireworks key.
|
|
18
|
+
*/
|
|
19
|
+
export declare const FIREWORKS_FUNDED_MODEL = "deepseek-v4p1-flash";
|
|
20
|
+
export declare function getFireworksModelName(slug: string): string | undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const FIREWORKS_CHAT_MODELS = [
|
|
2
|
+
{ slug: 'deepseek-v4p1-flash', name: 'DeepSeek V4.1 Flash' },
|
|
3
|
+
{ slug: 'glm-5p3-flash', name: 'GLM 5.3 Flash' },
|
|
4
|
+
{ slug: 'glm-5p3', name: 'GLM 5.3' },
|
|
5
|
+
{ slug: 'kimi-k3', name: 'Kimi K3' },
|
|
6
|
+
{ slug: 'deepseek-v4-pro-0813', name: 'DeepSeek V4 Pro 0813' },
|
|
7
|
+
{ slug: 'qwen3p8-max', name: 'Qwen 3.8 Max' },
|
|
8
|
+
{ slug: 'muse-glimmer-30b', name: 'Muse Glimmer 30B' },
|
|
9
|
+
{ slug: 'deepseek-v4-flash-0731', name: 'DeepSeek V4 Flash 0731' },
|
|
10
|
+
{ slug: 'glm-5p2', name: 'GLM 5.2' },
|
|
11
|
+
{ slug: 'kimi-k2p7-code', name: 'Kimi K2.7 Code' },
|
|
12
|
+
{ slug: 'minimax-m3', name: 'MiniMax M3' },
|
|
13
|
+
{ slug: 'ember-1', name: 'Ember-1' },
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* ZergRouter funds exactly this slug on platform credits
|
|
17
|
+
* (ZROUTER_FUNDED_DEEPSEEK_PROVIDER === 'fireworks' in fly.prod.toml), so it is
|
|
18
|
+
* the default a Router-only user can select without a personal Fireworks key.
|
|
19
|
+
*/
|
|
20
|
+
export const FIREWORKS_FUNDED_MODEL = 'deepseek-v4p1-flash';
|
|
21
|
+
export function getFireworksModelName(slug) {
|
|
22
|
+
return FIREWORKS_CHAT_MODELS.find(model => model.slug === slug)?.name;
|
|
23
|
+
}
|
package/dist/utils/models.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { CodexReasoningEffort } from '../config/types.js';
|
|
2
|
-
export declare const OFFICIAL_CODEX_MODEL_IDS: readonly ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex", "gpt-5.2"];
|
|
2
|
+
export declare const OFFICIAL_CODEX_MODEL_IDS: readonly ["gpt-6-sol", "gpt-6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex", "gpt-5.2"];
|
|
3
3
|
export interface OfficialCodexModelInfo {
|
|
4
4
|
displayName: string;
|
|
5
5
|
description: string;
|
|
6
6
|
defaultReasoningEffort: CodexReasoningEffort;
|
|
7
7
|
supportedReasoningEfforts: readonly CodexReasoningEffort[];
|
|
8
8
|
}
|
|
9
|
+
export declare const GPT_6_API_EFFORTS: readonly ["none", "low", "medium", "high", "xhigh", "max"];
|
|
9
10
|
export declare const CODEX_LEGACY_EFFORTS: readonly ["none", "minimal", "low", "medium", "high", "xhigh"];
|
|
10
11
|
/** Picker metadata mirrored from Codex App Server model/list. */
|
|
11
12
|
export declare const OFFICIAL_CODEX_MODEL_INFO: Record<(typeof OFFICIAL_CODEX_MODEL_IDS)[number], OfficialCodexModelInfo>;
|
package/dist/utils/models.js
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
import { isCodexAppServerProvider, isCodexLegacyProvider } from '../agent/backends/provider_ids.js';
|
|
7
7
|
import { DEEPSEEK_FLASH_EFFORTS, DEEPSEEK_FLASH_MODEL, MODAL_DEEPSEEK_FLASH_MODEL, MODAL_DEEPSEEK_FLASH_EFFORTS, isModalDeepSeekFlashModel, isDeepSeekFlashModel } from './deepseek.js';
|
|
8
8
|
export const OFFICIAL_CODEX_MODEL_IDS = [
|
|
9
|
+
'gpt-6-sol',
|
|
10
|
+
'gpt-6-luna',
|
|
9
11
|
'gpt-5.6-sol',
|
|
10
12
|
'gpt-5.6-terra',
|
|
11
13
|
'gpt-5.6-luna',
|
|
@@ -18,9 +20,22 @@ export const OFFICIAL_CODEX_MODEL_IDS = [
|
|
|
18
20
|
const STANDARD_CODEX_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
|
|
19
21
|
const GPT_5_6_EFFORTS = [...STANDARD_CODEX_EFFORTS, 'max'];
|
|
20
22
|
const GPT_5_6_ULTRA_EFFORTS = [...GPT_5_6_EFFORTS, 'ultra'];
|
|
23
|
+
export const GPT_6_API_EFFORTS = ['none', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
21
24
|
export const CODEX_LEGACY_EFFORTS = ['none', 'minimal', ...STANDARD_CODEX_EFFORTS];
|
|
22
25
|
/** Picker metadata mirrored from Codex App Server model/list. */
|
|
23
26
|
export const OFFICIAL_CODEX_MODEL_INFO = {
|
|
27
|
+
'gpt-6-sol': {
|
|
28
|
+
displayName: 'GPT-6 Sol',
|
|
29
|
+
description: 'Frontier agentic coding model.',
|
|
30
|
+
defaultReasoningEffort: 'medium',
|
|
31
|
+
supportedReasoningEfforts: GPT_5_6_ULTRA_EFFORTS,
|
|
32
|
+
},
|
|
33
|
+
'gpt-6-luna': {
|
|
34
|
+
displayName: 'GPT-6 Luna',
|
|
35
|
+
description: 'Fast and affordable model for everyday work.',
|
|
36
|
+
defaultReasoningEffort: 'medium',
|
|
37
|
+
supportedReasoningEfforts: GPT_5_6_EFFORTS,
|
|
38
|
+
},
|
|
24
39
|
'gpt-5.6-sol': {
|
|
25
40
|
displayName: 'GPT-5.6 Sol',
|
|
26
41
|
description: 'Latest frontier agentic coding model.',
|
|
@@ -208,6 +223,10 @@ export function getSupportedCodexReasoningEfforts(model, provider) {
|
|
|
208
223
|
const appServerEfforts = getOfficialCodexModelInfo(model)?.supportedReasoningEfforts;
|
|
209
224
|
if (!appServerEfforts)
|
|
210
225
|
return getSupportedClaudeReasoningEfforts(model);
|
|
226
|
+
if ((provider === 'openai' || provider === 'openai_compatible')
|
|
227
|
+
&& (model === 'gpt-6-sol' || model === 'gpt-6-luna')) {
|
|
228
|
+
return GPT_6_API_EFFORTS;
|
|
229
|
+
}
|
|
211
230
|
if (provider && isCodexLegacyProvider(provider))
|
|
212
231
|
return CODEX_LEGACY_EFFORTS;
|
|
213
232
|
if (provider === undefined) {
|
|
@@ -13,6 +13,7 @@ import type { LoginCommand } from '../agent/backends/account_pool.js';
|
|
|
13
13
|
import type { AccountCredentialSource } from '../agent/backends/account_pool.js';
|
|
14
14
|
import type { LoginAttempt } from '../agent/backends/account_login.js';
|
|
15
15
|
import { AccountStateStore } from '../agent/backends/account_state.js';
|
|
16
|
+
import { type CodexAccountStatus } from '../agent/backends/codex_account_status.js';
|
|
16
17
|
import { type KimiDeviceFlowStart, type KimiDeviceLoginResult } from '../agent/backends/kimi_code_auth.js';
|
|
17
18
|
import type { AccountProvider } from '../config/types.js';
|
|
18
19
|
export declare const ACCOUNT_PROVIDER_LABELS: Record<AccountProvider, string>;
|
|
@@ -31,6 +32,7 @@ export interface AccountView {
|
|
|
31
32
|
active: boolean;
|
|
32
33
|
/** How this account's credentials are backed (isolated / shared_keychain / none). */
|
|
33
34
|
credentialSource: AccountCredentialSource;
|
|
35
|
+
codexStatus?: CodexAccountStatus;
|
|
34
36
|
/** Epoch ms reset when this account is cooling in the rotation sidecar, else absent. */
|
|
35
37
|
cooldownUntil?: number;
|
|
36
38
|
/**
|
|
@@ -110,6 +112,10 @@ export interface AccountsPayload {
|
|
|
110
112
|
loginStates?: Record<string, KimiLoginState>;
|
|
111
113
|
}
|
|
112
114
|
export declare function getAccountsPayload(store?: ConfigStore, deps?: AccountsQueryDeps): AccountsPayload;
|
|
115
|
+
export declare function getAccountsPayloadWithCodexStatus(store?: ConfigStore, deps?: AccountsQueryDeps & {
|
|
116
|
+
readCodexStatus?: (home: string) => Promise<CodexAccountStatus>;
|
|
117
|
+
forceCodexStatus?: boolean;
|
|
118
|
+
}): Promise<AccountsPayload>;
|
|
113
119
|
export interface ActiveAccountBadge {
|
|
114
120
|
provider: AccountProvider;
|
|
115
121
|
providerLabel: string;
|
|
@@ -201,6 +207,7 @@ export interface AccountOpResult {
|
|
|
201
207
|
* without spawning the real claude/codex CLIs. Production leaves them defaulted.
|
|
202
208
|
*/
|
|
203
209
|
export interface AccountOpDeps {
|
|
210
|
+
readCodexStatus?: (home: string) => Promise<CodexAccountStatus>;
|
|
204
211
|
runLogin?: (login: LoginCommand, isUsable: () => boolean) => Promise<LoginAttempt>;
|
|
205
212
|
autoRunEnabled?: () => boolean;
|
|
206
213
|
onAccountsChanged?: () => void;
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
import { homedir } from 'os';
|
|
12
12
|
import { join } from 'path';
|
|
13
13
|
import { configStore as defaultStore } from '../config.js';
|
|
14
|
-
import { accountCredentialSource, buildLoginCommand, canonicalAccountProvider, defaultAccountDir, defaultSourceFor, ensureAccountDir, isAccountProvider, probeAccountUsable, sharedClaudeKeychainPresent, sourceDir, } from '../agent/backends/account_pool.js';
|
|
14
|
+
import { accountCredentialSource, buildLoginCommand, canonicalAccountProvider, defaultAccountDir, defaultSourceFor, ensureAccountDir, findCodexHomeCollision, isAccountProvider, probeAccountUsable, sharedClaudeKeychainPresent, sourceDir, } from '../agent/backends/account_pool.js';
|
|
15
15
|
import { loginAutoRunEnabled, runLoginCommand } from '../agent/backends/account_login.js';
|
|
16
16
|
import { notifyAccountsChanged } from '../agent/backends/account_events.js';
|
|
17
17
|
import { AccountStateStore } from '../agent/backends/account_state.js';
|
|
18
|
+
import { codexStatusCache } from '../agent/backends/codex_account_status.js';
|
|
18
19
|
import { hasUsableClaudeCodeAuth } from '../agent/backends/claude_code_auth.js';
|
|
19
20
|
import { hasUsableCodexAuth } from '../agent/backends/codex_auth.js';
|
|
20
21
|
import { hasUsableKimiCodeAuth, runKimiDevicePollLoop, startKimiDeviceFlow, } from '../agent/backends/kimi_code_auth.js';
|
|
@@ -276,6 +277,33 @@ export function getAccountsPayload(store = defaultStore, deps = {}) {
|
|
|
276
277
|
...(Object.keys(loginStates).length > 0 ? { loginStates } : {}),
|
|
277
278
|
};
|
|
278
279
|
}
|
|
280
|
+
export async function getAccountsPayloadWithCodexStatus(store = defaultStore, deps = {}) {
|
|
281
|
+
const payload = getAccountsPayload(store, deps);
|
|
282
|
+
const accounts = await Promise.all(payload.accounts.map(async (account) => {
|
|
283
|
+
if (account.provider !== 'codex_app_server')
|
|
284
|
+
return account;
|
|
285
|
+
// The Default row is the machine's own ~/.codex, already verified usable
|
|
286
|
+
// by its native credential store. The status probe pins the file store, so
|
|
287
|
+
// it would call a keychain login signed out, and every cache miss would
|
|
288
|
+
// refresh the main login's token.
|
|
289
|
+
if (account.id.startsWith(AMBIENT_ACCOUNT_ID_PREFIX))
|
|
290
|
+
return account;
|
|
291
|
+
if (!account.usable && !deps.readCodexStatus) {
|
|
292
|
+
return { ...account, codexStatus: { state: 'reauth_required', windows: [] } };
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const status = await (deps.readCodexStatus
|
|
296
|
+
? deps.readCodexStatus(account.path)
|
|
297
|
+
: codexStatusCache.read(account.path, deps.forceCodexStatus));
|
|
298
|
+
return { ...account, codexStatus: status,
|
|
299
|
+
usable: status.state === 'ready' || status.state === 'quota_unavailable' };
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
return { ...account, codexStatus: { state: 'unavailable', windows: [] } };
|
|
303
|
+
}
|
|
304
|
+
}));
|
|
305
|
+
return { ...payload, accounts };
|
|
306
|
+
}
|
|
279
307
|
/**
|
|
280
308
|
* The display-only badge payload: the live account per provider (only those with
|
|
281
309
|
* one configured) plus any cooldown, read from the sidecar. Drives the system-bar
|
|
@@ -385,6 +413,17 @@ export async function applyAccountOp(body, store = defaultStore, reload, deps =
|
|
|
385
413
|
const account = store.getAccount(id);
|
|
386
414
|
if (!account)
|
|
387
415
|
return { ok: false, error: `No account "${id}".` };
|
|
416
|
+
if (account.source.kind === 'codex_home') {
|
|
417
|
+
const status = await (deps.readCodexStatus
|
|
418
|
+
? deps.readCodexStatus(account.source.path)
|
|
419
|
+
: codexStatusCache.read(account.source.path, true));
|
|
420
|
+
if (status.state === 'reauth_required') {
|
|
421
|
+
return { ok: false, error: `"${account.label}" needs a fresh ChatGPT sign-in. Sign in, then select it again.` };
|
|
422
|
+
}
|
|
423
|
+
if (status.state === 'unavailable') {
|
|
424
|
+
return { ok: false, error: `"${account.label}" could not be verified. Refresh its status or sign in, then select it again.` };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
388
427
|
store.setActiveAccount(account.provider, account.id);
|
|
389
428
|
await store.save();
|
|
390
429
|
// Explicit operator selection overrides a stranded cooldown. Clear it BEFORE
|
|
@@ -420,6 +459,11 @@ export async function applyAccountOp(body, store = defaultStore, reload, deps =
|
|
|
420
459
|
const label = asString(body.label) ?? id;
|
|
421
460
|
const dir = asString(body.path) ?? defaultAccountDir(provider, id);
|
|
422
461
|
const source = defaultSourceFor(provider, dir);
|
|
462
|
+
// Check for a shared credential home before creating anything, so a
|
|
463
|
+
// refused add leaves no stray directory behind.
|
|
464
|
+
const collision = findCodexHomeCollision(store.listAccounts(), source);
|
|
465
|
+
if (collision)
|
|
466
|
+
return { ok: false, error: `That Codex credential home is already used by "${collision.label}".` };
|
|
423
467
|
ensureAccountDir(source);
|
|
424
468
|
const beforeActive = store.getActiveAccount(store.getProvider())?.id;
|
|
425
469
|
store.addAccount({ id, label, provider: provider, source });
|
|
@@ -583,18 +627,26 @@ export async function applyAccountOp(body, store = defaultStore, reload, deps =
|
|
|
583
627
|
// `accounts` SSE refreshes when the sign-in finishes (or fails).
|
|
584
628
|
const run = deps.runLogin ?? ((l, u) => runLoginCommand(l, u));
|
|
585
629
|
const notify = deps.onAccountsChanged ?? notifyAccountsChanged;
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
const isCurrentActive = store.getActiveAccount(store.getProvider())?.id === account.id;
|
|
630
|
+
// Check the active pointer again when the detached login finishes: the user
|
|
631
|
+
// may have selected a different account while completing browser OAuth.
|
|
589
632
|
void run(login, () => probeAccountUsable(account))
|
|
590
|
-
.then((attempt) => {
|
|
633
|
+
.then(async (attempt) => {
|
|
591
634
|
// A successful sign-in mints fresh creds, so a stranded cooldown (an
|
|
592
635
|
// auth-failover rest that account_rotation cools "until the user
|
|
593
636
|
// re-auths") is now stale. Clear it before the reload rebuilds the pool.
|
|
594
|
-
|
|
637
|
+
let verified = attempt.outcome === 'succeeded';
|
|
638
|
+
if (verified && account.source.kind === 'codex_home') {
|
|
639
|
+
codexStatusCache.invalidate(account.source.path);
|
|
640
|
+
const status = await (deps.readCodexStatus
|
|
641
|
+
? deps.readCodexStatus(account.source.path)
|
|
642
|
+
: codexStatusCache.read(account.source.path, true));
|
|
643
|
+
verified = status.state === 'ready' || status.state === 'quota_unavailable';
|
|
644
|
+
}
|
|
645
|
+
if (verified) {
|
|
595
646
|
(deps.stateStore ?? new AccountStateStore()).clear(account.id);
|
|
647
|
+
}
|
|
596
648
|
notify();
|
|
597
|
-
if (reload &&
|
|
649
|
+
if (reload && verified && store.getActiveAccount(store.getProvider())?.id === account.id)
|
|
598
650
|
reload();
|
|
599
651
|
})
|
|
600
652
|
.catch(() => notify());
|
|
@@ -31,7 +31,7 @@ import { MemoryHistoryStore, JsonlHistoryStore } from './event_history_store.js'
|
|
|
31
31
|
import { PaneRegistry } from './pane_registry.js';
|
|
32
32
|
import { createMirrorAuthenticatorFromEnv, } from './mirror_authenticator.js';
|
|
33
33
|
import { areMirrorCorsRequestHeadersAllowed, getMirrorOriginPolicy, MIRROR_CORS_ALLOW_HEADERS, rejectReasonForUnsafeMirrorWrite, } from './mirror_origin_policy.js';
|
|
34
|
-
import { getAccountsPayload, applyAccountOp, getActiveAccountsSummary } from './accounts_mirror.js';
|
|
34
|
+
import { getAccountsPayload, getAccountsPayloadWithCodexStatus, applyAccountOp, getActiveAccountsSummary } from './accounts_mirror.js';
|
|
35
35
|
import { onAccountsChanged } from '../agent/backends/account_events.js';
|
|
36
36
|
import { getMirrorCommandUsages } from '../agent/commands/mirror_usages.js';
|
|
37
37
|
const MAX_MIRROR_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
@@ -864,9 +864,8 @@ export class MirrorStreamServer {
|
|
|
864
864
|
// settings_page is a live-only navigation signal: replaying it on
|
|
865
865
|
// reconnect would hijack the user's view every time.
|
|
866
866
|
&& event.type !== 'settings_page'
|
|
867
|
-
// open_accounts is
|
|
868
|
-
//
|
|
869
|
-
// --resume restart and spring the modal open unbidden.
|
|
867
|
+
// Legacy open_accounts is also live-only: replay would navigate a fresh
|
|
868
|
+
// browser tab to Accounts without a new user command.
|
|
870
869
|
&& event.type !== 'open_accounts') {
|
|
871
870
|
this.history.append(fullEvent);
|
|
872
871
|
}
|
|
@@ -2662,8 +2661,23 @@ export class MirrorStreamServer {
|
|
|
2662
2661
|
res.end(JSON.stringify({ error: 'Host account details are unavailable in hosted mode.' }));
|
|
2663
2662
|
return;
|
|
2664
2663
|
}
|
|
2664
|
+
// Codex status carries the ChatGPT email, plan and quota, and a refresh
|
|
2665
|
+
// spawns the host's Codex App Server against the operator's login. Both
|
|
2666
|
+
// are operator-only. A forced refresh also needs a same-origin request:
|
|
2667
|
+
// an auth-disabled loopback mirror treats every caller as the operator,
|
|
2668
|
+
// including a cross-site <img> pointing at ?refresh=1.
|
|
2669
|
+
const operator = this.canMutateAccounts(auth);
|
|
2670
|
+
const originPolicy = getMirrorOriginPolicy(req);
|
|
2671
|
+
const fetchSite = String(req.headers['sec-fetch-site'] || '').trim().toLowerCase();
|
|
2672
|
+
const sameOrigin = fetchSite !== 'cross-site'
|
|
2673
|
+
&& !(originPolicy.requestOrigin && !originPolicy.isTrustedOrigin);
|
|
2674
|
+
const payload = operator
|
|
2675
|
+
? await getAccountsPayloadWithCodexStatus(undefined, {
|
|
2676
|
+
forceCodexStatus: sameOrigin && url.searchParams.get('refresh') === '1',
|
|
2677
|
+
})
|
|
2678
|
+
: getAccountsPayload();
|
|
2665
2679
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache' });
|
|
2666
|
-
res.end(JSON.stringify(
|
|
2680
|
+
res.end(JSON.stringify(payload));
|
|
2667
2681
|
return;
|
|
2668
2682
|
}
|
|
2669
2683
|
if (url.pathname === '/mirror/account' && req.method === 'POST') {
|