@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,10 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useContext, useState } from "react";
|
|
4
|
+
import { Pencil, Plus, Trash2 } from "lucide-react";
|
|
4
5
|
import { toast } from "sonner";
|
|
5
6
|
import { Button } from "@/components/ui/button";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
7
|
+
import { StatusPill } from "@/components/ui/status-pill";
|
|
8
|
+
import {
|
|
9
|
+
matchesQuery,
|
|
10
|
+
SettingBlock,
|
|
11
|
+
SettingSection,
|
|
12
|
+
SettingsQueryContext,
|
|
13
|
+
} from "@/components/Settings/SettingRow";
|
|
8
14
|
import type { RuleRow } from "@/lib/orchestrator/rules";
|
|
9
15
|
import {
|
|
10
16
|
useAddRule,
|
|
@@ -12,83 +18,148 @@ import {
|
|
|
12
18
|
useRulesQuery,
|
|
13
19
|
useUpdateRule,
|
|
14
20
|
} from "@/data/orchestrators/rules";
|
|
21
|
+
import { RuleEditor } from "./RuleEditor";
|
|
15
22
|
|
|
16
|
-
const
|
|
23
|
+
const ROW = "border-foreground/5 border-t px-3 py-2 first:border-t-0";
|
|
17
24
|
|
|
18
25
|
// The orchestrator's standing rules for one workspace: what it keeps
|
|
19
26
|
// knowing across fresh starts. It adds them itself too; Saad edits here.
|
|
20
|
-
// Every
|
|
21
|
-
//
|
|
22
|
-
// the
|
|
27
|
+
// Every add, edit and remove takes his passkey (data/orchestrators/rules), so
|
|
28
|
+
// nothing saves until Save, never on blur; a cancelled or refused passkey
|
|
29
|
+
// leaves the editor open with what was typed.
|
|
23
30
|
export function RulesForm({ workspaceId }: { workspaceId: string }) {
|
|
24
31
|
const { data: rules, isPending, isError, error } = useRulesQuery(workspaceId);
|
|
25
32
|
const add = useAddRule(workspaceId);
|
|
33
|
+
const [adding, setAdding] = useState(false);
|
|
34
|
+
const query = useContext(SettingsQueryContext);
|
|
26
35
|
|
|
27
36
|
return (
|
|
28
|
-
<
|
|
29
|
-
|
|
37
|
+
<SettingSection
|
|
38
|
+
title="Rules"
|
|
39
|
+
help="What the orchestrator keeps across fresh starts, like a PR never to merge. Each change asks for your passkey."
|
|
40
|
+
>
|
|
30
41
|
{isPending ? (
|
|
31
|
-
<div className="bg-muted/60 h-11 animate-pulse rounded-md" />
|
|
42
|
+
<div className="bg-muted/60 m-3 h-11 animate-pulse rounded-md" />
|
|
32
43
|
) : isError ? (
|
|
33
|
-
<p className="text-destructive text-
|
|
44
|
+
<p className="text-destructive px-3 py-2 text-sm">{error.message}</p>
|
|
34
45
|
) : (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
rules
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
>
|
|
46
|
+
<>
|
|
47
|
+
{rules.length === 0 && !adding && (
|
|
48
|
+
<SettingBlock id={`rules-none-${workspaceId}`} label="Rules">
|
|
49
|
+
<p className="text-muted-foreground text-xs">None yet.</p>
|
|
50
|
+
</SettingBlock>
|
|
51
|
+
)}
|
|
42
52
|
{rules.map((r) => (
|
|
43
53
|
<RuleItem key={`${r.id}:${r.text}`} rule={r} />
|
|
44
54
|
))}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
{matchesQuery(query, "add rule") && (
|
|
56
|
+
<div data-setting={`rule-new-${workspaceId}`} className={ROW}>
|
|
57
|
+
{adding ? (
|
|
58
|
+
<RuleEditor
|
|
59
|
+
id={`rule-new-${workspaceId}`}
|
|
60
|
+
label="New rule"
|
|
61
|
+
initial=""
|
|
62
|
+
saveLabel="Add rule"
|
|
63
|
+
placeholder="A PR never to merge, a URL, a house rule"
|
|
64
|
+
onSave={async (text) => {
|
|
65
|
+
try {
|
|
66
|
+
await add.mutateAsync(text);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
toast.error(
|
|
69
|
+
err instanceof Error ? err.message : "Couldn't add"
|
|
70
|
+
);
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
setAdding(false);
|
|
74
|
+
}}
|
|
75
|
+
onCancel={() => setAdding(false)}
|
|
76
|
+
/>
|
|
77
|
+
) : (
|
|
78
|
+
<Button
|
|
79
|
+
variant="ghost"
|
|
80
|
+
size="sm"
|
|
81
|
+
className="text-primary -ml-2 h-11 sm:h-9"
|
|
82
|
+
onClick={() => setAdding(true)}
|
|
83
|
+
>
|
|
84
|
+
<Plus className="size-4" />
|
|
85
|
+
Add rule
|
|
86
|
+
</Button>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
</>
|
|
57
91
|
)}
|
|
58
|
-
</
|
|
92
|
+
</SettingSection>
|
|
59
93
|
);
|
|
60
94
|
}
|
|
61
95
|
|
|
62
96
|
function RuleItem({ rule }: { rule: RuleRow }) {
|
|
63
97
|
const update = useUpdateRule(rule.workspace_id);
|
|
64
98
|
const remove = useRemoveRule(rule.workspace_id);
|
|
99
|
+
const [editing, setEditing] = useState(false);
|
|
100
|
+
const query = useContext(SettingsQueryContext);
|
|
101
|
+
if (!matchesQuery(query, rule.text)) return null;
|
|
102
|
+
|
|
65
103
|
return (
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
104
|
+
<div data-setting={`rule-${rule.id}`} className={ROW}>
|
|
105
|
+
{editing ? (
|
|
106
|
+
<RuleEditor
|
|
107
|
+
id={`rule-${rule.id}`}
|
|
108
|
+
label="Edit rule"
|
|
109
|
+
initial={rule.text}
|
|
110
|
+
onSave={async (text) => {
|
|
111
|
+
try {
|
|
112
|
+
await update.mutateAsync({ id: rule.id, text });
|
|
113
|
+
} catch (err) {
|
|
114
|
+
toast.error(err instanceof Error ? err.message : "Couldn't save");
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
setEditing(false);
|
|
118
|
+
}}
|
|
119
|
+
onCancel={() => setEditing(false)}
|
|
120
|
+
/>
|
|
121
|
+
) : (
|
|
122
|
+
<div className="flex items-start gap-1">
|
|
123
|
+
<button
|
|
124
|
+
type="button"
|
|
125
|
+
onClick={() => setEditing(true)}
|
|
126
|
+
className="hover:bg-muted/60 -my-1 min-h-11 min-w-0 flex-1 cursor-pointer rounded-md px-2 py-1 text-left"
|
|
127
|
+
>
|
|
128
|
+
<StatusPill
|
|
129
|
+
tone={rule.added_by === "saad" ? "primary" : "muted"}
|
|
130
|
+
className="mb-1"
|
|
131
|
+
>
|
|
132
|
+
{rule.added_by === "saad" ? "You" : "Orchestrator"}
|
|
133
|
+
</StatusPill>
|
|
134
|
+
<span className="block text-sm break-words whitespace-pre-wrap">
|
|
135
|
+
{rule.text}
|
|
136
|
+
</span>
|
|
137
|
+
</button>
|
|
138
|
+
<Button
|
|
139
|
+
variant="ghost"
|
|
140
|
+
size="icon"
|
|
141
|
+
className="size-11 shrink-0 sm:size-8"
|
|
142
|
+
onClick={() => setEditing(true)}
|
|
143
|
+
aria-label={`Edit rule: ${rule.text}`}
|
|
144
|
+
>
|
|
145
|
+
<Pencil className="size-4" />
|
|
146
|
+
</Button>
|
|
147
|
+
<Button
|
|
148
|
+
variant="ghost"
|
|
149
|
+
size="icon"
|
|
150
|
+
className="size-11 shrink-0 sm:size-8"
|
|
151
|
+
disabled={remove.isPending}
|
|
152
|
+
onClick={() =>
|
|
153
|
+
remove.mutate(rule.id, {
|
|
154
|
+
onError: (err) => toast.error(err.message),
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
aria-label={`Remove rule: ${rule.text}`}
|
|
158
|
+
>
|
|
159
|
+
<Trash2 className="size-4" />
|
|
160
|
+
</Button>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
93
164
|
);
|
|
94
165
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { SettingBlock } from "@/components/Settings/SettingRow";
|
|
3
4
|
import { ModeChooser } from "@/components/Report/ModeChooser";
|
|
4
5
|
import { useSetWorkspaceMode, useWorkspaceMode } from "@/data/reports";
|
|
5
6
|
|
|
@@ -9,14 +10,13 @@ export function WorkspaceModeForm({ workspaceId }: { workspaceId: string }) {
|
|
|
9
10
|
const set = useSetWorkspaceMode(workspaceId);
|
|
10
11
|
if (!mode) return null;
|
|
11
12
|
return (
|
|
12
|
-
<
|
|
13
|
-
<h4 className="text-sm font-medium">Operating mode</h4>
|
|
13
|
+
<SettingBlock id={`mode-${workspaceId}`} label="Operating mode">
|
|
14
14
|
<ModeChooser
|
|
15
15
|
mode={mode}
|
|
16
16
|
onChange={(m) => set.mutate(m)}
|
|
17
17
|
disabled={set.isPending}
|
|
18
18
|
note="New tasks start in this mode; any task can change its own."
|
|
19
19
|
/>
|
|
20
|
-
</
|
|
20
|
+
</SettingBlock>
|
|
21
21
|
);
|
|
22
22
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { PickerSetting } from "@/components/Settings/PickerSetting";
|
|
5
|
-
import {
|
|
5
|
+
import { SettingBlock } from "@/components/Settings/SettingRow";
|
|
6
6
|
import { ModelSelect } from "@/components/Models/ModelSelect";
|
|
7
7
|
import {
|
|
8
8
|
useModelsQuery,
|
|
@@ -28,45 +28,44 @@ export function WorkspaceModelsForm({ workspaceId }: { workspaceId: string }) {
|
|
|
28
28
|
const set = Object.keys(roles).length;
|
|
29
29
|
|
|
30
30
|
return (
|
|
31
|
-
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
</div>
|
|
31
|
+
<>
|
|
32
|
+
<SettingBlock id={`models-${workspaceId}`} label="Models">
|
|
33
|
+
<button
|
|
34
|
+
type="button"
|
|
35
|
+
className="text-muted-foreground hover:text-foreground flex min-h-11 w-full items-center text-left text-xs md:min-h-0"
|
|
36
|
+
onClick={() => setOpen((o) => !o)}
|
|
37
|
+
aria-expanded={open}
|
|
38
|
+
>
|
|
39
|
+
{set ? `${set} set for this workspace` : "As Settings › Models"}{" "}
|
|
40
|
+
{open ? "▴" : "▾"}
|
|
41
|
+
</button>
|
|
42
|
+
</SettingBlock>
|
|
43
|
+
{open &&
|
|
44
|
+
WORKSPACE_ROLES.map((role) => (
|
|
45
|
+
<PickerSetting<string | null>
|
|
46
|
+
key={role}
|
|
47
|
+
id={`ws-${workspaceId}-${role}`}
|
|
48
|
+
label={ROLE_LABELS[role].label}
|
|
49
|
+
value={roles[role] ?? null}
|
|
50
|
+
defaultValue={null}
|
|
51
|
+
onSave={(ref) => {
|
|
52
|
+
const next = { ...roles };
|
|
53
|
+
if (ref) next[role] = ref;
|
|
54
|
+
else delete next[role];
|
|
55
|
+
return update.mutateAsync({ roles: next });
|
|
56
|
+
}}
|
|
57
|
+
render={({ value, commit }) => (
|
|
58
|
+
<ModelSelect
|
|
59
|
+
id={`ws-${workspaceId}-${role}`}
|
|
60
|
+
value={value}
|
|
61
|
+
effective={models.data?.effective}
|
|
62
|
+
inheritLabel={`As Settings (${models.data?.effective.roles[role] ?? "…"})`}
|
|
63
|
+
disabled={!models.data}
|
|
64
|
+
onChange={commit}
|
|
65
|
+
/>
|
|
66
|
+
)}
|
|
67
|
+
/>
|
|
68
|
+
))}
|
|
69
|
+
</>
|
|
71
70
|
);
|
|
72
71
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useUpdateWorkspace } from "@/data/workspaces";
|
|
4
|
+
import type { Workspace } from "@/lib/db";
|
|
5
|
+
import {
|
|
6
|
+
NumberSetting,
|
|
7
|
+
SwitchSetting,
|
|
8
|
+
} from "@/components/Settings/settings-fields";
|
|
9
|
+
import { SettingSection } from "@/components/Settings/SettingRow";
|
|
10
|
+
import { SETTING_DEFAULTS } from "@/lib/settings/defaults";
|
|
11
|
+
import { HqFolderForm } from "./HqFolderForm";
|
|
12
|
+
import { McpServersForm } from "./McpServersForm";
|
|
13
|
+
import { ProjectTiersForm } from "./ProjectTiersForm";
|
|
14
|
+
import { RulesForm } from "./RulesForm";
|
|
15
|
+
import { WorkspaceModeForm } from "./WorkspaceModeForm";
|
|
16
|
+
import { WorkspaceModelsForm } from "./WorkspaceModelsForm";
|
|
17
|
+
|
|
18
|
+
// One workspace's settings, in titled groups.
|
|
19
|
+
export function WorkspacePanel({ workspace: w }: { workspace: Workspace }) {
|
|
20
|
+
const update = useUpdateWorkspace();
|
|
21
|
+
return (
|
|
22
|
+
<div aria-label={`${w.name} settings`} role="group">
|
|
23
|
+
<SettingSection
|
|
24
|
+
title="Limits & mode"
|
|
25
|
+
help="Tasks over the limit wait in Queued and start by themselves, in order, when a running one finishes."
|
|
26
|
+
>
|
|
27
|
+
<NumberSetting
|
|
28
|
+
id={`task-limit-${w.id}`}
|
|
29
|
+
label="Tasks at a time"
|
|
30
|
+
value={w.max_running_tasks ?? 0}
|
|
31
|
+
defaultValue={0}
|
|
32
|
+
onSave={(n) =>
|
|
33
|
+
update.mutateAsync({
|
|
34
|
+
id: w.id,
|
|
35
|
+
maxRunningTasks: n === 0 ? null : n,
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
min={1}
|
|
39
|
+
integer
|
|
40
|
+
unit="at a time"
|
|
41
|
+
optional={{ off: 0, on: SETTING_DEFAULTS.taskLimitOn }}
|
|
42
|
+
/>
|
|
43
|
+
<WorkspaceModeForm workspaceId={w.id} />
|
|
44
|
+
</SettingSection>
|
|
45
|
+
<SettingSection
|
|
46
|
+
title="Orchestrator"
|
|
47
|
+
help="What this workspace's orchestrator may do, which models it runs on, and where its session logs go."
|
|
48
|
+
>
|
|
49
|
+
<SwitchSetting
|
|
50
|
+
id={`full-access-${w.id}`}
|
|
51
|
+
label="Full access"
|
|
52
|
+
help="Its shell and file writes aren't checked. Applies when its conversation next restarts."
|
|
53
|
+
checked={w.orch_full_access}
|
|
54
|
+
confirm="Let this orchestrator act without approval? It can push, commit, edit any repository or file on this machine, merge a PR directly, and edit .claude files. Off: the deny list, write limits and merge-only-through-the-gates apply."
|
|
55
|
+
confirmLabel="Turn on full access"
|
|
56
|
+
onSave={(on) => update.mutateAsync({ id: w.id, orchFullAccess: on })}
|
|
57
|
+
/>
|
|
58
|
+
<SwitchSetting
|
|
59
|
+
id={`wake-${w.id}`}
|
|
60
|
+
label="Wake tasks on CI failures and reviews"
|
|
61
|
+
help="A task's agent is messaged once when a check fails on its PR, and when someone reviews or comments."
|
|
62
|
+
checked={w.wake_tasks_on_pr}
|
|
63
|
+
onSave={(on) => update.mutateAsync({ id: w.id, wakeTasksOnPR: on })}
|
|
64
|
+
/>
|
|
65
|
+
<WorkspaceModelsForm workspaceId={w.id} />
|
|
66
|
+
<HqFolderForm workspaceId={w.id} />
|
|
67
|
+
</SettingSection>
|
|
68
|
+
<RulesForm workspaceId={w.id} />
|
|
69
|
+
<McpServersForm workspace={w} />
|
|
70
|
+
<ProjectTiersForm workspaceId={w.id} />
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Select,
|
|
5
|
+
SelectContent,
|
|
6
|
+
SelectItem,
|
|
7
|
+
SelectTrigger,
|
|
8
|
+
SelectValue,
|
|
9
|
+
} from "@/components/ui/select";
|
|
10
|
+
import type { Workspace } from "@/lib/db";
|
|
11
|
+
|
|
12
|
+
// Which workspace's settings the page shows.
|
|
13
|
+
export function WorkspacePicker({
|
|
14
|
+
workspaces,
|
|
15
|
+
value,
|
|
16
|
+
onChange,
|
|
17
|
+
}: {
|
|
18
|
+
workspaces: Workspace[];
|
|
19
|
+
value: string;
|
|
20
|
+
onChange: (id: string) => void;
|
|
21
|
+
}) {
|
|
22
|
+
return (
|
|
23
|
+
<div className="mb-4 flex items-center gap-3">
|
|
24
|
+
<label htmlFor="settings-workspace" className="text-sm font-medium">
|
|
25
|
+
Workspace
|
|
26
|
+
</label>
|
|
27
|
+
<Select value={value} onValueChange={onChange}>
|
|
28
|
+
<SelectTrigger
|
|
29
|
+
id="settings-workspace"
|
|
30
|
+
className="h-11 min-w-0 flex-1 text-base sm:max-w-xs md:h-9 md:text-sm"
|
|
31
|
+
>
|
|
32
|
+
<SelectValue />
|
|
33
|
+
</SelectTrigger>
|
|
34
|
+
<SelectContent>
|
|
35
|
+
{workspaces.map((w) => (
|
|
36
|
+
<SelectItem key={w.id} value={w.id}>
|
|
37
|
+
{w.name}
|
|
38
|
+
</SelectItem>
|
|
39
|
+
))}
|
|
40
|
+
</SelectContent>
|
|
41
|
+
</Select>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import "@/components/Chat/composer/test-dom";
|
|
2
|
+
import { act, createElement } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
for (const key of Object.getOwnPropertyNames(window))
|
|
7
|
+
if (
|
|
8
|
+
/^(HTML|SVG|Event|CustomEvent|FocusEvent|DocumentFragment|Node|Element|Mutation)/.test(
|
|
9
|
+
key
|
|
10
|
+
) &&
|
|
11
|
+
!(key in globalThis)
|
|
12
|
+
)
|
|
13
|
+
Object.defineProperty(globalThis, key, {
|
|
14
|
+
value: (window as unknown as Record<string, unknown>)[key],
|
|
15
|
+
configurable: true,
|
|
16
|
+
writable: true,
|
|
17
|
+
});
|
|
18
|
+
(
|
|
19
|
+
globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
|
20
|
+
).IS_REACT_ACT_ENVIRONMENT = true;
|
|
21
|
+
|
|
22
|
+
const ws = (id: string, name: string, limit: number | null) => ({
|
|
23
|
+
id,
|
|
24
|
+
name,
|
|
25
|
+
max_running_tasks: limit,
|
|
26
|
+
orch_full_access: false,
|
|
27
|
+
wake_tasks_on_pr: true,
|
|
28
|
+
orch_mcp_servers: [],
|
|
29
|
+
});
|
|
30
|
+
const state = vi.hoisted(() => ({
|
|
31
|
+
workspaces: [] as unknown[],
|
|
32
|
+
projects: [] as unknown[],
|
|
33
|
+
sidebar: null as unknown,
|
|
34
|
+
update: vi.fn(),
|
|
35
|
+
}));
|
|
36
|
+
vi.mock("sonner", () => ({ toast: { error: vi.fn() } }));
|
|
37
|
+
vi.mock("@/data/workspaces", () => ({
|
|
38
|
+
useWorkspacesQuery: () => ({ isPending: false, data: state.workspaces }),
|
|
39
|
+
useUpdateWorkspace: () => ({ mutateAsync: state.update }),
|
|
40
|
+
}));
|
|
41
|
+
vi.mock("@/data/projects", () => ({
|
|
42
|
+
useProjectsQuery: () => ({ data: state.projects }),
|
|
43
|
+
}));
|
|
44
|
+
vi.mock("@/hooks/useSelectedWorkspace", () => ({
|
|
45
|
+
useSelectedWorkspace: () => ({ workspace: state.sidebar }),
|
|
46
|
+
}));
|
|
47
|
+
vi.mock("@/components/Settings/AppWideLimits", () => ({
|
|
48
|
+
AppWideLimits: () => null,
|
|
49
|
+
}));
|
|
50
|
+
for (const name of [
|
|
51
|
+
"RulesForm",
|
|
52
|
+
"McpServersForm",
|
|
53
|
+
"HqFolderForm",
|
|
54
|
+
"ProjectTiersForm",
|
|
55
|
+
"WorkspaceModeForm",
|
|
56
|
+
"WorkspaceModelsForm",
|
|
57
|
+
])
|
|
58
|
+
vi.doMock(`./${name}`, () => ({
|
|
59
|
+
[name]: () => null,
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
const { WorkspaceSettings } = await import("./WorkspaceSettings");
|
|
63
|
+
const { SettingsQueryContext } =
|
|
64
|
+
await import("@/components/Settings/SettingRow");
|
|
65
|
+
|
|
66
|
+
let root: Root;
|
|
67
|
+
let host: HTMLDivElement;
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
localStorage.clear();
|
|
70
|
+
state.update.mockReset().mockResolvedValue(undefined);
|
|
71
|
+
state.workspaces = [ws("w1", "Alpha", null), ws("w2", "Beta", 2)];
|
|
72
|
+
state.projects = [
|
|
73
|
+
{ id: "p1", name: "beta-api", workspace_id: "w2", is_uncategorized: false },
|
|
74
|
+
];
|
|
75
|
+
state.sidebar = null;
|
|
76
|
+
host = document.createElement("div");
|
|
77
|
+
document.body.appendChild(host);
|
|
78
|
+
root = createRoot(host);
|
|
79
|
+
});
|
|
80
|
+
afterEach(async () => {
|
|
81
|
+
await act(async () => root.unmount());
|
|
82
|
+
host.remove();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const render = (query = "") =>
|
|
86
|
+
act(async () =>
|
|
87
|
+
root.render(
|
|
88
|
+
createElement(
|
|
89
|
+
SettingsQueryContext.Provider,
|
|
90
|
+
{ value: query },
|
|
91
|
+
createElement(WorkspaceSettings)
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
);
|
|
95
|
+
const picker = () => host.querySelector<HTMLElement>("#settings-workspace")!;
|
|
96
|
+
const titles = () => [...host.querySelectorAll("h3")].map((h) => h.textContent);
|
|
97
|
+
const limit = () =>
|
|
98
|
+
host.querySelector<HTMLInputElement>("#task-limit-w1, #task-limit-w2")!;
|
|
99
|
+
|
|
100
|
+
describe("WorkspaceSettings", () => {
|
|
101
|
+
it("shows one workspace at a time, in titled groups", async () => {
|
|
102
|
+
await render();
|
|
103
|
+
expect(host.querySelectorAll('[role="group"]')).toHaveLength(1);
|
|
104
|
+
expect(picker().textContent).toBe("Alpha");
|
|
105
|
+
expect(titles()).toEqual(
|
|
106
|
+
expect.arrayContaining(["Limits & mode", "Orchestrator"])
|
|
107
|
+
);
|
|
108
|
+
expect(host.textContent).not.toContain("Running task limit:");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("preselects the sidebar's workspace", async () => {
|
|
112
|
+
state.sidebar = state.workspaces[1];
|
|
113
|
+
await render();
|
|
114
|
+
expect(picker().textContent).toBe("Beta");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("remembers the choice", async () => {
|
|
118
|
+
localStorage.setItem("agentOS-settings-workspace", "w2");
|
|
119
|
+
await render();
|
|
120
|
+
expect(picker().textContent).toBe("Beta");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("greys the number while the limit is off, keeping its value", async () => {
|
|
124
|
+
await render();
|
|
125
|
+
expect(limit().id).toBe("task-limit-w1");
|
|
126
|
+
expect(limit().disabled).toBe(true);
|
|
127
|
+
expect(limit().value).toBe("");
|
|
128
|
+
expect(limit().placeholder).toBe("Off");
|
|
129
|
+
expect(host.textContent).not.toContain("Reset");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("enables the number when a limit is set, and offers Reset to off", async () => {
|
|
133
|
+
localStorage.setItem("agentOS-settings-workspace", "w2");
|
|
134
|
+
await render();
|
|
135
|
+
expect(limit().disabled).toBe(false);
|
|
136
|
+
expect(limit().value).toBe("2");
|
|
137
|
+
expect(
|
|
138
|
+
[...host.querySelectorAll("button")].some(
|
|
139
|
+
(b) => b.textContent === "Reset to off"
|
|
140
|
+
)
|
|
141
|
+
).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("a search matching only another workspace's project switches to it", async () => {
|
|
145
|
+
await render("beta-api");
|
|
146
|
+
expect(picker().textContent).toBe("Beta");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("a search matching rows on every page stays on the current workspace", async () => {
|
|
150
|
+
await render("mcp");
|
|
151
|
+
expect(picker().textContent).toBe("Alpha");
|
|
152
|
+
});
|
|
153
|
+
});
|