@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.
Files changed (54) hide show
  1. package/app/api/fresh-start/route.test.ts +33 -0
  2. package/app/api/fresh-start/route.ts +3 -0
  3. package/components/Devices/DeviceRow.tsx +83 -69
  4. package/components/Devices/PasskeysSection.test.ts +89 -0
  5. package/components/Devices/PasskeysSection.tsx +36 -26
  6. package/components/Health/HealthPanel.tsx +41 -16
  7. package/components/Merge/GlobalMergeSection.tsx +2 -5
  8. package/components/Merge/MergeSettingsFields.tsx +39 -77
  9. package/components/Merge/ProjectMergeSection.tsx +2 -6
  10. package/components/Merge/WorktreeSweepSection.tsx +40 -14
  11. package/components/Models/AgentRoleCard.tsx +61 -66
  12. package/components/Models/AgentRolesSection.tsx +5 -8
  13. package/components/Models/ModelsSection.tsx +7 -18
  14. package/components/Models/PriceInput.tsx +59 -0
  15. package/components/PhoneNotify/PhoneNotifyForm.tsx +37 -58
  16. package/components/Settings/AppWideLimits.tsx +61 -0
  17. package/components/Settings/ContextSection.tsx +96 -0
  18. package/components/Settings/ExperimentsSection.test.ts +9 -7
  19. package/components/Settings/ExperimentsSection.tsx +22 -23
  20. package/components/Settings/InlineConfirm.tsx +51 -0
  21. package/components/Settings/OwnerNameRow.tsx +10 -40
  22. package/components/Settings/PickerSetting.tsx +59 -0
  23. package/components/Settings/SettingRow.tsx +131 -0
  24. package/components/Settings/SettingsDialog.test.ts +1 -0
  25. package/components/Settings/SettingsDialog.tsx +47 -2
  26. package/components/Settings/settings-fields.test.ts +103 -0
  27. package/components/Settings/settings-fields.tsx +427 -0
  28. package/components/Shares/SharedLinksPanel.tsx +62 -53
  29. package/components/Workspaces/HqFolderForm.tsx +21 -54
  30. package/components/Workspaces/McpServersForm.tsx +20 -19
  31. package/components/Workspaces/ProjectTiersForm.tsx +10 -8
  32. package/components/Workspaces/RulesForm.test.ts +127 -0
  33. package/components/Workspaces/RulesForm.tsx +54 -126
  34. package/components/Workspaces/WorkspaceModelsForm.tsx +27 -26
  35. package/components/Workspaces/WorkspaceSettings.tsx +61 -24
  36. package/content/whats-new.json +16 -0
  37. package/data/fresh-start/index.ts +1 -1
  38. package/data/workspaces/index.ts +9 -0
  39. package/hooks/useAutoSave.test.ts +116 -0
  40. package/hooks/useAutoSave.ts +98 -0
  41. package/lib/away/window-limits.ts +3 -1
  42. package/lib/chat/refresh.ts +10 -6
  43. package/lib/chat/stall-limits.ts +3 -1
  44. package/lib/settings/defaults.ts +27 -0
  45. package/lib/settings/parse.ts +17 -0
  46. package/lib/settings-url.ts +1 -0
  47. package/lib/worktree-sweep/limits.ts +3 -0
  48. package/lib/worktree-sweep/settings.ts +4 -2
  49. package/package.json +1 -1
  50. package/components/Away/AwayWindowForm.tsx +0 -91
  51. package/components/Workspaces/FreshStartForm.tsx +0 -177
  52. package/components/Workspaces/StallForm.tsx +0 -101
  53. package/components/Workspaces/TaskLimitForm.tsx +0 -84
  54. 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 { Input } from "@/components/ui/input";
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
- <form
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
- <div className="space-y-2">
66
- <label className="text-sm font-medium" htmlFor="tg-token">
67
- Bot token
68
- </label>
69
- <Input
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
- type="password"
72
- autoComplete="off"
73
- value={token}
74
- onChange={(e) => setToken(e.target.value)}
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 ? "Saved; type to replace" : "123:ABC…"
71
+ settings.telegram.tokenSet
72
+ ? "Connected; paste to replace"
73
+ : "123:ABC…"
77
74
  }
78
- className="h-11 font-mono md:h-9"
79
- />
80
- </div>
81
- <div className="space-y-2">
82
- <label className="text-sm font-medium" htmlFor="tg-chat">
83
- Chat id
84
- </label>
85
- <Input
86
- id="tg-chat"
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
- </div>
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
- </form>
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(() => ({ mutate: vi.fn() }));
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: () => ({ mutate: state.mutate }),
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.mutate.mockReset();
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.mutate).toHaveBeenCalledWith(
91
- { id: "learning-cards", on: true },
92
- expect.any(Object)
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 { SettingToggle } from "@/components/Settings/SettingToggle";
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
- <div key={e.id} className="space-y-2">
198
- <SettingToggle
199
- title={`${e.name} · ${e.status === "beta" ? "Beta" : "Experimental"}`}
200
- detail={e.description}
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
- onChange={(on) =>
204
- set.mutate(
205
- { id: e.id, on },
206
- { onError: (err) => toast.error(err.message) }
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
- <div className="px-3">
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
- <form
18
- className="bg-muted/40 flex flex-col gap-2 rounded-lg px-3 py-2.5 sm:flex-row sm:items-center"
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
- value={value}
39
- maxLength={40}
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
- onChange={(e) => setDraft(e.target.value)}
42
- className="min-h-11 sm:min-h-8 sm:w-44"
19
+ value={saved}
20
+ maxLength={40}
21
+ onSave={(name) => save.mutateAsync(name || null)}
43
22
  />
44
- <Button
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
+ }