@saadnvd1/agent-os 0.3.839 → 0.3.848
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/approvals/route.ts +5 -1
- package/app/api/health/config/route.ts +2 -0
- package/app/api/health/route.ts +4 -2
- package/app/globals.css +6 -0
- package/components/Audits/AuditsPanel.tsx +42 -35
- package/components/Chat/quote-selection.test.ts +6 -0
- package/components/Chat/quote-selection.ts +13 -10
- package/components/Devices/ConnectCard.tsx +52 -56
- package/components/Devices/DeviceGroupRow.tsx +65 -0
- package/components/Devices/DeviceRow.tsx +2 -2
- package/components/Devices/DevicesPanel.tsx +63 -49
- package/components/Devices/NetworkSection.tsx +29 -26
- package/components/Devices/TailscaleCard.tsx +9 -10
- package/components/Devices/group.test.ts +66 -0
- package/components/Devices/group.ts +67 -0
- package/components/Health/HealthPanel.tsx +5 -81
- package/components/Health/MachineChecks.tsx +20 -0
- package/components/Health/SweepRuns.tsx +46 -51
- package/components/Health/Thresholds.tsx +50 -0
- package/components/Merge/GlobalMergeSection.tsx +5 -6
- package/components/Merge/MergeApprovalToggle.tsx +20 -14
- package/components/Merge/MergeSettingsFields.tsx +0 -2
- package/components/Merge/WorktreeSweepSection.tsx +15 -34
- package/components/Models/AgentRoleCard.tsx +7 -7
- package/components/Models/ModelSelect.tsx +14 -3
- package/components/Models/ModelsSection.tsx +42 -37
- package/components/Models/ProviderCard.tsx +1 -1
- package/components/Models/RoleModelSelect.tsx +15 -3
- package/components/Models/SubagentModels.tsx +60 -45
- package/components/PhoneNotify/PhoneNotifyForm.tsx +6 -13
- package/components/Recipes/RecipesPanel.tsx +20 -9
- package/components/Recipes/RunHistory.tsx +1 -1
- package/components/Schedules/SchedulesPanel.tsx +12 -10
- package/components/SessionList/GroupShelves.tsx +8 -3
- package/components/SessionList/OrchestratorPinRow.tsx +2 -2
- package/components/SessionList/RowParts.tsx +39 -35
- package/components/SessionList/RowRename.test.ts +86 -17
- package/components/SessionList/RowShell.tsx +2 -5
- package/components/SessionList/SessionRow.tsx +0 -1
- package/components/Settings/AppWideLimits.tsx +2 -2
- package/components/Settings/{NoApprovalsSection.test.ts → ApprovalsSection.test.ts} +61 -22
- package/components/Settings/ApprovalsSection.tsx +93 -0
- package/components/Settings/ExperimentsSection.test.ts +20 -4
- package/components/Settings/ExperimentsSection.tsx +54 -34
- package/components/Settings/GeneralSection.tsx +39 -0
- package/components/Settings/ListSection.test.ts +85 -0
- package/components/Settings/ListSection.tsx +132 -0
- package/components/Settings/NotificationsSection.tsx +31 -29
- package/components/Settings/PickerSetting.tsx +5 -1
- package/components/Settings/SettingRow.tsx +91 -49
- package/components/Settings/SettingsDialog.test.ts +31 -4
- package/components/Settings/SettingsDialog.tsx +22 -17
- package/components/Settings/ValueLabel.tsx +31 -0
- package/components/Settings/settings-fields.test.ts +81 -2
- package/components/Settings/settings-fields.tsx +28 -27
- package/components/Shares/SharedLinksPanel.tsx +11 -14
- package/components/Updates/AutoUpdates.tsx +12 -8
- package/components/Updates/UpdatesSection.tsx +18 -17
- package/components/Workspaces/HqFolderForm.tsx +21 -19
- package/components/Workspaces/McpServersForm.tsx +20 -28
- package/components/Workspaces/ProjectTiersForm.tsx +25 -12
- package/components/Workspaces/RuleEditor.tsx +86 -0
- package/components/Workspaces/RulesForm.test.ts +58 -31
- package/components/Workspaces/RulesForm.tsx +129 -58
- package/components/Workspaces/WorkspaceModeForm.tsx +3 -3
- package/components/Workspaces/WorkspaceModelsForm.tsx +40 -41
- package/components/Workspaces/WorkspacePanel.tsx +73 -0
- package/components/Workspaces/WorkspacePicker.tsx +44 -0
- package/components/Workspaces/WorkspaceSettings.test.ts +153 -0
- package/components/Workspaces/WorkspaceSettings.tsx +51 -79
- package/components/Workspaces/useSettingsWorkspace.ts +47 -0
- package/components/ui/switch.tsx +1 -1
- package/content/whats-new.json +40 -0
- package/data/approvals/index.ts +2 -2
- package/data/health/index.ts +3 -0
- package/hooks/useAutoSave.ts +18 -5
- package/hooks/useSettingsUrl.ts +6 -3
- package/lib/approvals/describe.ts +64 -0
- package/lib/away/deploy.ts +34 -7
- package/lib/health/config.ts +70 -17
- package/lib/models/plan-fresh.ts +61 -0
- package/lib/models/plan-step.ts +40 -24
- package/lib/models/providers.ts +1 -1
- package/lib/orchestrator/scope.ts +10 -0
- package/lib/recipes/settle.ts +22 -0
- package/lib/settings-url.ts +14 -2
- package/lib/updates/banner.ts +2 -0
- package/lib/updates/run.ts +9 -0
- package/lib/updates/view.ts +28 -6
- package/lib/worktree-deps/paths.ts +9 -4
- package/lib/worktrees.ts +3 -0
- package/package.json +1 -1
- package/scripts/autodeploy +11 -0
- package/scripts/autodeploy.test.ts +30 -0
- package/scripts/lib/update-verify.mjs +11 -0
- package/scripts/update-run.mjs +29 -0
- package/scripts/update-run.test.ts +62 -4
- package/components/Settings/NoApprovalsSection.tsx +0 -103
- package/components/Settings/SettingToggle.tsx +0 -36
- package/components/Settings/SetupSection.tsx +0 -46
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { canManageDevices } from "@/components/Devices";
|
|
4
|
-
import {
|
|
4
|
+
import { SwitchSetting } from "@/components/Settings/settings-fields";
|
|
5
5
|
import {
|
|
6
6
|
useDevicesQuery,
|
|
7
7
|
useNetworkQuery,
|
|
@@ -11,7 +11,12 @@ import {
|
|
|
11
11
|
// Switching it off asks for a passkey (useUpdateNetwork), and only the
|
|
12
12
|
// machine itself or the tailnet may change it, like the other access
|
|
13
13
|
// switches.
|
|
14
|
-
export function MergeApprovalToggle(
|
|
14
|
+
export function MergeApprovalToggle({
|
|
15
|
+
overriddenBy,
|
|
16
|
+
}: {
|
|
17
|
+
// While No approvals is on this switch has no effect: say so, greyed.
|
|
18
|
+
overriddenBy?: boolean;
|
|
19
|
+
}) {
|
|
15
20
|
const devices = useDevicesQuery(true);
|
|
16
21
|
const { data, isPending, isError, error } = useNetworkQuery(true);
|
|
17
22
|
const update = useUpdateNetwork();
|
|
@@ -23,17 +28,18 @@ export function MergeApprovalToggle() {
|
|
|
23
28
|
return <p className="text-destructive text-sm">{error.message}</p>;
|
|
24
29
|
|
|
25
30
|
return (
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
<SwitchSetting
|
|
32
|
+
id="merge-approvals"
|
|
33
|
+
label="Require my approval to merge sensitive or large PRs"
|
|
34
|
+
help={
|
|
35
|
+
overriddenBy
|
|
36
|
+
? "Not in effect while No approvals is on: every PR merges through the usual gates."
|
|
37
|
+
: "PRs touching CI, deploys, secrets, build scripts, agent config or AgentOS's security code, or too big to review whole, wait for you to approve them with your passkey. Off, they merge through the usual gates, and a big PR is reviewed in parts."
|
|
38
|
+
}
|
|
39
|
+
disabled={overriddenBy}
|
|
40
|
+
checked={data.mergeApprovals.on}
|
|
41
|
+
locked={data.mergeApprovals.locked || !canManage}
|
|
42
|
+
onSave={(mergeApprovals) => update.mutateAsync({ mergeApprovals })}
|
|
43
|
+
/>
|
|
38
44
|
);
|
|
39
45
|
}
|
|
@@ -77,7 +77,6 @@ export function MergeSettingsFields({
|
|
|
77
77
|
}
|
|
78
78
|
options={[inheritOption(key, onOff(inherited[key])), ...TOGGLE]}
|
|
79
79
|
defaultValue={INHERIT}
|
|
80
|
-
hint=""
|
|
81
80
|
onSave={(v) => set(key, v === INHERIT ? undefined : v === "on")}
|
|
82
81
|
/>
|
|
83
82
|
);
|
|
@@ -102,7 +101,6 @@ export function MergeSettingsFields({
|
|
|
102
101
|
})),
|
|
103
102
|
]}
|
|
104
103
|
defaultValue={INHERIT}
|
|
105
|
-
hint=""
|
|
106
104
|
onSave={(v) =>
|
|
107
105
|
set("method", v === INHERIT ? undefined : (v as MergeMethod))
|
|
108
106
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState } from "react";
|
|
4
|
+
import { NumberSetting } from "@/components/Settings/settings-fields";
|
|
5
|
+
import { SettingSection } from "@/components/Settings/SettingRow";
|
|
4
6
|
import { Input } from "@/components/ui/input";
|
|
5
7
|
import { useSetSweepDays, useWorktreeSweep } from "@/data/worktree-sweep";
|
|
6
|
-
import { compactTimeAgo, fromSqliteTime } from "@/lib/session-meta";
|
|
7
|
-
import { formatBytes } from "@/lib/share/view";
|
|
8
8
|
import { parseSettingNumber } from "@/lib/settings/parse";
|
|
9
9
|
import { MAX_SWEEP_DAYS } from "@/lib/worktree-sweep/limits";
|
|
10
10
|
|
|
@@ -66,19 +66,6 @@ function DaysInput({
|
|
|
66
66
|
);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function LastRun() {
|
|
70
|
-
const { data } = useWorktreeSweep();
|
|
71
|
-
const last = data?.runs.find((r) => r.finishedAt && !r.dryRun);
|
|
72
|
-
if (!last) return null;
|
|
73
|
-
return (
|
|
74
|
-
<p className="text-muted-foreground text-xs">
|
|
75
|
-
Last run {compactTimeAgo(fromSqliteTime(last.finishedAt!))}: removed{" "}
|
|
76
|
-
{last.removed}, {formatBytes(last.freedBytes)} freed, {last.skipped}{" "}
|
|
77
|
-
skipped. Details in Health > Clean-up.
|
|
78
|
-
</p>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
69
|
// How long a settled worktree stays on disk. The branch and the conversation
|
|
83
70
|
// stay; restoring the session rebuilds it.
|
|
84
71
|
export function GlobalSweepSection() {
|
|
@@ -86,27 +73,25 @@ export function GlobalSweepSection() {
|
|
|
86
73
|
const save = useSetSweepDays();
|
|
87
74
|
return (
|
|
88
75
|
<div className="space-y-2">
|
|
89
|
-
<h3 className="text-sm font-medium">Worktree clean-up</h3>
|
|
90
|
-
<p className="text-muted-foreground text-sm">
|
|
91
|
-
Remove the worktree of a merged, dropped, done or archived session this
|
|
92
|
-
many days after it settled, when nothing in it would be lost. 0 turns it
|
|
93
|
-
off.
|
|
94
|
-
</p>
|
|
95
76
|
{isPending && (
|
|
96
77
|
<div className="bg-muted/40 h-11 animate-pulse rounded-lg" />
|
|
97
78
|
)}
|
|
98
79
|
{isError && <p className="text-destructive text-sm">{error.message}</p>}
|
|
99
80
|
{data && (
|
|
100
|
-
<
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
81
|
+
<SettingSection title="Worktree clean-up">
|
|
82
|
+
<NumberSetting
|
|
83
|
+
id="sweep-days"
|
|
84
|
+
label="Remove settled worktrees after"
|
|
85
|
+
help="A merged, dropped, done or archived session's worktree goes this long after it settled, when nothing in it would be lost."
|
|
86
|
+
value={data.days ?? data.defaultDays}
|
|
87
|
+
defaultValue={data.defaultDays}
|
|
88
|
+
min={1}
|
|
89
|
+
max={MAX_SWEEP_DAYS}
|
|
90
|
+
unit="days"
|
|
91
|
+
optional={{ off: 0, on: data.defaultDays }}
|
|
92
|
+
onSave={(days) => save.mutateAsync({ days })}
|
|
107
93
|
/>
|
|
108
|
-
|
|
109
|
-
</div>
|
|
94
|
+
</SettingSection>
|
|
110
95
|
)}
|
|
111
96
|
{data && data.overrides.length > 0 && (
|
|
112
97
|
<p className="text-muted-foreground text-xs">
|
|
@@ -114,10 +99,6 @@ export function GlobalSweepSection() {
|
|
|
114
99
|
{data.overrides.map((o) => `${o.name} (${o.days})`).join(", ")}
|
|
115
100
|
</p>
|
|
116
101
|
)}
|
|
117
|
-
{save.isError && (
|
|
118
|
-
<p className="text-destructive text-sm">{save.error.message}</p>
|
|
119
|
-
)}
|
|
120
|
-
<LastRun />
|
|
121
102
|
</div>
|
|
122
103
|
);
|
|
123
104
|
}
|
|
@@ -32,9 +32,8 @@ export function AgentRoleCard({
|
|
|
32
32
|
}) {
|
|
33
33
|
const down = role.choice?.availability.state === "down";
|
|
34
34
|
const analyst = role.id === "analyst";
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
: `Default (${role.default.split("/").slice(1).join("/")})`;
|
|
35
|
+
const modelId = (ref: string) => ref.split("/").slice(1).join("/");
|
|
36
|
+
const label = analyst ? defaultAnalystLabel(role.default, []) : "Default";
|
|
38
37
|
const blockedBy = analyst ? analystBlock : undefined;
|
|
39
38
|
const inheritLabel =
|
|
40
39
|
scope === "settings"
|
|
@@ -60,10 +59,11 @@ export function AgentRoleCard({
|
|
|
60
59
|
<RoleModelSelect
|
|
61
60
|
id={`agent-role-${role.id}`}
|
|
62
61
|
value={value}
|
|
63
|
-
inheritLabel={
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
inheritLabel={inheritLabel}
|
|
63
|
+
inheritDetail={
|
|
64
|
+
scope === "settings" && role.modelRole === "advise" && !analyst
|
|
65
|
+
? undefined
|
|
66
|
+
: modelId(role.binding?.ref ?? role.default)
|
|
67
67
|
}
|
|
68
68
|
blockedBy={blockedBy}
|
|
69
69
|
onChange={commit}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
SelectTrigger,
|
|
10
10
|
SelectValue,
|
|
11
11
|
} from "@/components/ui/select";
|
|
12
|
+
import { TWO_LINE_TRIGGER, ValueLabel } from "@/components/Settings/ValueLabel";
|
|
12
13
|
import { modelChoices } from "@/data/models";
|
|
13
14
|
import type { EffectiveModels } from "@/lib/models/config";
|
|
14
15
|
|
|
@@ -21,6 +22,7 @@ export function ModelSelect({
|
|
|
21
22
|
value,
|
|
22
23
|
effective,
|
|
23
24
|
inheritLabel,
|
|
25
|
+
inheritDetail,
|
|
24
26
|
disabled,
|
|
25
27
|
onChange,
|
|
26
28
|
}: {
|
|
@@ -28,6 +30,7 @@ export function ModelSelect({
|
|
|
28
30
|
value: string | null;
|
|
29
31
|
effective: EffectiveModels | undefined;
|
|
30
32
|
inheritLabel?: string;
|
|
33
|
+
inheritDetail?: string;
|
|
31
34
|
disabled?: boolean;
|
|
32
35
|
onChange: (ref: string | null) => void;
|
|
33
36
|
}) {
|
|
@@ -41,12 +44,20 @@ export function ModelSelect({
|
|
|
41
44
|
onValueChange={(v) => onChange(v === INHERIT ? null : v)}
|
|
42
45
|
disabled={disabled}
|
|
43
46
|
>
|
|
44
|
-
<SelectTrigger id={id} className=
|
|
45
|
-
<SelectValue
|
|
47
|
+
<SelectTrigger id={id} className={TWO_LINE_TRIGGER}>
|
|
48
|
+
<SelectValue>
|
|
49
|
+
{value === null && inheritLabel ? (
|
|
50
|
+
<ValueLabel label={inheritLabel} detail={inheritDetail} />
|
|
51
|
+
) : (
|
|
52
|
+
(choices.find((c) => c.ref === value)?.label ?? value)
|
|
53
|
+
)}
|
|
54
|
+
</SelectValue>
|
|
46
55
|
</SelectTrigger>
|
|
47
56
|
<SelectContent>
|
|
48
57
|
{inheritLabel && (
|
|
49
|
-
<SelectItem value={INHERIT}>
|
|
58
|
+
<SelectItem value={INHERIT}>
|
|
59
|
+
<ValueLabel label={inheritLabel} detail={inheritDetail} />
|
|
60
|
+
</SelectItem>
|
|
50
61
|
)}
|
|
51
62
|
{missing && <SelectItem value={value}>{value}</SelectItem>}
|
|
52
63
|
{groups.map((g) => (
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
4
|
import { PriceInput } from "./PriceInput";
|
|
5
|
-
import {
|
|
5
|
+
import { PickerSetting } from "@/components/Settings/PickerSetting";
|
|
6
|
+
import { ScrollTable } from "@/components/Settings/ListSection";
|
|
7
|
+
import { SettingSection } from "@/components/Settings/SettingRow";
|
|
8
|
+
import { SwitchSetting } from "@/components/Settings/settings-fields";
|
|
6
9
|
import { useModelsQuery, useUpdateModels } from "@/data/models";
|
|
7
10
|
import type { ModelSettings } from "@/lib/models/config";
|
|
8
11
|
import {
|
|
@@ -63,42 +66,42 @@ export function ModelsSection() {
|
|
|
63
66
|
and Supervisor are in Agent roles. A workspace can set its own
|
|
64
67
|
(Settings › Workspaces), and a task its own when it starts or resumes.
|
|
65
68
|
</p>
|
|
66
|
-
<
|
|
69
|
+
<SettingSection>
|
|
67
70
|
{INTERNAL_MODEL_ROLES(MODEL_ROLES).map((role) => (
|
|
68
|
-
<
|
|
71
|
+
<PickerSetting<string | null>
|
|
69
72
|
key={role}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
</li>
|
|
73
|
+
id={`role-${role}`}
|
|
74
|
+
label={ROLE_LABELS[role].label}
|
|
75
|
+
help={ROLE_LABELS[role].hint}
|
|
76
|
+
value={saved.roles?.[role] ?? null}
|
|
77
|
+
defaultValue={null}
|
|
78
|
+
resetLabel="default"
|
|
79
|
+
onSave={(ref) => {
|
|
80
|
+
const roles = { ...saved.roles };
|
|
81
|
+
if (ref) roles[role] = ref;
|
|
82
|
+
else delete roles[role];
|
|
83
|
+
return update.mutateAsync({ ...saved, roles });
|
|
84
|
+
}}
|
|
85
|
+
render={({ value, commit }) => (
|
|
86
|
+
<ModelSelect
|
|
87
|
+
id={`role-${role}`}
|
|
88
|
+
value={value}
|
|
89
|
+
effective={effective}
|
|
90
|
+
inheritLabel="Default"
|
|
91
|
+
inheritDetail={defaults[role].split("/").slice(1).join("/")}
|
|
92
|
+
onChange={commit}
|
|
93
|
+
/>
|
|
94
|
+
)}
|
|
95
|
+
/>
|
|
94
96
|
))}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
<SwitchSetting
|
|
98
|
+
id="plan-step"
|
|
99
|
+
label="Tasks plan first"
|
|
100
|
+
help="A chat task's first turn writes its plan on the planning model; the implementation model then follows it in the same conversation."
|
|
101
|
+
checked={effective.planStep}
|
|
102
|
+
onSave={(planStep) => update.mutateAsync({ ...saved, planStep })}
|
|
103
|
+
/>
|
|
104
|
+
</SettingSection>
|
|
102
105
|
</section>
|
|
103
106
|
|
|
104
107
|
<SubagentModels
|
|
@@ -151,7 +154,7 @@ export function ModelsSection() {
|
|
|
151
154
|
Dollars per million tokens, for Usage's estimates. Claude's
|
|
152
155
|
are API list prices: the plan itself is measured by its window.
|
|
153
156
|
</p>
|
|
154
|
-
<
|
|
157
|
+
<ScrollTable>
|
|
155
158
|
<table className="w-full text-xs">
|
|
156
159
|
<thead className="text-muted-foreground text-left">
|
|
157
160
|
<tr>
|
|
@@ -166,7 +169,9 @@ export function ModelsSection() {
|
|
|
166
169
|
<tbody>
|
|
167
170
|
{Object.entries(effective.prices).map(([ref, price]) => (
|
|
168
171
|
<tr key={ref} className="border-border border-t">
|
|
169
|
-
<td className="py-1 pr-2 font-mono">
|
|
172
|
+
<td className="py-1 pr-2 font-mono whitespace-nowrap">
|
|
173
|
+
{ref}
|
|
174
|
+
</td>
|
|
170
175
|
{PRICE_FIELDS.map((f) => (
|
|
171
176
|
<td key={f.key} className="py-1 pr-2">
|
|
172
177
|
<PriceInput
|
|
@@ -187,7 +192,7 @@ export function ModelsSection() {
|
|
|
187
192
|
))}
|
|
188
193
|
</tbody>
|
|
189
194
|
</table>
|
|
190
|
-
</
|
|
195
|
+
</ScrollTable>
|
|
191
196
|
</section>
|
|
192
197
|
{update.isError && (
|
|
193
198
|
<p className="text-destructive text-sm">{update.error.message}</p>
|
|
@@ -28,7 +28,7 @@ const KINDS: {
|
|
|
28
28
|
}[] = [
|
|
29
29
|
{ value: "tokenvault", label: "tokenvault", a: "Project", b: "Description" },
|
|
30
30
|
{ value: "keychain", label: "macOS Keychain", a: "Service", b: "Account" },
|
|
31
|
-
{ value: "env", label: "Environment variable", a: "Variable
|
|
31
|
+
{ value: "env", label: "Environment variable", a: "Variable name" },
|
|
32
32
|
];
|
|
33
33
|
|
|
34
34
|
function refFields(ref: SecretRef | undefined): [string, string] {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "@/components/ui/select";
|
|
12
12
|
import { useModelChoices } from "@/data/models";
|
|
13
13
|
import type { ModelChoice } from "@/lib/models/catalog";
|
|
14
|
+
import { TWO_LINE_TRIGGER, ValueLabel } from "@/components/Settings/ValueLabel";
|
|
14
15
|
import { AvailabilityDot, LocalityBadge } from "./ModelBadges";
|
|
15
16
|
|
|
16
17
|
const INHERIT = "__inherit";
|
|
@@ -41,6 +42,7 @@ export function RoleModelSelect({
|
|
|
41
42
|
id,
|
|
42
43
|
value,
|
|
43
44
|
inheritLabel,
|
|
45
|
+
inheritDetail,
|
|
44
46
|
claudeOnly,
|
|
45
47
|
gateOnly,
|
|
46
48
|
blockedBy,
|
|
@@ -50,6 +52,8 @@ export function RoleModelSelect({
|
|
|
50
52
|
id?: string;
|
|
51
53
|
value: string | null;
|
|
52
54
|
inheritLabel: string;
|
|
55
|
+
// What the inherited choice resolves to (a model id), shown under it.
|
|
56
|
+
inheritDetail?: string;
|
|
53
57
|
claudeOnly?: boolean;
|
|
54
58
|
// Only models that can serve a merge gate are selectable.
|
|
55
59
|
gateOnly?: boolean;
|
|
@@ -71,13 +75,21 @@ export function RoleModelSelect({
|
|
|
71
75
|
onValueChange={(v) => onChange(v === INHERIT ? null : v)}
|
|
72
76
|
disabled={disabled}
|
|
73
77
|
>
|
|
74
|
-
<SelectTrigger id={id} className=
|
|
78
|
+
<SelectTrigger id={id} className={TWO_LINE_TRIGGER}>
|
|
75
79
|
<SelectValue>
|
|
76
|
-
{current ?
|
|
80
|
+
{current ? (
|
|
81
|
+
<Row choice={current} />
|
|
82
|
+
) : value ? (
|
|
83
|
+
<ValueLabel label={value} />
|
|
84
|
+
) : (
|
|
85
|
+
<ValueLabel label={inheritLabel} detail={inheritDetail} />
|
|
86
|
+
)}
|
|
77
87
|
</SelectValue>
|
|
78
88
|
</SelectTrigger>
|
|
79
89
|
<SelectContent>
|
|
80
|
-
<SelectItem value={INHERIT}>
|
|
90
|
+
<SelectItem value={INHERIT}>
|
|
91
|
+
<ValueLabel label={inheritLabel} detail={inheritDetail} />
|
|
92
|
+
</SelectItem>
|
|
81
93
|
{value && !current && <SelectItem value={value}>{value}</SelectItem>}
|
|
82
94
|
{groups.map((g) => (
|
|
83
95
|
<SelectGroup key={g}>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState } from "react";
|
|
4
|
+
import { SettingRow, SettingSection } from "@/components/Settings/SettingRow";
|
|
4
5
|
import { Button } from "@/components/ui/button";
|
|
5
6
|
import { Input } from "@/components/ui/input";
|
|
6
7
|
import {
|
|
@@ -46,58 +47,72 @@ export function SubagentModels({
|
|
|
46
47
|
on Haiku, anything that plans, decides or reviews on Sonnet. A new
|
|
47
48
|
session picks a change up.
|
|
48
49
|
</p>
|
|
49
|
-
<
|
|
50
|
+
<SettingSection>
|
|
50
51
|
{Object.entries(rows).map(([type, tier]) => {
|
|
51
52
|
const custom = !(type in DEFAULT_SUBAGENT_MODELS);
|
|
53
|
+
const changed = saved?.[type] !== undefined;
|
|
52
54
|
return (
|
|
53
|
-
<
|
|
55
|
+
<SettingRow
|
|
54
56
|
key={type}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
57
|
+
id={`subagent-${type}`}
|
|
58
|
+
label={type}
|
|
59
|
+
resetLabel={
|
|
60
|
+
custom ? undefined : TIER_LABELS[DEFAULT_SUBAGENT_MODELS[type]]
|
|
61
|
+
}
|
|
62
|
+
onReset={
|
|
63
|
+
!custom && changed
|
|
64
|
+
? () => {
|
|
65
|
+
const next = { ...saved };
|
|
66
|
+
delete next[type];
|
|
67
|
+
onChange(next);
|
|
68
|
+
}
|
|
69
|
+
: undefined
|
|
70
|
+
}
|
|
71
|
+
control={
|
|
72
|
+
<>
|
|
73
|
+
<Select
|
|
74
|
+
value={tier}
|
|
75
|
+
disabled={disabled}
|
|
76
|
+
onValueChange={(v) => set(type, v as SubagentTier)}
|
|
77
|
+
>
|
|
78
|
+
<SelectTrigger
|
|
79
|
+
aria-label={`Model for ${type}`}
|
|
80
|
+
className="min-h-11 w-40 md:min-h-9"
|
|
77
81
|
>
|
|
78
|
-
|
|
79
|
-
</
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
82
|
+
<SelectValue />
|
|
83
|
+
</SelectTrigger>
|
|
84
|
+
<SelectContent>
|
|
85
|
+
{SUBAGENT_TIERS.map((t) => (
|
|
86
|
+
<SelectItem
|
|
87
|
+
key={t}
|
|
88
|
+
value={t}
|
|
89
|
+
disabled={t === "haiku" && mustNotBeHaiku(type)}
|
|
90
|
+
>
|
|
91
|
+
{TIER_LABELS[t]}
|
|
92
|
+
</SelectItem>
|
|
93
|
+
))}
|
|
94
|
+
</SelectContent>
|
|
95
|
+
</Select>
|
|
96
|
+
{custom && (
|
|
97
|
+
<Button
|
|
98
|
+
variant="ghost"
|
|
99
|
+
className="min-h-11 md:min-h-9"
|
|
100
|
+
disabled={disabled}
|
|
101
|
+
onClick={() => {
|
|
102
|
+
const next = { ...saved };
|
|
103
|
+
delete next[type];
|
|
104
|
+
onChange(next);
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
Remove
|
|
108
|
+
</Button>
|
|
109
|
+
)}
|
|
110
|
+
</>
|
|
111
|
+
}
|
|
112
|
+
/>
|
|
98
113
|
);
|
|
99
114
|
})}
|
|
100
|
-
</
|
|
115
|
+
</SettingSection>
|
|
101
116
|
<div className="flex gap-2">
|
|
102
117
|
<Input
|
|
103
118
|
value={adding}
|
|
@@ -22,32 +22,25 @@ export function PhoneNotifyForm() {
|
|
|
22
22
|
return (
|
|
23
23
|
<div className="space-y-5">
|
|
24
24
|
<p className="text-muted-foreground text-sm">
|
|
25
|
-
Your phone gets a failed schedule run
|
|
26
|
-
|
|
27
|
-
minute from each; the rest wait and go together.
|
|
25
|
+
Your phone gets a failed schedule run and anything an agent sends you.
|
|
26
|
+
At most one a minute from each; the rest wait and go together.
|
|
28
27
|
</p>
|
|
29
28
|
|
|
30
29
|
<div className="bg-foreground/[0.03] rounded-lg px-3 py-2.5 text-sm">
|
|
31
30
|
{settings.active === "command" ? (
|
|
32
31
|
<>
|
|
33
|
-
Sending through
|
|
34
|
-
|
|
35
|
-
on this machine. It takes priority over Telegram below.
|
|
32
|
+
Sending through a command set up on this machine. It takes priority
|
|
33
|
+
over Telegram below.
|
|
36
34
|
</>
|
|
37
35
|
) : settings.active === "telegram" ? (
|
|
38
36
|
<>Sending through the Telegram bot below.</>
|
|
39
37
|
) : (
|
|
40
|
-
<>
|
|
41
|
-
Not set up. Add a Telegram bot below, or set{" "}
|
|
42
|
-
<code className="font-mono text-xs">AGENTOS_NOTIFY_CMD</code> to a
|
|
43
|
-
command that reads the message on stdin.
|
|
44
|
-
</>
|
|
38
|
+
<>Not set up. Add a Telegram bot below.</>
|
|
45
39
|
)}
|
|
46
40
|
</div>
|
|
47
41
|
|
|
48
42
|
<div className="space-y-2">
|
|
49
|
-
<
|
|
50
|
-
<SettingSection>
|
|
43
|
+
<SettingSection title="Telegram">
|
|
51
44
|
<TextSetting
|
|
52
45
|
id="tg-chat"
|
|
53
46
|
label="Chat id"
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { useSnapshot } from "valtio";
|
|
5
5
|
import { ArrowLeft, ChefHat, Plus } from "lucide-react";
|
|
6
|
+
import { ListSection } from "@/components/Settings/ListSection";
|
|
6
7
|
import { Button } from "@/components/ui/button";
|
|
7
8
|
import {
|
|
8
9
|
useRecipeDraft,
|
|
@@ -30,7 +31,7 @@ function Row({ r, onOpen }: { r: RecipeView; onOpen: () => void }) {
|
|
|
30
31
|
<button
|
|
31
32
|
type="button"
|
|
32
33
|
onClick={onOpen}
|
|
33
|
-
className="
|
|
34
|
+
className="hover:bg-foreground/[0.03] focus-visible:ring-ring flex min-h-14 w-full items-center gap-3 px-3 py-2.5 text-left outline-none focus-visible:ring-2 focus-visible:ring-inset"
|
|
34
35
|
>
|
|
35
36
|
<div className="min-w-0 flex-1 space-y-0.5">
|
|
36
37
|
<div className="flex min-w-0 items-center gap-2">
|
|
@@ -40,11 +41,15 @@ function Row({ r, onOpen }: { r: RecipeView; onOpen: () => void }) {
|
|
|
40
41
|
</span>
|
|
41
42
|
</div>
|
|
42
43
|
<p className="text-muted-foreground truncate text-xs">
|
|
43
|
-
{
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
{[
|
|
45
|
+
r.timer
|
|
46
|
+
? `${r.timer.description}${r.timer.enabled && r.timer.nextRunAt ? ` · next ${formatRunTime(r.timer.nextRunAt, r.timer.timezone)}` : ""}`
|
|
47
|
+
: null,
|
|
48
|
+
last ? formatRunTime(sqlMs(last.created_at)) : null,
|
|
49
|
+
r.totals.costUsd > 0 ? `${money(r.totals.costUsd)} total` : null,
|
|
50
|
+
]
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.join(" · ")}
|
|
48
53
|
</p>
|
|
49
54
|
</div>
|
|
50
55
|
{last && <RunChip status={last.status} />}
|
|
@@ -185,9 +190,15 @@ export function RecipesPanel() {
|
|
|
185
190
|
</p>
|
|
186
191
|
</div>
|
|
187
192
|
)}
|
|
188
|
-
{recipes.
|
|
189
|
-
<
|
|
190
|
-
|
|
193
|
+
{recipes.length > 0 && (
|
|
194
|
+
<ListSection
|
|
195
|
+
title="Saved"
|
|
196
|
+
items={recipes}
|
|
197
|
+
keyOf={(r) => r.id}
|
|
198
|
+
render={(r) => <Row r={r} onOpen={() => toDetail(r.id)} />}
|
|
199
|
+
empty=""
|
|
200
|
+
/>
|
|
201
|
+
)}
|
|
191
202
|
</div>
|
|
192
203
|
)}
|
|
193
204
|
{view.mode === "detail" && (
|
|
@@ -52,7 +52,7 @@ export function RunHistory({ runs }: { runs: RecipeRun[] }) {
|
|
|
52
52
|
</p>
|
|
53
53
|
<p className="text-muted-foreground truncate text-xs">
|
|
54
54
|
{[
|
|
55
|
-
triggerLabel(r),
|
|
55
|
+
r.trigger === "manual" ? null : triggerLabel(r),
|
|
56
56
|
r.pr_number ? `PR #${r.pr_number}` : null,
|
|
57
57
|
stats,
|
|
58
58
|
]
|