@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.
Files changed (74) 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/app/api/sessions/route.ts +7 -1
  4. package/app/globals.css +14 -0
  5. package/components/Chat/ChatPanel.tsx +41 -29
  6. package/components/Chat/Forks.tsx +16 -1
  7. package/components/Chat/Messages.tsx +14 -0
  8. package/components/Chat/WorktreeRemovedNotice.tsx +17 -0
  9. package/components/Chat/composer/ComposerBody.tsx +10 -1
  10. package/components/Chat/composer/ForkHeader.tsx +26 -15
  11. package/components/Chat/fork-draft.test.ts +57 -0
  12. package/components/Chat/fork-draft.ts +34 -0
  13. package/components/Devices/DeviceRow.tsx +83 -69
  14. package/components/Devices/PasskeysSection.test.ts +89 -0
  15. package/components/Devices/PasskeysSection.tsx +36 -26
  16. package/components/Health/HealthPanel.tsx +41 -16
  17. package/components/Merge/GlobalMergeSection.tsx +2 -5
  18. package/components/Merge/MergeSettingsFields.tsx +39 -77
  19. package/components/Merge/ProjectMergeSection.tsx +2 -6
  20. package/components/Merge/WorktreeSweepSection.tsx +40 -14
  21. package/components/Models/AgentRoleCard.tsx +61 -66
  22. package/components/Models/AgentRolesSection.tsx +5 -8
  23. package/components/Models/ModelsSection.tsx +7 -18
  24. package/components/Models/PriceInput.tsx +59 -0
  25. package/components/PhoneNotify/PhoneNotifyForm.tsx +37 -58
  26. package/components/Settings/AppWideLimits.tsx +61 -0
  27. package/components/Settings/ContextSection.tsx +96 -0
  28. package/components/Settings/ExperimentsSection.test.ts +9 -7
  29. package/components/Settings/ExperimentsSection.tsx +22 -23
  30. package/components/Settings/InlineConfirm.tsx +51 -0
  31. package/components/Settings/OwnerNameRow.tsx +10 -40
  32. package/components/Settings/PickerSetting.tsx +59 -0
  33. package/components/Settings/SettingRow.tsx +131 -0
  34. package/components/Settings/SettingsDialog.test.ts +1 -0
  35. package/components/Settings/SettingsDialog.tsx +47 -2
  36. package/components/Settings/settings-fields.test.ts +103 -0
  37. package/components/Settings/settings-fields.tsx +427 -0
  38. package/components/Shares/SharedLinksPanel.tsx +62 -53
  39. package/components/Workspaces/HqFolderForm.tsx +21 -54
  40. package/components/Workspaces/McpServersForm.tsx +20 -19
  41. package/components/Workspaces/ProjectTiersForm.tsx +10 -8
  42. package/components/Workspaces/RulesForm.test.ts +127 -0
  43. package/components/Workspaces/RulesForm.tsx +54 -126
  44. package/components/Workspaces/WorkspaceModelsForm.tsx +27 -26
  45. package/components/Workspaces/WorkspaceSettings.tsx +61 -24
  46. package/content/whats-new.json +32 -0
  47. package/data/fresh-start/index.ts +1 -1
  48. package/data/workspaces/index.ts +9 -0
  49. package/hooks/useAutoSave.test.ts +116 -0
  50. package/hooks/useAutoSave.ts +98 -0
  51. package/lib/away/window-limits.ts +3 -1
  52. package/lib/chat/refresh.ts +10 -6
  53. package/lib/chat/socket.ts +7 -3
  54. package/lib/chat/stall-limits.ts +3 -1
  55. package/lib/chat/start-error.ts +13 -0
  56. package/lib/chat/worker/cwd.ts +77 -0
  57. package/lib/chat/worker/host.ts +11 -2
  58. package/lib/chat/worker/main.ts +11 -2
  59. package/lib/db/types.ts +2 -0
  60. package/lib/fork/note.ts +6 -0
  61. package/lib/fork/start.ts +6 -2
  62. package/lib/fork/types.ts +2 -0
  63. package/lib/fork/view.ts +14 -1
  64. package/lib/settings/defaults.ts +27 -0
  65. package/lib/settings/parse.ts +17 -0
  66. package/lib/settings-url.ts +1 -0
  67. package/lib/worktree-sweep/limits.ts +3 -0
  68. package/lib/worktree-sweep/settings.ts +4 -2
  69. package/package.json +1 -1
  70. package/components/Away/AwayWindowForm.tsx +0 -91
  71. package/components/Workspaces/FreshStartForm.tsx +0 -177
  72. package/components/Workspaces/StallForm.tsx +0 -101
  73. package/components/Workspaces/TaskLimitForm.tsx +0 -84
  74. 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(() => ({ 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
+ }
@@ -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
- <SectionBody
371
+ <SearchableBody
327
372
  key={shown}
328
373
  section={shown}
329
374
  browser={notifications}