@spzxg/cli-ui 0.6.1 → 0.6.5
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/CHANGELOG.md +26 -0
- package/README.md +145 -13
- package/dist/cli-ui-embed-BmLN2Jjg.js +43290 -0
- package/dist/{cli-ui-index-BFpRZXn4.js → cli-ui-index-D4qfPHMd.js} +2 -2
- package/dist/{cli-ui-jszip.min-B9tqMA-e.js → cli-ui-jszip.min-DC9898pC.js} +1 -1
- package/dist/{cli-ui-jszip.min-DIavJzUG.js → cli-ui-jszip.min-DpEl0G1z.js} +2 -2
- package/dist/{cli-ui-xlsx-BKHhqcoa.js → cli-ui-xlsx-zs7oEtPj.js} +1 -1
- package/dist/cli-ui.css +32 -1
- package/dist/cli-ui.js +1 -1
- package/package.json +14 -8
- package/src/board/BrowserPane.tsx +160 -74
- package/src/board/ChangesPane.tsx +2 -2
- package/src/board/CliBoard.tsx +79 -50
- package/src/board/FileViewer.tsx +32 -22
- package/src/board/FilesPane.tsx +32 -15
- package/src/board/PanelControls.tsx +28 -0
- package/src/board/blocks.tsx +5 -5
- package/src/board/context.ts +65 -0
- package/src/board/fileOps.ts +26 -3
- package/src/board/fileWatch.ts +88 -0
- package/src/board/folderDownload.ts +30 -14
- package/src/board/panels.ts +33 -0
- package/src/board/textLoad.ts +2 -2
- package/src/board/workspace.ts +38 -9
- package/src/chat/CliChat.tsx +113 -53
- package/src/chat/Composer.tsx +68 -57
- package/src/chat/RunControls.tsx +99 -0
- package/src/chat/configuration.ts +15 -0
- package/src/chat/draft.ts +41 -0
- package/src/chat/export.ts +2 -2
- package/src/components.tsx +13 -0
- package/src/core/blocks.ts +2 -2
- package/src/core/callbacks.ts +8 -0
- package/src/core/client.ts +16 -6
- package/src/core/config.ts +11 -15
- package/src/core/conversation.ts +4 -4
- package/src/core/id.ts +15 -0
- package/src/core/identity.ts +33 -0
- package/src/core/provider.tsx +24 -14
- package/src/core/store.ts +83 -26
- package/src/core/types.ts +34 -0
- package/src/elements.tsx +135 -29
- package/src/embed.ts +6 -8
- package/src/index.ts +24 -17
- package/src/layout/CliSplit.tsx +1 -1
- package/src/msglist/CliMsgList.tsx +10 -10
- package/src/operator/branchFlow.ts +35 -0
- package/src/operator/index.ts +9 -0
- package/src/operator/liveStream.ts +1 -0
- package/src/operator/node/AgentConnect.tsx +624 -0
- package/src/operator/node/AppearancePanel.tsx +35 -0
- package/src/operator/node/BranchHistory.tsx +67 -0
- package/src/operator/node/BranchNavigator.tsx +36 -0
- package/src/operator/node/BrowserPane.tsx +128 -0
- package/src/operator/node/BrowserPopup.tsx +22 -0
- package/src/operator/node/Chat.tsx +517 -0
- package/src/operator/node/CommandPalette.tsx +213 -0
- package/src/operator/node/Composer.tsx +287 -0
- package/src/operator/node/InsightsView.tsx +831 -0
- package/src/operator/node/Layout.tsx +190 -0
- package/src/operator/node/Markdown.tsx +61 -0
- package/src/operator/node/Modal.tsx +179 -0
- package/src/operator/node/NeedsYouMenu.tsx +54 -0
- package/src/operator/node/OnboardingWizard.tsx +250 -0
- package/src/operator/node/Orchestrator.tsx +634 -0
- package/src/operator/node/ProjectLanding.tsx +86 -0
- package/src/operator/node/ProjectsColumn.tsx +137 -0
- package/src/operator/node/ProviderAuth.tsx +561 -0
- package/src/operator/node/RunControls.tsx +2 -0
- package/src/operator/node/RunSwitch.tsx +113 -0
- package/src/operator/node/Services.tsx +245 -0
- package/src/operator/node/SessionRail.tsx +260 -0
- package/src/operator/node/SessionView.tsx +288 -0
- package/src/operator/node/SettingsView.tsx +690 -0
- package/src/operator/node/TaskBoard.tsx +352 -0
- package/src/operator/node/TaskChanges.tsx +530 -0
- package/src/operator/node/TaskHero.tsx +50 -0
- package/src/operator/node/TasksColumn.tsx +200 -0
- package/src/operator/node/Terminal.tsx +143 -0
- package/src/operator/node/Transcript.tsx +463 -0
- package/src/operator/node/ViewFile.tsx +79 -0
- package/src/operator/node/Welcome.tsx +101 -0
- package/src/operator/node/WorktreePrune.tsx +333 -0
- package/src/operator/node/agents.ts +58 -0
- package/src/operator/node/api.ts +40 -0
- package/src/operator/node/boardBackend.ts +80 -0
- package/src/operator/node/boardEvents.ts +22 -0
- package/src/operator/node/boardInteraction.ts +15 -0
- package/src/operator/node/boardState.ts +57 -0
- package/src/operator/node/boardWorkspace.ts +12 -0
- package/src/operator/node/branchFlow.ts +1 -0
- package/src/operator/node/composerDraft.ts +52 -0
- package/src/operator/node/fileLinks.ts +81 -0
- package/src/operator/node/format.ts +290 -0
- package/src/operator/node/github.tsx +234 -0
- package/src/operator/node/icons.tsx +88 -0
- package/src/operator/node/liveStream.ts +270 -0
- package/src/operator/node/modals.tsx +720 -0
- package/src/operator/node/navHistory.ts +70 -0
- package/src/operator/node/persist.ts +26 -0
- package/src/operator/node/protocol/agents/optIn.ts +9 -0
- package/src/operator/node/protocol/agents/types.ts +394 -0
- package/src/operator/node/protocol/approvalFailure.ts +77 -0
- package/src/operator/node/protocol/authFailure.ts +66 -0
- package/src/operator/node/protocol/customModels.ts +23 -0
- package/src/operator/node/protocol/events.ts +13 -0
- package/src/operator/node/protocol/features.ts +70 -0
- package/src/operator/node/protocol/internalUsage.ts +1 -0
- package/src/operator/node/protocol/promptLimits.ts +52 -0
- package/src/operator/node/protocol/storageRetention.ts +13 -0
- package/src/operator/node/protocol/types.ts +332 -0
- package/src/operator/node/protocol/usageLimit.ts +72 -0
- package/src/operator/node/shared.tsx +89 -0
- package/src/operator/node/transcriptModel.ts +129 -0
- package/src/operator/node/types.ts +296 -0
- package/src/operator/node/useGlobalEvents.ts +97 -0
- package/src/operator/node/useOrchestrator.ts +696 -0
- package/src/operator/node/usePrefs.ts +114 -0
- package/src/operator/node/useRecaps.ts +91 -0
- package/src/operator/node/useTaskStream.ts +279 -0
- package/src/operator.base.css +1798 -0
- package/src/operator.css +186 -16
- package/src/styles.css +51 -0
- package/src/ui/Markdown.tsx +7 -2
- package/src/ui/Popover.tsx +77 -0
- package/src/ui/TextBlock.tsx +3 -3
- package/src/ui/Transcript.tsx +10 -8
- package/src/ui/fileLinks.ts +6 -0
- package/src/ui/format.ts +1 -1
- package/src/ui/icons.tsx +1 -0
- package/src/ui/shared.tsx +1 -1
- package/src/ui/transcriptModel.ts +2 -2
- package/dist/cli-ui-embed-Dk2QGHxn.js +0 -41146
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Browser-history mechanics for the mobile projects → tasks → session panes,
|
|
2
|
+
// kept pure (no React, no globals) so the Back-button behaviour can be unit-tested.
|
|
3
|
+
//
|
|
4
|
+
// Why a "trap" instead of one-entry-per-level: the live task list re-selects the
|
|
5
|
+
// open task on every background refresh, so selTask churns null↔id constantly.
|
|
6
|
+
// Any scheme that pushes a history entry when the selection gets *deeper* turns
|
|
7
|
+
// that churn into a pile of duplicate session entries, and Back can never escape
|
|
8
|
+
// them (the original "back acts very strange / does nothing" bug on prod).
|
|
9
|
+
//
|
|
10
|
+
// Instead we keep at most ONE "trap" entry on top whenever a detail pane is open.
|
|
11
|
+
// Arming is keyed on the BOOLEAN "is a pane open" (isDeep), which selTask churn
|
|
12
|
+
// does not change — so churn only ever rewrites the current entry's URL, never
|
|
13
|
+
// adds entries. Back consumes the trap; the app then closes exactly one level
|
|
14
|
+
// (closeOneLevel) and re-arms, so Back walks session → tasks → projects → exit.
|
|
15
|
+
import type { View } from "./types";
|
|
16
|
+
|
|
17
|
+
export interface NavSel {
|
|
18
|
+
proj: string | null;
|
|
19
|
+
task: string | null;
|
|
20
|
+
view: View;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Minimal slice of window.history this logic needs — lets tests pass a fake.
|
|
24
|
+
export interface HistoryLike {
|
|
25
|
+
readonly state: unknown;
|
|
26
|
+
pushState(state: unknown, unused: string, url: string): void;
|
|
27
|
+
replaceState(state: unknown, unused: string, url: string): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function selectionUrl(sel: NavSel, pathname: string): string {
|
|
31
|
+
const q = new URLSearchParams();
|
|
32
|
+
if (sel.proj) q.set("project", sel.proj);
|
|
33
|
+
if (sel.task) q.set("task", sel.task);
|
|
34
|
+
if (sel.view === "settings" || sel.view === "insights") q.set("view", sel.view);
|
|
35
|
+
const s = q.toString();
|
|
36
|
+
return s ? `?${s}` : pathname;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Deeper than the projects list? i.e. a detail pane (a project's task list, an
|
|
40
|
+
// open task, or settings) is showing and Back should close it rather than leave.
|
|
41
|
+
export const isDeep = (sel: NavSel): boolean => !!sel.proj || sel.view === "settings" || sel.view === "insights";
|
|
42
|
+
|
|
43
|
+
const trapArmed = (h: HistoryLike): boolean =>
|
|
44
|
+
!!(h.state && (h.state as { trap?: unknown }).trap);
|
|
45
|
+
|
|
46
|
+
// Keep the URL mirrored to the current selection (so a refresh lands back where
|
|
47
|
+
// you were) and, when `armTrap`, ensure exactly one trap entry sits on top while
|
|
48
|
+
// a pane is open. armTrap is the caller's "is mobile" gate — on desktop all
|
|
49
|
+
// columns are visible at once, so Back should not hijack to close a panel.
|
|
50
|
+
export function reconcileHistory(h: HistoryLike, pathname: string, sel: NavSel, armTrap: boolean): void {
|
|
51
|
+
const url = selectionUrl(sel, pathname);
|
|
52
|
+
const armed = trapArmed(h);
|
|
53
|
+
if (armTrap && isDeep(sel) && !armed) {
|
|
54
|
+
// Entering a detail pane from a non-trap entry: arm the single trap.
|
|
55
|
+
h.pushState({ trap: true }, "", url);
|
|
56
|
+
} else {
|
|
57
|
+
// Already armed (pane open — incl. selTask churn), or shallow/desktop: just
|
|
58
|
+
// mirror the URL onto the current entry, preserving the trap flag.
|
|
59
|
+
h.replaceState({ trap: armed }, "", url);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// One Back press closes the deepest open level. Returns the selection to apply;
|
|
64
|
+
// at the projects root it returns the input unchanged (Back then leaves the app).
|
|
65
|
+
export function closeOneLevel(sel: NavSel): NavSel {
|
|
66
|
+
if (sel.view === "settings" || sel.view === "insights") return { ...sel, view: "workspace" };
|
|
67
|
+
if (sel.task) return { ...sel, task: null };
|
|
68
|
+
if (sel.proj) return { ...sel, proj: null };
|
|
69
|
+
return sel;
|
|
70
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// localStorage + URL persistence for the open project/task, layout and prefs.
|
|
2
|
+
import type { Appearance, Layout, Settings, TaskView } from "./types";
|
|
3
|
+
|
|
4
|
+
export const LS = "orchestrator_ui_v2";
|
|
5
|
+
|
|
6
|
+
type Persisted = { selProj?: string; selTask?: string; appearance?: Partial<Appearance>; layout?: Layout; settings?: Settings; taskView?: TaskView };
|
|
7
|
+
|
|
8
|
+
export function loadPersist(): Persisted {
|
|
9
|
+
if (typeof window === "undefined") return {};
|
|
10
|
+
try {
|
|
11
|
+
// `appearance` used to be called `tweaks` — read the legacy key so an existing
|
|
12
|
+
// install keeps its theme/density across the rename (dropped fields are ignored
|
|
13
|
+
// by the DEFAULT_APPEARANCE spread at the call site).
|
|
14
|
+
const raw = JSON.parse(localStorage.getItem(LS) || "{}") as Persisted & { tweaks?: Partial<Appearance> };
|
|
15
|
+
if (!raw.appearance && raw.tweaks) raw.appearance = raw.tweaks;
|
|
16
|
+
return raw;
|
|
17
|
+
} catch { return {}; }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The open project/task live in the URL query (?project=…&task=…) so a refresh
|
|
21
|
+
// lands back where you were and the view is shareable. URL wins over localStorage.
|
|
22
|
+
export function readUrlSel(): { project?: string; task?: string; view?: string } {
|
|
23
|
+
if (typeof window === "undefined") return {};
|
|
24
|
+
const q = new URLSearchParams(window.location.search);
|
|
25
|
+
return { project: q.get("project") ?? undefined, task: q.get("task") ?? undefined, view: q.get("view") ?? undefined };
|
|
26
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Agents that run only when explicitly chosen (task / project default / app
|
|
2
|
+
// default). Connecting one never retargets existing work: implicit fallbacks
|
|
3
|
+
// (firstConnectedAgent, the new-task default, the "connect another agent"
|
|
4
|
+
// nudge) skip them. Dependency-free so server and client code can both import it.
|
|
5
|
+
export const OPT_IN_AGENTS: readonly string[] = ["grok", "pi"];
|
|
6
|
+
|
|
7
|
+
export function isOptInAgent(id: string | null | undefined): boolean {
|
|
8
|
+
return !!id && OPT_IN_AGENTS.includes(id);
|
|
9
|
+
}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
// The agent-driver seam. The app never talks to a specific coding agent
|
|
2
|
+
// directly — every call site resolves an AgentDriver through
|
|
3
|
+
// lib/agents/registry.ts (keyed by tasks.agent / projects.default_agent) and
|
|
4
|
+
// speaks this interface. Adding an agent (Codex, Gemini, …) is a new driver
|
|
5
|
+
// module + a registry entry, with no edits to the runner, routes, or UI data
|
|
6
|
+
// flow — the same swappable-seam pattern as lib/billing/ and
|
|
7
|
+
// lib/control-plane/provisioner/.
|
|
8
|
+
|
|
9
|
+
import type { Project, Task, StreamEvent, TurnUsage } from "../types";
|
|
10
|
+
|
|
11
|
+
export type { StreamEvent };
|
|
12
|
+
|
|
13
|
+
// One selectable model in a driver's picker. `value` is what's persisted in
|
|
14
|
+
// tasks.model (null there = inherit the driver's default); `contextWindow` is
|
|
15
|
+
// the model's token window, driving the context-occupancy gauge. `group` is an
|
|
16
|
+
// optional section heading — drivers with long lists (Claude Code offers a
|
|
17
|
+
// dozen-plus pins) split them so the picker stays scannable; consecutive
|
|
18
|
+
// options sharing a group render under one header.
|
|
19
|
+
export interface AgentModelOption {
|
|
20
|
+
value: string;
|
|
21
|
+
label: string;
|
|
22
|
+
sub: string; // short picker subtitle, e.g. "most capable"
|
|
23
|
+
contextWindow: number;
|
|
24
|
+
group?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A reasoning preset / permission mode a driver supports. `value` is what's
|
|
28
|
+
// persisted in tasks.reasoning / tasks.permission_mode (null = driver default).
|
|
29
|
+
export interface AgentPickerOption {
|
|
30
|
+
value: string;
|
|
31
|
+
label: string;
|
|
32
|
+
sub: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* What a driver can do, as data. Exposed to the client (GET /api/agents) so
|
|
37
|
+
* the UI renders model/reasoning/permission pickers and gates features
|
|
38
|
+
* (asks, cost display, resume) from the descriptor instead of hardcoding
|
|
39
|
+
* per-agent knowledge.
|
|
40
|
+
*/
|
|
41
|
+
export interface AgentCapabilities {
|
|
42
|
+
models: AgentModelOption[];
|
|
43
|
+
reasoningOptions: AgentPickerOption[];
|
|
44
|
+
permissionModes: AgentPickerOption[];
|
|
45
|
+
/** Can surface interactive AskUserQuestion-style prompts mid-turn ("ask" events). */
|
|
46
|
+
supportsAsks: boolean;
|
|
47
|
+
/** Can mount the orchestrator's MCP tools (suggest_task / expose_service). */
|
|
48
|
+
supportsMcpTools: boolean;
|
|
49
|
+
/** Usage events carry a real dollar cost (not just token counts). */
|
|
50
|
+
reportsCostUsd: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* The dollar cost in usage events is an ESTIMATE (token counts × published
|
|
53
|
+
* API prices) rather than a billed amount — set by drivers whose auth
|
|
54
|
+
* reports tokens only (Codex on a ChatGPT plan). The UI shows the figure
|
|
55
|
+
* with an ~ and labels it estimated. Mutually exclusive with reportsCostUsd.
|
|
56
|
+
*/
|
|
57
|
+
costIsEstimated: boolean;
|
|
58
|
+
/** Turns can resume a prior session/thread id (tasks.session_id). */
|
|
59
|
+
supportsResume: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Absolute path to the CLI binary this driver spawns, when the app PINS one
|
|
62
|
+
* (Claude Code: CLAUDE_CLI_PATH, passed to the SDK as
|
|
63
|
+
* pathToClaudeCodeExecutable). A pinned path is also an installability test:
|
|
64
|
+
* the SDK does NOT fall back to PATH, so a missing file means every turn on
|
|
65
|
+
* this agent dies with "native binary not found" — see isAgentInstalled() in
|
|
66
|
+
* lib/agents/capabilities.ts, which is what keeps agent resolution from
|
|
67
|
+
* picking an agent that cannot possibly run. null/undefined = the driver
|
|
68
|
+
* resolves its own executable (bundled with its SDK, or found on PATH), so
|
|
69
|
+
* installation isn't knowable from here and the agent counts as installed.
|
|
70
|
+
*/
|
|
71
|
+
cliPath?: string | null;
|
|
72
|
+
/** The driver accepts a user-supplied model id in addition to its catalog. */
|
|
73
|
+
supportsCustomModels?: boolean;
|
|
74
|
+
/** Claude Code can use Amazon Bedrock instead of Anthropic-hosted auth. */
|
|
75
|
+
supportsBedrock?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* Placeholder for the "I have an API key instead" field, e.g. "sk-ant-…".
|
|
78
|
+
* null = this agent has no per-token API-key path (subscription login only),
|
|
79
|
+
* so the client hides the api-key toggle.
|
|
80
|
+
*/
|
|
81
|
+
apiKeyHint: string | null;
|
|
82
|
+
/**
|
|
83
|
+
* How the subscription login completes, so the generic connect UI knows what
|
|
84
|
+
* to render: "paste_code" — the user pastes an authorization code back into
|
|
85
|
+
* the app (Claude); "device_code" — the app shows a one-time code the user
|
|
86
|
+
* enters in the browser, then polls until it lands (Codex).
|
|
87
|
+
*/
|
|
88
|
+
loginStyle: "paste_code" | "device_code";
|
|
89
|
+
/**
|
|
90
|
+
* Replaces the connect card's default "you'll get a login link" copy, for an
|
|
91
|
+
* agent whose sign-in happens outside Operator (Pi: /login in its own TUI).
|
|
92
|
+
* Unset = the default copy.
|
|
93
|
+
*/
|
|
94
|
+
loginHint?: string;
|
|
95
|
+
/** The driver fronts many model providers and exposes `providers` (per-provider
|
|
96
|
+
* logins, API keys, custom endpoints) — the connect card renders that panel. */
|
|
97
|
+
supportsProviderAuth?: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---------- per-provider auth (an agent that fronts many model providers) ----------
|
|
101
|
+
// Pi talks to 40+ providers, each with its own OAuth and/or API-key login, plus
|
|
102
|
+
// user-defined endpoints. One "connect" button can't cover that, so such a
|
|
103
|
+
// driver exposes this optional surface and the UI lists providers to manage.
|
|
104
|
+
// Secrets only ever flow IN (prompt answers, endpoint keys); nothing here
|
|
105
|
+
// returns a credential value.
|
|
106
|
+
|
|
107
|
+
export interface AgentProviderEndpoint {
|
|
108
|
+
baseUrl: string;
|
|
109
|
+
api: string | null;
|
|
110
|
+
models: string[];
|
|
111
|
+
contextWindow: number | null;
|
|
112
|
+
reasoning: boolean;
|
|
113
|
+
/** The config file itself carries a key (set outside Operator). */
|
|
114
|
+
inlineKey: boolean;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface AgentProviderInfo {
|
|
118
|
+
id: string;
|
|
119
|
+
name: string;
|
|
120
|
+
/** Usable credentials from any source (stored login, env var, config file). */
|
|
121
|
+
configured: boolean;
|
|
122
|
+
/** Where the working credential comes from, as the agent reports it. */
|
|
123
|
+
source: string | null;
|
|
124
|
+
authType: "api_key" | "oauth" | null;
|
|
125
|
+
/** A credential in the agent's own store — what logout removes. */
|
|
126
|
+
credential: "api_key" | "oauth" | null;
|
|
127
|
+
/** Models usable right now through this provider. */
|
|
128
|
+
models: number;
|
|
129
|
+
apiKey: { name: string; canLogin: boolean } | null;
|
|
130
|
+
oauth: { name: string; label: string | null; subscription: boolean } | null;
|
|
131
|
+
/** Ships with the agent (vs. a user-defined endpoint). */
|
|
132
|
+
builtin: boolean;
|
|
133
|
+
/** A built-in provider pointed at another address (gateway / proxy), plus model ids added for it. */
|
|
134
|
+
override: { baseUrl: string | null; models: string[] } | null;
|
|
135
|
+
/** A user-defined endpoint; null for built-in providers. */
|
|
136
|
+
endpoint: AgentProviderEndpoint | null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** "Fill in an API key" for a provider, optionally through another address. */
|
|
140
|
+
export interface AgentProviderKeyInput {
|
|
141
|
+
/** Omit to keep the stored key (e.g. only changing the address). */
|
|
142
|
+
key?: string;
|
|
143
|
+
/** Empty = the provider's official address. */
|
|
144
|
+
baseUrl?: string;
|
|
145
|
+
/** Extra model ids the address serves (gateways often rename models). */
|
|
146
|
+
models?: string[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface AgentProviderEndpointInput {
|
|
150
|
+
id: string;
|
|
151
|
+
name?: string;
|
|
152
|
+
baseUrl: string;
|
|
153
|
+
api: string;
|
|
154
|
+
models: string[];
|
|
155
|
+
contextWindow?: number;
|
|
156
|
+
reasoning?: boolean;
|
|
157
|
+
/** Optional; stored through the agent's own credential store, never in the config file. */
|
|
158
|
+
apiKey?: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Something the provider's login flow wants the user to see. */
|
|
162
|
+
export type AgentLoginEvent =
|
|
163
|
+
| { type: "info"; message: string; links?: { url: string; label?: string }[] }
|
|
164
|
+
| { type: "auth_url"; url: string; instructions?: string }
|
|
165
|
+
| { type: "device_code"; userCode: string; verificationUri: string; expiresInSeconds?: number }
|
|
166
|
+
| { type: "progress"; message: string };
|
|
167
|
+
|
|
168
|
+
/** Something the provider's login flow needs the user to answer. */
|
|
169
|
+
export type AgentLoginPrompt = { id: string } & (
|
|
170
|
+
| { type: "text" | "secret" | "manual_code"; message: string; placeholder?: string }
|
|
171
|
+
| { type: "select"; message: string; options: { id: string; label: string; description?: string }[] }
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
export interface AgentProviderLogin {
|
|
175
|
+
provider: string;
|
|
176
|
+
type: "oauth" | "api_key";
|
|
177
|
+
status: "running" | "success" | "error" | "cancelled";
|
|
178
|
+
events: AgentLoginEvent[];
|
|
179
|
+
/** The question the flow is waiting on, if any. */
|
|
180
|
+
prompt: AgentLoginPrompt | null;
|
|
181
|
+
error: string | null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface AgentProviderAuth {
|
|
185
|
+
list(): Promise<{ providers: AgentProviderInfo[]; error: string | null }>;
|
|
186
|
+
startLogin(provider: string, type: "oauth" | "api_key"): Promise<AgentProviderLogin>;
|
|
187
|
+
getLogin(): AgentProviderLogin | null;
|
|
188
|
+
answer(promptId: string, value: string): Promise<AgentProviderLogin | null>;
|
|
189
|
+
cancelLogin(): void;
|
|
190
|
+
/** Save an API key (and optional address override). Returns the login flow when a key
|
|
191
|
+
* was given — it may still ask follow-up questions — or null when only the address changed. */
|
|
192
|
+
saveKey(provider: string, input: AgentProviderKeyInput): Promise<AgentProviderLogin | null>;
|
|
193
|
+
/** Remove the provider's stored credential. */
|
|
194
|
+
logout(provider: string): Promise<void>;
|
|
195
|
+
saveEndpoint(input: AgentProviderEndpointInput): Promise<{ backup: string | null }>;
|
|
196
|
+
removeEndpoint(id: string): Promise<{ backup: string | null }>;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------- auth surface (shape after lib/claude-auth.ts) ----------
|
|
200
|
+
|
|
201
|
+
export interface AgentAuthStatus {
|
|
202
|
+
authenticated: boolean;
|
|
203
|
+
method: string | null; // how the account is signed in (raw provider text)
|
|
204
|
+
email: string | null;
|
|
205
|
+
plan: string | null;
|
|
206
|
+
error: string | null;
|
|
207
|
+
/** Underlying API provider when the CLI reports it (for example "bedrock"). */
|
|
208
|
+
provider?: string | null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// A headless device-style login in progress (start → awaiting code → success).
|
|
212
|
+
export interface AgentLoginSession {
|
|
213
|
+
status: "starting" | "awaiting" | "submitting" | "success" | "error";
|
|
214
|
+
url: string | null; // the authorize URL for the user to open
|
|
215
|
+
code?: string | null; // device-code drivers: the one-time code to enter in the browser
|
|
216
|
+
email: string | null;
|
|
217
|
+
plan: string | null;
|
|
218
|
+
error: string | null;
|
|
219
|
+
log: string; // tail of the login terminal output, for the UI's pane
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---------- account quota (the "how much have I got left" surface) ----------
|
|
223
|
+
|
|
224
|
+
/** A rate-limit window's duration, named where it's one of the common ones. */
|
|
225
|
+
export type AgentQuotaWindowKind = "hourly" | "5h" | "daily" | "weekly" | "monthly" | "custom";
|
|
226
|
+
|
|
227
|
+
/** One metered window on the account (a plan can meter several at once, e.g. a
|
|
228
|
+
* 5-hour burst cap alongside a weekly cap, and per model family). */
|
|
229
|
+
export interface AgentQuotaWindow {
|
|
230
|
+
/** The provider's bucket id, e.g. "codex" — stable, unlike the label. */
|
|
231
|
+
limitId: string | null;
|
|
232
|
+
/** Human name when the provider gives one, else the bucket id. */
|
|
233
|
+
label: string | null;
|
|
234
|
+
/** Which of the bucket's two windows this is, as the provider ordered them. */
|
|
235
|
+
scope: "primary" | "secondary";
|
|
236
|
+
kind: AgentQuotaWindowKind;
|
|
237
|
+
windowMinutes: number | null;
|
|
238
|
+
/** Pre-rendered duration ("1 周", "5 小时") so callers don't re-derive it. */
|
|
239
|
+
windowLabel: string | null;
|
|
240
|
+
usedPercent: number;
|
|
241
|
+
remainingPercent: number;
|
|
242
|
+
/** Epoch MS (the app's unit; providers often report seconds). */
|
|
243
|
+
resetsAt: number | null;
|
|
244
|
+
resetsInMs: number | null;
|
|
245
|
+
/** This particular window is spent. */
|
|
246
|
+
reached: boolean;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Who the quota belongs to, so a multi-account instance can tell them apart. */
|
|
250
|
+
export interface AgentQuotaAccount {
|
|
251
|
+
/** How the account authenticates, e.g. "chatgpt" | "apiKey" | "amazonBedrock". */
|
|
252
|
+
authType: string;
|
|
253
|
+
email: string | null;
|
|
254
|
+
plan: string | null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* An account's live quota state — the AUTHORITATIVE answer to "am I out?",
|
|
259
|
+
* as opposed to lib/usageLimit.ts, which can only pattern-match a provider's
|
|
260
|
+
* error text and so cannot distinguish a spent plan from a transient 429.
|
|
261
|
+
* A driver that can't read its provider's quota simply omits `quota()`.
|
|
262
|
+
*/
|
|
263
|
+
export interface AgentQuota {
|
|
264
|
+
account: AgentQuotaAccount | null;
|
|
265
|
+
plan: string | null;
|
|
266
|
+
/** The provider says a limit is actually reached — not merely a 429. */
|
|
267
|
+
exhausted: boolean;
|
|
268
|
+
/** The provider's own reason code when it declared a limit reached. */
|
|
269
|
+
reachedType: string | null;
|
|
270
|
+
/** A spend control (not a rate limit) is what stopped the account. */
|
|
271
|
+
spendControlReached: boolean;
|
|
272
|
+
/** The most-consumed weekly window — the headline "周额度". */
|
|
273
|
+
weekly: AgentQuotaWindow | null;
|
|
274
|
+
/** Every metered window, including the one surfaced as `weekly`. */
|
|
275
|
+
limits: AgentQuotaWindow[];
|
|
276
|
+
credits: { hasCredits: boolean; unlimited: boolean; balance: string | null } | null;
|
|
277
|
+
/** One-off "reset my limit early" credits the account holds, when reported. */
|
|
278
|
+
resetCreditsAvailable: number | null;
|
|
279
|
+
totals: {
|
|
280
|
+
lifetimeTokens: number | null;
|
|
281
|
+
peakDailyTokens: number | null;
|
|
282
|
+
longestRunningTurnSec: number | null;
|
|
283
|
+
currentStreakDays: number | null;
|
|
284
|
+
longestStreakDays: number | null;
|
|
285
|
+
} | null;
|
|
286
|
+
daily: { date: string; tokens: number }[] | null;
|
|
287
|
+
fetchedAt: number;
|
|
288
|
+
/** Populated instead of the figures when the quota couldn't be read. */
|
|
289
|
+
error: string | null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export interface AgentVerifyResult {
|
|
293
|
+
ok: boolean;
|
|
294
|
+
output: string;
|
|
295
|
+
error: string | null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export interface OneShotResult {
|
|
299
|
+
text: string;
|
|
300
|
+
usage?: TurnUsage;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* The "I have an API key instead" alternative to the subscription login, as a
|
|
305
|
+
* small surface a driver optionally provides (mirrors lib/anthropic-key.ts /
|
|
306
|
+
* lib/openai-key.ts). The key is persisted to a 0600 file on the volume and
|
|
307
|
+
* mirrored into process.env so the agent's children bill per-token against it.
|
|
308
|
+
* Drivers without a per-token path simply omit `apiKey`.
|
|
309
|
+
*/
|
|
310
|
+
export interface AgentApiKeyAuth {
|
|
311
|
+
/** Placeholder for the input, e.g. "sk-ant-…". Also surfaced via capabilities.apiKeyHint. */
|
|
312
|
+
hint: string;
|
|
313
|
+
/** Loose shape check; the real validation is the verify turn working. */
|
|
314
|
+
looksValid(key: string): boolean;
|
|
315
|
+
/** Whether a key is currently persisted. */
|
|
316
|
+
has(): boolean;
|
|
317
|
+
/** Persist + apply the key. */
|
|
318
|
+
set(key: string): void;
|
|
319
|
+
/** Forget the key. */
|
|
320
|
+
clear(): void;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* A pluggable coding-agent backend.
|
|
325
|
+
*
|
|
326
|
+
* `runTurn` is THE contract: one user turn in, a stream of normalized
|
|
327
|
+
* StreamEvents out (session/model/assistant/tool/tool_result/ask/ask_answered/
|
|
328
|
+
* suggested/usage/notice/error/done — see lib/types.ts). Drivers normalize
|
|
329
|
+
* their native event stream into it; everything downstream (lib/runner.ts
|
|
330
|
+
* persistence + publish, the SSE tail, the UI) is agent-agnostic.
|
|
331
|
+
*
|
|
332
|
+
* The session/thread id a driver reports via "session"/"done" events is opaque
|
|
333
|
+
* to the app — it's stored in tasks.session_id / sessions.claude_session_id
|
|
334
|
+
* and handed back verbatim on resume (a Codex thread id fits the same column).
|
|
335
|
+
*/
|
|
336
|
+
export interface AgentDriver {
|
|
337
|
+
id: string; // persisted in tasks.agent / projects.default_agent
|
|
338
|
+
label: string; // human name, e.g. "Claude Code"
|
|
339
|
+
capabilities: AgentCapabilities;
|
|
340
|
+
/** Instance-wide provider selected outside Operator, when applicable. */
|
|
341
|
+
configuredProvider?(): string | null;
|
|
342
|
+
/** Whether the configured provider's credentials can be refreshed from the
|
|
343
|
+
* UI via the login surface (e.g. an AWS SSO device-code flow). Drives the
|
|
344
|
+
* refresh button in the connect card and gates POST /api/agents/[id]/login
|
|
345
|
+
* when a third-party provider is active. */
|
|
346
|
+
providerAuthRefreshable?(): boolean;
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Run one user turn. Resumes task.session_id when set, otherwise starts a
|
|
350
|
+
* fresh session seeded with the project context. `abort` (the Stop button)
|
|
351
|
+
* must end the stream without emitting an error event.
|
|
352
|
+
*/
|
|
353
|
+
runTurn(task: Task, project: Project, userText: string, abort?: AbortController): AsyncGenerator<StreamEvent>;
|
|
354
|
+
|
|
355
|
+
// ---------- one-shot helpers (no session, text in → text out) ----------
|
|
356
|
+
//
|
|
357
|
+
// All three are OPTIONAL: a driver can ship runTurn() alone and the app
|
|
358
|
+
// backstops the missing helper with the configured utility agent (see
|
|
359
|
+
// lib/agents/oneshots.ts). summarizeTranscript is task-scoped (runs on the
|
|
360
|
+
// task's own agent so the work bills the right login); draft/recap are
|
|
361
|
+
// project-scoped and run on the utility agent.
|
|
362
|
+
|
|
363
|
+
/** Condense a session transcript into a handoff note for the /clear flow. */
|
|
364
|
+
summarizeTranscript?(transcript: string, project: Project): Promise<OneShotResult>;
|
|
365
|
+
/** Draft a fresh "what we're building" context by exploring the repo (read-only). */
|
|
366
|
+
draftProjectContext?(project: Project, digest: string): Promise<OneShotResult>;
|
|
367
|
+
/** Short "where you left off" recap from a recent-activity digest. */
|
|
368
|
+
summarizeProjectRecap?(project: Project, digest: string): Promise<OneShotResult>;
|
|
369
|
+
|
|
370
|
+
// ---------- auth (the setup wizard's connect / verify flow) ----------
|
|
371
|
+
|
|
372
|
+
/** Whether the agent's CLI/SDK is signed in, and as whom. */
|
|
373
|
+
authStatus(): Promise<AgentAuthStatus>;
|
|
374
|
+
/** Start (or rejoin) a headless login; resolves once the authorize URL is known. */
|
|
375
|
+
startLogin(): Promise<AgentLoginSession>;
|
|
376
|
+
/** The in-progress login, if any (for the UI's poll loop). */
|
|
377
|
+
getLogin(): AgentLoginSession | null;
|
|
378
|
+
/** Hand the pasted authorization code to the waiting login. */
|
|
379
|
+
submitLoginCode(code: string): Promise<AgentLoginSession>;
|
|
380
|
+
/** Abandon an in-progress login. */
|
|
381
|
+
cancelLogin(): void;
|
|
382
|
+
/** Prove the connection works by running a real one-shot test turn. */
|
|
383
|
+
verify(): Promise<AgentVerifyResult>;
|
|
384
|
+
/**
|
|
385
|
+
* The account's live quota / usage, when the provider exposes it. Optional:
|
|
386
|
+
* a driver without it means the app falls back to error-text classification
|
|
387
|
+
* (lib/usageLimit.ts) alone.
|
|
388
|
+
*/
|
|
389
|
+
quota?(): Promise<AgentQuota>;
|
|
390
|
+
/** The per-token API-key path, if this agent supports one (else undefined). */
|
|
391
|
+
apiKey?: AgentApiKeyAuth;
|
|
392
|
+
/** Per-provider auth management (see AgentProviderAuth); with capabilities.supportsProviderAuth. */
|
|
393
|
+
providers?: AgentProviderAuth;
|
|
394
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Detection + recovery constants for the "Codex's approval policy blocked the
|
|
2
|
+
// turn" failure mode.
|
|
3
|
+
//
|
|
4
|
+
// Operator runs Codex through `codex exec` (via @openai/codex-sdk), which is
|
|
5
|
+
// non-interactive: nobody can answer a command-approval prompt mid-turn. So the
|
|
6
|
+
// driver asks for `approval_policy=never` (the auto-run analog of Claude's
|
|
7
|
+
// bypassPermissions). Two configurations defeat that and produce the same
|
|
8
|
+
// baffling failure:
|
|
9
|
+
//
|
|
10
|
+
// 1. Enterprise-managed Codex requirements disallow `never`. The CLI warns
|
|
11
|
+
// ("Configured value for `approval_policy` is disallowed by requirements;
|
|
12
|
+
// falling back to required value UnlessTrusted") and silently downgrades.
|
|
13
|
+
// Under UnlessTrusted every non-allowlisted command needs an approval that
|
|
14
|
+
// exec mode cannot service — codex_core rejects each one ("approval
|
|
15
|
+
// request failed"), and the agent flails through its tools with no
|
|
16
|
+
// explanation.
|
|
17
|
+
// 2. The user's own ~/.codex/config.toml sets an approval-requiring policy
|
|
18
|
+
// and Operator runs with CODEX_APPROVAL_POLICY=inherit.
|
|
19
|
+
//
|
|
20
|
+
// The downgrade warning arrives as an error item on the very first affected
|
|
21
|
+
// turn (lib/agents/codex/events.ts maps it to a StreamEvent error), so the
|
|
22
|
+
// Codex driver matches it there and flips an instance-wide flag that makes it
|
|
23
|
+
// request the exec-compatible "on-request" policy from the next turn on
|
|
24
|
+
// (self-healing, no user action). This module classifies the failure for
|
|
25
|
+
// lib/runner.ts — same pattern as lib/promptLimits.ts / lib/authFailure.ts —
|
|
26
|
+
// which appends APPROVAL_BLOCKED_NOTICE so the UI can render a one-click Retry.
|
|
27
|
+
// Kept dependency-free so both server and client bundles can import it.
|
|
28
|
+
|
|
29
|
+
/** The CLI's managed-requirements downgrade warning — the signal that the
|
|
30
|
+
* approval policy Operator asked for was rejected and a stricter one applies.
|
|
31
|
+
* Matched by the Codex driver to trigger auto-negotiation. */
|
|
32
|
+
const APPROVAL_DOWNGRADE_RES = [
|
|
33
|
+
/approval_policy[^\n]{0,80}disallowed by requirements/i,
|
|
34
|
+
/disallowed by requirements[^\n]{0,120}approval_policy/i,
|
|
35
|
+
/invalid value for [`'"]?approval_policy[`'"]?[^\n]{0,80}not in the allowed set/i,
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
/** True when an error is the CLI's "your approval_policy was rejected by
|
|
39
|
+
* managed requirements, falling back to a stricter value" warning. */
|
|
40
|
+
export function isApprovalDowngrade(msg: string | null | undefined): boolean {
|
|
41
|
+
return !!msg && APPROVAL_DOWNGRADE_RES.some((re) => re.test(msg));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The rejections codex_core emits when an approval-requiring policy meets exec
|
|
45
|
+
// mode's inability to ask anyone:
|
|
46
|
+
// - "command execution approval is not supported in exec mode for thread …"
|
|
47
|
+
// - "exec_command failed …: Rejected(\"approval request failed\")"
|
|
48
|
+
// - "approval policy is UnlessTrusted; reject command — you cannot ask for
|
|
49
|
+
// escalated permissions if the approval policy is UnlessTrusted"
|
|
50
|
+
const APPROVAL_BLOCKED_RES = [
|
|
51
|
+
...APPROVAL_DOWNGRADE_RES,
|
|
52
|
+
/approval request failed/i,
|
|
53
|
+
/approval policy is \w+; reject/i,
|
|
54
|
+
/(?:command execution )?approval is not supported in exec mode/i,
|
|
55
|
+
/cannot ask for escalated permissions/i,
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/** True when a turn's error text is an approval-policy rejection (the managed
|
|
59
|
+
* downgrade warning, or exec mode failing to service an approval) rather than
|
|
60
|
+
* a work failure. */
|
|
61
|
+
export function isApprovalBlocked(msg: string | null | undefined): boolean {
|
|
62
|
+
return !!msg && APPROVAL_BLOCKED_RES.some((re) => re.test(msg));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Appended to the persisted error line when a turn is blocked by the approval
|
|
66
|
+
* policy. The UI (app/orchestrator/Transcript.tsx) matches this exact string
|
|
67
|
+
* to render the "Retry" button — the driver has already switched future turns
|
|
68
|
+
* to the exec-compatible "on-request" policy by the time anyone reads this, so
|
|
69
|
+
* resending the same message is the recovery. Persisted message content is the
|
|
70
|
+
* durable channel — it survives SSE reconnects because the snapshot replays
|
|
71
|
+
* from SQLite. */
|
|
72
|
+
export const APPROVAL_BLOCKED_NOTICE =
|
|
73
|
+
"智能体的审批策略拦截了这一轮:它要求交互式命令审批,而 " +
|
|
74
|
+
"Operator 的无人值守会话无法提供。Operator 现已为其 Codex 会话改用兼容的 'on-request' " +
|
|
75
|
+
"策略,因此重试这条消息通常就能通过。(如果你使用了 " +
|
|
76
|
+
"CODEX_APPROVAL_POLICY=inherit,请在 ~/.codex/config.toml 中改为设置一个与 exec 兼容的 " +
|
|
77
|
+
"approval_policy,例如 \"on-request\"。)";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Detection + recovery constants for the "your agent login stopped working"
|
|
2
|
+
// failure mode.
|
|
3
|
+
//
|
|
4
|
+
// An expired OAuth session is invisible until you send a turn, and then it looks
|
|
5
|
+
// like a code problem: the raw provider string ("Failed to authenticate: OAuth
|
|
6
|
+
// session expired and could not be refreshed") lands in ONE task's transcript,
|
|
7
|
+
// while every other task on the instance is equally dead — same CLI, same
|
|
8
|
+
// credentials on disk. Worse, each queued follow-up would dequeue and fail the
|
|
9
|
+
// same way, emptying the queue for nothing.
|
|
10
|
+
//
|
|
11
|
+
// So an auth failure is classified here (agent-agnostically — Codex phrases it
|
|
12
|
+
// differently and fails identically), which lets lib/runner.ts append
|
|
13
|
+
// AUTH_EXPIRED_NOTICE for a one-click reconnect, park the queue instead of
|
|
14
|
+
// burning it, and flag the agent instance-wide (lib/agents/connections.ts) so
|
|
15
|
+
// the app can say so in the titlebar rather than only inside the task that
|
|
16
|
+
// happened to run first. Kept dependency-free so both server and client bundles
|
|
17
|
+
// can import it (same rule as lib/promptLimits.ts).
|
|
18
|
+
|
|
19
|
+
// Each agent CLI/SDK words a dead credential differently, and the recovery
|
|
20
|
+
// ("reconnect this agent") is identical for all of them:
|
|
21
|
+
// - Claude Code: "Failed to authenticate: OAuth session expired and could
|
|
22
|
+
// not be refreshed"; API-key path: "invalid x-api-key" / 401.
|
|
23
|
+
// - Codex: "not logged in", "please run codex login", "401 Unauthorized".
|
|
24
|
+
// - Either provider's raw HTTP rejection: 401 + authentication_error.
|
|
25
|
+
// The 401/403 patterns require a nearby auth word so an unrelated error that
|
|
26
|
+
// merely contains a status code can't trip the banner.
|
|
27
|
+
const AUTH_FAILURE_RES = [
|
|
28
|
+
/failed to authenticate/i,
|
|
29
|
+
/authentication (?:failed|error|required)/i,
|
|
30
|
+
/could not be refreshed/i,
|
|
31
|
+
/(?:oauth|login|access|refresh) (?:session|token)[^.\n]{0,40}(?:expired|revoked|invalid|refresh)/i,
|
|
32
|
+
/(?:session|token|credentials?)[^.\n]{0,30}(?:has |have |is |are )?(?:expired|revoked)/i,
|
|
33
|
+
/invalid[_ ]grant/i,
|
|
34
|
+
/invalid[^.\n]{0,20}(?:api[- ]?key|x-api-key|credentials?|token)/i,
|
|
35
|
+
/(?:api[- ]?key|credentials?)[^.\n]{0,30}(?:invalid|expired|revoked|missing|not found)/i,
|
|
36
|
+
/\bnot (?:logged in|authenticated|signed in)\b/i,
|
|
37
|
+
/(?:re-?authenticate|re-?login|log ?in again|sign ?in again)/i,
|
|
38
|
+
/run [`'"]?(?:claude|codex)(?: auth)? login/i,
|
|
39
|
+
/\b40[13]\b[^\n]{0,60}(?:unauthorized|forbidden|authentication|invalid|expired|api[- ]?key)/i,
|
|
40
|
+
/(?:unauthorized|forbidden)[^\n]{0,40}\b40[13]\b/i,
|
|
41
|
+
/unable to locate (?:aws )?credentials/i,
|
|
42
|
+
/aws default-chain credential resolve (?:timed out|failed)/i,
|
|
43
|
+
/(?:ExpiredToken|InvalidClientTokenId|UnrecognizedClientException)/i,
|
|
44
|
+
/(?:aws|sso)[^.\n]{0,30}(?:credentials?|session|token)[^.\n]{0,30}(?:expired|invalid|missing)/i,
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
/** True when a turn's error text is a dead-credential rejection (expired OAuth
|
|
48
|
+
* session, revoked token, bad/missing API key) rather than a work failure. */
|
|
49
|
+
export function isAuthFailure(msg: string | null | undefined): boolean {
|
|
50
|
+
return !!msg && AUTH_FAILURE_RES.some((re) => re.test(msg));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Appended to the persisted error line when a turn dies on authentication. The
|
|
54
|
+
* UI (app/orchestrator/Transcript.tsx) matches this exact string to render the
|
|
55
|
+
* "Reconnect" button, so it must stay agent-neutral — the client already knows
|
|
56
|
+
* the task's agent and labels the button with it. Persisted message content is
|
|
57
|
+
* the durable channel: it survives SSE reconnects because the snapshot replays
|
|
58
|
+
* from SQLite. */
|
|
59
|
+
export const AUTH_EXPIRED_NOTICE =
|
|
60
|
+
"该智能体的连接已失效 —— 登录凭据过期、被撤销或不可用," +
|
|
61
|
+
"在你重新连接或刷新之前无法运行任何轮次。没有任何内容丢失:本次会话及其 " +
|
|
62
|
+
"worktree 均未受影响,排队中的消息也仍在队列里。";
|
|
63
|
+
|
|
64
|
+
/** The one-line, instance-wide version of the same news, for the titlebar banner
|
|
65
|
+
* (app/orchestrator/AgentConnect.tsx → AgentAuthBanner). */
|
|
66
|
+
export const AUTH_BANNER_HINT = "在凭据刷新之前,任何会话都无法运行。";
|