@spzxg/cli-ui 0.6.2 → 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 +137 -11
- package/dist/cli-ui-embed-BmLN2Jjg.js +43290 -0
- package/dist/{cli-ui-index-BY5i3wId.js → cli-ui-index-D4qfPHMd.js} +2 -2
- package/dist/{cli-ui-jszip.min-BZsUwI-s.js → cli-ui-jszip.min-DC9898pC.js} +1 -1
- package/dist/{cli-ui-jszip.min-m01fexHW.js → cli-ui-jszip.min-DpEl0G1z.js} +2 -2
- package/dist/{cli-ui-xlsx-CpWicJLT.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 +73 -30
- package/src/chat/Composer.tsx +41 -41
- 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 +8 -6
- package/src/core/conversation.ts +3 -3
- 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 +20 -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/shared.tsx +1 -1
- package/src/ui/transcriptModel.ts +2 -2
- package/dist/cli-ui-embed-6IJ_jRVh.js +0 -41223
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AgentModelOption } from "./agents/types";
|
|
2
|
+
|
|
3
|
+
export function validModelId(value: unknown): value is string {
|
|
4
|
+
return typeof value === "string" && value.trim().length > 0 && value.trim().length <= 2048 && !/[\0-\x1f\x7f]/.test(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function savedCustomModels(raw: string | null | undefined): string[] {
|
|
8
|
+
try {
|
|
9
|
+
const values: unknown = JSON.parse(raw || "[]");
|
|
10
|
+
return Array.isArray(values) ? [...new Set(values.filter(validModelId).map(value => value.trim()))] : [];
|
|
11
|
+
} catch {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function withCustomModels(models: AgentModelOption[], names: string[]): AgentModelOption[] {
|
|
17
|
+
const known = new Set(models.map(model => model.value));
|
|
18
|
+
return [...models, ...names.filter(name => !known.has(name)).map(name => ({
|
|
19
|
+
value: name, label: name, sub: "自定义模型", group: "自定义",
|
|
20
|
+
// Match the existing conservative fallback for provider-native model IDs.
|
|
21
|
+
contextWindow: /\[1m\]|claude-sonnet-5|claude-fable-5/i.test(name) ? 1_000_000 : 200_000,
|
|
22
|
+
}))];
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AgentAuthEvent, GlobalTaskEvent } from "./types";
|
|
2
|
+
export type GlobalTaskWireEvent = Omit<GlobalTaskEvent, "event"> & {
|
|
3
|
+
event: GlobalTaskEvent["event"] | "task_created" | "task_updated";
|
|
4
|
+
};
|
|
5
|
+
export type TaskDeletedWireEvent = {
|
|
6
|
+
type: "task_deleted";
|
|
7
|
+
taskId: string;
|
|
8
|
+
projectId: string;
|
|
9
|
+
/** The project's awaiting count recomputed AFTER the row was deleted. */
|
|
10
|
+
awaiting_count: number;
|
|
11
|
+
};
|
|
12
|
+
export type GlobalWireEvent = GlobalTaskWireEvent | TaskDeletedWireEvent | AgentAuthEvent;
|
|
13
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
declare const process: { env: Record<string, string | undefined> };
|
|
2
|
+
/**
|
|
3
|
+
* Instance-level feature flags.
|
|
4
|
+
*
|
|
5
|
+
* In-progress features default OFF and are turned on per-instance via env, the
|
|
6
|
+
* same env-driven philosophy as lib/config.ts. Flags are resolved server-side
|
|
7
|
+
* (`resolveFeatures`) and handed to the client by injecting `window.__FEATURES`
|
|
8
|
+
* in app/layout.tsx — mirroring how PUBLIC_BASE_URL crosses the boundary — so
|
|
9
|
+
* both sides gate identically without a per-flag NEXT_PUBLIC_ var.
|
|
10
|
+
*
|
|
11
|
+
* To add a flag: extend `Features` + `DEFAULT_FEATURES`, read its env var in
|
|
12
|
+
* `resolveFeatures`, document it in .env.example, and gate the UI on
|
|
13
|
+
* `clientFeatures().<flag>` (client) or `resolveFeatures().<flag>` (server).
|
|
14
|
+
*/
|
|
15
|
+
export interface Features {
|
|
16
|
+
/** PREVIEW tab (project live-URL view). WIP — depends on the remote-execution
|
|
17
|
+
* backend landing first, so it stays off until the live URL is real. */
|
|
18
|
+
livePreview: boolean;
|
|
19
|
+
/** Command palette: the toolbar "Jump to project, session, or command…"
|
|
20
|
+
* omni-search bar and its ⌘K/Ctrl-K shortcut (app/orchestrator/CommandPalette). */
|
|
21
|
+
omniSearch: boolean;
|
|
22
|
+
/** Managed Services — the toolbar "Services" button, the Services config
|
|
23
|
+
* block in the project-context editor, and the persisted supervisor
|
|
24
|
+
* (lib/services.ts). Shipped: ON by default; set ORCH_FEATURE_SERVICES=0 to
|
|
25
|
+
* disable. NOTE: public service hostnames are NOT part of this flag — they
|
|
26
|
+
* stay opt-in via ORCH_SERVICE_HOSTS (lib/service-host.mjs), so enabling
|
|
27
|
+
* services exposes nothing publicly. */
|
|
28
|
+
services: boolean;
|
|
29
|
+
/** Per-task real browser (docs/BROWSER_PLAN.md): Operator runs one ego lite
|
|
30
|
+
* instance per node with one task space per task, agents drive it with
|
|
31
|
+
* ego-browser scripts, and the session rail streams it. WIP — default OFF
|
|
32
|
+
* (ORCH_FEATURE_BROWSER=1). With the flag off nothing is injected into any
|
|
33
|
+
* agent and no route is active. */
|
|
34
|
+
browser: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_FEATURES: Features = {
|
|
38
|
+
livePreview: false,
|
|
39
|
+
omniSearch: false,
|
|
40
|
+
services: true,
|
|
41
|
+
browser: false,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const truthy = (v: string | undefined) => v === "1" || v === "true" || v === "on";
|
|
45
|
+
// Unset/empty env keeps the flag's shipped default; any explicit value decides.
|
|
46
|
+
// A shipped flag (default ON) is therefore disabled with =0, not by omission.
|
|
47
|
+
// lib/service-router.mjs (plain JS, can't import this file) mirrors this read.
|
|
48
|
+
const flag = (v: string | undefined, dflt: boolean) => (v ? truthy(v) : dflt);
|
|
49
|
+
|
|
50
|
+
/** Server-side resolve from env. Never call this from client code (reads env). */
|
|
51
|
+
export function resolveFeatures(): Features {
|
|
52
|
+
return {
|
|
53
|
+
livePreview: flag(process.env.ORCH_FEATURE_LIVE_PREVIEW, DEFAULT_FEATURES.livePreview),
|
|
54
|
+
omniSearch: flag(process.env.ORCH_FEATURE_OMNI_SEARCH, DEFAULT_FEATURES.omniSearch),
|
|
55
|
+
services: flag(process.env.ORCH_FEATURE_SERVICES, DEFAULT_FEATURES.services),
|
|
56
|
+
browser: flag(process.env.ORCH_FEATURE_BROWSER, DEFAULT_FEATURES.browser),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Client-side read of the flags injected onto `window` by the root layout. */
|
|
61
|
+
export function clientFeatures(): Features {
|
|
62
|
+
// SSR of a client component has no `window` yet. Resolve from env — the same
|
|
63
|
+
// values layout.tsx injects as window.__FEATURES — so the server HTML and the
|
|
64
|
+
// client's first render agree (returning DEFAULT_FEATURES here made every
|
|
65
|
+
// enabled flag a hydration mismatch, e.g. the ⌘K omni button). In the browser
|
|
66
|
+
// this branch never runs, so the process.env reads never execute client-side.
|
|
67
|
+
if (typeof window === "undefined") return resolveFeatures();
|
|
68
|
+
const w = window as unknown as { __FEATURES?: Partial<Features> };
|
|
69
|
+
return { ...DEFAULT_FEATURES, ...(w.__FEATURES ?? {}) };
|
|
70
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type InternalUsageEstimate = { tokens: number; cost_usd: number; source: "project_latest" | "instance_median" | "scaled_history" };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Guards + recovery constants for the "Prompt is too long" failure mode.
|
|
2
|
+
//
|
|
3
|
+
// A giant paste used to permanently poison a task: the SDK reports the API's
|
|
4
|
+
// "prompt is too long" error as a soft stream error AFTER the session has
|
|
5
|
+
// already opened, so lib/runner.ts persists the session id anyway and every
|
|
6
|
+
// later message resumes the same over-limit session — the error repeats
|
|
7
|
+
// forever. These shared limits let the composer defuse big pastes up front
|
|
8
|
+
// (attach as a file instead of inlining), the messages route reject anything
|
|
9
|
+
// that slips through, and the runner recognize the error to surface a
|
|
10
|
+
// one-click recovery. Kept dependency-free so both server and client bundles
|
|
11
|
+
// can import it.
|
|
12
|
+
|
|
13
|
+
/** Client: a text paste larger than this becomes a `.txt` attachment instead
|
|
14
|
+
* of being inlined into the message (see app/orchestrator/Composer.tsx). */
|
|
15
|
+
export const PASTE_ATTACH_THRESHOLD = 100_000; // ~100 KB
|
|
16
|
+
|
|
17
|
+
/** Server: hard cap on a single message's characters (POST /messages). Big
|
|
18
|
+
* content should ride as an attachment, whose bytes never enter the prompt. */
|
|
19
|
+
export const MAX_MESSAGE_CHARS = 262_144; // 256 KB
|
|
20
|
+
|
|
21
|
+
// Each provider phrases a context-window overflow differently, and the recovery
|
|
22
|
+
// flow ("Start fresh context") is agent-agnostic, so we match every driver's
|
|
23
|
+
// signature here:
|
|
24
|
+
// - Anthropic (Claude driver): "prompt is too long: N tokens > M maximum".
|
|
25
|
+
// - OpenAI/Codex (Codex driver): the API returns "context_length_exceeded"
|
|
26
|
+
// with a message like "maximum context length is N tokens. However, your
|
|
27
|
+
// messages resulted in M tokens" / "reduce the length of the messages".
|
|
28
|
+
// The codex CLI surfaces the same as an "exceeds the context window" / "input
|
|
29
|
+
// is too long" turn failure. Any of these should trip the same recovery.
|
|
30
|
+
const CONTEXT_OVERFLOW_RES = [
|
|
31
|
+
/prompt is too long/i,
|
|
32
|
+
/context[_ ]length[_ ]exceeded/i,
|
|
33
|
+
/maximum context length/i,
|
|
34
|
+
/exceeds? the (?:model'?s? )?context (?:window|length|limit)/i,
|
|
35
|
+
/(?:input|conversation|message)s? (?:is |are )?too long/i,
|
|
36
|
+
/reduce the length of the messages/i,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** True when an error/stream message is a provider's context-overflow rejection
|
|
40
|
+
* (Anthropic "prompt is too long", OpenAI/Codex "context_length_exceeded", …). */
|
|
41
|
+
export function isPromptTooLong(msg: string | null | undefined): boolean {
|
|
42
|
+
return !!msg && CONTEXT_OVERFLOW_RES.some((re) => re.test(msg));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Appended to the persisted error line when a turn overflows the context
|
|
46
|
+
* window. The UI (app/orchestrator/Transcript.tsx) matches this exact string
|
|
47
|
+
* to render the "Start fresh context" recovery button. Persisted message
|
|
48
|
+
* content is the durable channel — it survives SSE reconnects because the
|
|
49
|
+
* snapshot replays messages from SQLite. */
|
|
50
|
+
export const CONTEXT_OVERFLOW_NOTICE =
|
|
51
|
+
"本次会话的上下文已超出模型上限,无法继续。" +
|
|
52
|
+
"请开启一个新的上下文窗口以继续 —— 本次会话的摘要会一并带过去。";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Shared by the storage form and its API; keep this module browser-safe.
|
|
2
|
+
export const DEFAULT_RETENTION_DAYS = 30;
|
|
3
|
+
export const DAY_MS = 86_400_000;
|
|
4
|
+
|
|
5
|
+
export function validRetentionDays(value: unknown): value is string {
|
|
6
|
+
if (typeof value !== "string" || !/^\d+$/.test(value)) return false;
|
|
7
|
+
const days = Number(value);
|
|
8
|
+
return days >= 1 && Number.isSafeInteger(days * DAY_MS);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function retentionDays(value: string | null | undefined): number {
|
|
12
|
+
return validRetentionDays(value) ? Number(value) : DEFAULT_RETENTION_DAYS;
|
|
13
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
interface AgentProfile { base_url: string; token: string; model: string; wire_api?: string | null; api?: string | null; reasoning?: string | null }
|
|
2
|
+
|
|
3
|
+
export type Priority = "hi" | "med" | "lo";
|
|
4
|
+
export type Status = "not_started" | "in_progress" | "on_hold" | "done" | "cancelled";
|
|
5
|
+
export type MsgRole = "user" | "assistant" | "tool" | "system" | "session_break";
|
|
6
|
+
|
|
7
|
+
export interface Project {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
icon: string;
|
|
11
|
+
sub: string; // short tagline, e.g. "notes app"
|
|
12
|
+
color: string; // accent color for the project glyph
|
|
13
|
+
context: string; // "what we're building" — description, stack & conventions (single field)
|
|
14
|
+
building: string; // legacy — kept for back-compat, folded into context
|
|
15
|
+
conventions: string; // legacy — kept for back-compat, folded into context
|
|
16
|
+
repo_path: string; // working dir for Claude Code
|
|
17
|
+
branch: string; // git branch (context only)
|
|
18
|
+
dev_command: string; // long-running dev server command supervised by lib/services.ts ("" = none)
|
|
19
|
+
setup_command: string; // optional one-shot setup command (install/migrate/etc.)
|
|
20
|
+
test_command: string; // optional one-shot test command
|
|
21
|
+
port: number; // deterministic per-project port, injected as PORT into services + the PTY
|
|
22
|
+
default_agent: string; // agent driver new tasks in this project run under (lib/agents/registry.ts)
|
|
23
|
+
send_context: number; // 1 = include the saved project context in new agent sessions (default for new tasks)
|
|
24
|
+
recap: string; // last LLM "where you left off" recap (auto-generated when idle)
|
|
25
|
+
recap_at: number; // when the recap was generated (0 = none)
|
|
26
|
+
recap_covers_at: number; // the project's last-activity ts the recap was based on
|
|
27
|
+
// Detached "Refresh with AI" job (drafts run in the background; see lib/contextRefresh.ts).
|
|
28
|
+
refresh_status: "idle" | "running" | "done" | "error";
|
|
29
|
+
refresh_draft: string; // drafted context awaiting the user's review (when status="done")
|
|
30
|
+
refresh_error: string; // failure message (when status="error")
|
|
31
|
+
refresh_started_at: number; // when the current/last job started (ms epoch, 0 = never)
|
|
32
|
+
position: number; // manual sidebar order (ascending)
|
|
33
|
+
deprecated: number; // 1 = hidden in the sidebar's "deprecated" area, not built on
|
|
34
|
+
seeded: number; // 1 = the built-in "Welcome" tutorial project (see lib/db.ts seedIfEmpty)
|
|
35
|
+
created_at: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface Task {
|
|
39
|
+
id: string;
|
|
40
|
+
parent_task_id?: string | null;
|
|
41
|
+
fork_commit?: string;
|
|
42
|
+
fork_source_title?: string;
|
|
43
|
+
fork_context?: string;
|
|
44
|
+
fork_group_id?: string;
|
|
45
|
+
project_id: string;
|
|
46
|
+
title: string;
|
|
47
|
+
description: string;
|
|
48
|
+
priority: Priority;
|
|
49
|
+
status: Status;
|
|
50
|
+
suggested: number; // 1 = Claude-proposed, idle in the suggested tray
|
|
51
|
+
agent: string; // agent driver this task's sessions run under (default "claude"; see lib/agents/)
|
|
52
|
+
send_context: number; // 1 = include the saved project context in this task's sessions (seeded from projects.send_context)
|
|
53
|
+
model: string | null; // chosen model alias ("fable"|"opus"|"sonnet"|"haiku"); null = inherit default
|
|
54
|
+
resolved_model: string | null; // model the SDK actually ran last turn (for the badge)
|
|
55
|
+
reasoning: string | null; // thinking preset ("off"|"think"|"think_hard"|"ultrathink"); null = inherit default
|
|
56
|
+
permission_mode: string | null; // run permission ("acceptEdits"|"plan"); null = bypassPermissions (default)
|
|
57
|
+
session_id: string | null; // the agent's opaque session/thread id for the current generation
|
|
58
|
+
worktree_path: string; // isolated git worktree this task runs in ("" = runs in repo_path)
|
|
59
|
+
work_branch: string; // the worktree's branch (e.g. "orch/<id>")
|
|
60
|
+
base_sha: string; // commit the worktree branched from — the stable diff/merge base
|
|
61
|
+
merged_at: number; // when this task's branch was merged back (0 = not merged)
|
|
62
|
+
pr_url: string; // GitHub PR opened from this task's branch via "Create PR" ("" = none)
|
|
63
|
+
generation: number; // increments on each /clear
|
|
64
|
+
position: number; // manual order within the project (list groups + board columns, ascending)
|
|
65
|
+
started: number; // 1 once the initial prompt has been sent
|
|
66
|
+
auto_start: number; // 1 = start automatically when the last unfinished blocker is marked done (lib/autoStart.ts)
|
|
67
|
+
running: number; // 1 while a Claude turn is actively streaming
|
|
68
|
+
awaiting_input: number; // 1 when it's your turn: Claude's turn ended mid-task, or it's parked on an AskUserQuestion
|
|
69
|
+
created_at: number;
|
|
70
|
+
updated_at: number;
|
|
71
|
+
// Not a column: the runner attaches the CLITaskCenter-supplied model account for
|
|
72
|
+
// the agent this turn runs on (lib/agentProfile.ts). Absent/null = the CLI uses
|
|
73
|
+
// this machine's own configuration.
|
|
74
|
+
agent_profile?: AgentProfile | null;
|
|
75
|
+
// Per-launch contract, never a task column. Durable run records live in the
|
|
76
|
+
// managed project's .workspace directory; queued UI turns get their own ID.
|
|
77
|
+
workspace_request?: { run_id?: string; input?: unknown; [key: string]: unknown };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface Message {
|
|
81
|
+
id: string;
|
|
82
|
+
task_id: string;
|
|
83
|
+
generation: number;
|
|
84
|
+
role: MsgRole;
|
|
85
|
+
content: string;
|
|
86
|
+
created_at: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface Summary {
|
|
90
|
+
id: string;
|
|
91
|
+
task_id: string;
|
|
92
|
+
generation: number;
|
|
93
|
+
summary: string;
|
|
94
|
+
created_at: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// A follow-up the user typed while a turn was still running. Parked in the
|
|
98
|
+
// pending_messages table (FIFO per task) and shown as "queued" in the
|
|
99
|
+
// transcript; the runner dequeues the oldest one as the next turn when the
|
|
100
|
+
// current turn ends. Distinct from `messages` (the committed transcript).
|
|
101
|
+
export interface PendingMessage {
|
|
102
|
+
id: string;
|
|
103
|
+
task_id: string;
|
|
104
|
+
generation: number;
|
|
105
|
+
content: string;
|
|
106
|
+
created_at: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface Session {
|
|
110
|
+
id: string;
|
|
111
|
+
project_id: string;
|
|
112
|
+
task_id: string;
|
|
113
|
+
generation: number;
|
|
114
|
+
// The agent's own opaque session/thread id (named for the first driver; any
|
|
115
|
+
// driver's id — e.g. a Codex thread id — lands in this same column).
|
|
116
|
+
claude_session_id: string | null;
|
|
117
|
+
started_at: number;
|
|
118
|
+
ended_at: number | null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------- managed services (lib/services.ts) ----------
|
|
122
|
+
|
|
123
|
+
// A supervised process belonging to a project. `kind` is the configured slot the
|
|
124
|
+
// service maps to ("dev"/"setup"/"test"), or "exposed" for a server Claude
|
|
125
|
+
// registered at runtime via the expose_service MCP tool (we don't own that
|
|
126
|
+
// process, we only track its url). The orchestrator can start/stop/restart the
|
|
127
|
+
// configured kinds; an exposed entry is informational (its url is reportable).
|
|
128
|
+
export type ServiceStatus = "stopped" | "starting" | "running" | "exited" | "errored";
|
|
129
|
+
export type ServiceKind = "dev" | "setup" | "test" | "exposed";
|
|
130
|
+
// Who can open a service's public URL: private = the instance's own session
|
|
131
|
+
// auth, shared = anyone holding the tokened link (?t=…), public = anyone.
|
|
132
|
+
export type ServiceVisibility = "private" | "shared" | "public";
|
|
133
|
+
|
|
134
|
+
export interface ServiceInfo {
|
|
135
|
+
projectId: string;
|
|
136
|
+
name: string; // unique per project — the kind for configured services, or Claude's chosen name
|
|
137
|
+
kind: ServiceKind;
|
|
138
|
+
command: string; // the shell command (empty for an exposed, externally-started service)
|
|
139
|
+
status: ServiceStatus;
|
|
140
|
+
pid: number | null;
|
|
141
|
+
exitCode: number | null;
|
|
142
|
+
port: number; // the port injected as PORT (configured) or reported by expose_service
|
|
143
|
+
url: string | null; // browseable URL once running/exposed
|
|
144
|
+
startedAt: number | null;
|
|
145
|
+
managed: boolean; // true if the orchestrator owns the process (can stop/restart)
|
|
146
|
+
slug: string | null; // public hostname label (<slug>--<host>); null until first persisted
|
|
147
|
+
visibility: ServiceVisibility;
|
|
148
|
+
shareUrl: string | null; // tokened link (url?t=…) when visibility is "shared"
|
|
149
|
+
// Human-readable supervisor-level failure (port already in use, spawn failed).
|
|
150
|
+
// Distinct from a nonzero exit, which the logs explain; cleared on next start.
|
|
151
|
+
error: string | null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// One captured line of a service's combined stdout/stderr.
|
|
155
|
+
export interface ServiceLogLine {
|
|
156
|
+
ts: number;
|
|
157
|
+
stream: "stdout" | "stderr" | "system"; // "system" = supervisor notice (started/exited)
|
|
158
|
+
text: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Live events on a project's services SSE stream (GET .../services/stream).
|
|
162
|
+
export type ServiceEvent =
|
|
163
|
+
| { type: "snapshot"; services: ServiceInfo[]; logs: Record<string, ServiceLogLine[]> }
|
|
164
|
+
| { type: "status"; service: ServiceInfo }
|
|
165
|
+
| { type: "log"; name: string; line: ServiceLogLine }
|
|
166
|
+
| { type: "removed"; name: string };
|
|
167
|
+
|
|
168
|
+
// A multiple-choice question Claude raised via the AskUserQuestion tool.
|
|
169
|
+
export interface AskOption {
|
|
170
|
+
label: string;
|
|
171
|
+
description?: string;
|
|
172
|
+
}
|
|
173
|
+
export interface AskQuestion {
|
|
174
|
+
question: string;
|
|
175
|
+
header: string; // short chip label (≤12 chars)
|
|
176
|
+
multiSelect?: boolean;
|
|
177
|
+
options: AskOption[];
|
|
178
|
+
}
|
|
179
|
+
// answers[i] = the chosen value(s) for question i — option labels and/or the
|
|
180
|
+
// free-text typed into "Other". One entry per question, in question order.
|
|
181
|
+
export type AskAnswers = string[][];
|
|
182
|
+
|
|
183
|
+
// browser_ask_user (lib/browserInput.ts): the agent needs something typed or
|
|
184
|
+
// picked in the chat — an SMS / 2FA code, an account, a password, a choice —
|
|
185
|
+
// to carry on in the task's browser. Shown as an input card in the transcript.
|
|
186
|
+
export interface InputField {
|
|
187
|
+
label: string;
|
|
188
|
+
/** A password or similar: masked in the card, never stored or shown to the model. */
|
|
189
|
+
secret?: boolean;
|
|
190
|
+
/** Pick one of these (the user may still type something else). */
|
|
191
|
+
options?: string[];
|
|
192
|
+
placeholder?: string;
|
|
193
|
+
}
|
|
194
|
+
export interface InputRequest {
|
|
195
|
+
id: string;
|
|
196
|
+
message: string;
|
|
197
|
+
fields: InputField[];
|
|
198
|
+
status: "pending" | "answered" | "cancelled";
|
|
199
|
+
/** What the transcript keeps once answered: secret fields are masked. */
|
|
200
|
+
values?: string[];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Token usage + dollar cost for one Claude turn, parsed from the SDK result
|
|
204
|
+
// message. Persisted per turn (task_usage table) and summed for cumulative spend.
|
|
205
|
+
export interface TurnUsage {
|
|
206
|
+
cost_usd: number;
|
|
207
|
+
input_tokens: number;
|
|
208
|
+
output_tokens: number;
|
|
209
|
+
cache_read_tokens: number;
|
|
210
|
+
cache_creation_tokens: number;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Cumulative usage totals across one task (or project): summed turn usage plus
|
|
214
|
+
// `total_tokens` (the four token counts combined) and the turn count.
|
|
215
|
+
export interface UsageTotals extends TurnUsage {
|
|
216
|
+
total_tokens: number;
|
|
217
|
+
turns: number;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// One rendered diff line: added (+, green), removed (-, red), or unchanged
|
|
221
|
+
// context (" ", dim). Used by both the capped peek and the full expanded diff.
|
|
222
|
+
export type DiffLine = { sign: "+" | "-" | " "; text: string };
|
|
223
|
+
|
|
224
|
+
// An always-visible "peek" at a tool's effect — mimics Claude Code's `⎿` line.
|
|
225
|
+
// `count`: a one-liner (Read N lines / Found N matches) with no content shown.
|
|
226
|
+
// `diff`: a -/+ hunk for Edits/Writes. `lines`: a short snippet (Bash output)
|
|
227
|
+
// with a +N-more affordance. `todos`: a rendered checklist.
|
|
228
|
+
export type ToolPeek =
|
|
229
|
+
| { kind: "count"; text: string }
|
|
230
|
+
| { kind: "diff"; added: number; removed: number; label?: string; lines: DiffLine[]; truncated?: number }
|
|
231
|
+
| { kind: "lines"; label?: string; lines: string[]; truncated?: number }
|
|
232
|
+
| { kind: "todos"; items: { text: string; status: string }[] };
|
|
233
|
+
|
|
234
|
+
// Server-sent stream events from a Claude turn.
|
|
235
|
+
export type StreamEvent =
|
|
236
|
+
| { type: "session"; sessionId: string }
|
|
237
|
+
| { type: "model"; model: string }
|
|
238
|
+
| { type: "assistant"; content: string }
|
|
239
|
+
| { type: "tool"; id: string; title: string; detail: string; peek?: ToolPeek; diff?: DiffLine[] }
|
|
240
|
+
| { type: "tool_result"; id: string; content: string; isError: boolean; peek?: ToolPeek }
|
|
241
|
+
| { type: "ask"; id: string; questions: AskQuestion[] }
|
|
242
|
+
| { type: "ask_answered"; id: string; answers: AskAnswers }
|
|
243
|
+
// An input card was raised or settled (the whole card each time; upsert by msgId).
|
|
244
|
+
| { type: "input"; input: InputRequest }
|
|
245
|
+
| { type: "suggested"; title: string }
|
|
246
|
+
| { type: "usage"; usage: TurnUsage }
|
|
247
|
+
| { type: "notice"; content: string } // a quiet, non-error system note (e.g. "caught up to main")
|
|
248
|
+
| { type: "error"; content: string }
|
|
249
|
+
| { type: "done"; sessionId: string | null };
|
|
250
|
+
|
|
251
|
+
// Events as delivered over the task event bus and the GET /messages SSE tail.
|
|
252
|
+
// Turn events are enriched with the persisted DB message id (+ generation) so
|
|
253
|
+
// reconnecting clients can upsert idempotently instead of blindly appending:
|
|
254
|
+
// `msgId` is the row the event created (assistant/tool/ask/notice/error/user)
|
|
255
|
+
// or updated in place (tool_result/ask_answered). `snapshot` opens every
|
|
256
|
+
// stream — the full persisted transcript plus whether a turn is live — and
|
|
257
|
+
// `turn_end` marks the runner's finally block (running flag is off, task row
|
|
258
|
+
// settled), letting clients refresh without owning the turn's lifetime.
|
|
259
|
+
// `queued` is a follow-up parked while a turn runs; `dequeued` removes a parked
|
|
260
|
+
// follow-up from the transcript — either because the runner is now running it
|
|
261
|
+
// as the next turn, or because it was cancelled. `snapshot` carries the parked
|
|
262
|
+
// queue too, so a reload mid-run re-renders the queued bubbles.
|
|
263
|
+
// `ts` mirrors the persisted row's created_at (ms epoch) so a live-tail bubble
|
|
264
|
+
// carries the same clock a reconnect's snapshot row would.
|
|
265
|
+
export type TaskStreamEvent =
|
|
266
|
+
| (StreamEvent & { msgId?: string; generation?: number; ts?: number })
|
|
267
|
+
| { type: "user"; content: string; msgId: string; generation: number; ts?: number }
|
|
268
|
+
| { type: "queued"; msgId: string; content: string; generation: number; ts?: number }
|
|
269
|
+
| { type: "dequeued"; msgId: string }
|
|
270
|
+
// `older` is set only when the viewer asked for a tail (?tail=N): how many
|
|
271
|
+
// earlier messages were left out (fetch them from /api/tasks/:id/history).
|
|
272
|
+
| { type: "snapshot"; messages: Message[]; pending: PendingMessage[]; running: boolean; older?: number }
|
|
273
|
+
| AgentAuthEvent
|
|
274
|
+
| { type: "turn_end" };
|
|
275
|
+
|
|
276
|
+
// An agent's credentials died (broken=true) or started working again
|
|
277
|
+
// (broken=false). Published by the runner on the failing/succeeding task's
|
|
278
|
+
// channel — but it's really an INSTANCE-wide fact (one login per agent, shared
|
|
279
|
+
// by every task), so GET /api/events relays it verbatim to every tab, where it
|
|
280
|
+
// drives the titlebar reconnect banner. See lib/authFailure.ts.
|
|
281
|
+
export type AgentAuthEvent = {
|
|
282
|
+
type: "agent_auth";
|
|
283
|
+
agent: string;
|
|
284
|
+
broken: boolean;
|
|
285
|
+
reason: string | null;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Coarse cross-task lifecycle events on the always-open GET /api/events stream
|
|
289
|
+
// (the wildcard channel of lib/events.ts). One event per turn boundary — turn
|
|
290
|
+
// launched, parked on a question, question answered, suggestion created, turn
|
|
291
|
+
// ended — carrying the task row's settled running/awaiting_input/status (the
|
|
292
|
+
// runner persists before it publishes, so these are authoritative) plus the
|
|
293
|
+
// project's fresh awaiting count. This is what keeps spinners, project badges,
|
|
294
|
+
// and the "N need you" pill live for tasks whose transcript stream isn't open.
|
|
295
|
+
export type GlobalTaskEvent = {
|
|
296
|
+
type: "task";
|
|
297
|
+
event: "turn_started" | "awaiting_input" | "ask_answered" | "suggested" | "turn_end";
|
|
298
|
+
taskId: string;
|
|
299
|
+
projectId: string;
|
|
300
|
+
running: boolean;
|
|
301
|
+
awaiting_input: boolean;
|
|
302
|
+
status: Status;
|
|
303
|
+
/** In-progress tasks awaiting the user across this task's project. */
|
|
304
|
+
awaiting_count: number;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Everything GET /api/events can send. Task lifecycle is the bulk of it;
|
|
308
|
+
// agent_auth rides the same stream because a dead login affects every task in
|
|
309
|
+
// every project at once, so there's nothing task-shaped to hang it off.
|
|
310
|
+
export type GlobalEvent = GlobalTaskEvent | AgentAuthEvent;
|
|
311
|
+
|
|
312
|
+
// How a tool call is stored (JSON) in a "tool" message's content.
|
|
313
|
+
export interface ToolData {
|
|
314
|
+
title: string;
|
|
315
|
+
detail?: string;
|
|
316
|
+
result?: string;
|
|
317
|
+
isError?: boolean;
|
|
318
|
+
// Always-visible summary/snippet of the call's effect (see ToolPeek). Input-
|
|
319
|
+
// derived peeks (diff/todos/write) are set with the tool event; result-derived
|
|
320
|
+
// peeks (read count, bash output) are filled in when the tool_result arrives.
|
|
321
|
+
peek?: ToolPeek;
|
|
322
|
+
// Full colored diff for Edit/Write, rendered in the expanded body (the peek
|
|
323
|
+
// shows a capped slice of the same lines). Absent on older persisted messages,
|
|
324
|
+
// which fall back to the plaintext `detail`.
|
|
325
|
+
diff?: DiffLine[];
|
|
326
|
+
// Present when this "tool" message is an AskUserQuestion prompt. `id` is the
|
|
327
|
+
// tool_use id (stored here so it survives a reload — there's no DB column for
|
|
328
|
+
// it). `answers` is absent while awaiting the user, set once answered.
|
|
329
|
+
ask?: { id: string; questions: AskQuestion[]; answers?: AskAnswers };
|
|
330
|
+
// Present when this "tool" message is a browser_ask_user input card.
|
|
331
|
+
input?: InputRequest;
|
|
332
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Detection + recovery constants for the "your subscription's usage limit is
|
|
2
|
+
// spent" failure mode.
|
|
3
|
+
//
|
|
4
|
+
// A subscription limit (Claude's 5-hour window, the weekly cap, an API 429)
|
|
5
|
+
// hits mid-run and looks like a code problem: the raw provider string ("Claude
|
|
6
|
+
// AI usage limit reached|1735689600") lands in ONE task's transcript with no
|
|
7
|
+
// hint that nothing is broken — the quota is simply spent and refills on its
|
|
8
|
+
// own. Worse, without classification the runner would treat it as an ordinary
|
|
9
|
+
// work failure and drain the pending queue: each queued follow-up would
|
|
10
|
+
// dequeue, run straight into the same dead quota, and fail identically,
|
|
11
|
+
// emptying the queue for nothing.
|
|
12
|
+
//
|
|
13
|
+
// So a usage-limit failure is classified here (agent-agnostically — a Codex
|
|
14
|
+
// quota phrases it differently and fails identically), which lets
|
|
15
|
+
// lib/runner.ts append USAGE_LIMIT_NOTICE and park the queue instead of
|
|
16
|
+
// burning it. Unlike the auth twin (lib/authFailure.ts) there is no button to
|
|
17
|
+
// click — the recovery is waiting for the reset — so the notice is purely
|
|
18
|
+
// informational, and the Claude driver enriches the error line with the
|
|
19
|
+
// machine-reported reset time when the SDK provided one. Kept dependency-free
|
|
20
|
+
// so both server and client bundles can import it (same rule as
|
|
21
|
+
// lib/promptLimits.ts).
|
|
22
|
+
|
|
23
|
+
// Each provider words a spent quota differently, and the recovery ("wait for
|
|
24
|
+
// the reset") is identical for all of them:
|
|
25
|
+
// - Claude Code (subscription): "Claude AI usage limit reached|<epoch>";
|
|
26
|
+
// the CLI's display forms "5-hour limit reached ∙ resets 3pm" and
|
|
27
|
+
// "Weekly limit reached".
|
|
28
|
+
// - Anthropic API: 429 rate_limit_error ("Number of request tokens has
|
|
29
|
+
// exceeded your per-minute rate limit", "This request would exceed your
|
|
30
|
+
// organization's rate limit").
|
|
31
|
+
// - OpenAI/Codex: "You've hit your usage limit", 429 insufficient_quota
|
|
32
|
+
// ("You exceeded your current quota").
|
|
33
|
+
// The generic patterns (429, "rate limit", "quota") require a nearby
|
|
34
|
+
// limit/exceeded word so an unrelated error that merely mentions a status
|
|
35
|
+
// code or the word "quota" can't trip the parking behavior.
|
|
36
|
+
const USAGE_LIMIT_RES = [
|
|
37
|
+
/usage limit reached/i,
|
|
38
|
+
/\b(?:5|five)[- ]hour limit/i,
|
|
39
|
+
/\bweekly limit\b/i,
|
|
40
|
+
/(?:usage|subscription|plan) limit[^.\n]{0,40}(?:reached|exceeded|hit)/i,
|
|
41
|
+
/(?:reached|exceeded|hit)[^.\n]{0,30}(?:usage|subscription|plan|rate) limit/i,
|
|
42
|
+
/rate[_ ]limit(?:_error|ed)?[^.\n]{0,40}(?:exceeded|reached|exhausted)/i,
|
|
43
|
+
/(?:exceed|exceeded|exceeds)[^.\n]{0,40}rate[_ ]limit/i,
|
|
44
|
+
/(?:exceeded|exhausted)[^.\n]{0,30}(?:your|current|the)[^.\n]{0,20}quota/i,
|
|
45
|
+
/quota[^.\n]{0,30}(?:exceeded|exhausted|reached)/i,
|
|
46
|
+
/insufficient[_ ]quota/i,
|
|
47
|
+
/out of (?:usage|quota|credits)/i,
|
|
48
|
+
/\b429\b[^\n]{0,60}(?:rate[_ ]limit|quota|usage limit|too many requests)/i,
|
|
49
|
+
/too many requests[^\n]{0,40}\b429\b/i,
|
|
50
|
+
/(?:ThrottlingException|TooManyRequestsException)/i,
|
|
51
|
+
/(?:bedrock|aws)[^.\n]{0,40}(?:service quota|throttl)[^.\n]{0,40}(?:exceed|reached|exception)/i,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
/** True when a turn's error text is a spent-quota rejection (Claude's 5-hour /
|
|
55
|
+
* weekly subscription limit, an API 429 rate limit) rather than a work
|
|
56
|
+
* failure. Checked AFTER isPromptTooLong and isAuthFailure in lib/runner.ts,
|
|
57
|
+
* so those classifiers keep first claim on their own signatures. */
|
|
58
|
+
export function isUsageLimit(msg: string | null | undefined): boolean {
|
|
59
|
+
return !!msg && USAGE_LIMIT_RES.some((re) => re.test(msg));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Appended to the persisted error line when a turn dies on a spent usage
|
|
63
|
+
* limit. The UI (app/orchestrator/Transcript.tsx) matches this exact string
|
|
64
|
+
* to render the informational recovery hint — keep it stable. No action
|
|
65
|
+
* button: the quota refills on its own, so the recovery is waiting (the raw
|
|
66
|
+
* provider text above the notice carries the reset time when the SDK reported
|
|
67
|
+
* one). Persisted message content is the durable channel — it survives SSE
|
|
68
|
+
* reconnects because the snapshot replays from SQLite. */
|
|
69
|
+
export const USAGE_LIMIT_NOTICE =
|
|
70
|
+
"该智能体的用量已达上限,在额度重置之前无法运行任何轮次。" +
|
|
71
|
+
"没有任何内容丢失:本次会话及其 worktree 均未受影响,排队中的消息也仍在队列里 —— " +
|
|
72
|
+
"额度重置后会自动继续运行。";
|