@saadnvd1/agent-os 0.3.845 → 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/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/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 +13 -8
- 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 +27 -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/WorkspaceSettings.test.ts +5 -5
- package/components/Workspaces/WorkspaceSettings.tsx +0 -2
- package/components/ui/switch.tsx +1 -1
- package/content/whats-new.json +16 -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/health/config.ts +70 -17
- package/lib/models/providers.ts +1 -1
- package/lib/recipes/settle.ts +22 -0
- package/lib/settings-url.ts +14 -2
- package/package.json +1 -1
- package/components/Settings/NoApprovalsSection.tsx +0 -103
- package/components/Settings/SettingToggle.tsx +0 -36
- package/components/Settings/SetupSection.tsx +0 -46
|
@@ -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
|
]
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
Trash2,
|
|
13
13
|
} from "lucide-react";
|
|
14
14
|
import { Button } from "@/components/ui/button";
|
|
15
|
+
import { ListSection } from "@/components/Settings/ListSection";
|
|
15
16
|
import { Switch } from "@/components/ui/switch";
|
|
16
17
|
import { useWorkspacesQuery } from "@/data/workspaces";
|
|
17
18
|
import { useNotifySettings } from "@/data/notify";
|
|
@@ -93,7 +94,7 @@ function ScheduleRow({ s, onOpen }: { s: ScheduleView; onOpen: () => void }) {
|
|
|
93
94
|
(e.key === "Enter" || e.key === " ") &&
|
|
94
95
|
onOpen()
|
|
95
96
|
}
|
|
96
|
-
className="
|
|
97
|
+
className="hover:bg-foreground/[0.03] focus-visible:ring-ring flex min-h-14 cursor-pointer items-center gap-3 px-3 py-2.5 outline-none focus-visible:ring-2 focus-visible:ring-inset"
|
|
97
98
|
>
|
|
98
99
|
<div className="min-w-0 flex-1 space-y-0.5">
|
|
99
100
|
<div className="flex min-w-0 items-center gap-2">
|
|
@@ -114,7 +115,9 @@ function ScheduleRow({ s, onOpen }: { s: ScheduleView; onOpen: () => void }) {
|
|
|
114
115
|
{s.oneOff && ` · by ${s.createdBy.name}`}
|
|
115
116
|
</p>
|
|
116
117
|
</div>
|
|
117
|
-
{s.lastRun &&
|
|
118
|
+
{s.lastRun && !s.done_at && s.lastRun.outcome !== "started" && (
|
|
119
|
+
<OutcomeChip run={s.lastRun} />
|
|
120
|
+
)}
|
|
118
121
|
{!s.done_at && <EnableSwitch s={s} />}
|
|
119
122
|
</div>
|
|
120
123
|
);
|
|
@@ -131,14 +134,13 @@ function Group({
|
|
|
131
134
|
}) {
|
|
132
135
|
if (rows.length === 0) return null;
|
|
133
136
|
return (
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
{
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
</section>
|
|
137
|
+
<ListSection
|
|
138
|
+
title={title}
|
|
139
|
+
items={rows}
|
|
140
|
+
keyOf={(s) => s.id}
|
|
141
|
+
render={(s) => <ScheduleRow s={s} onOpen={() => onOpen(s.id)} />}
|
|
142
|
+
empty=""
|
|
143
|
+
/>
|
|
142
144
|
);
|
|
143
145
|
}
|
|
144
146
|
|
|
@@ -19,8 +19,8 @@ export function AppWideLimits() {
|
|
|
19
19
|
const updateAway = useUpdateAwaySettings();
|
|
20
20
|
return (
|
|
21
21
|
<SettingSection
|
|
22
|
-
title="
|
|
23
|
-
help="
|
|
22
|
+
title="Alerts and Away"
|
|
23
|
+
help="Apply across every workspace."
|
|
24
24
|
>
|
|
25
25
|
{stall.data && (
|
|
26
26
|
<NumberSetting
|