@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,11 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { SettingSection } from "@/components/Settings/SettingRow";
|
|
4
|
+
import { SwitchSetting } from "@/components/Settings/settings-fields";
|
|
4
5
|
import { useNetworkQuery, useUpdateNetwork } from "@/data/devices";
|
|
5
6
|
import { TailscaleCard } from "./TailscaleCard";
|
|
6
7
|
import { ConnectCard } from "./ConnectCard";
|
|
7
8
|
import { ReachRow } from "@/components/Reach/ReachRow";
|
|
8
9
|
|
|
10
|
+
// How devices reach this machine: Connect, Wi-Fi and Tailscale.
|
|
9
11
|
export function NetworkSection({
|
|
10
12
|
open,
|
|
11
13
|
canManage,
|
|
@@ -24,31 +26,32 @@ export function NetworkSection({
|
|
|
24
26
|
return (
|
|
25
27
|
<div className="space-y-2">
|
|
26
28
|
<ReachRow enabled={open} />
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
29
|
+
<SettingSection>
|
|
30
|
+
<ConnectCard
|
|
31
|
+
connect={data.connect}
|
|
32
|
+
canManage={canManage}
|
|
33
|
+
onToggle={(connect) => update.mutateAsync({ connect })}
|
|
34
|
+
/>
|
|
35
|
+
<TailscaleCard tailscale={data.tailscale} />
|
|
36
|
+
<SwitchSetting
|
|
37
|
+
id="network-lan"
|
|
38
|
+
label="Allow devices on this Wi-Fi"
|
|
39
|
+
help="Paired devices on the same network can connect. Use it on networks you trust: Wi-Fi traffic isn't encrypted."
|
|
40
|
+
checked={data.lan.on}
|
|
41
|
+
locked={data.lan.locked || !canManage}
|
|
42
|
+
onSave={(lan) => update.mutateAsync({ lan })}
|
|
43
|
+
/>
|
|
44
|
+
<SwitchSetting
|
|
45
|
+
id="network-tailnet-pairing"
|
|
46
|
+
label="Require pairing on Tailscale too"
|
|
47
|
+
help="By default anything on your tailnet is trusted. Turn this on if others share your tailnet."
|
|
48
|
+
checked={data.requirePairingOnTailnet.on}
|
|
49
|
+
locked={data.requirePairingOnTailnet.locked || !canManage}
|
|
50
|
+
onSave={(requirePairingOnTailnet) =>
|
|
51
|
+
update.mutateAsync({ requirePairingOnTailnet })
|
|
52
|
+
}
|
|
53
|
+
/>
|
|
54
|
+
</SettingSection>
|
|
52
55
|
</div>
|
|
53
56
|
);
|
|
54
57
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { SettingRow } from "@/components/Settings/SettingRow";
|
|
4
|
+
import { Address } from "./ConnectCard";
|
|
4
5
|
import type { TailscaleState } from "@/lib/tailscale";
|
|
5
6
|
|
|
6
7
|
const DOWNLOAD = "https://tailscale.com/download";
|
|
@@ -12,8 +13,7 @@ export function TailscaleCard({ tailscale }: { tailscale: TailscaleState }) {
|
|
|
12
13
|
const url = `http://${tailscale.dnsName ?? tailscale.ips[0]}:${window.location.port || 3011}`;
|
|
13
14
|
body = (
|
|
14
15
|
<>
|
|
15
|
-
Reachable from anywhere on your tailnet at{
|
|
16
|
-
<span className="text-foreground font-mono break-all">{url}</span>.
|
|
16
|
+
Reachable from anywhere on your tailnet at <Address>{url}</Address>.
|
|
17
17
|
Install Tailscale on your phone and sign in to the same account to use
|
|
18
18
|
it there.
|
|
19
19
|
</>
|
|
@@ -52,12 +52,11 @@ export function TailscaleCard({ tailscale }: { tailscale: TailscaleState }) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
return (
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
</div>
|
|
55
|
+
<SettingRow
|
|
56
|
+
id="tailscale"
|
|
57
|
+
label="Use it away from home"
|
|
58
|
+
help={body}
|
|
59
|
+
control={null}
|
|
60
|
+
/>
|
|
62
61
|
);
|
|
63
62
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { ListedDevice } from "@/lib/security/devices";
|
|
3
|
+
import { groupDevices } from "./group";
|
|
4
|
+
|
|
5
|
+
const NOW = Date.parse("2026-11-02T12:00:00Z");
|
|
6
|
+
const dev = (id: string, name: string, daysAgo: number, ua = "ua") =>
|
|
7
|
+
({
|
|
8
|
+
id,
|
|
9
|
+
name,
|
|
10
|
+
user_agent: ua,
|
|
11
|
+
created_at: "2026-01-01 00:00:00",
|
|
12
|
+
last_seen_at: new Date(NOW - daysAgo * 86_400_000)
|
|
13
|
+
.toISOString()
|
|
14
|
+
.slice(0, 19)
|
|
15
|
+
.replace("T", " "),
|
|
16
|
+
}) as unknown as ListedDevice;
|
|
17
|
+
|
|
18
|
+
describe("groupDevices", () => {
|
|
19
|
+
it("folds identical devices into one group, newest first", () => {
|
|
20
|
+
const sims = Array.from({ length: 40 }, (_, i) =>
|
|
21
|
+
dev(`s${i}`, "Simulator", i / 100)
|
|
22
|
+
);
|
|
23
|
+
const { recent, older } = groupDevices(
|
|
24
|
+
[dev("me", "Laptop", 0, "mac"), ...sims],
|
|
25
|
+
"me",
|
|
26
|
+
NOW
|
|
27
|
+
);
|
|
28
|
+
expect(older).toHaveLength(0);
|
|
29
|
+
expect(recent).toHaveLength(2);
|
|
30
|
+
const group = recent.find((e) => e.kind === "group")!;
|
|
31
|
+
expect(group.kind === "group" && group.devices).toHaveLength(40);
|
|
32
|
+
expect(group.device.id).toBe("s0");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("keeps a pair of identical devices as two rows", () => {
|
|
36
|
+
const { recent } = groupDevices(
|
|
37
|
+
[dev("a", "Phone", 1), dev("b", "Phone", 2)],
|
|
38
|
+
null,
|
|
39
|
+
NOW
|
|
40
|
+
);
|
|
41
|
+
expect(recent.map((e) => e.kind)).toEqual(["device", "device"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("never folds or ages out the device in use", () => {
|
|
45
|
+
const list = [
|
|
46
|
+
dev("me", "Simulator", 90),
|
|
47
|
+
dev("a", "Simulator", 90),
|
|
48
|
+
dev("b", "Simulator", 90),
|
|
49
|
+
dev("c", "Simulator", 90),
|
|
50
|
+
];
|
|
51
|
+
const { recent, older } = groupDevices(list, "me", NOW);
|
|
52
|
+
expect(recent.map((e) => e.key)).toEqual(["me"]);
|
|
53
|
+
expect(older).toHaveLength(1);
|
|
54
|
+
expect(older[0].kind).toBe("group");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("sets devices unused for a month apart", () => {
|
|
58
|
+
const { recent, older } = groupDevices(
|
|
59
|
+
[dev("new", "Phone", 1), dev("old", "iPad", 45)],
|
|
60
|
+
null,
|
|
61
|
+
NOW
|
|
62
|
+
);
|
|
63
|
+
expect(recent.map((e) => e.key)).toEqual(["new"]);
|
|
64
|
+
expect(older.map((e) => e.key)).toEqual(["old"]);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ListedDevice } from "@/lib/security/devices";
|
|
2
|
+
|
|
3
|
+
export const STALE_DAYS = 30;
|
|
4
|
+
// Fewer identical devices than this stay separate rows.
|
|
5
|
+
export const GROUP_MIN = 3;
|
|
6
|
+
|
|
7
|
+
export type DeviceEntry =
|
|
8
|
+
| { kind: "device"; key: string; device: ListedDevice }
|
|
9
|
+
// Devices with the same name and browser, none of them the one in use: a
|
|
10
|
+
// simulator paired over and over, say. `device` is the most recently seen.
|
|
11
|
+
| {
|
|
12
|
+
kind: "group";
|
|
13
|
+
key: string;
|
|
14
|
+
device: ListedDevice;
|
|
15
|
+
devices: ListedDevice[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const parseUtc = (s: string) => new Date(`${s.replace(" ", "T")}Z`).getTime();
|
|
19
|
+
|
|
20
|
+
const seenAt = (d: ListedDevice) =>
|
|
21
|
+
d.last_seen_at ? parseUtc(d.last_seen_at) : parseUtc(d.created_at);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The list the Devices page shows: identical devices folded into one row,
|
|
25
|
+
* and the ones not used for a month set apart as `older`. The device in use
|
|
26
|
+
* is never folded or aged out.
|
|
27
|
+
*/
|
|
28
|
+
export function groupDevices(
|
|
29
|
+
devices: ListedDevice[],
|
|
30
|
+
currentId: string | null | undefined,
|
|
31
|
+
now = Date.now()
|
|
32
|
+
): { recent: DeviceEntry[]; older: DeviceEntry[] } {
|
|
33
|
+
const buckets = new Map<string, ListedDevice[]>();
|
|
34
|
+
const entries: DeviceEntry[] = [];
|
|
35
|
+
for (const d of devices) {
|
|
36
|
+
if (d.id === currentId) {
|
|
37
|
+
entries.push({ kind: "device", key: d.id, device: d });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const k = `${d.name}\u0000${d.user_agent ?? ""}`;
|
|
41
|
+
buckets.set(k, [...(buckets.get(k) ?? []), d]);
|
|
42
|
+
}
|
|
43
|
+
for (const [k, list] of buckets) {
|
|
44
|
+
const sorted = [...list].sort((a, b) => seenAt(b) - seenAt(a));
|
|
45
|
+
if (sorted.length >= GROUP_MIN)
|
|
46
|
+
entries.push({
|
|
47
|
+
kind: "group",
|
|
48
|
+
key: `group:${k}`,
|
|
49
|
+
device: sorted[0],
|
|
50
|
+
devices: sorted,
|
|
51
|
+
});
|
|
52
|
+
else
|
|
53
|
+
for (const d of sorted)
|
|
54
|
+
entries.push({ kind: "device", key: d.id, device: d });
|
|
55
|
+
}
|
|
56
|
+
entries.sort((a, b) => seenAt(b.device) - seenAt(a.device));
|
|
57
|
+
const cutoff = now - STALE_DAYS * 86_400_000;
|
|
58
|
+
const recent: DeviceEntry[] = [];
|
|
59
|
+
const older: DeviceEntry[] = [];
|
|
60
|
+
for (const e of entries) {
|
|
61
|
+
const keep =
|
|
62
|
+
(e.kind === "device" && e.device.id === currentId) ||
|
|
63
|
+
seenAt(e.device) >= cutoff;
|
|
64
|
+
(keep ? recent : older).push(e);
|
|
65
|
+
}
|
|
66
|
+
return { recent, older };
|
|
67
|
+
}
|
|
@@ -7,10 +7,8 @@ import {
|
|
|
7
7
|
useAddNote,
|
|
8
8
|
useDeleteNote,
|
|
9
9
|
useDismissAnomaly,
|
|
10
|
-
useHealthConfig,
|
|
11
10
|
useHealthNotes,
|
|
12
11
|
useHealthQuery,
|
|
13
|
-
useSaveConfig,
|
|
14
12
|
} from "@/data/health";
|
|
15
13
|
import { AREAS, AREA_LABELS, type Area } from "@/lib/health/types";
|
|
16
14
|
import {
|
|
@@ -20,7 +18,9 @@ import {
|
|
|
20
18
|
} from "@/lib/health/view";
|
|
21
19
|
import { cn } from "@/lib/utils";
|
|
22
20
|
import { EnvFailures } from "./EnvFailures";
|
|
21
|
+
import { MachineChecks } from "./MachineChecks";
|
|
23
22
|
import { SweepRuns } from "./SweepRuns";
|
|
23
|
+
import { Thresholds } from "./Thresholds";
|
|
24
24
|
|
|
25
25
|
const TABS = [
|
|
26
26
|
"open",
|
|
@@ -29,6 +29,7 @@ const TABS = [
|
|
|
29
29
|
"notes",
|
|
30
30
|
"thresholds",
|
|
31
31
|
"cleanup",
|
|
32
|
+
"machine",
|
|
32
33
|
] as const;
|
|
33
34
|
type Tab = (typeof TABS)[number];
|
|
34
35
|
const TAB_LABEL: Record<Tab, string> = {
|
|
@@ -38,6 +39,7 @@ const TAB_LABEL: Record<Tab, string> = {
|
|
|
38
39
|
notes: "Notes",
|
|
39
40
|
thresholds: "Thresholds",
|
|
40
41
|
cleanup: "Clean-up",
|
|
42
|
+
machine: "This machine",
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
const tokens = (n: number) =>
|
|
@@ -302,85 +304,6 @@ function Notes() {
|
|
|
302
304
|
);
|
|
303
305
|
}
|
|
304
306
|
|
|
305
|
-
function Thresholds() {
|
|
306
|
-
const { data } = useHealthConfig();
|
|
307
|
-
const save = useSaveConfig();
|
|
308
|
-
const [draft, setDraft] = useState<Record<string, string>>({});
|
|
309
|
-
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
310
|
-
if (!data) return null;
|
|
311
|
-
const drop = (k: string) => {
|
|
312
|
-
setDraft(({ [k]: _, ...rest }) => rest);
|
|
313
|
-
setErrors(({ [k]: _, ...rest }) => rest);
|
|
314
|
-
};
|
|
315
|
-
return (
|
|
316
|
-
<div className="space-y-3">
|
|
317
|
-
<p className="text-muted-foreground text-sm">
|
|
318
|
-
When a check trips. Clear a box to go back to the default.
|
|
319
|
-
</p>
|
|
320
|
-
<ul className="border-border/60 overflow-hidden rounded-xl border">
|
|
321
|
-
{Object.keys(data.defaults).map((k) => {
|
|
322
|
-
const value = draft[k] ?? String(data.config[k]);
|
|
323
|
-
return (
|
|
324
|
-
<li
|
|
325
|
-
key={k}
|
|
326
|
-
className="border-border/60 flex items-center gap-3 border-b px-3 py-2 last:border-b-0"
|
|
327
|
-
>
|
|
328
|
-
<label htmlFor={`health-${k}`} className="min-w-0 flex-1 text-sm">
|
|
329
|
-
{data.labels[k]}
|
|
330
|
-
<span className="text-muted-foreground block text-xs">
|
|
331
|
-
default {data.defaults[k]}
|
|
332
|
-
</span>
|
|
333
|
-
</label>
|
|
334
|
-
<span className="flex flex-col items-end gap-1">
|
|
335
|
-
<Input
|
|
336
|
-
id={`health-${k}`}
|
|
337
|
-
inputMode="decimal"
|
|
338
|
-
value={value}
|
|
339
|
-
aria-invalid={!!errors[k]}
|
|
340
|
-
onChange={(e) => {
|
|
341
|
-
setDraft({ ...draft, [k]: e.target.value });
|
|
342
|
-
setErrors(({ [k]: _, ...rest }) => rest);
|
|
343
|
-
}}
|
|
344
|
-
onBlur={() => {
|
|
345
|
-
if (draft[k] === undefined) return;
|
|
346
|
-
const raw = draft[k].trim();
|
|
347
|
-
const n = raw === "" ? null : Number(raw);
|
|
348
|
-
if (n !== null && (!Number.isFinite(n) || n < 0))
|
|
349
|
-
return setErrors({
|
|
350
|
-
...errors,
|
|
351
|
-
[k]: "Enter a number, 0 or more.",
|
|
352
|
-
});
|
|
353
|
-
if (n === data.config[k]) return drop(k);
|
|
354
|
-
save.mutate({ [k]: n }, { onSettled: () => drop(k) });
|
|
355
|
-
}}
|
|
356
|
-
onKeyDown={(e) => {
|
|
357
|
-
if (e.key === "Enter") e.currentTarget.blur();
|
|
358
|
-
if (e.key === "Escape") {
|
|
359
|
-
drop(k);
|
|
360
|
-
e.currentTarget.blur();
|
|
361
|
-
}
|
|
362
|
-
}}
|
|
363
|
-
className="h-11 w-24 text-right tabular-nums md:h-9"
|
|
364
|
-
/>
|
|
365
|
-
{errors[k] && (
|
|
366
|
-
<span role="alert" className="text-destructive text-xs">
|
|
367
|
-
{errors[k]}
|
|
368
|
-
</span>
|
|
369
|
-
)}
|
|
370
|
-
</span>
|
|
371
|
-
</li>
|
|
372
|
-
);
|
|
373
|
-
})}
|
|
374
|
-
</ul>
|
|
375
|
-
{save.error && (
|
|
376
|
-
<p role="alert" className="text-destructive text-sm">
|
|
377
|
-
{save.error.message}
|
|
378
|
-
</p>
|
|
379
|
-
)}
|
|
380
|
-
</div>
|
|
381
|
-
);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
307
|
export function HealthPanel() {
|
|
385
308
|
const { data, error } = useHealthQuery();
|
|
386
309
|
const dismiss = useDismissAnomaly();
|
|
@@ -445,6 +368,7 @@ export function HealthPanel() {
|
|
|
445
368
|
{tab === "notes" && <Notes />}
|
|
446
369
|
{tab === "thresholds" && <Thresholds />}
|
|
447
370
|
{tab === "cleanup" && <SweepRuns />}
|
|
371
|
+
{tab === "machine" && <MachineChecks />}
|
|
448
372
|
</div>
|
|
449
373
|
);
|
|
450
374
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { HealthList } from "@/components/Welcome/HealthList";
|
|
4
|
+
import { AgentList } from "@/components/Welcome/AgentList";
|
|
5
|
+
|
|
6
|
+
// What this machine has for running AgentOS and agents.
|
|
7
|
+
export function MachineChecks() {
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex flex-col gap-5">
|
|
10
|
+
<div className="flex flex-col gap-2">
|
|
11
|
+
<h3 className="text-sm font-semibold">Checks</h3>
|
|
12
|
+
<HealthList />
|
|
13
|
+
</div>
|
|
14
|
+
<div className="flex flex-col gap-2">
|
|
15
|
+
<h3 className="text-sm font-semibold">Agents on this machine</h3>
|
|
16
|
+
<AgentList />
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { ListSection } from "@/components/Settings/ListSection";
|
|
5
|
+
import { SettingRow, SettingSection } from "@/components/Settings/SettingRow";
|
|
6
|
+
import { SwitchSetting } from "@/components/Settings/settings-fields";
|
|
4
7
|
import {
|
|
5
8
|
useRunSweep,
|
|
6
9
|
useSetReaperArmed,
|
|
@@ -49,7 +52,7 @@ function Run({ run }: { run: SweepRun }) {
|
|
|
49
52
|
const skipped = run.details?.skipped ?? [];
|
|
50
53
|
const removed = run.details?.removed ?? [];
|
|
51
54
|
return (
|
|
52
|
-
<
|
|
55
|
+
<div className="px-3 py-1">
|
|
53
56
|
<details>
|
|
54
57
|
<summary className="flex min-h-11 cursor-pointer flex-wrap items-center gap-x-3 gap-y-1 text-sm">
|
|
55
58
|
<span className="font-medium">
|
|
@@ -111,7 +114,7 @@ function Run({ run }: { run: SweepRun }) {
|
|
|
111
114
|
</div>
|
|
112
115
|
)}
|
|
113
116
|
</details>
|
|
114
|
-
</
|
|
117
|
+
</div>
|
|
115
118
|
);
|
|
116
119
|
}
|
|
117
120
|
|
|
@@ -132,45 +135,40 @@ export function SweepRuns() {
|
|
|
132
135
|
</p>
|
|
133
136
|
)}
|
|
134
137
|
{data && (
|
|
135
|
-
<
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
138
|
+
<SettingSection>
|
|
139
|
+
<SwitchSetting
|
|
140
|
+
id="sweep-full"
|
|
141
|
+
label="Full clean-up"
|
|
142
|
+
help="Test leftovers, stale task databases, spares, temp folders and build caches are removed with each run. Off, they are only listed: read a dry run first. Settled worktrees go after the number of days set in Merging, every 6 hours; branches and conversations stay."
|
|
143
|
+
checked={data.armed}
|
|
144
|
+
defaultValue={false}
|
|
145
|
+
onSave={(on) => arm.mutateAsync(on)}
|
|
146
|
+
/>
|
|
147
|
+
<SettingRow
|
|
148
|
+
id="sweep-run"
|
|
149
|
+
label="Run a clean-up now"
|
|
150
|
+
control={
|
|
151
|
+
<>
|
|
152
|
+
<Button
|
|
153
|
+
variant="outline"
|
|
154
|
+
className="min-h-11 md:min-h-8"
|
|
155
|
+
disabled={run.isPending}
|
|
156
|
+
onClick={() => run.mutate(true)}
|
|
157
|
+
>
|
|
158
|
+
Dry run
|
|
159
|
+
</Button>
|
|
160
|
+
<Button
|
|
161
|
+
className="min-h-11 md:min-h-8"
|
|
162
|
+
disabled={run.isPending}
|
|
163
|
+
onClick={() => run.mutate(false)}
|
|
164
|
+
>
|
|
165
|
+
Run now
|
|
166
|
+
</Button>
|
|
167
|
+
</>
|
|
168
|
+
}
|
|
169
|
+
/>
|
|
170
|
+
</SettingSection>
|
|
152
171
|
)}
|
|
153
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
154
|
-
<p className="text-muted-foreground min-w-0 flex-1 text-sm">
|
|
155
|
-
Settled worktrees older than {data?.days ?? data?.defaultDays ?? 7}{" "}
|
|
156
|
-
days are removed every 6 hours. Branches and conversations stay.
|
|
157
|
-
</p>
|
|
158
|
-
<Button
|
|
159
|
-
variant="outline"
|
|
160
|
-
className="min-h-11 md:min-h-8"
|
|
161
|
-
disabled={run.isPending}
|
|
162
|
-
onClick={() => run.mutate(true)}
|
|
163
|
-
>
|
|
164
|
-
Dry run
|
|
165
|
-
</Button>
|
|
166
|
-
<Button
|
|
167
|
-
className="min-h-11 md:min-h-8"
|
|
168
|
-
disabled={run.isPending}
|
|
169
|
-
onClick={() => run.mutate(false)}
|
|
170
|
-
>
|
|
171
|
-
Run now
|
|
172
|
-
</Button>
|
|
173
|
-
</div>
|
|
174
172
|
{run.error && (
|
|
175
173
|
<p role="alert" className="text-destructive text-sm">
|
|
176
174
|
{run.error.message}
|
|
@@ -184,17 +182,14 @@ export function SweepRuns() {
|
|
|
184
182
|
{isPending && (
|
|
185
183
|
<div className="bg-muted/40 h-24 animate-pulse rounded-xl" />
|
|
186
184
|
)}
|
|
187
|
-
{data &&
|
|
188
|
-
<
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
<Run key={r.id} run={r} />
|
|
196
|
-
))}
|
|
197
|
-
</ul>
|
|
185
|
+
{data && (
|
|
186
|
+
<ListSection
|
|
187
|
+
title="Clean-up runs"
|
|
188
|
+
items={data.runs}
|
|
189
|
+
keyOf={(r) => String(r.id)}
|
|
190
|
+
render={(r) => <Run run={r} />}
|
|
191
|
+
empty="No run yet. The first one starts 10 minutes after the server does."
|
|
192
|
+
/>
|
|
198
193
|
)}
|
|
199
194
|
</div>
|
|
200
195
|
);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { SettingSection } from "@/components/Settings/SettingRow";
|
|
4
|
+
import {
|
|
5
|
+
NumberSetting,
|
|
6
|
+
SwitchSetting,
|
|
7
|
+
} from "@/components/Settings/settings-fields";
|
|
8
|
+
import { useHealthConfig, useSaveConfig } from "@/data/health";
|
|
9
|
+
|
|
10
|
+
// When each check trips. The server says how every one reads (label, unit,
|
|
11
|
+
// range, switch), so a new threshold shows up here without a client change.
|
|
12
|
+
export function Thresholds() {
|
|
13
|
+
const { data } = useHealthConfig();
|
|
14
|
+
const save = useSaveConfig();
|
|
15
|
+
if (!data) return null;
|
|
16
|
+
return (
|
|
17
|
+
<div className="space-y-3">
|
|
18
|
+
<SettingSection>
|
|
19
|
+
{Object.keys(data.defaults).map((k) => {
|
|
20
|
+
const meta = data.meta[k];
|
|
21
|
+
const put = (n: number) => save.mutateAsync({ [k]: n });
|
|
22
|
+
return meta.switch ? (
|
|
23
|
+
<SwitchSetting
|
|
24
|
+
key={k}
|
|
25
|
+
id={`health-${k}`}
|
|
26
|
+
label={meta.label}
|
|
27
|
+
help="Off, anomalies still open and reach the chat, but nothing wakes to look at them."
|
|
28
|
+
checked={data.config[k] !== 0}
|
|
29
|
+
defaultValue={data.defaults[k] !== 0}
|
|
30
|
+
onSave={(on) => put(on ? 1 : 0)}
|
|
31
|
+
/>
|
|
32
|
+
) : (
|
|
33
|
+
<NumberSetting
|
|
34
|
+
key={k}
|
|
35
|
+
id={`health-${k}`}
|
|
36
|
+
label={meta.label}
|
|
37
|
+
value={data.config[k]}
|
|
38
|
+
defaultValue={data.defaults[k]}
|
|
39
|
+
min={meta.min}
|
|
40
|
+
max={meta.max}
|
|
41
|
+
integer={meta.integer ?? true}
|
|
42
|
+
unit={meta.unit}
|
|
43
|
+
onSave={put}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
})}
|
|
47
|
+
</SettingSection>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
type MergeOverride,
|
|
10
10
|
type MergeSettings,
|
|
11
11
|
} from "@/lib/tasks/merge-methods";
|
|
12
|
-
import { MergeApprovalToggle } from "./MergeApprovalToggle";
|
|
13
12
|
import { MergeSettingsFields } from "./MergeSettingsFields";
|
|
14
13
|
import { GlobalSweepSection } from "./WorktreeSweepSection";
|
|
15
14
|
|
|
@@ -37,7 +36,7 @@ function Overrides({ overrides }: { overrides: MergeOverride[] }) {
|
|
|
37
36
|
<p className="truncate font-medium">{o.name}</p>
|
|
38
37
|
{Object.keys(o.config).length > 0 && (
|
|
39
38
|
<p className="text-muted-foreground text-xs">
|
|
40
|
-
|
|
39
|
+
Repo config: {describe(o.config)}
|
|
41
40
|
</p>
|
|
42
41
|
)}
|
|
43
42
|
{Object.keys(o.project).length > 0 && (
|
|
@@ -49,21 +48,21 @@ function Overrides({ overrides }: { overrides: MergeOverride[] }) {
|
|
|
49
48
|
))}
|
|
50
49
|
</ul>
|
|
51
50
|
<p className="text-muted-foreground text-xs">
|
|
52
|
-
A project’s own setting wins over its
|
|
53
|
-
|
|
51
|
+
A project’s own setting wins over its repo config, which wins over
|
|
52
|
+
these. Change a project’s in its Project Settings.
|
|
54
53
|
</p>
|
|
55
54
|
</div>
|
|
56
55
|
);
|
|
57
56
|
}
|
|
58
57
|
|
|
59
|
-
// The global merge settings
|
|
58
|
+
// The global merge settings and worktree clean-up. The approval gate for
|
|
59
|
+
// sensitive PRs is in Approvals.
|
|
60
60
|
export function GlobalMergeSection() {
|
|
61
61
|
const { data, isPending, isError, error } = useGlobalMergeSettings(true);
|
|
62
62
|
const update = useUpdateGlobalMergeSettings();
|
|
63
63
|
|
|
64
64
|
return (
|
|
65
65
|
<div className="space-y-5">
|
|
66
|
-
<MergeApprovalToggle />
|
|
67
66
|
<div className="space-y-3">
|
|
68
67
|
<p className="text-muted-foreground text-sm">
|
|
69
68
|
How tasks’ PRs merge and what’s cleaned up after, for
|