@saadnvd1/agent-os 0.3.836 → 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/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 +16 -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/stall-limits.ts +3 -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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState } from "react";
|
|
4
3
|
import { Button } from "@/components/ui/button";
|
|
5
|
-
import {
|
|
4
|
+
import { SettingSection } from "@/components/Settings/SettingRow";
|
|
5
|
+
import { TextSetting } from "@/components/Settings/settings-fields";
|
|
6
6
|
import {
|
|
7
7
|
useNotifySettings,
|
|
8
8
|
useSaveTelegram,
|
|
@@ -16,11 +16,8 @@ export function PhoneNotifyForm() {
|
|
|
16
16
|
const { data: settings, isPending } = useNotifySettings();
|
|
17
17
|
const save = useSaveTelegram();
|
|
18
18
|
const test = useTestNotify();
|
|
19
|
-
const [token, setToken] = useState("");
|
|
20
|
-
const [chatId, setChatId] = useState<string | null>(null);
|
|
21
19
|
if (isPending || !settings)
|
|
22
20
|
return <div className="bg-muted/40 h-32 animate-pulse rounded-xl" />;
|
|
23
|
-
const chat = chatId ?? settings.telegram.chatId ?? "";
|
|
24
21
|
|
|
25
22
|
return (
|
|
26
23
|
<div className="space-y-5">
|
|
@@ -48,60 +45,45 @@ export function PhoneNotifyForm() {
|
|
|
48
45
|
)}
|
|
49
46
|
</div>
|
|
50
47
|
|
|
51
|
-
<
|
|
52
|
-
className="space-y-3"
|
|
53
|
-
onSubmit={(e) => {
|
|
54
|
-
e.preventDefault();
|
|
55
|
-
save.mutate(
|
|
56
|
-
{
|
|
57
|
-
...(token.trim() && { telegramToken: token.trim() }),
|
|
58
|
-
telegramChatId: chat,
|
|
59
|
-
},
|
|
60
|
-
{ onSuccess: () => setToken("") }
|
|
61
|
-
);
|
|
62
|
-
}}
|
|
63
|
-
>
|
|
48
|
+
<div className="space-y-2">
|
|
64
49
|
<p className="label-mono text-muted-foreground">Telegram</p>
|
|
65
|
-
<
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
50
|
+
<SettingSection>
|
|
51
|
+
<TextSetting
|
|
52
|
+
id="tg-chat"
|
|
53
|
+
label="Chat id"
|
|
54
|
+
help="Where the bot sends alerts: your chat id with it, or @channel."
|
|
55
|
+
placeholder="123456789"
|
|
56
|
+
value={settings.telegram.chatId ?? ""}
|
|
57
|
+
validate={(t) =>
|
|
58
|
+
!t || /^-?\d+$|^@\w+$/.test(t)
|
|
59
|
+
? null
|
|
60
|
+
: "A chat id is a whole number like 123456789, or @channel."
|
|
61
|
+
}
|
|
62
|
+
onSave={(t) => save.mutateAsync({ telegramChatId: t || null })}
|
|
63
|
+
/>
|
|
64
|
+
<TextSetting
|
|
70
65
|
id="tg-token"
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
label="Bot token"
|
|
67
|
+
help="Write-only. Saved with the button, never on blur: a half-typed secret must not save."
|
|
68
|
+
secret
|
|
69
|
+
buttonLabel={settings.telegram.tokenSet ? "Replace" : "Connect"}
|
|
75
70
|
placeholder={
|
|
76
|
-
settings.telegram.tokenSet
|
|
71
|
+
settings.telegram.tokenSet
|
|
72
|
+
? "Connected; paste to replace"
|
|
73
|
+
: "123:ABC…"
|
|
77
74
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
inputMode="numeric"
|
|
88
|
-
value={chat}
|
|
89
|
-
onChange={(e) => setChatId(e.target.value)}
|
|
90
|
-
placeholder="Your chat id with the bot"
|
|
91
|
-
className="h-11 font-mono md:h-9"
|
|
75
|
+
value=""
|
|
76
|
+
validate={(t) =>
|
|
77
|
+
!t
|
|
78
|
+
? "Paste the bot token from BotFather."
|
|
79
|
+
: /\s/.test(t)
|
|
80
|
+
? "A token has no spaces."
|
|
81
|
+
: null
|
|
82
|
+
}
|
|
83
|
+
onSave={(t) => save.mutateAsync({ telegramToken: t })}
|
|
92
84
|
/>
|
|
93
|
-
</
|
|
94
|
-
{save.error && (
|
|
95
|
-
<p className="text-destructive text-sm">{save.error.message}</p>
|
|
96
|
-
)}
|
|
85
|
+
</SettingSection>
|
|
97
86
|
<div className="flex flex-wrap gap-2">
|
|
98
|
-
<Button
|
|
99
|
-
type="submit"
|
|
100
|
-
className="h-11 md:h-9"
|
|
101
|
-
disabled={save.isPending}
|
|
102
|
-
>
|
|
103
|
-
{save.isPending ? "Saving..." : "Save"}
|
|
104
|
-
</Button>
|
|
105
87
|
{settings.telegram.tokenSet && (
|
|
106
88
|
<Button
|
|
107
89
|
type="button"
|
|
@@ -109,10 +91,7 @@ export function PhoneNotifyForm() {
|
|
|
109
91
|
className="text-muted-foreground h-11 md:h-9"
|
|
110
92
|
disabled={save.isPending}
|
|
111
93
|
onClick={() =>
|
|
112
|
-
save.mutate(
|
|
113
|
-
{ telegramToken: null, telegramChatId: null },
|
|
114
|
-
{ onSuccess: () => setChatId(null) }
|
|
115
|
-
)
|
|
94
|
+
save.mutate({ telegramToken: null, telegramChatId: null })
|
|
116
95
|
}
|
|
117
96
|
>
|
|
118
97
|
Remove bot
|
|
@@ -136,7 +115,7 @@ export function PhoneNotifyForm() {
|
|
|
136
115
|
{testSummary(test.data)}
|
|
137
116
|
</p>
|
|
138
117
|
)}
|
|
139
|
-
</
|
|
118
|
+
</div>
|
|
140
119
|
</div>
|
|
141
120
|
);
|
|
142
121
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useAwaySettingsQuery, useUpdateAwaySettings } from "@/data/away";
|
|
4
|
+
import { useStallQuery, useUpdateStall } from "@/data/stall";
|
|
5
|
+
import { MAX_STALL_MINUTES, MIN_STALL_MINUTES } from "@/lib/chat/stall-limits";
|
|
6
|
+
import {
|
|
7
|
+
MAX_WINDOW_MINUTES,
|
|
8
|
+
MIN_WINDOW_MINUTES,
|
|
9
|
+
} from "@/lib/away/window-limits";
|
|
10
|
+
import { SETTING_DEFAULTS } from "@/lib/settings/defaults";
|
|
11
|
+
import { SettingSection } from "./SettingRow";
|
|
12
|
+
import { NumberSetting } from "./settings-fields";
|
|
13
|
+
|
|
14
|
+
// Stall alerts and the Away window: app-wide, each saved on its own.
|
|
15
|
+
export function AppWideLimits() {
|
|
16
|
+
const stall = useStallQuery();
|
|
17
|
+
const away = useAwaySettingsQuery();
|
|
18
|
+
const updateStall = useUpdateStall();
|
|
19
|
+
const updateAway = useUpdateAwaySettings();
|
|
20
|
+
return (
|
|
21
|
+
<SettingSection
|
|
22
|
+
title="All workspaces"
|
|
23
|
+
help="Stall alerts and Away mode apply across every workspace."
|
|
24
|
+
>
|
|
25
|
+
{stall.data && (
|
|
26
|
+
<NumberSetting
|
|
27
|
+
id="stall-minutes"
|
|
28
|
+
label="Flag stalled turns"
|
|
29
|
+
help="Warn, and tell the orchestrator, when a turn makes no progress for this long."
|
|
30
|
+
value={stall.data.minutes}
|
|
31
|
+
defaultValue={SETTING_DEFAULTS.stallMinutes}
|
|
32
|
+
onSave={(n) => updateStall.mutateAsync(n)}
|
|
33
|
+
min={MIN_STALL_MINUTES}
|
|
34
|
+
max={MAX_STALL_MINUTES}
|
|
35
|
+
unit="minutes"
|
|
36
|
+
short=" min"
|
|
37
|
+
optional={{ off: 0, on: SETTING_DEFAULTS.stallMinutes }}
|
|
38
|
+
/>
|
|
39
|
+
)}
|
|
40
|
+
{away.data && (
|
|
41
|
+
<NumberSetting
|
|
42
|
+
id="away-window"
|
|
43
|
+
label="Away window"
|
|
44
|
+
help="Away mode only works through sessions with a real turn this recently."
|
|
45
|
+
value={away.data.windowMinutes}
|
|
46
|
+
defaultValue={SETTING_DEFAULTS.awayWindowMinutes}
|
|
47
|
+
onSave={(n) => updateAway.mutateAsync({ windowMinutes: n })}
|
|
48
|
+
min={MIN_WINDOW_MINUTES}
|
|
49
|
+
max={MAX_WINDOW_MINUTES}
|
|
50
|
+
unit="minutes"
|
|
51
|
+
short=" min"
|
|
52
|
+
/>
|
|
53
|
+
)}
|
|
54
|
+
{(stall.isError || away.isError) && (
|
|
55
|
+
<p className="text-destructive px-3 py-2 text-sm">
|
|
56
|
+
{stall.error?.message ?? away.error?.message}
|
|
57
|
+
</p>
|
|
58
|
+
)}
|
|
59
|
+
</SettingSection>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -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
|
+
}
|