@saadnvd1/agent-os 0.3.836 → 0.3.839
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/fresh-start/route.test.ts +33 -0
- package/app/api/fresh-start/route.ts +3 -0
- package/components/Devices/DeviceRow.tsx +83 -69
- package/components/Devices/PasskeysSection.test.ts +89 -0
- package/components/Devices/PasskeysSection.tsx +36 -26
- package/components/Health/HealthPanel.tsx +41 -16
- package/components/Merge/GlobalMergeSection.tsx +2 -5
- package/components/Merge/MergeSettingsFields.tsx +39 -77
- package/components/Merge/ProjectMergeSection.tsx +2 -6
- package/components/Merge/WorktreeSweepSection.tsx +40 -14
- package/components/Models/AgentRoleCard.tsx +61 -66
- package/components/Models/AgentRolesSection.tsx +5 -8
- package/components/Models/ModelsSection.tsx +7 -18
- package/components/Models/PriceInput.tsx +59 -0
- package/components/PhoneNotify/PhoneNotifyForm.tsx +37 -58
- package/components/Settings/AppWideLimits.tsx +61 -0
- package/components/Settings/ContextSection.tsx +96 -0
- package/components/Settings/ExperimentsSection.test.ts +9 -7
- package/components/Settings/ExperimentsSection.tsx +22 -23
- package/components/Settings/InlineConfirm.tsx +51 -0
- package/components/Settings/OwnerNameRow.tsx +10 -40
- package/components/Settings/PickerSetting.tsx +59 -0
- package/components/Settings/SettingRow.tsx +131 -0
- package/components/Settings/SettingsDialog.test.ts +1 -0
- package/components/Settings/SettingsDialog.tsx +47 -2
- package/components/Settings/settings-fields.test.ts +103 -0
- package/components/Settings/settings-fields.tsx +427 -0
- package/components/Shares/SharedLinksPanel.tsx +62 -53
- package/components/Workspaces/HqFolderForm.tsx +21 -54
- package/components/Workspaces/McpServersForm.tsx +20 -19
- package/components/Workspaces/ProjectTiersForm.tsx +10 -8
- package/components/Workspaces/RulesForm.test.ts +127 -0
- package/components/Workspaces/RulesForm.tsx +54 -126
- package/components/Workspaces/WorkspaceModelsForm.tsx +27 -26
- package/components/Workspaces/WorkspaceSettings.tsx +61 -24
- package/content/whats-new.json +16 -0
- package/data/fresh-start/index.ts +1 -1
- package/data/workspaces/index.ts +9 -0
- package/hooks/useAutoSave.test.ts +116 -0
- package/hooks/useAutoSave.ts +98 -0
- package/lib/away/window-limits.ts +3 -1
- package/lib/chat/refresh.ts +10 -6
- package/lib/chat/stall-limits.ts +3 -1
- package/lib/settings/defaults.ts +27 -0
- package/lib/settings/parse.ts +17 -0
- package/lib/settings-url.ts +1 -0
- package/lib/worktree-sweep/limits.ts +3 -0
- package/lib/worktree-sweep/settings.ts +4 -2
- package/package.json +1 -1
- package/components/Away/AwayWindowForm.tsx +0 -91
- package/components/Workspaces/FreshStartForm.tsx +0 -177
- package/components/Workspaces/StallForm.tsx +0 -101
- package/components/Workspaces/TaskLimitForm.tsx +0 -84
- package/components/Workspaces/fresh-start.ts +0 -19
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
Select,
|
|
6
|
-
SelectContent,
|
|
7
|
-
SelectItem,
|
|
8
|
-
SelectTrigger,
|
|
9
|
-
SelectValue,
|
|
10
|
-
} from "@/components/ui/select";
|
|
3
|
+
import { SelectSetting } from "@/components/Settings/settings-fields";
|
|
4
|
+
import { SettingSection } from "@/components/Settings/SettingRow";
|
|
11
5
|
import {
|
|
12
6
|
MERGE_METHODS,
|
|
13
7
|
METHOD_LABEL,
|
|
@@ -29,47 +23,6 @@ const SOURCE: Record<MergeSource, string> = {
|
|
|
29
23
|
|
|
30
24
|
const onOff = (on: boolean) => (on ? "On" : "Off");
|
|
31
25
|
|
|
32
|
-
function Field({
|
|
33
|
-
label,
|
|
34
|
-
detail,
|
|
35
|
-
value,
|
|
36
|
-
inheritLabel,
|
|
37
|
-
options,
|
|
38
|
-
onChange,
|
|
39
|
-
disabled,
|
|
40
|
-
}: {
|
|
41
|
-
label: string;
|
|
42
|
-
detail: string;
|
|
43
|
-
value: string;
|
|
44
|
-
inheritLabel: string;
|
|
45
|
-
options: Array<{ value: string; label: string }>;
|
|
46
|
-
onChange: (value: string) => void;
|
|
47
|
-
disabled?: boolean;
|
|
48
|
-
}) {
|
|
49
|
-
const id = useId();
|
|
50
|
-
return (
|
|
51
|
-
<div className="space-y-1.5">
|
|
52
|
-
<label htmlFor={id} className="text-sm font-medium">
|
|
53
|
-
{label}
|
|
54
|
-
</label>
|
|
55
|
-
<Select value={value} onValueChange={onChange} disabled={disabled}>
|
|
56
|
-
<SelectTrigger id={id} className="h-11 md:h-9">
|
|
57
|
-
<SelectValue />
|
|
58
|
-
</SelectTrigger>
|
|
59
|
-
<SelectContent>
|
|
60
|
-
<SelectItem value={INHERIT}>{inheritLabel}</SelectItem>
|
|
61
|
-
{options.map((o) => (
|
|
62
|
-
<SelectItem key={o.value} value={o.value}>
|
|
63
|
-
{o.label}
|
|
64
|
-
</SelectItem>
|
|
65
|
-
))}
|
|
66
|
-
</SelectContent>
|
|
67
|
-
</Select>
|
|
68
|
-
<p className="text-muted-foreground text-xs">{detail}</p>
|
|
69
|
-
</div>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
26
|
const TOGGLE = [
|
|
74
27
|
{ value: "on", label: "On" },
|
|
75
28
|
{ value: "off", label: "Off" },
|
|
@@ -78,38 +31,43 @@ const TOGGLE = [
|
|
|
78
31
|
/**
|
|
79
32
|
* The merge method and the two deletions after a merge, each either set
|
|
80
33
|
* here or left to what this level inherits (`inherited`, with where that
|
|
81
|
-
* comes from).
|
|
34
|
+
* comes from). Each saves the moment it changes.
|
|
82
35
|
*/
|
|
83
36
|
export function MergeSettingsFields({
|
|
37
|
+
idPrefix,
|
|
84
38
|
settings,
|
|
85
39
|
inherited,
|
|
86
40
|
from,
|
|
87
41
|
allowed,
|
|
88
|
-
|
|
89
|
-
onChange,
|
|
42
|
+
onSave,
|
|
90
43
|
}: {
|
|
44
|
+
idPrefix: string;
|
|
91
45
|
settings: MergeSettings;
|
|
92
46
|
inherited: MergePolicy;
|
|
93
47
|
from?: Record<keyof MergePolicy, MergeSource>;
|
|
94
48
|
// The repository's allowed methods, when known.
|
|
95
49
|
allowed?: MergeMethod[] | null;
|
|
96
|
-
|
|
97
|
-
onChange: (settings: MergeSettings) => void;
|
|
50
|
+
onSave: (settings: MergeSettings) => Promise<unknown>;
|
|
98
51
|
}) {
|
|
99
|
-
const
|
|
100
|
-
|
|
52
|
+
const inheritOption = (key: keyof MergePolicy, shown: string) => ({
|
|
53
|
+
value: INHERIT,
|
|
54
|
+
label: from
|
|
55
|
+
? `Inherit: ${shown} (${SOURCE[from[key]]})`
|
|
56
|
+
: `Default: ${shown}`,
|
|
57
|
+
});
|
|
101
58
|
const set = <K extends keyof MergeSettings>(
|
|
102
59
|
key: K,
|
|
103
60
|
value: MergeSettings[K] | undefined
|
|
104
|
-
) =>
|
|
61
|
+
) => onSave({ ...settings, [key]: value });
|
|
105
62
|
const toggle = (key: "delete_remote_branch" | "delete_worktree") => (
|
|
106
|
-
<
|
|
63
|
+
<SelectSetting
|
|
64
|
+
id={`${idPrefix}-${key}`}
|
|
107
65
|
label={
|
|
108
66
|
key === "delete_remote_branch"
|
|
109
67
|
? "Delete the branch on origin after a merge"
|
|
110
68
|
: "Delete the local worktree and branch after a merge"
|
|
111
69
|
}
|
|
112
|
-
|
|
70
|
+
help={
|
|
113
71
|
key === "delete_remote_branch"
|
|
114
72
|
? "Never main, master, or a branch a stacked task still targets."
|
|
115
73
|
: "Never one with uncommitted work or commits the merge didn't include."
|
|
@@ -117,10 +75,10 @@ export function MergeSettingsFields({
|
|
|
117
75
|
value={
|
|
118
76
|
settings[key] === undefined ? INHERIT : settings[key] ? "on" : "off"
|
|
119
77
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
78
|
+
options={[inheritOption(key, onOff(inherited[key])), ...TOGGLE]}
|
|
79
|
+
defaultValue={INHERIT}
|
|
80
|
+
hint=""
|
|
81
|
+
onSave={(v) => set(key, v === INHERIT ? undefined : v === "on")}
|
|
124
82
|
/>
|
|
125
83
|
);
|
|
126
84
|
|
|
@@ -128,25 +86,29 @@ export function MergeSettingsFields({
|
|
|
128
86
|
const refused = allowed && !allowed.includes(method);
|
|
129
87
|
|
|
130
88
|
return (
|
|
131
|
-
<
|
|
132
|
-
<
|
|
89
|
+
<SettingSection>
|
|
90
|
+
<SelectSetting
|
|
91
|
+
id={`${idPrefix}-method`}
|
|
133
92
|
label="Merge method"
|
|
134
|
-
|
|
93
|
+
help="How sign-off, Done, Land and external PRs merge. If GitHub refuses it, the merge fails and says so; it never falls back to another method."
|
|
135
94
|
value={settings.method ?? INHERIT}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
95
|
+
options={[
|
|
96
|
+
inheritOption("method", METHOD_LABEL[inherited.method]),
|
|
97
|
+
...MERGE_METHODS.map((m) => ({
|
|
98
|
+
value: m,
|
|
99
|
+
label:
|
|
100
|
+
METHOD_LABEL[m] +
|
|
101
|
+
(allowed && !allowed.includes(m) ? " (off on GitHub)" : ""),
|
|
102
|
+
})),
|
|
103
|
+
]}
|
|
104
|
+
defaultValue={INHERIT}
|
|
105
|
+
hint=""
|
|
106
|
+
onSave={(v) =>
|
|
145
107
|
set("method", v === INHERIT ? undefined : (v as MergeMethod))
|
|
146
108
|
}
|
|
147
109
|
/>
|
|
148
110
|
{refused && (
|
|
149
|
-
<p className="text-destructive text-xs">
|
|
111
|
+
<p className="text-destructive px-3 py-2 text-xs">
|
|
150
112
|
This repository has “{REPO_SETTING[method]}” turned off on
|
|
151
113
|
GitHub, so merges will be refused until it’s on or the method
|
|
152
114
|
changes.
|
|
@@ -154,6 +116,6 @@ export function MergeSettingsFields({
|
|
|
154
116
|
)}
|
|
155
117
|
{toggle("delete_remote_branch")}
|
|
156
118
|
{toggle("delete_worktree")}
|
|
157
|
-
</
|
|
119
|
+
</SettingSection>
|
|
158
120
|
);
|
|
159
121
|
}
|
|
@@ -35,14 +35,10 @@ export function ProjectMergeSection({
|
|
|
35
35
|
inherited={data.inherited}
|
|
36
36
|
from={data.inherited.from}
|
|
37
37
|
allowed={data.allowed}
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
idPrefix={`merge-${projectId}`}
|
|
39
|
+
onSave={(s) => update.mutateAsync(s)}
|
|
40
40
|
/>
|
|
41
41
|
)}
|
|
42
|
-
{update.isError && (
|
|
43
|
-
<p className="text-destructive text-sm">{update.error.message}</p>
|
|
44
|
-
)}
|
|
45
|
-
<p className="text-muted-foreground text-xs">Saved as you change them.</p>
|
|
46
42
|
<ProjectSweepSection projectId={projectId} />
|
|
47
43
|
</div>
|
|
48
44
|
);
|
|
@@ -5,6 +5,8 @@ import { Input } from "@/components/ui/input";
|
|
|
5
5
|
import { useSetSweepDays, useWorktreeSweep } from "@/data/worktree-sweep";
|
|
6
6
|
import { compactTimeAgo, fromSqliteTime } from "@/lib/session-meta";
|
|
7
7
|
import { formatBytes } from "@/lib/share/view";
|
|
8
|
+
import { parseSettingNumber } from "@/lib/settings/parse";
|
|
9
|
+
import { MAX_SWEEP_DAYS } from "@/lib/worktree-sweep/limits";
|
|
8
10
|
|
|
9
11
|
// Days as the box shows them; empty means "inherit".
|
|
10
12
|
function DaysInput({
|
|
@@ -19,24 +21,48 @@ function DaysInput({
|
|
|
19
21
|
onSave: (days: number | null) => void;
|
|
20
22
|
}) {
|
|
21
23
|
const [text, setText] = useState(value === null ? "" : String(value));
|
|
24
|
+
const [error, setError] = useState<string | null>(null);
|
|
22
25
|
const commit = () => {
|
|
23
26
|
const raw = text.trim();
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
+
let next: number | null = null;
|
|
28
|
+
if (raw !== "") {
|
|
29
|
+
const parsed = parseSettingNumber(raw, { min: 0, max: MAX_SWEEP_DAYS });
|
|
30
|
+
if ("error" in parsed) return setError(parsed.error);
|
|
31
|
+
next = parsed.value;
|
|
32
|
+
}
|
|
33
|
+
setError(null);
|
|
34
|
+
if (next !== value) onSave(next);
|
|
27
35
|
};
|
|
28
36
|
return (
|
|
29
|
-
<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
<span className="flex flex-col gap-1">
|
|
38
|
+
<Input
|
|
39
|
+
aria-label="Days before a settled worktree is removed"
|
|
40
|
+
aria-invalid={!!error}
|
|
41
|
+
inputMode="numeric"
|
|
42
|
+
value={text}
|
|
43
|
+
placeholder={placeholder}
|
|
44
|
+
disabled={disabled}
|
|
45
|
+
onChange={(e) => {
|
|
46
|
+
setText(e.target.value);
|
|
47
|
+
setError(null);
|
|
48
|
+
}}
|
|
49
|
+
onBlur={commit}
|
|
50
|
+
onKeyDown={(e) => {
|
|
51
|
+
if (e.key === "Enter") e.currentTarget.blur();
|
|
52
|
+
if (e.key === "Escape") {
|
|
53
|
+
setText(value === null ? "" : String(value));
|
|
54
|
+
setError(null);
|
|
55
|
+
e.currentTarget.blur();
|
|
56
|
+
}
|
|
57
|
+
}}
|
|
58
|
+
className="h-11 w-24 text-right tabular-nums md:h-9"
|
|
59
|
+
/>
|
|
60
|
+
{error && (
|
|
61
|
+
<span role="alert" className="text-destructive text-xs">
|
|
62
|
+
{error}
|
|
63
|
+
</span>
|
|
64
|
+
)}
|
|
65
|
+
</span>
|
|
40
66
|
);
|
|
41
67
|
}
|
|
42
68
|
|
|
@@ -4,6 +4,7 @@ import { AlertTriangle } from "lucide-react";
|
|
|
4
4
|
import type { AgentRoleView } from "@/lib/models/agent-roles-view";
|
|
5
5
|
import { analystBlock, defaultAnalystLabel } from "@/lib/models/usable";
|
|
6
6
|
import { cn } from "@/lib/utils";
|
|
7
|
+
import { PickerSetting } from "@/components/Settings/PickerSetting";
|
|
7
8
|
import { RoleModelSelect } from "./RoleModelSelect";
|
|
8
9
|
|
|
9
10
|
const SOURCE: Record<string, string> = {
|
|
@@ -16,22 +17,18 @@ const SOURCE: Record<string, string> = {
|
|
|
16
17
|
default: "Default",
|
|
17
18
|
};
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// One role: what it does, the model it runs on, where that came from, and
|
|
20
|
+
// One role as settings rows: the model it runs on, where that came from, and
|
|
22
21
|
// whether the model's server answers. A role whose local model is down says so.
|
|
23
22
|
export function AgentRoleCard({
|
|
24
23
|
role,
|
|
25
24
|
scope,
|
|
26
|
-
pending,
|
|
27
25
|
onChange,
|
|
28
26
|
onFallback,
|
|
29
27
|
}: {
|
|
30
28
|
role: AgentRoleView;
|
|
31
29
|
scope: "settings" | "workspace" | "project";
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
onFallback: (ref: string | null) => void;
|
|
30
|
+
onChange: (ref: string | null) => Promise<unknown>;
|
|
31
|
+
onFallback: (ref: string | null) => Promise<unknown>;
|
|
35
32
|
}) {
|
|
36
33
|
const down = role.choice?.availability.state === "down";
|
|
37
34
|
const analyst = role.id === "analyst";
|
|
@@ -45,77 +42,75 @@ export function AgentRoleCard({
|
|
|
45
42
|
? "Same as Gate review"
|
|
46
43
|
: label
|
|
47
44
|
: "Inherit";
|
|
45
|
+
const source = role.binding
|
|
46
|
+
? `${SOURCE[role.binding.source] ?? role.binding.source}${role.choice?.endpoint ? ` · ${role.choice.endpoint}` : ""}`
|
|
47
|
+
: undefined;
|
|
48
48
|
return (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"bg-muted/40 space-y-2 rounded-xl px-3 py-3",
|
|
52
|
-
down && "ring-destructive/50 ring-1"
|
|
53
|
-
)}
|
|
54
|
-
>
|
|
55
|
-
<label htmlFor={`agent-role-${role.id}`} className="flex gap-2.5">
|
|
56
|
-
<span className="bg-accent text-primary grid h-8 w-8 shrink-0 place-items-center rounded-lg text-sm font-semibold">
|
|
57
|
-
{initial(role.label)}
|
|
58
|
-
</span>
|
|
59
|
-
<span className="min-w-0">
|
|
60
|
-
<span className="block text-sm font-medium">{role.label}</span>
|
|
61
|
-
<span className="text-muted-foreground block text-xs">
|
|
62
|
-
{role.purpose}
|
|
63
|
-
</span>
|
|
64
|
-
</span>
|
|
65
|
-
</label>
|
|
66
|
-
<RoleModelSelect
|
|
49
|
+
<>
|
|
50
|
+
<PickerSetting<string | null>
|
|
67
51
|
id={`agent-role-${role.id}`}
|
|
52
|
+
label={role.label}
|
|
53
|
+
help={role.purpose}
|
|
68
54
|
value={role.value}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
55
|
+
defaultValue={null}
|
|
56
|
+
hint={source}
|
|
57
|
+
error={role.error}
|
|
58
|
+
onSave={onChange}
|
|
59
|
+
render={({ value, commit }) => (
|
|
60
|
+
<RoleModelSelect
|
|
61
|
+
id={`agent-role-${role.id}`}
|
|
62
|
+
value={value}
|
|
63
|
+
inheritLabel={
|
|
64
|
+
value === null && role.binding
|
|
65
|
+
? `${inheritLabel} · ${role.binding.ref.split("/").slice(1).join("/")}`
|
|
66
|
+
: inheritLabel
|
|
67
|
+
}
|
|
68
|
+
blockedBy={blockedBy}
|
|
69
|
+
onChange={commit}
|
|
70
|
+
/>
|
|
71
|
+
)}
|
|
72
|
+
below={
|
|
73
|
+
down && (
|
|
74
|
+
<div
|
|
75
|
+
role="alert"
|
|
76
|
+
className={cn(
|
|
77
|
+
"bg-destructive/10 text-foreground ring-destructive/35 my-1 flex gap-2 rounded-lg p-2.5 text-xs ring-1"
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
<AlertTriangle className="text-destructive mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
81
|
+
<span>
|
|
82
|
+
<b className="font-medium">
|
|
83
|
+
This model's server isn't answering.
|
|
84
|
+
</b>{" "}
|
|
85
|
+
{role.choice?.availability.error
|
|
86
|
+
? `${role.choice.availability.error}. `
|
|
87
|
+
: ""}
|
|
88
|
+
New {role.label} sessions can't start until it is back
|
|
89
|
+
{role.fallback ? ", or you start on the fallback" : ""}.
|
|
90
|
+
</span>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
73
93
|
}
|
|
74
|
-
blockedBy={blockedBy}
|
|
75
|
-
disabled={pending}
|
|
76
|
-
onChange={onChange}
|
|
77
94
|
/>
|
|
78
|
-
<p className="text-muted-foreground text-xs">
|
|
79
|
-
{role.error
|
|
80
|
-
? role.error
|
|
81
|
-
: role.binding
|
|
82
|
-
? `${SOURCE[role.binding.source] ?? role.binding.source}${role.choice?.endpoint ? ` · ${role.choice.endpoint}` : ""}`
|
|
83
|
-
: ""}
|
|
84
|
-
</p>
|
|
85
|
-
{down && (
|
|
86
|
-
<div
|
|
87
|
-
role="alert"
|
|
88
|
-
className="bg-destructive/10 text-foreground ring-destructive/35 flex gap-2 rounded-lg p-2.5 text-xs ring-1"
|
|
89
|
-
>
|
|
90
|
-
<AlertTriangle className="text-destructive mt-0.5 h-3.5 w-3.5 shrink-0" />
|
|
91
|
-
<span>
|
|
92
|
-
<b className="font-medium">
|
|
93
|
-
This model's server isn't answering.
|
|
94
|
-
</b>{" "}
|
|
95
|
-
{role.choice?.availability.error
|
|
96
|
-
? `${role.choice.availability.error}. `
|
|
97
|
-
: ""}
|
|
98
|
-
New {role.label} sessions can't start until it is back
|
|
99
|
-
{role.fallback ? ", or you start on the fallback" : ""}.
|
|
100
|
-
</span>
|
|
101
|
-
</div>
|
|
102
|
-
)}
|
|
103
95
|
{scope === "settings" && (
|
|
104
|
-
<
|
|
105
|
-
|
|
106
|
-
|
|
96
|
+
<PickerSetting<string | null>
|
|
97
|
+
id={`agent-role-fallback-${role.id}`}
|
|
98
|
+
label={`${role.label} fallback`}
|
|
99
|
+
value={role.fallback}
|
|
100
|
+
defaultValue={null}
|
|
101
|
+
onSave={onFallback}
|
|
102
|
+
render={({ value, commit }) => (
|
|
107
103
|
<RoleModelSelect
|
|
108
104
|
id={`agent-role-fallback-${role.id}`}
|
|
109
|
-
value={
|
|
105
|
+
value={value}
|
|
110
106
|
inheritLabel="None (don't fall back)"
|
|
111
107
|
claudeOnly={!analyst}
|
|
112
108
|
blockedBy={blockedBy}
|
|
113
|
-
|
|
114
|
-
onChange={onFallback}
|
|
109
|
+
onChange={commit}
|
|
115
110
|
/>
|
|
116
|
-
|
|
117
|
-
|
|
111
|
+
)}
|
|
112
|
+
/>
|
|
118
113
|
)}
|
|
119
|
-
|
|
114
|
+
</>
|
|
120
115
|
);
|
|
121
116
|
}
|
|
@@ -7,6 +7,7 @@ import { useWorkspacesQuery } from "@/data/workspaces";
|
|
|
7
7
|
import type { RoleMap } from "@/lib/models/config";
|
|
8
8
|
import type { RoleScope } from "@/lib/models/agent-roles-view";
|
|
9
9
|
import { cn } from "@/lib/utils";
|
|
10
|
+
import { SettingSection } from "@/components/Settings/SettingRow";
|
|
10
11
|
import { AgentRoleCard } from "./AgentRoleCard";
|
|
11
12
|
|
|
12
13
|
const SCOPES: { id: RoleScope; label: string }[] = [
|
|
@@ -44,13 +45,13 @@ export function AgentRolesSection() {
|
|
|
44
45
|
const roles: RoleMap = { ...saved("value") };
|
|
45
46
|
if (ref) roles[modelRole as keyof RoleMap] = ref;
|
|
46
47
|
else delete roles[modelRole as keyof RoleMap];
|
|
47
|
-
save.
|
|
48
|
+
return save.mutateAsync({ roles });
|
|
48
49
|
};
|
|
49
50
|
const setFallback = (modelRole: string, ref: string | null) => {
|
|
50
51
|
const fallbacks: RoleMap = { ...saved("fallback") };
|
|
51
52
|
if (ref) fallbacks[modelRole as keyof RoleMap] = ref;
|
|
52
53
|
else delete fallbacks[modelRole as keyof RoleMap];
|
|
53
|
-
save.
|
|
54
|
+
return save.mutateAsync({ roles: saved("value"), fallbacks });
|
|
54
55
|
};
|
|
55
56
|
|
|
56
57
|
return (
|
|
@@ -111,22 +112,18 @@ export function AgentRolesSection() {
|
|
|
111
112
|
) : isError ? (
|
|
112
113
|
<p className="text-destructive text-sm">{error.message}</p>
|
|
113
114
|
) : data ? (
|
|
114
|
-
<
|
|
115
|
+
<SettingSection>
|
|
115
116
|
{data.roles.map((r) => (
|
|
116
117
|
<AgentRoleCard
|
|
117
118
|
key={r.id}
|
|
118
119
|
role={r}
|
|
119
120
|
scope={scope}
|
|
120
|
-
pending={save.isPending}
|
|
121
121
|
onChange={(ref) => setRole(r.modelRole, ref)}
|
|
122
122
|
onFallback={(ref) => setFallback(r.modelRole, ref)}
|
|
123
123
|
/>
|
|
124
124
|
))}
|
|
125
|
-
</
|
|
125
|
+
</SettingSection>
|
|
126
126
|
) : null}
|
|
127
|
-
{save.isError && (
|
|
128
|
-
<p className="text-destructive text-xs">{save.error.message}</p>
|
|
129
|
-
)}
|
|
130
127
|
</div>
|
|
131
128
|
);
|
|
132
129
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
|
-
import {
|
|
4
|
+
import { PriceInput } from "./PriceInput";
|
|
5
5
|
import { SettingToggle } from "@/components/Settings/SettingToggle";
|
|
6
6
|
import { useModelsQuery, useUpdateModels } from "@/data/models";
|
|
7
7
|
import type { ModelSettings } from "@/lib/models/config";
|
|
@@ -169,28 +169,17 @@ export function ModelsSection() {
|
|
|
169
169
|
<td className="py-1 pr-2 font-mono">{ref}</td>
|
|
170
170
|
{PRICE_FIELDS.map((f) => (
|
|
171
171
|
<td key={f.key} className="py-1 pr-2">
|
|
172
|
-
<
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
defaultValue={price[f.key]}
|
|
177
|
-
aria-label={`${ref} ${f.label}`}
|
|
178
|
-
className="h-11 w-20 tabular-nums md:h-8"
|
|
179
|
-
onBlur={(e) => {
|
|
180
|
-
const v = Number(e.target.value);
|
|
181
|
-
if (
|
|
182
|
-
!Number.isFinite(v) ||
|
|
183
|
-
v < 0 ||
|
|
184
|
-
v === price[f.key]
|
|
185
|
-
)
|
|
186
|
-
return;
|
|
172
|
+
<PriceInput
|
|
173
|
+
label={`${ref} ${f.label}`}
|
|
174
|
+
value={price[f.key]}
|
|
175
|
+
onSave={(v) =>
|
|
187
176
|
save({
|
|
188
177
|
prices: {
|
|
189
178
|
...saved.prices,
|
|
190
179
|
[ref]: { ...price, [f.key]: v },
|
|
191
180
|
},
|
|
192
|
-
})
|
|
193
|
-
}
|
|
181
|
+
})
|
|
182
|
+
}
|
|
194
183
|
/>
|
|
195
184
|
</td>
|
|
196
185
|
))}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Input } from "@/components/ui/input";
|
|
5
|
+
import { parseSettingNumber } from "@/lib/settings/parse";
|
|
6
|
+
|
|
7
|
+
// A model price in $ per million tokens, saved on blur or Enter. Blank or
|
|
8
|
+
// invalid stays in the box with a message and is never sent (a blank used to
|
|
9
|
+
// save as $0); Esc puts the saved price back.
|
|
10
|
+
export function PriceInput({
|
|
11
|
+
label,
|
|
12
|
+
value,
|
|
13
|
+
onSave,
|
|
14
|
+
}: {
|
|
15
|
+
label: string;
|
|
16
|
+
value: number;
|
|
17
|
+
onSave: (price: number) => void;
|
|
18
|
+
}) {
|
|
19
|
+
const [text, setText] = useState<string | null>(null);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
const revert = () => {
|
|
22
|
+
setText(null);
|
|
23
|
+
setError(null);
|
|
24
|
+
};
|
|
25
|
+
return (
|
|
26
|
+
<span className="flex flex-col gap-1">
|
|
27
|
+
<Input
|
|
28
|
+
inputMode="decimal"
|
|
29
|
+
value={text ?? String(value)}
|
|
30
|
+
aria-label={label}
|
|
31
|
+
aria-invalid={!!error}
|
|
32
|
+
className="h-11 w-20 tabular-nums md:h-8"
|
|
33
|
+
onChange={(e) => {
|
|
34
|
+
setText(e.target.value);
|
|
35
|
+
setError(null);
|
|
36
|
+
}}
|
|
37
|
+
onBlur={() => {
|
|
38
|
+
if (text === null) return;
|
|
39
|
+
const parsed = parseSettingNumber(text, { min: 0, integer: false });
|
|
40
|
+
if ("error" in parsed) return setError(parsed.error);
|
|
41
|
+
revert();
|
|
42
|
+
if (parsed.value !== value) onSave(parsed.value);
|
|
43
|
+
}}
|
|
44
|
+
onKeyDown={(e) => {
|
|
45
|
+
if (e.key === "Enter") e.currentTarget.blur();
|
|
46
|
+
if (e.key === "Escape") {
|
|
47
|
+
revert();
|
|
48
|
+
e.currentTarget.blur();
|
|
49
|
+
}
|
|
50
|
+
}}
|
|
51
|
+
/>
|
|
52
|
+
{error && (
|
|
53
|
+
<span role="alert" className="text-destructive text-xs">
|
|
54
|
+
{error}
|
|
55
|
+
</span>
|
|
56
|
+
)}
|
|
57
|
+
</span>
|
|
58
|
+
);
|
|
59
|
+
}
|