@saadnvd1/agent-os 0.3.833 → 0.3.839
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/fresh-start/route.test.ts +33 -0
- package/app/api/fresh-start/route.ts +3 -0
- package/app/api/sessions/route.ts +7 -1
- package/app/globals.css +14 -0
- package/components/Chat/ChatPanel.tsx +41 -29
- package/components/Chat/Forks.tsx +16 -1
- package/components/Chat/Messages.tsx +14 -0
- package/components/Chat/WorktreeRemovedNotice.tsx +17 -0
- package/components/Chat/composer/ComposerBody.tsx +10 -1
- package/components/Chat/composer/ForkHeader.tsx +26 -15
- package/components/Chat/fork-draft.test.ts +57 -0
- package/components/Chat/fork-draft.ts +34 -0
- package/components/Devices/DeviceRow.tsx +83 -69
- package/components/Devices/PasskeysSection.test.ts +89 -0
- package/components/Devices/PasskeysSection.tsx +36 -26
- package/components/Health/HealthPanel.tsx +41 -16
- package/components/Merge/GlobalMergeSection.tsx +2 -5
- package/components/Merge/MergeSettingsFields.tsx +39 -77
- package/components/Merge/ProjectMergeSection.tsx +2 -6
- package/components/Merge/WorktreeSweepSection.tsx +40 -14
- package/components/Models/AgentRoleCard.tsx +61 -66
- package/components/Models/AgentRolesSection.tsx +5 -8
- package/components/Models/ModelsSection.tsx +7 -18
- package/components/Models/PriceInput.tsx +59 -0
- package/components/PhoneNotify/PhoneNotifyForm.tsx +37 -58
- package/components/Settings/AppWideLimits.tsx +61 -0
- package/components/Settings/ContextSection.tsx +96 -0
- package/components/Settings/ExperimentsSection.test.ts +9 -7
- package/components/Settings/ExperimentsSection.tsx +22 -23
- package/components/Settings/InlineConfirm.tsx +51 -0
- package/components/Settings/OwnerNameRow.tsx +10 -40
- package/components/Settings/PickerSetting.tsx +59 -0
- package/components/Settings/SettingRow.tsx +131 -0
- package/components/Settings/SettingsDialog.test.ts +1 -0
- package/components/Settings/SettingsDialog.tsx +47 -2
- package/components/Settings/settings-fields.test.ts +103 -0
- package/components/Settings/settings-fields.tsx +427 -0
- package/components/Shares/SharedLinksPanel.tsx +62 -53
- package/components/Workspaces/HqFolderForm.tsx +21 -54
- package/components/Workspaces/McpServersForm.tsx +20 -19
- package/components/Workspaces/ProjectTiersForm.tsx +10 -8
- package/components/Workspaces/RulesForm.test.ts +127 -0
- package/components/Workspaces/RulesForm.tsx +54 -126
- package/components/Workspaces/WorkspaceModelsForm.tsx +27 -26
- package/components/Workspaces/WorkspaceSettings.tsx +61 -24
- package/content/whats-new.json +32 -0
- package/data/fresh-start/index.ts +1 -1
- package/data/workspaces/index.ts +9 -0
- package/hooks/useAutoSave.test.ts +116 -0
- package/hooks/useAutoSave.ts +98 -0
- package/lib/away/window-limits.ts +3 -1
- package/lib/chat/refresh.ts +10 -6
- package/lib/chat/socket.ts +7 -3
- package/lib/chat/stall-limits.ts +3 -1
- package/lib/chat/start-error.ts +13 -0
- package/lib/chat/worker/cwd.ts +77 -0
- package/lib/chat/worker/host.ts +11 -2
- package/lib/chat/worker/main.ts +11 -2
- package/lib/db/types.ts +2 -0
- package/lib/fork/note.ts +6 -0
- package/lib/fork/start.ts +6 -2
- package/lib/fork/types.ts +2 -0
- package/lib/fork/view.ts +14 -1
- package/lib/settings/defaults.ts +27 -0
- package/lib/settings/parse.ts +17 -0
- package/lib/settings-url.ts +1 -0
- package/lib/worktree-sweep/limits.ts +3 -0
- package/lib/worktree-sweep/settings.ts +4 -2
- package/package.json +1 -1
- package/components/Away/AwayWindowForm.tsx +0 -91
- package/components/Workspaces/FreshStartForm.tsx +0 -177
- package/components/Workspaces/StallForm.tsx +0 -101
- package/components/Workspaces/TaskLimitForm.tsx +0 -84
- package/components/Workspaces/fresh-start.ts +0 -19
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useFreshStartQuery, useUpdateFreshStart } from "@/data/fresh-start";
|
|
4
|
+
import type { RefreshSettings } from "@/lib/chat/refresh";
|
|
5
|
+
import {
|
|
6
|
+
MAX_REFRESH_TOKENS,
|
|
7
|
+
MIN_REFRESH_TOKENS,
|
|
8
|
+
} from "@/lib/chat/refresh-limits";
|
|
9
|
+
import {
|
|
10
|
+
SETTING_DEFAULTS,
|
|
11
|
+
freshOnValue,
|
|
12
|
+
type FreshKey,
|
|
13
|
+
} from "@/lib/settings/defaults";
|
|
14
|
+
import { SettingSection } from "./SettingRow";
|
|
15
|
+
import { NumberSetting } from "./settings-fields";
|
|
16
|
+
|
|
17
|
+
type Row = {
|
|
18
|
+
key: keyof RefreshSettings & FreshKey;
|
|
19
|
+
label: string;
|
|
20
|
+
help: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const FRESH: Row[] = [
|
|
24
|
+
{
|
|
25
|
+
key: "orchestratorTokens",
|
|
26
|
+
label: "Orchestrators",
|
|
27
|
+
help: "Start fresh past this size, at the next turn. Its brief, rules and the workspace's state carry over.",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: "taskTokens",
|
|
31
|
+
label: "Tasks",
|
|
32
|
+
help: "A task keeps only its brief and its branch.",
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const COMPACT: Row[] = [
|
|
37
|
+
{
|
|
38
|
+
key: "autoCompactTokens",
|
|
39
|
+
label: "Mid-turn, tasks",
|
|
40
|
+
help: "A task's agent compacts itself while a job runs long. Keep it high: it is holding its files.",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
key: "interactiveAutoCompactTokens",
|
|
44
|
+
label: "Mid-turn, your sessions",
|
|
45
|
+
help: "Your own chats and terminals. A terminal only gets this one.",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: "compactTokens",
|
|
49
|
+
label: "Between turns",
|
|
50
|
+
help: "Compact chats and tasks before the next message. The composer offers first.",
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// When agents start fresh or compact: app-wide, each value saved on its own.
|
|
55
|
+
export function ContextSection() {
|
|
56
|
+
const { data, isPending, isError, error } = useFreshStartQuery();
|
|
57
|
+
const update = useUpdateFreshStart();
|
|
58
|
+
if (isPending)
|
|
59
|
+
return <div className="bg-muted/40 h-24 animate-pulse rounded-xl" />;
|
|
60
|
+
if (isError)
|
|
61
|
+
return <p className="text-destructive text-sm">{error.message}</p>;
|
|
62
|
+
|
|
63
|
+
const row = ({ key, label, help }: Row) => (
|
|
64
|
+
<NumberSetting
|
|
65
|
+
key={key}
|
|
66
|
+
id={`context-${key}`}
|
|
67
|
+
label={label}
|
|
68
|
+
help={help}
|
|
69
|
+
value={data[key]}
|
|
70
|
+
defaultValue={SETTING_DEFAULTS.fresh[key]}
|
|
71
|
+
onSave={(n) => update.mutateAsync({ [key]: n })}
|
|
72
|
+
min={MIN_REFRESH_TOKENS}
|
|
73
|
+
max={MAX_REFRESH_TOKENS}
|
|
74
|
+
scale={1000}
|
|
75
|
+
unit="k tokens"
|
|
76
|
+
short="k"
|
|
77
|
+
optional={{ off: 0, on: freshOnValue(key) }}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
return (
|
|
81
|
+
<div className="space-y-1">
|
|
82
|
+
<SettingSection
|
|
83
|
+
title="Fresh starts"
|
|
84
|
+
help="When a conversation passes the size, its agent starts a new one with its brief."
|
|
85
|
+
>
|
|
86
|
+
{FRESH.map(row)}
|
|
87
|
+
</SettingSection>
|
|
88
|
+
<SettingSection
|
|
89
|
+
title="Compact"
|
|
90
|
+
help="Mid-turn, Claude Code summarizes itself; between turns, AgentOS compacts before the next message. AgentOS hands the task, files and todos back."
|
|
91
|
+
>
|
|
92
|
+
{COMPACT.map(row)}
|
|
93
|
+
</SettingSection>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -29,7 +29,9 @@ const w = (calls: number) => ({
|
|
|
29
29
|
dismissed: 1,
|
|
30
30
|
acceptRate: 0.5,
|
|
31
31
|
});
|
|
32
|
-
const state = vi.hoisted(() => ({
|
|
32
|
+
const state = vi.hoisted(() => ({
|
|
33
|
+
mutateAsync: vi.fn().mockResolvedValue({}),
|
|
34
|
+
}));
|
|
33
35
|
vi.mock("@/data/experiments", () => ({
|
|
34
36
|
useExperimentsQuery: () => ({
|
|
35
37
|
isPending: false,
|
|
@@ -50,7 +52,7 @@ vi.mock("@/data/experiments", () => ({
|
|
|
50
52
|
],
|
|
51
53
|
},
|
|
52
54
|
}),
|
|
53
|
-
useSetExperiment: () => ({
|
|
55
|
+
useSetExperiment: () => ({ mutateAsync: state.mutateAsync }),
|
|
54
56
|
useExperimentDetailQuery: () => ({ isPending: true }),
|
|
55
57
|
}));
|
|
56
58
|
|
|
@@ -59,7 +61,7 @@ const { ExperimentsSection } = await import("./ExperimentsSection");
|
|
|
59
61
|
let root: Root;
|
|
60
62
|
let host: HTMLDivElement;
|
|
61
63
|
beforeEach(() => {
|
|
62
|
-
state.
|
|
64
|
+
state.mutateAsync.mockClear();
|
|
63
65
|
host = document.createElement("div");
|
|
64
66
|
document.body.appendChild(host);
|
|
65
67
|
root = createRoot(host);
|
|
@@ -87,9 +89,9 @@ describe("ExperimentsSection", () => {
|
|
|
87
89
|
.querySelector('[role="switch"]')
|
|
88
90
|
?.dispatchEvent(new window.MouseEvent("click", { bubbles: true }));
|
|
89
91
|
});
|
|
90
|
-
expect(state.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
);
|
|
92
|
+
expect(state.mutateAsync).toHaveBeenCalledWith({
|
|
93
|
+
id: "learning-cards",
|
|
94
|
+
on: true,
|
|
95
|
+
});
|
|
94
96
|
});
|
|
95
97
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState } from "react";
|
|
4
|
-
import { toast } from "sonner";
|
|
5
4
|
import { ArrowLeft } from "lucide-react";
|
|
6
5
|
import { Button } from "@/components/ui/button";
|
|
7
|
-
import {
|
|
6
|
+
import { SettingSection } from "@/components/Settings/SettingRow";
|
|
7
|
+
import { SwitchSetting } from "@/components/Settings/settings-fields";
|
|
8
8
|
import { formatTokens } from "@/lib/chat/context";
|
|
9
9
|
import {
|
|
10
10
|
useExperimentDetailQuery,
|
|
@@ -194,31 +194,30 @@ export function ExperimentsSection() {
|
|
|
194
194
|
work while off, and shows what it costs here.
|
|
195
195
|
</p>
|
|
196
196
|
{data.experiments.map((e) => (
|
|
197
|
-
<
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
197
|
+
<SettingSection key={e.id}>
|
|
198
|
+
<SwitchSetting
|
|
199
|
+
id={`experiment-${e.id}`}
|
|
200
|
+
label={`${e.name} · ${e.status === "beta" ? "Beta" : "Experimental"}`}
|
|
201
|
+
help={e.description}
|
|
201
202
|
checked={e.on}
|
|
202
203
|
locked={data.locked}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
204
|
+
defaultValue={false}
|
|
205
|
+
onSave={(on) => set.mutateAsync({ id: e.id, on })}
|
|
206
|
+
extra={
|
|
207
|
+
<div className="px-1 pb-1">
|
|
208
|
+
<Usage e={e} />
|
|
209
|
+
<Button
|
|
210
|
+
variant="link"
|
|
211
|
+
size="sm"
|
|
212
|
+
className="min-h-11 px-0 md:min-h-0"
|
|
213
|
+
onClick={() => setOpen(e.id)}
|
|
214
|
+
>
|
|
215
|
+
Details
|
|
216
|
+
</Button>
|
|
217
|
+
</div>
|
|
208
218
|
}
|
|
209
219
|
/>
|
|
210
|
-
|
|
211
|
-
<Usage e={e} />
|
|
212
|
-
<Button
|
|
213
|
-
variant="link"
|
|
214
|
-
size="sm"
|
|
215
|
-
className="min-h-11 px-0 md:min-h-0"
|
|
216
|
-
onClick={() => setOpen(e.id)}
|
|
217
|
-
>
|
|
218
|
-
Details
|
|
219
|
-
</Button>
|
|
220
|
-
</div>
|
|
221
|
-
</div>
|
|
220
|
+
</SettingSection>
|
|
222
221
|
))}
|
|
223
222
|
</div>
|
|
224
223
|
);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The in-app confirm under a row: a message and two buttons, never a browser
|
|
7
|
+
* dialog (which a webview drops). The full-access switch, and every revoke,
|
|
8
|
+
* ask this way.
|
|
9
|
+
*/
|
|
10
|
+
export function InlineConfirm({
|
|
11
|
+
label,
|
|
12
|
+
message,
|
|
13
|
+
confirmLabel,
|
|
14
|
+
onConfirm,
|
|
15
|
+
onCancel,
|
|
16
|
+
}: {
|
|
17
|
+
// The dialog's accessible name.
|
|
18
|
+
label: string;
|
|
19
|
+
message: string;
|
|
20
|
+
confirmLabel: string;
|
|
21
|
+
onConfirm: () => void;
|
|
22
|
+
onCancel: () => void;
|
|
23
|
+
}) {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
role="alertdialog"
|
|
27
|
+
aria-label={label}
|
|
28
|
+
className="bg-destructive/10 my-1 space-y-2 rounded-lg px-3 py-2 text-sm"
|
|
29
|
+
>
|
|
30
|
+
<p>{message}</p>
|
|
31
|
+
<div className="flex gap-2">
|
|
32
|
+
<Button
|
|
33
|
+
size="sm"
|
|
34
|
+
variant="destructive"
|
|
35
|
+
className="h-11 sm:h-9"
|
|
36
|
+
onClick={onConfirm}
|
|
37
|
+
>
|
|
38
|
+
{confirmLabel}
|
|
39
|
+
</Button>
|
|
40
|
+
<Button
|
|
41
|
+
size="sm"
|
|
42
|
+
variant="ghost"
|
|
43
|
+
className="h-11 sm:h-9"
|
|
44
|
+
onClick={onCancel}
|
|
45
|
+
>
|
|
46
|
+
Cancel
|
|
47
|
+
</Button>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -1,55 +1,25 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import { Button } from "@/components/ui/button";
|
|
5
|
-
import { Input } from "@/components/ui/input";
|
|
6
3
|
import { useClientConfig } from "@/data/client-config/queries";
|
|
7
4
|
import { useSetOwnerName } from "@/data/owner/mutations";
|
|
5
|
+
import { SettingSection } from "./SettingRow";
|
|
6
|
+
import { TextSetting } from "./settings-fields";
|
|
8
7
|
|
|
9
8
|
// The name AgentOS and the orchestrators use when they refer to you.
|
|
10
9
|
export function OwnerNameRow() {
|
|
11
10
|
const saved = useClientConfig().data?.ownerName ?? "";
|
|
12
|
-
// null: show what is saved.
|
|
13
|
-
const [draft, setDraft] = useState<string | null>(null);
|
|
14
|
-
const value = draft ?? saved;
|
|
15
11
|
const save = useSetOwnerName();
|
|
16
12
|
return (
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
onSubmit={(e) => {
|
|
20
|
-
e.preventDefault();
|
|
21
|
-
save.mutate(value.trim() || null, { onSuccess: () => setDraft(null) });
|
|
22
|
-
}}
|
|
23
|
-
>
|
|
24
|
-
<div className="min-w-0 flex-1">
|
|
25
|
-
<label htmlFor="owner-name" className="text-sm font-medium">
|
|
26
|
-
Your name
|
|
27
|
-
</label>
|
|
28
|
-
<p className="text-muted-foreground text-xs">
|
|
29
|
-
Used where AgentOS and your orchestrators refer to you. Orchestrators
|
|
30
|
-
pick it up when they next start.
|
|
31
|
-
</p>
|
|
32
|
-
{save.error && (
|
|
33
|
-
<p className="text-destructive text-xs">{save.error.message}</p>
|
|
34
|
-
)}
|
|
35
|
-
</div>
|
|
36
|
-
<Input
|
|
13
|
+
<SettingSection>
|
|
14
|
+
<TextSetting
|
|
37
15
|
id="owner-name"
|
|
38
|
-
|
|
39
|
-
|
|
16
|
+
label="Your name"
|
|
17
|
+
help="Used where AgentOS and your orchestrators refer to you. Orchestrators pick it up when they next start."
|
|
40
18
|
placeholder="Not set"
|
|
41
|
-
|
|
42
|
-
|
|
19
|
+
value={saved}
|
|
20
|
+
maxLength={40}
|
|
21
|
+
onSave={(name) => save.mutateAsync(name || null)}
|
|
43
22
|
/>
|
|
44
|
-
|
|
45
|
-
type="submit"
|
|
46
|
-
size="sm"
|
|
47
|
-
variant="outline"
|
|
48
|
-
className="min-h-11 sm:min-h-8"
|
|
49
|
-
disabled={save.isPending || value.trim() === saved}
|
|
50
|
-
>
|
|
51
|
-
Save
|
|
52
|
-
</Button>
|
|
53
|
-
</form>
|
|
23
|
+
</SettingSection>
|
|
54
24
|
);
|
|
55
25
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { useAutoSave } from "@/hooks/useAutoSave";
|
|
5
|
+
import { SettingRow } from "./SettingRow";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A setting whose control is a picker of your own (a model list, say): the
|
|
9
|
+
* row, the instant save, rollback with a toast, Saved and Reset come from
|
|
10
|
+
* here. `render` gets the value to show and a `commit` that saves a new one.
|
|
11
|
+
*/
|
|
12
|
+
export function PickerSetting<T>({
|
|
13
|
+
id,
|
|
14
|
+
label,
|
|
15
|
+
help,
|
|
16
|
+
value,
|
|
17
|
+
onSave,
|
|
18
|
+
defaultValue,
|
|
19
|
+
hint,
|
|
20
|
+
error,
|
|
21
|
+
render,
|
|
22
|
+
below,
|
|
23
|
+
}: {
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
26
|
+
help?: string;
|
|
27
|
+
value: T;
|
|
28
|
+
onSave: (next: T) => Promise<unknown>;
|
|
29
|
+
// Reset shows while the value differs from this.
|
|
30
|
+
defaultValue?: T;
|
|
31
|
+
hint?: string;
|
|
32
|
+
error?: string | null;
|
|
33
|
+
render: (picker: { value: T; commit: (next: T) => void }) => ReactNode;
|
|
34
|
+
below?: ReactNode;
|
|
35
|
+
}) {
|
|
36
|
+
const auto = useAutoSave({ saved: value, save: onSave });
|
|
37
|
+
return (
|
|
38
|
+
<SettingRow
|
|
39
|
+
id={id}
|
|
40
|
+
label={label}
|
|
41
|
+
help={help}
|
|
42
|
+
hint={hint}
|
|
43
|
+
error={error}
|
|
44
|
+
status={auto.status}
|
|
45
|
+
onUndo={auto.undo}
|
|
46
|
+
onReset={
|
|
47
|
+
defaultValue !== undefined && auto.value !== defaultValue
|
|
48
|
+
? () => void auto.commit(defaultValue, { undoable: true })
|
|
49
|
+
: undefined
|
|
50
|
+
}
|
|
51
|
+
control={
|
|
52
|
+
<div className="w-60 max-w-full max-sm:w-full">
|
|
53
|
+
{render({ value: auto.value, commit: (v) => void auto.commit(v) })}
|
|
54
|
+
</div>
|
|
55
|
+
}
|
|
56
|
+
below={below}
|
|
57
|
+
/>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Check, Loader2 } from "lucide-react";
|
|
4
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
5
|
+
import type { SaveStatus } from "@/hooks/useAutoSave";
|
|
6
|
+
import { cn } from "@/lib/utils";
|
|
7
|
+
|
|
8
|
+
// What the Settings search box holds; a row that doesn't match hides.
|
|
9
|
+
export const SettingsQueryContext = createContext("");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* One setting on a Settings page, 44px at least: label and one-line help on
|
|
13
|
+
* the left, the control on the right (below the label on a phone), and a meta
|
|
14
|
+
* line for the default hint or the error, "Saved", Undo and Reset. Every
|
|
15
|
+
* variant (settings-fields) renders through this.
|
|
16
|
+
*/
|
|
17
|
+
export function SettingRow({
|
|
18
|
+
id,
|
|
19
|
+
label,
|
|
20
|
+
help,
|
|
21
|
+
control,
|
|
22
|
+
controlClassName,
|
|
23
|
+
hint,
|
|
24
|
+
error,
|
|
25
|
+
status = "idle",
|
|
26
|
+
onUndo,
|
|
27
|
+
onReset,
|
|
28
|
+
below,
|
|
29
|
+
}: {
|
|
30
|
+
id: string;
|
|
31
|
+
label: string;
|
|
32
|
+
help?: string;
|
|
33
|
+
control: ReactNode;
|
|
34
|
+
controlClassName?: string;
|
|
35
|
+
hint?: string;
|
|
36
|
+
error?: string | null;
|
|
37
|
+
status?: SaveStatus;
|
|
38
|
+
onUndo?: () => void;
|
|
39
|
+
// Given only while the value differs from the default.
|
|
40
|
+
onReset?: () => void;
|
|
41
|
+
below?: ReactNode;
|
|
42
|
+
}) {
|
|
43
|
+
const query = useContext(SettingsQueryContext).trim().toLowerCase();
|
|
44
|
+
const haystack = `${label} ${help ?? ""}`.toLowerCase();
|
|
45
|
+
if (query && !query.split(/\s+/).every((w) => haystack.includes(w)))
|
|
46
|
+
return null;
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
data-setting={id}
|
|
50
|
+
className="border-foreground/5 grid min-h-11 grid-cols-[minmax(0,1fr)_auto] items-center gap-x-4 gap-y-0.5 border-t px-3 py-2 first:border-t-0"
|
|
51
|
+
>
|
|
52
|
+
<div className="min-w-0">
|
|
53
|
+
<label htmlFor={id} className="block text-sm font-medium">
|
|
54
|
+
{label}
|
|
55
|
+
</label>
|
|
56
|
+
{help && (
|
|
57
|
+
<p className="text-muted-foreground text-xs leading-snug">{help}</p>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
<div
|
|
61
|
+
className={cn(
|
|
62
|
+
"flex min-w-0 items-center justify-end gap-2 max-sm:col-span-2 max-sm:row-start-2 max-sm:mt-1 max-sm:justify-start",
|
|
63
|
+
controlClassName
|
|
64
|
+
)}
|
|
65
|
+
>
|
|
66
|
+
{control}
|
|
67
|
+
</div>
|
|
68
|
+
<div className="text-muted-foreground col-span-2 flex min-h-5 items-center justify-between gap-2 text-xs">
|
|
69
|
+
<span
|
|
70
|
+
id={`${id}-hint`}
|
|
71
|
+
className={cn("min-w-0", error && "text-destructive")}
|
|
72
|
+
role={error ? "alert" : undefined}
|
|
73
|
+
>
|
|
74
|
+
{error ?? hint}
|
|
75
|
+
</span>
|
|
76
|
+
<span className="flex shrink-0 items-center gap-3" aria-live="polite">
|
|
77
|
+
{status === "saving" && (
|
|
78
|
+
<span className="inline-flex items-center gap-1">
|
|
79
|
+
<Loader2 className="size-3 animate-spin" />
|
|
80
|
+
Saving…
|
|
81
|
+
</span>
|
|
82
|
+
)}
|
|
83
|
+
{(status === "saved" || status === "undo") && (
|
|
84
|
+
<span className="inline-flex items-center gap-1 text-emerald-600 dark:text-emerald-400">
|
|
85
|
+
<Check className="size-3" />
|
|
86
|
+
Saved
|
|
87
|
+
</span>
|
|
88
|
+
)}
|
|
89
|
+
{status === "undo" && onUndo && (
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
onClick={onUndo}
|
|
93
|
+
className="text-primary -my-3 -mr-1 min-h-11 px-1 hover:underline"
|
|
94
|
+
>
|
|
95
|
+
Undo
|
|
96
|
+
</button>
|
|
97
|
+
)}
|
|
98
|
+
{onReset && status !== "undo" && (
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
onClick={onReset}
|
|
102
|
+
className="text-primary -my-3 -mr-1 min-h-11 px-1 hover:underline"
|
|
103
|
+
>
|
|
104
|
+
Reset
|
|
105
|
+
</button>
|
|
106
|
+
)}
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
{below && <div className="col-span-2">{below}</div>}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A titled group of rows with one line of help. */
|
|
115
|
+
export function SettingSection({
|
|
116
|
+
title,
|
|
117
|
+
help,
|
|
118
|
+
children,
|
|
119
|
+
}: {
|
|
120
|
+
title?: string;
|
|
121
|
+
help?: string;
|
|
122
|
+
children: ReactNode;
|
|
123
|
+
}) {
|
|
124
|
+
return (
|
|
125
|
+
<section className="mb-5 [&:not(:has([data-setting]))]:hidden">
|
|
126
|
+
{title && <h3 className="text-sm font-semibold">{title}</h3>}
|
|
127
|
+
{help && <p className="text-muted-foreground mb-2 text-xs">{help}</p>}
|
|
128
|
+
<div className="bg-muted/40 overflow-hidden rounded-xl">{children}</div>
|
|
129
|
+
</section>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
@@ -34,6 +34,7 @@ vi.mock("./NoApprovalsSection", () => ({
|
|
|
34
34
|
vi.mock("@/components/Workspaces", () => ({
|
|
35
35
|
WorkspaceSettings: stub("workspaces"),
|
|
36
36
|
}));
|
|
37
|
+
vi.mock("./ContextSection", () => ({ ContextSection: stub("context") }));
|
|
37
38
|
vi.mock("@/components/Models/ModelsSection", () => ({
|
|
38
39
|
ModelsSection: stub("models"),
|
|
39
40
|
}));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { memo, useEffect, useRef } from "react";
|
|
3
|
+
import { memo, useEffect, useRef, useState } from "react";
|
|
4
4
|
import { useSnapshot } from "valtio";
|
|
5
5
|
import {
|
|
6
6
|
ArrowLeft,
|
|
@@ -12,7 +12,9 @@ import {
|
|
|
12
12
|
Cpu,
|
|
13
13
|
GitMerge,
|
|
14
14
|
HeartPulse,
|
|
15
|
+
Layers,
|
|
15
16
|
LayoutGrid,
|
|
17
|
+
Search,
|
|
16
18
|
Link2,
|
|
17
19
|
ListChecks,
|
|
18
20
|
ShieldOff,
|
|
@@ -48,6 +50,8 @@ import { cn } from "@/lib/utils";
|
|
|
48
50
|
import { schedulesUiActions } from "@/stores/schedulesUi";
|
|
49
51
|
import { RecipesPanel } from "@/components/Recipes";
|
|
50
52
|
import { settingsUi, settingsUiActions } from "@/stores/settingsUi";
|
|
53
|
+
import { ContextSection } from "./ContextSection";
|
|
54
|
+
import { SettingsQueryContext } from "./SettingRow";
|
|
51
55
|
import { SetupSection } from "./SetupSection";
|
|
52
56
|
import { NoApprovalsSection } from "./NoApprovalsSection";
|
|
53
57
|
import {
|
|
@@ -84,6 +88,11 @@ export const SECTION_INFO: Record<
|
|
|
84
88
|
hint: "Snapshot links you made, with views, and revoke",
|
|
85
89
|
icon: Link2,
|
|
86
90
|
},
|
|
91
|
+
context: {
|
|
92
|
+
label: "Context",
|
|
93
|
+
hint: "When agents start fresh or compact",
|
|
94
|
+
icon: Layers,
|
|
95
|
+
},
|
|
87
96
|
models: {
|
|
88
97
|
label: "Models",
|
|
89
98
|
hint: "Which model each kind of work runs on, and providers",
|
|
@@ -150,6 +159,8 @@ function SectionBody({
|
|
|
150
159
|
return <NoApprovalsSection />;
|
|
151
160
|
case "workspaces":
|
|
152
161
|
return <WorkspaceSettings />;
|
|
162
|
+
case "context":
|
|
163
|
+
return <ContextSection />;
|
|
153
164
|
case "models":
|
|
154
165
|
return <ModelsSection />;
|
|
155
166
|
case "agent-roles":
|
|
@@ -177,6 +188,40 @@ function SectionBody({
|
|
|
177
188
|
}
|
|
178
189
|
}
|
|
179
190
|
|
|
191
|
+
// Sections built from SettingRow, which the search box filters.
|
|
192
|
+
const SEARCHABLE: ReadonlySet<SettingsSection> = new Set([
|
|
193
|
+
"context",
|
|
194
|
+
"workspaces",
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
function SearchableBody({
|
|
198
|
+
section,
|
|
199
|
+
browser,
|
|
200
|
+
}: {
|
|
201
|
+
section: SettingsSection;
|
|
202
|
+
browser: BrowserNotifications;
|
|
203
|
+
}) {
|
|
204
|
+
const [query, setQuery] = useState("");
|
|
205
|
+
if (!SEARCHABLE.has(section))
|
|
206
|
+
return <SectionBody section={section} browser={browser} />;
|
|
207
|
+
return (
|
|
208
|
+
<SettingsQueryContext.Provider value={query}>
|
|
209
|
+
<div className="relative mb-4">
|
|
210
|
+
<Search className="text-muted-foreground pointer-events-none absolute top-3.5 left-3 size-4 md:top-2.5" />
|
|
211
|
+
<input
|
|
212
|
+
type="search"
|
|
213
|
+
value={query}
|
|
214
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
215
|
+
placeholder="Search settings"
|
|
216
|
+
aria-label="Search settings"
|
|
217
|
+
className="bg-input/60 hairline h-11 w-full rounded-xl border-0 pr-3 pl-9 text-base outline-none md:h-9 md:text-sm"
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
<SectionBody section={section} browser={browser} />
|
|
221
|
+
</SettingsQueryContext.Provider>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
180
225
|
/**
|
|
181
226
|
* Every AgentOS setting, in one place. Wide screens show the sections beside
|
|
182
227
|
* the open one; a phone shows the list, then the section with a way back.
|
|
@@ -323,7 +368,7 @@ export const SettingsDialog = memo(function SettingsDialog({
|
|
|
323
368
|
{SECTION_INFO[shown].label}
|
|
324
369
|
</h2>
|
|
325
370
|
{open && (
|
|
326
|
-
<
|
|
371
|
+
<SearchableBody
|
|
327
372
|
key={shown}
|
|
328
373
|
section={shown}
|
|
329
374
|
browser={notifications}
|