@saadnvd1/agent-os 0.3.570 → 0.3.571
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/app/api/client-config/route.test.ts +11 -0
- package/app/api/client-config/route.ts +10 -0
- package/components/Chat/AccessPicker.tsx +10 -18
- package/components/Chat/ChatPanel.tsx +2 -0
- package/components/Chat/ModelPicker.tsx +9 -3
- package/components/Chat/composer/ComposerBody.tsx +3 -0
- package/components/Chat/composer/Toolbar.tsx +3 -0
- package/components/Recipes/format.ts +19 -30
- package/components/Tasks/task-state-labels.ts +16 -16
- package/data/chat/useChat.ts +3 -0
- package/lib/chat/access-levels.ts +24 -0
- package/lib/chat/events.ts +2 -0
- package/lib/chat/settings.ts +6 -2
- package/lib/config/client-config-types.ts +29 -0
- package/lib/config/client-config.ts +28 -0
- package/lib/model-catalog.ts +11 -0
- package/lib/recipes/status-labels.ts +27 -0
- package/lib/tasks/state-labels.ts +21 -0
- package/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { GET } from "./route";
|
|
3
|
+
|
|
4
|
+
describe("GET /api/client-config", () => {
|
|
5
|
+
it("serves the config uncached", async () => {
|
|
6
|
+
const res = GET();
|
|
7
|
+
expect(res.status).toBe(200);
|
|
8
|
+
expect(res.headers.get("cache-control")).toBe("no-store");
|
|
9
|
+
expect((await res.json()).v).toBe(1);
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { clientConfig } from "@/lib/config/client-config";
|
|
3
|
+
|
|
4
|
+
// GET /api/client-config - the labels and defaults a client renders with.
|
|
5
|
+
// Read-only; the app keeps its own cache, so the server never does.
|
|
6
|
+
export function GET() {
|
|
7
|
+
return NextResponse.json(clientConfig(), {
|
|
8
|
+
headers: { "Cache-Control": "no-store" },
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -14,28 +14,20 @@ import {
|
|
|
14
14
|
DropdownMenuItem,
|
|
15
15
|
DropdownMenuTrigger,
|
|
16
16
|
} from "@/components/ui/dropdown-menu";
|
|
17
|
+
import { ACCESS_LEVELS } from "@/lib/chat/access-levels";
|
|
17
18
|
import { CHAT_ACCESS, type ChatAccess } from "@/lib/chat/events";
|
|
18
19
|
|
|
19
|
-
const
|
|
20
|
+
const ICON: Record<ChatAccess, typeof Shield> = {
|
|
21
|
+
ask: ShieldCheck,
|
|
22
|
+
edits: Shield,
|
|
23
|
+
full: ShieldOff,
|
|
24
|
+
};
|
|
25
|
+
const LEVELS = Object.fromEntries(
|
|
26
|
+
CHAT_ACCESS.map((a) => [a, { ...ACCESS_LEVELS[a], icon: ICON[a] }])
|
|
27
|
+
) as Record<
|
|
20
28
|
ChatAccess,
|
|
21
29
|
{ label: string; description: string; icon: typeof Shield }
|
|
22
|
-
|
|
23
|
-
ask: {
|
|
24
|
-
label: "Ask first",
|
|
25
|
-
description: "Asks before edits and commands it isn't already allowed",
|
|
26
|
-
icon: ShieldCheck,
|
|
27
|
-
},
|
|
28
|
-
edits: {
|
|
29
|
-
label: "Accept edits",
|
|
30
|
-
description: "Edits files on its own, asks before other commands",
|
|
31
|
-
icon: Shield,
|
|
32
|
-
},
|
|
33
|
-
full: {
|
|
34
|
-
label: "Full access",
|
|
35
|
-
description: "Does everything without asking",
|
|
36
|
-
icon: ShieldOff,
|
|
37
|
-
},
|
|
38
|
-
};
|
|
30
|
+
>;
|
|
39
31
|
|
|
40
32
|
// What the agent may do without asking, switchable mid-conversation.
|
|
41
33
|
export function AccessPicker({
|
|
@@ -265,6 +265,7 @@ export function ChatPanel({
|
|
|
265
265
|
refreshingCommands,
|
|
266
266
|
models,
|
|
267
267
|
model,
|
|
268
|
+
modelLabel,
|
|
268
269
|
send,
|
|
269
270
|
interrupt,
|
|
270
271
|
setModel,
|
|
@@ -770,6 +771,7 @@ export function ChatPanel({
|
|
|
770
771
|
refreshingCommands={refreshingCommands}
|
|
771
772
|
models={models}
|
|
772
773
|
model={model}
|
|
774
|
+
modelLabel={modelLabel}
|
|
773
775
|
onSetModel={setModel}
|
|
774
776
|
access={access}
|
|
775
777
|
onSetAccess={accessLocked ? undefined : setAccess}
|
|
@@ -9,20 +9,26 @@ import {
|
|
|
9
9
|
} from "@/components/ui/dropdown-menu";
|
|
10
10
|
import type { ChatModel } from "@/lib/chat/events";
|
|
11
11
|
|
|
12
|
-
export function modelLabel(
|
|
13
|
-
|
|
12
|
+
export function modelLabel(
|
|
13
|
+
models: ChatModel[],
|
|
14
|
+
value: string,
|
|
15
|
+
serverLabel?: string
|
|
16
|
+
): string {
|
|
17
|
+
return models.find((m) => m.value === value)?.label ?? (serverLabel || value);
|
|
14
18
|
}
|
|
15
19
|
|
|
16
20
|
// The session's model, switchable mid-conversation. Opened by its button or /model.
|
|
17
21
|
export function ModelPicker({
|
|
18
22
|
models,
|
|
19
23
|
model,
|
|
24
|
+
label,
|
|
20
25
|
open,
|
|
21
26
|
onOpenChange,
|
|
22
27
|
onPick,
|
|
23
28
|
}: {
|
|
24
29
|
models: ChatModel[];
|
|
25
30
|
model: string;
|
|
31
|
+
label?: string;
|
|
26
32
|
open: boolean;
|
|
27
33
|
onOpenChange: (open: boolean) => void;
|
|
28
34
|
onPick: (value: string) => void;
|
|
@@ -37,7 +43,7 @@ export function ModelPicker({
|
|
|
37
43
|
className="text-muted-foreground hover:text-foreground hover:bg-foreground/[0.05] flex h-10 max-w-36 min-w-0 items-center gap-1 rounded-lg px-2 text-xs"
|
|
38
44
|
>
|
|
39
45
|
<span className="truncate">
|
|
40
|
-
{modelLabel(models, model) || "Model"}
|
|
46
|
+
{modelLabel(models, model, label) || "Model"}
|
|
41
47
|
</span>
|
|
42
48
|
<ChevronDown className="h-3 w-3 shrink-0" />
|
|
43
49
|
</button>
|
|
@@ -51,6 +51,7 @@ export interface ComposerProps {
|
|
|
51
51
|
refreshingCommands?: boolean;
|
|
52
52
|
models?: ChatModel[];
|
|
53
53
|
model?: string;
|
|
54
|
+
modelLabel?: string;
|
|
54
55
|
onSetModel?: (model: string) => void;
|
|
55
56
|
access?: ChatAccess;
|
|
56
57
|
onSetAccess?: (access: ChatAccess) => void;
|
|
@@ -88,6 +89,7 @@ export function ComposerBody({
|
|
|
88
89
|
refreshingCommands = false,
|
|
89
90
|
models = [],
|
|
90
91
|
model = "",
|
|
92
|
+
modelLabel,
|
|
91
93
|
onSetModel,
|
|
92
94
|
access,
|
|
93
95
|
onSetAccess,
|
|
@@ -329,6 +331,7 @@ export function ComposerBody({
|
|
|
329
331
|
onSetAccess={onSetAccess}
|
|
330
332
|
models={models}
|
|
331
333
|
model={model}
|
|
334
|
+
modelLabel={modelLabel}
|
|
332
335
|
onSetModel={onSetModel}
|
|
333
336
|
modelOpen={modelOpen}
|
|
334
337
|
onModelOpenChange={setModelOpen}
|
|
@@ -31,6 +31,7 @@ export function Toolbar({
|
|
|
31
31
|
onSetAccess,
|
|
32
32
|
models,
|
|
33
33
|
model,
|
|
34
|
+
modelLabel,
|
|
34
35
|
onSetModel,
|
|
35
36
|
modelOpen,
|
|
36
37
|
onModelOpenChange,
|
|
@@ -51,6 +52,7 @@ export function Toolbar({
|
|
|
51
52
|
onSetAccess?: (access: ChatAccess) => void;
|
|
52
53
|
models: ChatModel[];
|
|
53
54
|
model: string;
|
|
55
|
+
modelLabel?: string;
|
|
54
56
|
onSetModel?: (model: string) => void;
|
|
55
57
|
modelOpen: boolean;
|
|
56
58
|
onModelOpenChange: (open: boolean) => void;
|
|
@@ -135,6 +137,7 @@ export function Toolbar({
|
|
|
135
137
|
<ModelPicker
|
|
136
138
|
models={models}
|
|
137
139
|
model={model}
|
|
140
|
+
label={modelLabel}
|
|
138
141
|
open={modelOpen}
|
|
139
142
|
onOpenChange={onModelOpenChange}
|
|
140
143
|
onPick={onSetModel}
|
|
@@ -1,29 +1,26 @@
|
|
|
1
|
+
import type { Tone } from "@/lib/config/client-config-types";
|
|
2
|
+
import {
|
|
3
|
+
RUN_STATUS_LABELS,
|
|
4
|
+
RUN_TRIGGER_LABELS,
|
|
5
|
+
} from "@/lib/recipes/status-labels";
|
|
1
6
|
import type { RecipeRun, RunStatus } from "@/lib/recipes/types";
|
|
2
7
|
|
|
3
|
-
export const STATUS_LABEL
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
running: "Running",
|
|
7
|
-
succeeded: "Succeeded",
|
|
8
|
-
failed: "Failed",
|
|
9
|
-
blocked: "Needs you",
|
|
10
|
-
timed_out: "Timed out",
|
|
11
|
-
cancelled: "Cancelled",
|
|
12
|
-
skipped: "Skipped",
|
|
13
|
-
};
|
|
8
|
+
export const STATUS_LABEL = Object.fromEntries(
|
|
9
|
+
Object.entries(RUN_STATUS_LABELS).map(([k, v]) => [k, v.label])
|
|
10
|
+
) as Record<RunStatus, string>;
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
blocked: "bg-amber-500/15 text-amber-700 dark:text-amber-300",
|
|
22
|
-
timed_out: "bg-destructive/15 text-destructive",
|
|
23
|
-
cancelled: "bg-foreground/10 text-muted-foreground",
|
|
24
|
-
skipped: "bg-amber-500/15 text-amber-700 dark:text-amber-300",
|
|
12
|
+
const TONE_CHIP: Record<Tone, string> = {
|
|
13
|
+
muted: "bg-foreground/10 text-muted-foreground",
|
|
14
|
+
warning: "bg-amber-500/15 text-amber-700 dark:text-amber-300",
|
|
15
|
+
primary: "bg-primary/15 text-primary",
|
|
16
|
+
destructive: "bg-destructive/15 text-destructive",
|
|
17
|
+
success: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300",
|
|
25
18
|
};
|
|
26
19
|
|
|
20
|
+
export const STATUS_TONE = Object.fromEntries(
|
|
21
|
+
Object.entries(RUN_STATUS_LABELS).map(([k, v]) => [k, TONE_CHIP[v.tone]])
|
|
22
|
+
) as Record<RunStatus, string>;
|
|
23
|
+
|
|
27
24
|
// SQLite's datetime('now') is UTC without a zone marker.
|
|
28
25
|
export const sqlMs = (t: string | null) =>
|
|
29
26
|
t ? Date.parse(`${t.replace(" ", "T")}Z`) : 0;
|
|
@@ -39,13 +36,5 @@ export function duration(ms: number | null): string {
|
|
|
39
36
|
export const money = (n: number | null) =>
|
|
40
37
|
n == null ? "" : `$${n < 10 ? n.toFixed(2) : n.toFixed(0)}`;
|
|
41
38
|
|
|
42
|
-
const TRIGGER_LABEL = {
|
|
43
|
-
manual: "by hand",
|
|
44
|
-
tool: "by the orchestrator",
|
|
45
|
-
cli: "by an agent",
|
|
46
|
-
timer: "on its timer",
|
|
47
|
-
"catch-up": "on its timer, late",
|
|
48
|
-
} as const;
|
|
49
|
-
|
|
50
39
|
export const triggerLabel = (r: Pick<RecipeRun, "trigger">) =>
|
|
51
|
-
|
|
40
|
+
RUN_TRIGGER_LABELS[r.trigger];
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
+
import type { Tone } from "@/lib/config/client-config-types";
|
|
1
2
|
import type { TaskState } from "@/lib/tasks/state";
|
|
3
|
+
import { TASK_STATE_LABELS } from "@/lib/tasks/state-labels";
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
},
|
|
10
|
-
blocked: { label: "Blocked", tone: "text-amber-600 dark:text-amber-400" },
|
|
11
|
-
"plan-ready": { label: "Plan ready", tone: "text-primary" },
|
|
12
|
-
review: { label: "Ready for review", tone: "text-primary" },
|
|
13
|
-
"checks-failing": { label: "Checks failing", tone: "text-destructive" },
|
|
14
|
-
exited: { label: "Agent exited", tone: "text-amber-600 dark:text-amber-400" },
|
|
15
|
-
merged: { label: "Merged", tone: "text-emerald-600 dark:text-emerald-400" },
|
|
16
|
-
dropped: { label: "Dropped", tone: "text-muted-foreground/60" },
|
|
17
|
-
done: { label: "Done", tone: "text-emerald-600 dark:text-emerald-400" },
|
|
18
|
-
moving: { label: "Moving", tone: "text-amber-600 dark:text-amber-400" },
|
|
5
|
+
export const TONE_TEXT: Record<Tone, string> = {
|
|
6
|
+
muted: "text-muted-foreground",
|
|
7
|
+
warning: "text-amber-600 dark:text-amber-400",
|
|
8
|
+
primary: "text-primary",
|
|
9
|
+
destructive: "text-destructive",
|
|
10
|
+
success: "text-emerald-600 dark:text-emerald-400",
|
|
19
11
|
};
|
|
12
|
+
|
|
13
|
+
// How a task's state reads, and its tone class, wherever a task is shown.
|
|
14
|
+
export const TASK_STATE = Object.fromEntries(
|
|
15
|
+
Object.entries(TASK_STATE_LABELS).map(([k, v]) => [
|
|
16
|
+
k,
|
|
17
|
+
{ label: v.label, tone: TONE_TEXT[v.tone] },
|
|
18
|
+
])
|
|
19
|
+
) as Record<TaskState, { label: string; tone: string }>;
|
package/data/chat/useChat.ts
CHANGED
|
@@ -146,6 +146,7 @@ export function useChat(
|
|
|
146
146
|
const [commands, setCommands] = useState<ChatCommand[]>([]);
|
|
147
147
|
const [models, setModels] = useState<ChatModel[]>([]);
|
|
148
148
|
const [model, setModelState] = useState("");
|
|
149
|
+
const [modelLabel, setModelLabel] = useState<string | undefined>();
|
|
149
150
|
const [refreshingCommands, setRefreshingCommands] = useState(false);
|
|
150
151
|
const refreshTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
151
152
|
const [access, setAccessState] = useState<ChatAccess>("full");
|
|
@@ -251,6 +252,7 @@ export function useChat(
|
|
|
251
252
|
setCommands(m.commands);
|
|
252
253
|
setModels(m.models);
|
|
253
254
|
setModelState(m.model);
|
|
255
|
+
setModelLabel(m.modelLabel);
|
|
254
256
|
setAccessState(m.access);
|
|
255
257
|
setPlanState(m.plan);
|
|
256
258
|
setCanBackground(!!m.background);
|
|
@@ -519,6 +521,7 @@ export function useChat(
|
|
|
519
521
|
refreshingCommands,
|
|
520
522
|
models,
|
|
521
523
|
model,
|
|
524
|
+
modelLabel,
|
|
522
525
|
send,
|
|
523
526
|
interrupt,
|
|
524
527
|
setModel,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// What each chat access level is called and does, for every client. Synced to
|
|
2
|
+
// the native app, so it may only import synced files.
|
|
3
|
+
import type { ChatAccess } from "./events";
|
|
4
|
+
|
|
5
|
+
export const ACCESS_LEVELS: Record<
|
|
6
|
+
ChatAccess,
|
|
7
|
+
{ label: string; description: string }
|
|
8
|
+
> = {
|
|
9
|
+
ask: {
|
|
10
|
+
label: "Ask first",
|
|
11
|
+
description: "Asks before edits and commands it isn't already allowed",
|
|
12
|
+
},
|
|
13
|
+
edits: {
|
|
14
|
+
label: "Accept edits",
|
|
15
|
+
description: "Edits files on its own, asks before other commands",
|
|
16
|
+
},
|
|
17
|
+
full: {
|
|
18
|
+
label: "Full access",
|
|
19
|
+
description: "Does everything without asking",
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// The access a new chat starts with when the client doesn't pick one.
|
|
24
|
+
export const DEFAULT_NEW_CHAT_ACCESS: ChatAccess = "edits";
|
package/lib/chat/events.ts
CHANGED
|
@@ -359,6 +359,8 @@ export type ChatServerMessage =
|
|
|
359
359
|
commands: ChatCommand[];
|
|
360
360
|
models: ChatModel[];
|
|
361
361
|
model: string;
|
|
362
|
+
// A readable name for `model` when it is not one of `models`.
|
|
363
|
+
modelLabel?: string;
|
|
362
364
|
access: ChatAccess;
|
|
363
365
|
// Plan mode: on, or null when the session can't use it.
|
|
364
366
|
plan: boolean | null;
|
package/lib/chat/settings.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { effectiveChatAccess, noApprovalsOn } from "../approvals/no-approvals";
|
|
|
7
7
|
import os from "os";
|
|
8
8
|
import { db, type Session } from "../db";
|
|
9
9
|
import { agentEnv } from "../agents/launch";
|
|
10
|
-
import { resolveModelForAgent } from "../model-catalog";
|
|
10
|
+
import { modelDisplayName, resolveModelForAgent } from "../model-catalog";
|
|
11
11
|
import { chatDriverFor } from "./drivers";
|
|
12
12
|
import {
|
|
13
13
|
CHAT_ACCESS,
|
|
@@ -69,11 +69,15 @@ function visibleCommands(caps: Capabilities): ChatCommand[] {
|
|
|
69
69
|
export function emitCapabilities(session: Session, listener?: Listener): void {
|
|
70
70
|
const caps = registry.caps.get(capsKey(session));
|
|
71
71
|
if (!caps) return;
|
|
72
|
+
const model = chatModel(session);
|
|
72
73
|
const m: ChatServerMessage = {
|
|
73
74
|
type: "capabilities",
|
|
74
75
|
commands: visibleCommands(caps),
|
|
75
76
|
models: caps.models,
|
|
76
|
-
model
|
|
77
|
+
model,
|
|
78
|
+
modelLabel:
|
|
79
|
+
caps.models.find((o) => o.value === model)?.label ??
|
|
80
|
+
modelDisplayName(model),
|
|
77
81
|
access: effectiveChatAccess(session.chat_access),
|
|
78
82
|
plan: canPlan(session) ? !!session.chat_plan && !noApprovalsOn() : null,
|
|
79
83
|
background: chatDriverFor(session.agent_type)?.backgroundTasks === true,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Read-only config a client renders with. Additive: a new field never bumps
|
|
2
|
+
// `v`; only a change that breaks an old reader does, and then the old field
|
|
3
|
+
// keeps being served beside the new one for a release. Synced to the native
|
|
4
|
+
// app, so it must stay pure: no imports.
|
|
5
|
+
export const CLIENT_CONFIG_VERSION = 1;
|
|
6
|
+
|
|
7
|
+
export type Tone = "muted" | "warning" | "primary" | "destructive" | "success";
|
|
8
|
+
|
|
9
|
+
export interface LabelTone {
|
|
10
|
+
label: string;
|
|
11
|
+
tone: Tone;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AccessLevel {
|
|
15
|
+
value: string;
|
|
16
|
+
label: string;
|
|
17
|
+
description: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Keys are strings on purpose, so an older app can accept a state it doesn't
|
|
21
|
+
// know yet; each client narrows them through its own typed lookups.
|
|
22
|
+
export interface ClientConfig {
|
|
23
|
+
v: number;
|
|
24
|
+
server: { version: string | null; startedAt: string };
|
|
25
|
+
access: { levels: AccessLevel[]; default: string };
|
|
26
|
+
taskStates: Record<string, LabelTone>;
|
|
27
|
+
runStatuses: Record<string, LabelTone>;
|
|
28
|
+
runTriggers: Record<string, string>;
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { ACCESS_LEVELS, DEFAULT_NEW_CHAT_ACCESS } from "../chat/access-levels";
|
|
2
|
+
import { CHAT_ACCESS } from "../chat/events";
|
|
3
|
+
import {
|
|
4
|
+
RUN_STATUS_LABELS,
|
|
5
|
+
RUN_TRIGGER_LABELS,
|
|
6
|
+
} from "../recipes/status-labels";
|
|
7
|
+
import { TASK_STATE_LABELS } from "../tasks/state-labels";
|
|
8
|
+
import { cachedCheck } from "../updates/check";
|
|
9
|
+
import {
|
|
10
|
+
CLIENT_CONFIG_VERSION,
|
|
11
|
+
type ClientConfig,
|
|
12
|
+
} from "./client-config-types";
|
|
13
|
+
|
|
14
|
+
const startedAt = new Date(Date.now() - process.uptime() * 1000).toISOString();
|
|
15
|
+
|
|
16
|
+
export function clientConfig(): ClientConfig {
|
|
17
|
+
return {
|
|
18
|
+
v: CLIENT_CONFIG_VERSION,
|
|
19
|
+
server: { version: cachedCheck()?.current?.version ?? null, startedAt },
|
|
20
|
+
access: {
|
|
21
|
+
levels: CHAT_ACCESS.map((value) => ({ value, ...ACCESS_LEVELS[value] })),
|
|
22
|
+
default: DEFAULT_NEW_CHAT_ACCESS,
|
|
23
|
+
},
|
|
24
|
+
taskStates: TASK_STATE_LABELS,
|
|
25
|
+
runStatuses: RUN_STATUS_LABELS,
|
|
26
|
+
runTriggers: RUN_TRIGGER_LABELS,
|
|
27
|
+
};
|
|
28
|
+
}
|
package/lib/model-catalog.ts
CHANGED
|
@@ -84,3 +84,14 @@ export function resolveModelForAgent(
|
|
|
84
84
|
|
|
85
85
|
return getDefaultModelForAgent(agentType);
|
|
86
86
|
}
|
|
87
|
+
|
|
88
|
+
// A readable name for a model id: "claude-opus-5-5" is "Opus 5.5", the alias
|
|
89
|
+
// "opus" is "Opus". An id it can't read passes through unchanged.
|
|
90
|
+
export function modelDisplayName(id: string): string {
|
|
91
|
+
const alias = CLAUDE_MODEL_OPTIONS.find((o) => o.value === id);
|
|
92
|
+
if (alias) return alias.label;
|
|
93
|
+
const m = /^claude-([a-z]+)-(\d{1,2}(?:-\d{1,2})*)(?:-\d{8})?$/.exec(id);
|
|
94
|
+
if (!m) return id;
|
|
95
|
+
const family = m[1][0].toUpperCase() + m[1].slice(1);
|
|
96
|
+
return `${family} ${m[2].replace(/-/g, ".")}`;
|
|
97
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// How a recipe run's status and trigger read, for every client. Synced to the
|
|
2
|
+
// native app, so it may only import synced files.
|
|
3
|
+
import type { Tone } from "../config/client-config-types";
|
|
4
|
+
import type { RunStatus, RunTriggerKind } from "./types";
|
|
5
|
+
|
|
6
|
+
export const RUN_STATUS_LABELS: Record<
|
|
7
|
+
RunStatus,
|
|
8
|
+
{ label: string; tone: Tone }
|
|
9
|
+
> = {
|
|
10
|
+
queued: { label: "Queued", tone: "muted" },
|
|
11
|
+
starting: { label: "Starting", tone: "primary" },
|
|
12
|
+
running: { label: "Running", tone: "primary" },
|
|
13
|
+
succeeded: { label: "Succeeded", tone: "success" },
|
|
14
|
+
failed: { label: "Failed", tone: "destructive" },
|
|
15
|
+
blocked: { label: "Needs you", tone: "warning" },
|
|
16
|
+
timed_out: { label: "Timed out", tone: "destructive" },
|
|
17
|
+
cancelled: { label: "Cancelled", tone: "muted" },
|
|
18
|
+
skipped: { label: "Skipped", tone: "warning" },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const RUN_TRIGGER_LABELS: Record<RunTriggerKind, string> = {
|
|
22
|
+
manual: "by hand",
|
|
23
|
+
tool: "by the orchestrator",
|
|
24
|
+
cli: "by an agent",
|
|
25
|
+
timer: "on its timer",
|
|
26
|
+
"catch-up": "on its timer, late",
|
|
27
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// How a task's state reads, and its tone, wherever a task is shown. Synced to
|
|
2
|
+
// the native app, so it may only import synced files.
|
|
3
|
+
import type { Tone } from "../config/client-config-types";
|
|
4
|
+
import type { TaskState } from "./state";
|
|
5
|
+
|
|
6
|
+
export const TASK_STATE_LABELS: Record<
|
|
7
|
+
TaskState,
|
|
8
|
+
{ label: string; tone: Tone }
|
|
9
|
+
> = {
|
|
10
|
+
working: { label: "Working", tone: "muted" },
|
|
11
|
+
"needs-input": { label: "Needs input", tone: "warning" },
|
|
12
|
+
blocked: { label: "Blocked", tone: "warning" },
|
|
13
|
+
"plan-ready": { label: "Plan ready", tone: "primary" },
|
|
14
|
+
review: { label: "Ready for review", tone: "primary" },
|
|
15
|
+
"checks-failing": { label: "Checks failing", tone: "destructive" },
|
|
16
|
+
exited: { label: "Agent exited", tone: "warning" },
|
|
17
|
+
merged: { label: "Merged", tone: "success" },
|
|
18
|
+
dropped: { label: "Dropped", tone: "muted" },
|
|
19
|
+
done: { label: "Done", tone: "success" },
|
|
20
|
+
moving: { label: "Moving", tone: "warning" },
|
|
21
|
+
};
|