@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
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { toast } from "sonner";
|
|
4
4
|
import { CheckCircle2, CircleAlert, RotateCcw } from "lucide-react";
|
|
5
|
-
import {
|
|
5
|
+
import { SwitchSetting } from "@/components/Settings/settings-fields";
|
|
6
|
+
import { SettingSection } from "@/components/Settings/SettingRow";
|
|
6
7
|
import {
|
|
7
8
|
Select,
|
|
8
9
|
SelectContent,
|
|
@@ -80,13 +81,16 @@ export function AutoUpdates({ auto }: { auto: AutoUpdateView }) {
|
|
|
80
81
|
const onError = (e: Error) => toast.error(e.message);
|
|
81
82
|
return (
|
|
82
83
|
<div className="space-y-3">
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
<SettingSection>
|
|
85
|
+
<SwitchSetting
|
|
86
|
+
id="auto-update"
|
|
87
|
+
label="Install updates automatically"
|
|
88
|
+
help="Once everything has been idle for a while. Terminals and chats keep running through the restart, and a version that doesn't start cleanly is rolled back."
|
|
89
|
+
checked={auto.enabled}
|
|
90
|
+
locked={auto.locked}
|
|
91
|
+
onSave={(on) => save.mutateAsync({ auto: on })}
|
|
92
|
+
/>
|
|
93
|
+
</SettingSection>
|
|
90
94
|
{auto.enabled && (
|
|
91
95
|
<div className="flex flex-wrap items-center justify-between gap-2 px-3">
|
|
92
96
|
<label htmlFor="auto-idle" className="text-sm">
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import { toast } from "sonner";
|
|
4
4
|
import { Loader2 } from "lucide-react";
|
|
5
5
|
import { Button } from "@/components/ui/button";
|
|
6
|
-
import {
|
|
6
|
+
import { SwitchSetting } from "@/components/Settings/settings-fields";
|
|
7
|
+
import { SettingSection } from "@/components/Settings/SettingRow";
|
|
7
8
|
import { useSetTipsOff, useTips } from "@/data/tips";
|
|
8
9
|
import {
|
|
9
10
|
useCheckForUpdates,
|
|
@@ -141,16 +142,17 @@ export function UpdatesSection() {
|
|
|
141
142
|
|
|
142
143
|
{view.auto && <AutoUpdates auto={view.auto} />}
|
|
143
144
|
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
145
|
+
<SettingSection>
|
|
146
|
+
<SwitchSetting
|
|
147
|
+
id="update-banner"
|
|
148
|
+
label="Show update banner"
|
|
149
|
+
help="Tell everyone using this AgentOS when a newer version is out, and when it updated itself."
|
|
150
|
+
checked={view.bannerEnabled}
|
|
151
|
+
locked={view.bannerLocked}
|
|
152
|
+
onSave={(on) => banner.mutateAsync(on)}
|
|
153
|
+
/>
|
|
154
|
+
<TipsToggle />
|
|
155
|
+
</SettingSection>
|
|
154
156
|
</div>
|
|
155
157
|
);
|
|
156
158
|
}
|
|
@@ -159,13 +161,12 @@ function TipsToggle() {
|
|
|
159
161
|
const { data } = useTips();
|
|
160
162
|
const setOff = useSetTipsOff();
|
|
161
163
|
return (
|
|
162
|
-
<
|
|
163
|
-
|
|
164
|
-
|
|
164
|
+
<SwitchSetting
|
|
165
|
+
id="tips"
|
|
166
|
+
label="Show Tip of the Day"
|
|
167
|
+
help="A short tip about something AgentOS can do, behind the lightbulb in the sidebar. It never opens on its own."
|
|
165
168
|
checked={!!data && !data.off}
|
|
166
|
-
|
|
167
|
-
setOff.mutate(!on, { onError: (e) => toast.error(e.message) })
|
|
168
|
-
}
|
|
169
|
+
onSave={(on) => setOff.mutateAsync(!on)}
|
|
169
170
|
/>
|
|
170
171
|
);
|
|
171
172
|
}
|
|
@@ -124,19 +124,19 @@ describe("WorkspaceSettings", () => {
|
|
|
124
124
|
await render();
|
|
125
125
|
expect(limit().id).toBe("task-limit-w1");
|
|
126
126
|
expect(limit().disabled).toBe(true);
|
|
127
|
-
expect(limit().value).toBe("
|
|
128
|
-
expect(
|
|
127
|
+
expect(limit().value).toBe("");
|
|
128
|
+
expect(limit().placeholder).toBe("Off");
|
|
129
|
+
expect(host.textContent).not.toContain("Reset");
|
|
129
130
|
});
|
|
130
131
|
|
|
131
|
-
it("enables the number when a limit is set, and
|
|
132
|
+
it("enables the number when a limit is set, and offers Reset to off", async () => {
|
|
132
133
|
localStorage.setItem("agentOS-settings-workspace", "w2");
|
|
133
134
|
await render();
|
|
134
135
|
expect(limit().disabled).toBe(false);
|
|
135
136
|
expect(limit().value).toBe("2");
|
|
136
|
-
expect(host.textContent).toContain("Default: off");
|
|
137
137
|
expect(
|
|
138
138
|
[...host.querySelectorAll("button")].some(
|
|
139
|
-
(b) => b.textContent === "Reset"
|
|
139
|
+
(b) => b.textContent === "Reset to off"
|
|
140
140
|
)
|
|
141
141
|
).toBe(true);
|
|
142
142
|
});
|
|
@@ -4,7 +4,6 @@ import { useContext, useState } from "react";
|
|
|
4
4
|
import { settingsUiActions } from "@/stores/settingsUi";
|
|
5
5
|
import { useWorkspacesQuery } from "@/data/workspaces";
|
|
6
6
|
import { useProjectsQuery } from "@/data/projects";
|
|
7
|
-
import { AppWideLimits } from "@/components/Settings/AppWideLimits";
|
|
8
7
|
import {
|
|
9
8
|
matchesQuery,
|
|
10
9
|
SettingsQueryContext,
|
|
@@ -69,7 +68,6 @@ export function WorkspaceSettings() {
|
|
|
69
68
|
}}
|
|
70
69
|
/>
|
|
71
70
|
<WorkspacePanel key={shown.id} workspace={shown} />
|
|
72
|
-
<AppWideLimits />
|
|
73
71
|
<p className="text-muted-foreground text-sm">
|
|
74
72
|
Fresh starts and compaction are app-wide:{" "}
|
|
75
73
|
<button
|
package/components/ui/switch.tsx
CHANGED
|
@@ -13,7 +13,7 @@ function Switch({
|
|
|
13
13
|
<SwitchPrimitive.Root
|
|
14
14
|
data-slot="switch"
|
|
15
15
|
className={cn(
|
|
16
|
-
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-
|
|
16
|
+
"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-muted-foreground/35 data-[state=unchecked]:border-foreground/15 focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-muted-foreground/40 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
|
17
17
|
className
|
|
18
18
|
)}
|
|
19
19
|
{...props}
|
package/content/whats-new.json
CHANGED
|
@@ -2009,6 +2009,22 @@
|
|
|
2009
2009
|
"area": "Sidebar",
|
|
2010
2010
|
"text": "Renaming a session for the first time no longer closes the name box before you can type.",
|
|
2011
2011
|
"url": "https://github.com/saadnvd1/agent-os/pull/568"
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
"id": 569,
|
|
2015
|
+
"date": "2026-11-02",
|
|
2016
|
+
"kind": "fixed",
|
|
2017
|
+
"area": "Chat",
|
|
2018
|
+
"text": "Pressing Enter with a selection that ends on a reply's button row no longer quotes it.",
|
|
2019
|
+
"url": "https://github.com/saadnvd1/agent-os/pull/569"
|
|
2020
|
+
},
|
|
2021
|
+
{
|
|
2022
|
+
"id": 570,
|
|
2023
|
+
"date": "2026-11-02",
|
|
2024
|
+
"kind": "improved",
|
|
2025
|
+
"area": "Settings",
|
|
2026
|
+
"text": "Settings is tidier: clearer switches, shorter lists, one Approvals page, and a General page for your name and alerts.",
|
|
2027
|
+
"url": "https://github.com/saadnvd1/agent-os/pull/570"
|
|
2012
2028
|
}
|
|
2013
2029
|
]
|
|
2014
2030
|
}
|
package/data/approvals/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ApprovalLine } from "@/lib/approvals/describe";
|
|
3
3
|
import { provePresence } from "../presence";
|
|
4
4
|
import { usePollWhenOffline } from "../push/connection";
|
|
5
5
|
|
|
6
6
|
export interface ApprovalsMode {
|
|
7
7
|
on: boolean;
|
|
8
|
-
audit:
|
|
8
|
+
audit: ApprovalLine[];
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export const approvalsKeys = { all: ["approvals"] as const };
|
package/data/health/index.ts
CHANGED
|
@@ -91,10 +91,13 @@ export function useDeleteNote() {
|
|
|
91
91
|
});
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
import type { HealthMeta } from "@/lib/health/config";
|
|
95
|
+
|
|
94
96
|
export interface ConfigResponse {
|
|
95
97
|
config: Record<string, number>;
|
|
96
98
|
defaults: Record<string, number>;
|
|
97
99
|
labels: Record<string, string>;
|
|
100
|
+
meta: Record<string, HealthMeta>;
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
export function useHealthConfig() {
|
package/hooks/useAutoSave.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import { toast } from "sonner";
|
|
5
|
+
import { settingsUi } from "@/stores/settingsUi";
|
|
5
6
|
|
|
6
7
|
export type SaveStatus = "idle" | "saving" | "saved" | "undo";
|
|
7
8
|
|
|
@@ -13,6 +14,9 @@ export interface AutoSave<T> {
|
|
|
13
14
|
// rejected save drops the in-flight value, so this is the saved one again.
|
|
14
15
|
value: T;
|
|
15
16
|
status: SaveStatus;
|
|
17
|
+
// Why the last save was rejected; cleared by the next commit. Shown on the
|
|
18
|
+
// row while Settings is open (a toast would sit over the dialog).
|
|
19
|
+
error: string | null;
|
|
16
20
|
// Commit a new value. `undoable` keeps the previous one for Undo (large or
|
|
17
21
|
// destructive changes). Resolves false when the server rejected it.
|
|
18
22
|
commit: (next: T, opts?: { undoable?: boolean }) => Promise<boolean>;
|
|
@@ -27,7 +31,7 @@ export function useAutoSave<T>({
|
|
|
27
31
|
saved,
|
|
28
32
|
save,
|
|
29
33
|
equals = Object.is,
|
|
30
|
-
notify
|
|
34
|
+
notify,
|
|
31
35
|
}: {
|
|
32
36
|
saved: T;
|
|
33
37
|
save: (next: T) => Promise<unknown>;
|
|
@@ -36,6 +40,7 @@ export function useAutoSave<T>({
|
|
|
36
40
|
}): AutoSave<T> {
|
|
37
41
|
const [pending, setPending] = useState<{ value: T } | null>(null);
|
|
38
42
|
const [status, setStatus] = useState<SaveStatus>("idle");
|
|
43
|
+
const [error, setError] = useState<string | null>(null);
|
|
39
44
|
const previous = useRef<{ value: T } | null>(null);
|
|
40
45
|
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
41
46
|
const seq = useRef(0);
|
|
@@ -56,6 +61,7 @@ export function useAutoSave<T>({
|
|
|
56
61
|
const mine = ++seq.current;
|
|
57
62
|
clearTimeout(timer.current);
|
|
58
63
|
setPending({ value: next });
|
|
64
|
+
setError(null);
|
|
59
65
|
setStatus("saving");
|
|
60
66
|
try {
|
|
61
67
|
await latest.current.save(next);
|
|
@@ -64,9 +70,10 @@ export function useAutoSave<T>({
|
|
|
64
70
|
setPending(null);
|
|
65
71
|
setStatus("idle");
|
|
66
72
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
);
|
|
73
|
+
const message = err instanceof Error ? err.message : "Couldn't save";
|
|
74
|
+
if (latest.current.notify) latest.current.notify(message);
|
|
75
|
+
else if (settingsUi.open) setError(message);
|
|
76
|
+
else toast.error(message);
|
|
70
77
|
return false;
|
|
71
78
|
}
|
|
72
79
|
if (mine !== seq.current) return true;
|
|
@@ -94,5 +101,11 @@ export function useAutoSave<T>({
|
|
|
94
101
|
return run(back.value, false, latest.current.saved);
|
|
95
102
|
}, [run]);
|
|
96
103
|
|
|
97
|
-
return {
|
|
104
|
+
return {
|
|
105
|
+
value: pending ? pending.value : saved,
|
|
106
|
+
status,
|
|
107
|
+
error,
|
|
108
|
+
commit,
|
|
109
|
+
undo,
|
|
110
|
+
};
|
|
98
111
|
}
|
package/hooks/useSettingsUrl.ts
CHANGED
|
@@ -19,8 +19,7 @@ function fromAddress() {
|
|
|
19
19
|
*/
|
|
20
20
|
export function useSettingsUrl() {
|
|
21
21
|
useEffect(() => {
|
|
22
|
-
|
|
23
|
-
const unsubscribe = subscribe(settingsUi, () => {
|
|
22
|
+
const toAddress = () => {
|
|
24
23
|
const href = window.location.href;
|
|
25
24
|
const next = hrefWithSettings(href, {
|
|
26
25
|
open: settingsUi.open,
|
|
@@ -29,7 +28,11 @@ export function useSettingsUrl() {
|
|
|
29
28
|
const url = new URL(href);
|
|
30
29
|
if (next !== url.pathname + url.search + url.hash)
|
|
31
30
|
window.history.replaceState(window.history.state, "", next);
|
|
32
|
-
}
|
|
31
|
+
};
|
|
32
|
+
fromAddress();
|
|
33
|
+
// An old link (?settings=setup) is rewritten to where it landed.
|
|
34
|
+
toAddress();
|
|
35
|
+
const unsubscribe = subscribe(settingsUi, toAddress);
|
|
33
36
|
window.addEventListener("popstate", fromAddress);
|
|
34
37
|
return () => {
|
|
35
38
|
unsubscribe();
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// What an auto-approval reads as to a person: a sentence, with the command
|
|
2
|
+
// or other raw text behind a "details" toggle. The audit table keeps the raw
|
|
3
|
+
// subject; this is only how it is shown.
|
|
4
|
+
|
|
5
|
+
import type { AuditRow } from "./no-approvals";
|
|
6
|
+
|
|
7
|
+
export interface ApprovalLine {
|
|
8
|
+
id: number;
|
|
9
|
+
at: string;
|
|
10
|
+
// The source in words ("an ask", "a chat").
|
|
11
|
+
where: string;
|
|
12
|
+
summary: string;
|
|
13
|
+
// Raw text (a shell command, a path) for the details toggle.
|
|
14
|
+
details: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const WHERE: Record<AuditRow["source"], string> = {
|
|
18
|
+
ask: "An ask",
|
|
19
|
+
brake: "A brake",
|
|
20
|
+
plan: "A plan",
|
|
21
|
+
chat: "A chat",
|
|
22
|
+
terminal: "A terminal",
|
|
23
|
+
"merge-approval": "A merge",
|
|
24
|
+
mode: "The mode",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const READ = new Set(["Read", "Glob", "Grep", "LS"]);
|
|
28
|
+
const CHANGE = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
|
|
29
|
+
|
|
30
|
+
const SHELL_START = /^(bash|run|shell|execute|command)\b[:\s]/i;
|
|
31
|
+
const SHELL_WORDS =
|
|
32
|
+
/(^|\s)(cd|ssh|scp|git|npm|npx|pnpm|rm|curl|sudo|cat|ls|sed|grep|kill|tmux|docker|kubectl|python3?|node)\s/;
|
|
33
|
+
const SHELL_CHARS = /&&|\|\||[|;`]|\$\(|\s\/[\w.-]+\/|~\//;
|
|
34
|
+
|
|
35
|
+
export const looksLikeCommand = (text: string) =>
|
|
36
|
+
SHELL_START.test(text) || SHELL_WORDS.test(text) || SHELL_CHARS.test(text);
|
|
37
|
+
|
|
38
|
+
export function describeApproval(row: AuditRow): ApprovalLine {
|
|
39
|
+
const base = {
|
|
40
|
+
id: row.id,
|
|
41
|
+
at: row.at,
|
|
42
|
+
where: WHERE[row.source] ?? row.source,
|
|
43
|
+
};
|
|
44
|
+
const subject = row.subject.trim();
|
|
45
|
+
if (row.source === "terminal") {
|
|
46
|
+
const tool = subject.replace(/ in a terminal session$/, "");
|
|
47
|
+
const summary =
|
|
48
|
+
tool === "Bash"
|
|
49
|
+
? "Ran a command"
|
|
50
|
+
: CHANGE.has(tool)
|
|
51
|
+
? "Changed a file"
|
|
52
|
+
: READ.has(tool)
|
|
53
|
+
? "Read files"
|
|
54
|
+
: `Used ${tool}`;
|
|
55
|
+
return { ...base, summary, details: null };
|
|
56
|
+
}
|
|
57
|
+
if (row.source === "chat" && looksLikeCommand(subject))
|
|
58
|
+
return {
|
|
59
|
+
...base,
|
|
60
|
+
summary: "Ran a command",
|
|
61
|
+
details: subject.replace(SHELL_START, "").trim() || subject,
|
|
62
|
+
};
|
|
63
|
+
return { ...base, summary: subject, details: null };
|
|
64
|
+
}
|
package/lib/health/config.ts
CHANGED
|
@@ -30,25 +30,78 @@ export const HEALTH_DEFAULTS = {
|
|
|
30
30
|
export type HealthKey = keyof typeof HEALTH_DEFAULTS;
|
|
31
31
|
export type HealthConfig = Record<HealthKey, number>;
|
|
32
32
|
|
|
33
|
-
export
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
33
|
+
export interface HealthMeta {
|
|
34
|
+
label: string;
|
|
35
|
+
unit?: string;
|
|
36
|
+
min: number;
|
|
37
|
+
max?: number;
|
|
38
|
+
integer?: boolean;
|
|
39
|
+
// A 1/0 value shown as a switch.
|
|
40
|
+
switch?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// How each threshold reads in Settings: the server owns it, so a new
|
|
44
|
+
// threshold needs no client change.
|
|
45
|
+
export const HEALTH_META: Record<HealthKey, HealthMeta> = {
|
|
46
|
+
loadRedMinutes: { label: "Machine load red for", unit: "min", min: 1 },
|
|
47
|
+
queueHeldMinutes: { label: "Queued task held for", unit: "min", min: 1 },
|
|
48
|
+
bgQuietMinutes: { label: "Background job quiet for", unit: "min", min: 1 },
|
|
49
|
+
portWaitMinutes: {
|
|
50
|
+
label: "Waiting on a port someone else holds for",
|
|
51
|
+
unit: "min",
|
|
52
|
+
min: 1,
|
|
53
|
+
},
|
|
54
|
+
pieceDupOverlap: {
|
|
55
|
+
label: "Plan piece matches merged work",
|
|
56
|
+
min: 0,
|
|
57
|
+
max: 1,
|
|
58
|
+
integer: false,
|
|
59
|
+
},
|
|
60
|
+
restartsMax: { label: "Server restarts allowed per window", min: 1 },
|
|
61
|
+
restartsWindowMinutes: { label: "Restart window", unit: "min", min: 1 },
|
|
62
|
+
ciSlowFallbackMinutes: {
|
|
63
|
+
label: "CI pending, when no history yet",
|
|
64
|
+
unit: "min",
|
|
65
|
+
min: 1,
|
|
66
|
+
},
|
|
67
|
+
armedStuckMinutes: {
|
|
68
|
+
label: "PR armed without merging for",
|
|
69
|
+
unit: "min",
|
|
70
|
+
min: 1,
|
|
71
|
+
},
|
|
72
|
+
serverErrorsMin: {
|
|
73
|
+
label: "Server errors per 10 minutes, at least",
|
|
74
|
+
min: 1,
|
|
75
|
+
},
|
|
76
|
+
serverErrorsFactor: {
|
|
77
|
+
label: "Server errors, times the usual rate",
|
|
78
|
+
unit: "×",
|
|
79
|
+
min: 1,
|
|
80
|
+
integer: false,
|
|
81
|
+
},
|
|
82
|
+
envBurstSessions: {
|
|
83
|
+
label: "Environment failure: sessions in the window to alert",
|
|
84
|
+
min: 1,
|
|
85
|
+
},
|
|
86
|
+
envBurstHours: { label: "Environment failure window", unit: "hours", min: 1 },
|
|
87
|
+
envQuietDays: {
|
|
88
|
+
label: "Environment failure back after quiet for",
|
|
89
|
+
unit: "days",
|
|
90
|
+
min: 1,
|
|
91
|
+
},
|
|
92
|
+
wakeUsageCeilingPct: {
|
|
93
|
+
label: "Stop waking watchers above usage",
|
|
94
|
+
unit: "%",
|
|
95
|
+
min: 1,
|
|
96
|
+
max: 100,
|
|
97
|
+
},
|
|
98
|
+
watchersOn: { label: "Watchers", min: 0, max: 1, switch: true },
|
|
50
99
|
};
|
|
51
100
|
|
|
101
|
+
export const HEALTH_LABELS = Object.fromEntries(
|
|
102
|
+
Object.entries(HEALTH_META).map(([k, m]) => [k, m.label])
|
|
103
|
+
) as Record<HealthKey, string>;
|
|
104
|
+
|
|
52
105
|
const settingKey = (k: HealthKey) => `health.${k}`;
|
|
53
106
|
|
|
54
107
|
export function healthConfig(
|
package/lib/models/providers.ts
CHANGED
|
@@ -190,5 +190,5 @@ export function describeSecretRef(ref: SecretRef | undefined): string {
|
|
|
190
190
|
return `tokenvault ${ref.project}${ref.desc ? ` "${ref.desc}"` : ""}`;
|
|
191
191
|
if (ref.kind === "keychain")
|
|
192
192
|
return `Keychain ${ref.service}${ref.account ? ` (${ref.account})` : ""}`;
|
|
193
|
-
return
|
|
193
|
+
return "an environment variable";
|
|
194
194
|
}
|
package/lib/recipes/settle.ts
CHANGED
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
import { getRecipe, toRun } from "./store";
|
|
25
25
|
import type { Recipe, RecipeRun, RunStatus } from "./types";
|
|
26
26
|
|
|
27
|
+
// A live run with nothing running behind it for this long is given up on.
|
|
28
|
+
export const LOST_RUN_MS = 24 * 60 * 60_000;
|
|
27
29
|
// A session run that stops without a RESULT line settles from the model's
|
|
28
30
|
// reading after it has been idle this long.
|
|
29
31
|
export const IDLE_SETTLE_MS = 30 * 60_000;
|
|
@@ -379,6 +381,26 @@ async function settleOne(
|
|
|
379
381
|
session,
|
|
380
382
|
now
|
|
381
383
|
);
|
|
384
|
+
// Nothing live behind it for a day (a terminal session, an archived one, a
|
|
385
|
+
// worker that's gone): it isn't still running, so it must not say so.
|
|
386
|
+
if (
|
|
387
|
+
Number.isFinite(startedMs) &&
|
|
388
|
+
now - startedMs > LOST_RUN_MS &&
|
|
389
|
+
state !== "running" &&
|
|
390
|
+
state !== "waiting"
|
|
391
|
+
)
|
|
392
|
+
return void finish(
|
|
393
|
+
run,
|
|
394
|
+
{
|
|
395
|
+
status: "failed",
|
|
396
|
+
result: line,
|
|
397
|
+
source: "state",
|
|
398
|
+
error: "Lost track of it",
|
|
399
|
+
summary: "Lost track of it",
|
|
400
|
+
},
|
|
401
|
+
session,
|
|
402
|
+
now
|
|
403
|
+
);
|
|
382
404
|
const idle =
|
|
383
405
|
state === "idle" || session.archived_at != null || state === null;
|
|
384
406
|
if (idle && session.view === "chat") {
|
package/lib/settings-url.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
export const SETTINGS_PARAM = "settings";
|
|
6
6
|
|
|
7
7
|
export const SETTINGS_SECTIONS = [
|
|
8
|
+
"general",
|
|
8
9
|
"merging",
|
|
9
10
|
"approvals",
|
|
10
11
|
"workspaces",
|
|
@@ -18,13 +19,19 @@ export const SETTINGS_SECTIONS = [
|
|
|
18
19
|
"schedules",
|
|
19
20
|
"audits",
|
|
20
21
|
"health",
|
|
21
|
-
"setup",
|
|
22
22
|
"updates",
|
|
23
23
|
"experiments",
|
|
24
24
|
] as const;
|
|
25
25
|
|
|
26
26
|
export type SettingsSection = (typeof SETTINGS_SECTIONS)[number];
|
|
27
27
|
|
|
28
|
+
// Sections that were folded into another: an old link still lands in the
|
|
29
|
+
// right place.
|
|
30
|
+
export const SECTION_REDIRECTS: Record<string, SettingsSection> = {
|
|
31
|
+
setup: "health",
|
|
32
|
+
"no-approvals": "approvals",
|
|
33
|
+
};
|
|
34
|
+
|
|
28
35
|
export interface SettingsLocation {
|
|
29
36
|
open: boolean;
|
|
30
37
|
// null: the list on a phone, the first section on a wider screen.
|
|
@@ -40,7 +47,12 @@ export const isSettingsSection = (v: unknown): v is SettingsSection =>
|
|
|
40
47
|
export function settingsFromHref(href: string): SettingsLocation {
|
|
41
48
|
const value = new URL(href, BASE).searchParams.get(SETTINGS_PARAM);
|
|
42
49
|
if (value === null) return { open: false, section: null };
|
|
43
|
-
|
|
50
|
+
const section = isSettingsSection(value)
|
|
51
|
+
? value
|
|
52
|
+
: value !== null && Object.hasOwn(SECTION_REDIRECTS, value)
|
|
53
|
+
? SECTION_REDIRECTS[value]
|
|
54
|
+
: null;
|
|
55
|
+
return { open: true, section };
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
/** The same address with Settings set (or removed), as path+query+hash. */
|
package/package.json
CHANGED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import { Button } from "@/components/ui/button";
|
|
5
|
-
import { SettingToggle } from "@/components/Settings/SettingToggle";
|
|
6
|
-
import { useApprovalsModeQuery, useSetApprovalsMode } from "@/data/approvals";
|
|
7
|
-
|
|
8
|
-
// What it switches off, said plainly before it can be switched on.
|
|
9
|
-
export const NO_APPROVALS_WARNING = [
|
|
10
|
-
"Every ask is approved the moment it is raised, hard lines included: public posts and messages, money, irreversible or destructive changes, credentials.",
|
|
11
|
-
"A PR stuck failing its gates, with no CI, or too big to review still merges.",
|
|
12
|
-
"Brakes stop holding new starts. Plan-only tasks still wait for approve_plan.",
|
|
13
|
-
"Every agent AgentOS starts runs with its permission prompts off, and prompts already waiting are allowed.",
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
export function NoApprovalsSection() {
|
|
17
|
-
const { data, isPending, isError, error } = useApprovalsModeQuery();
|
|
18
|
-
const set = useSetApprovalsMode();
|
|
19
|
-
const [confirming, setConfirming] = useState(false);
|
|
20
|
-
|
|
21
|
-
if (isPending)
|
|
22
|
-
return <div className="bg-muted/40 h-16 animate-pulse rounded-lg" />;
|
|
23
|
-
if (isError)
|
|
24
|
-
return <p className="text-destructive text-sm">{error.message}</p>;
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<div className="space-y-3">
|
|
28
|
-
<SettingToggle
|
|
29
|
-
title="No approvals"
|
|
30
|
-
detail="Shuts off every approval AgentOS would otherwise wait on, and every agent's permission prompts. Off by default."
|
|
31
|
-
checked={data.on}
|
|
32
|
-
onChange={(on) => {
|
|
33
|
-
if (on) setConfirming(true);
|
|
34
|
-
else set.mutate(false);
|
|
35
|
-
}}
|
|
36
|
-
/>
|
|
37
|
-
{confirming && !data.on && (
|
|
38
|
-
<div
|
|
39
|
-
role="alertdialog"
|
|
40
|
-
aria-label="Turn on No approvals"
|
|
41
|
-
className="rounded-lg border border-red-500/20 bg-red-500/10 p-3"
|
|
42
|
-
>
|
|
43
|
-
<p className="mb-2 text-sm font-medium text-red-400">
|
|
44
|
-
Turn on No approvals?
|
|
45
|
-
</p>
|
|
46
|
-
<ul className="text-muted-foreground mb-3 list-disc space-y-1 pl-5 text-xs">
|
|
47
|
-
{NO_APPROVALS_WARNING.map((line) => (
|
|
48
|
-
<li key={line}>{line}</li>
|
|
49
|
-
))}
|
|
50
|
-
<li>
|
|
51
|
-
Terminals already running in an agent other than Claude keep
|
|
52
|
-
asking until they are restarted. Each approval made for you is
|
|
53
|
-
logged below.
|
|
54
|
-
</li>
|
|
55
|
-
</ul>
|
|
56
|
-
<div className="flex gap-2">
|
|
57
|
-
<Button
|
|
58
|
-
size="sm"
|
|
59
|
-
variant="destructive"
|
|
60
|
-
disabled={set.isPending}
|
|
61
|
-
onClick={() =>
|
|
62
|
-
set.mutate(true, { onSuccess: () => setConfirming(false) })
|
|
63
|
-
}
|
|
64
|
-
>
|
|
65
|
-
{set.isPending ? "Turning on..." : "Turn on No approvals"}
|
|
66
|
-
</Button>
|
|
67
|
-
<Button
|
|
68
|
-
size="sm"
|
|
69
|
-
variant="ghost"
|
|
70
|
-
disabled={set.isPending}
|
|
71
|
-
onClick={() => setConfirming(false)}
|
|
72
|
-
>
|
|
73
|
-
Cancel
|
|
74
|
-
</Button>
|
|
75
|
-
</div>
|
|
76
|
-
</div>
|
|
77
|
-
)}
|
|
78
|
-
{set.error && (
|
|
79
|
-
<p className="text-destructive text-xs">{set.error.message}</p>
|
|
80
|
-
)}
|
|
81
|
-
<div>
|
|
82
|
-
<p className="mb-1 text-sm font-medium">Approved for you</p>
|
|
83
|
-
{data.audit.length === 0 ? (
|
|
84
|
-
<p className="text-muted-foreground text-xs">Nothing yet.</p>
|
|
85
|
-
) : (
|
|
86
|
-
<ul className="space-y-1">
|
|
87
|
-
{data.audit.map((row) => (
|
|
88
|
-
<li key={row.id} className="text-xs">
|
|
89
|
-
<span className="text-muted-foreground">
|
|
90
|
-
{row.at} · {row.source} ·{" "}
|
|
91
|
-
</span>
|
|
92
|
-
{row.subject}
|
|
93
|
-
{row.detail ? (
|
|
94
|
-
<span className="text-muted-foreground"> ({row.detail})</span>
|
|
95
|
-
) : null}
|
|
96
|
-
</li>
|
|
97
|
-
))}
|
|
98
|
-
</ul>
|
|
99
|
-
)}
|
|
100
|
-
</div>
|
|
101
|
-
</div>
|
|
102
|
-
);
|
|
103
|
-
}
|