@saadnvd1/agent-os 0.3.794 → 0.3.807

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 (167) hide show
  1. package/app/api/busy/route.ts +2 -2
  2. package/app/api/chat-views/[sessionId]/route.ts +19 -0
  3. package/app/api/chat-views/route.test.ts +79 -0
  4. package/app/api/chat-views/route.ts +8 -0
  5. package/app/api/tips/route.test.ts +56 -0
  6. package/app/api/tips/route.ts +45 -0
  7. package/app/api/updates/health/route.ts +22 -0
  8. package/app/api/updates/route.test.ts +53 -0
  9. package/app/api/updates/settings/route.ts +37 -10
  10. package/app/page.tsx +17 -14
  11. package/components/Chat/ChatPanel.tsx +74 -59
  12. package/components/Chat/NewDivider.tsx +15 -0
  13. package/components/Chat/needs-more-history.test.ts +28 -0
  14. package/components/Chat/needs-more-history.ts +14 -0
  15. package/components/Chat/session-link.ts +7 -4
  16. package/components/Chat/useFind.ts +50 -1
  17. package/components/Chat/usePosition.ts +363 -0
  18. package/components/CodeSearchDialog.tsx +90 -0
  19. package/components/CommandPalette/CommandsBody.tsx +12 -4
  20. package/components/CommandPalette/useAppCommands.ts +29 -11
  21. package/components/DevServers/ServerLogsModal.tsx +24 -21
  22. package/components/Nav/HistoryButtons.tsx +128 -0
  23. package/components/Pane/MobileTabBar.tsx +3 -0
  24. package/components/Pane/index.tsx +2 -2
  25. package/components/Recipes/TimerFields.tsx +2 -1
  26. package/components/Schedules/ScheduleForm.tsx +1 -1
  27. package/components/SessionList/useSessionSwitching.ts +3 -2
  28. package/components/Settings/SettingsDialog.tsx +1 -1
  29. package/components/Shortcuts/PendingKeys.tsx +19 -0
  30. package/components/Shortcuts/ShortcutsButton.tsx +22 -0
  31. package/components/Shortcuts/ShortcutsDialog.tsx +122 -0
  32. package/components/SidebarFooter.tsx +4 -0
  33. package/components/Terminal/hooks/useTerminalConnection.ts +4 -3
  34. package/components/Tips/TipButton.tsx +39 -0
  35. package/components/Tips/TipDialog.tsx +148 -0
  36. package/components/Tips/TipHost.tsx +14 -0
  37. package/components/Tips/TipMedia.tsx +71 -0
  38. package/components/Tips/index.ts +2 -0
  39. package/components/Updates/AutoUpdates.tsx +141 -0
  40. package/components/Updates/UpdateBanner.test.ts +39 -1
  41. package/components/Updates/UpdateBanner.tsx +31 -5
  42. package/components/Updates/UpdatesSection.tsx +24 -3
  43. package/components/views/DesktopView.tsx +7 -9
  44. package/components/views/MobileView.tsx +2 -8
  45. package/components/views/lazy.ts +2 -2
  46. package/content/tips.json +541 -0
  47. package/content/whats-new.json +56 -0
  48. package/data/chat-views/index.ts +2 -0
  49. package/data/chat-views/keys.ts +3 -0
  50. package/data/chat-views/queries.ts +34 -0
  51. package/data/sessions/pin.ts +0 -1
  52. package/data/tips/index.ts +49 -0
  53. package/data/updates/index.ts +5 -0
  54. package/hooks/useChatViewsSync.ts +12 -0
  55. package/hooks/useNavActions.ts +104 -0
  56. package/hooks/useNavKeys.ts +105 -0
  57. package/hooks/useReloadWhenUpdated.ts +5 -1
  58. package/hooks/useSessionUrl.ts +23 -5
  59. package/lib/bus/index.ts +4 -2
  60. package/lib/bus/role-alias.ts +4 -2
  61. package/lib/bus/targets.ts +2 -1
  62. package/lib/busy.ts +23 -1
  63. package/lib/chat/escape.ts +8 -4
  64. package/lib/chat-views/body.ts +41 -0
  65. package/lib/chat-views/store.ts +67 -0
  66. package/lib/chat-views/types.ts +20 -0
  67. package/lib/db/migrations/20261031T1500_chat_views.ts +30 -0
  68. package/lib/db/migrations.ts +21 -0
  69. package/lib/keybindings/focus.ts +17 -0
  70. package/lib/keybindings/keymap.ts +120 -0
  71. package/lib/keybindings/sequence.ts +64 -0
  72. package/lib/live/service.ts +1 -1
  73. package/lib/nav/history-mirror.ts +59 -0
  74. package/lib/nav/history-writer.ts +109 -0
  75. package/lib/nav/position.ts +67 -0
  76. package/lib/nav/recent.ts +34 -0
  77. package/lib/orchestrator/armed.ts +5 -0
  78. package/lib/palette/sections.ts +12 -6
  79. package/lib/recipes/timer.ts +3 -1
  80. package/lib/schedules/cron.ts +14 -5
  81. package/lib/schedules/store.ts +3 -2
  82. package/lib/security/demo.ts +2 -0
  83. package/lib/session-url.ts +11 -5
  84. package/lib/tasks/finish.ts +5 -5
  85. package/lib/tips/conditions.ts +38 -0
  86. package/lib/tips/index.ts +2 -0
  87. package/lib/tips/load.ts +26 -0
  88. package/lib/tips/media.ts +101 -0
  89. package/lib/tips/pick.ts +35 -0
  90. package/lib/tips/schema.ts +82 -0
  91. package/lib/tips/state.ts +26 -0
  92. package/lib/tips/view.ts +60 -0
  93. package/lib/updates/auto.ts +149 -0
  94. package/lib/updates/banner.ts +78 -3
  95. package/lib/updates/check.ts +64 -8
  96. package/lib/updates/history.ts +99 -0
  97. package/lib/updates/run.ts +334 -23
  98. package/lib/updates/settings.ts +66 -0
  99. package/lib/updates/view.ts +14 -3
  100. package/package.json +2 -2
  101. package/public/tips/.gitkeep +0 -0
  102. package/public/tips/chat-access-picker-dark.webp +0 -0
  103. package/public/tips/chat-access-picker.webp +0 -0
  104. package/public/tips/chat-attach-files-poster.webp +0 -0
  105. package/public/tips/chat-attach-files.mp4 +0 -0
  106. package/public/tips/chat-bookmark-dark.webp +0 -0
  107. package/public/tips/chat-bookmark.webp +0 -0
  108. package/public/tips/chat-plan-mode-dark.webp +0 -0
  109. package/public/tips/chat-plan-mode.webp +0 -0
  110. package/public/tips/chat-search-poster.webp +0 -0
  111. package/public/tips/chat-search.mp4 +0 -0
  112. package/public/tips/chat-slash-menu-poster.webp +0 -0
  113. package/public/tips/chat-slash-menu.mp4 +0 -0
  114. package/public/tips/chat-tab-suggestion-poster.webp +0 -0
  115. package/public/tips/chat-tab-suggestion.mp4 +0 -0
  116. package/public/tips/chat-terminal-handoff-poster.webp +0 -0
  117. package/public/tips/chat-terminal-handoff.mp4 +0 -0
  118. package/public/tips/chat-undo-dark.webp +0 -0
  119. package/public/tips/chat-undo.webp +0 -0
  120. package/public/tips/export-ai-dark.webp +0 -0
  121. package/public/tips/export-ai.webp +0 -0
  122. package/public/tips/net-messages-dark.webp +0 -0
  123. package/public/tips/net-messages.webp +0 -0
  124. package/public/tips/recipe-save-dark.webp +0 -0
  125. package/public/tips/recipe-save.webp +0 -0
  126. package/public/tips/schedule-first-dark.webp +0 -0
  127. package/public/tips/schedule-first.webp +0 -0
  128. package/public/tips/share-check-dark.webp +0 -0
  129. package/public/tips/share-check.webp +0 -0
  130. package/public/tips/sidebar-draft-poster.webp +0 -0
  131. package/public/tips/sidebar-draft.mp4 +0 -0
  132. package/public/tips/sidebar-groups-dark.webp +0 -0
  133. package/public/tips/sidebar-groups.webp +0 -0
  134. package/public/tips/sidebar-load-dark.webp +0 -0
  135. package/public/tips/sidebar-load.webp +0 -0
  136. package/public/tips/sidebar-snooze-dark.webp +0 -0
  137. package/public/tips/sidebar-snooze.webp +0 -0
  138. package/public/tips/tasks-plan-pieces-dark.webp +0 -0
  139. package/public/tips/tasks-plan-pieces.webp +0 -0
  140. package/public/tips/tasks-sign-off-dark.webp +0 -0
  141. package/public/tips/tasks-sign-off.webp +0 -0
  142. package/public/tips/tasks-stacks-dark.webp +0 -0
  143. package/public/tips/tasks-stacks.webp +0 -0
  144. package/scripts/autodeploy +4 -0
  145. package/scripts/autodeploy.test.ts +12 -0
  146. package/scripts/check +2 -2
  147. package/scripts/docs-links.test.ts +1 -25
  148. package/scripts/lib/commands.sh +32 -1
  149. package/scripts/lib/deps-swap.sh +8 -6
  150. package/scripts/lib/heading-slugs.ts +26 -0
  151. package/scripts/lib/update-verify.mjs +90 -0
  152. package/scripts/redeploy +31 -2
  153. package/scripts/redeploy.test.ts +94 -3
  154. package/scripts/tips-lint.test.ts +77 -0
  155. package/scripts/tips-media.ts +31 -0
  156. package/scripts/update-e2e.test.ts +215 -0
  157. package/scripts/update-recover.mjs +130 -0
  158. package/scripts/update-recover.test.ts +144 -0
  159. package/scripts/update-run.mjs +182 -40
  160. package/scripts/update-run.test.ts +208 -2
  161. package/server.ts +3 -2
  162. package/stores/chatJump.ts +2 -6
  163. package/stores/chatPositions.ts +65 -0
  164. package/stores/navHistory.ts +98 -0
  165. package/stores/shortcutsUi.ts +20 -0
  166. package/stores/tips.ts +14 -0
  167. package/components/QuickSwitcher.tsx +0 -286
@@ -0,0 +1,71 @@
1
+ "use client";
2
+
3
+ import { Play } from "lucide-react";
4
+ import { useTheme } from "next-themes";
5
+ import { useRef, useState } from "react";
6
+ import { useMediaQuery } from "@/hooks/useMediaQuery";
7
+ import type { Media } from "@/lib/tips/schema";
8
+
9
+ /** A tip's still or clip, sized by its own dimensions so nothing jumps as it loads. */
10
+ export function TipMedia({ media }: { media: Media }) {
11
+ const { resolvedTheme } = useTheme();
12
+ const reduced = useMediaQuery("(prefers-reduced-motion: reduce)");
13
+ const [played, setPlayed] = useState(false);
14
+ const video = useRef<HTMLVideoElement>(null);
15
+ const dark = !!resolvedTheme && !resolvedTheme.startsWith("light");
16
+ const frame =
17
+ "bg-muted relative max-h-[40dvh] w-full overflow-hidden rounded-lg";
18
+ const style = { aspectRatio: `${media.width} / ${media.height}` };
19
+
20
+ if (media.kind === "image")
21
+ return (
22
+ <div className={frame} style={style}>
23
+ {/* eslint-disable-next-line @next/next/no-img-element */}
24
+ <img
25
+ src={dark && media.srcDark ? media.srcDark : media.src}
26
+ alt={media.alt}
27
+ width={media.width}
28
+ height={media.height}
29
+ loading="lazy"
30
+ decoding="async"
31
+ className="size-full object-contain"
32
+ />
33
+ </div>
34
+ );
35
+
36
+ const still = reduced && !played;
37
+ return (
38
+ <div className={frame} style={style}>
39
+ <video
40
+ ref={video}
41
+ key={media.src}
42
+ src={media.src}
43
+ poster={media.poster}
44
+ aria-label={media.alt}
45
+ width={media.width}
46
+ height={media.height}
47
+ muted
48
+ loop
49
+ playsInline
50
+ autoPlay={!reduced}
51
+ preload={reduced ? "none" : "metadata"}
52
+ className="size-full object-contain"
53
+ />
54
+ {still && (
55
+ <button
56
+ type="button"
57
+ aria-label="Play clip"
58
+ onClick={() => {
59
+ setPlayed(true);
60
+ void video.current?.play();
61
+ }}
62
+ className="absolute inset-0 grid place-items-center bg-black/20"
63
+ >
64
+ <span className="bg-background/90 grid size-11 place-items-center rounded-full shadow">
65
+ <Play className="size-5" />
66
+ </span>
67
+ </button>
68
+ )}
69
+ </div>
70
+ );
71
+ }
@@ -0,0 +1,2 @@
1
+ export { TipButton } from "./TipButton";
2
+ export { TipHost } from "./TipHost";
@@ -0,0 +1,141 @@
1
+ "use client";
2
+
3
+ import { toast } from "sonner";
4
+ import { CheckCircle2, CircleAlert, RotateCcw } from "lucide-react";
5
+ import { SettingToggle } from "@/components/Settings/SettingToggle";
6
+ import {
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectTrigger,
11
+ SelectValue,
12
+ } from "@/components/ui/select";
13
+ import { useAutoUpdateSetting } from "@/data/updates";
14
+ import {
15
+ IDLE_MINUTES,
16
+ type AutoUpdateView,
17
+ type UpdateAttempt,
18
+ type UpdateOutcome,
19
+ } from "@/lib/updates/banner";
20
+
21
+ const OUTCOME: Record<UpdateOutcome, string> = {
22
+ done: "Updated",
23
+ build_failed: "Didn't build; nothing changed",
24
+ rolled_back: "Rolled back",
25
+ broken: "Failed, and the rollback too",
26
+ interrupted: "Cut off; will try again",
27
+ };
28
+
29
+ function when(at: number): string {
30
+ return new Date(at).toLocaleString([], {
31
+ month: "short",
32
+ day: "numeric",
33
+ hour: "numeric",
34
+ minute: "2-digit",
35
+ });
36
+ }
37
+
38
+ function Attempt({ a }: { a: UpdateAttempt }) {
39
+ const Icon =
40
+ a.outcome === "done"
41
+ ? CheckCircle2
42
+ : a.outcome === "rolled_back" || a.outcome === "interrupted"
43
+ ? RotateCcw
44
+ : CircleAlert;
45
+ return (
46
+ <li className="flex items-start gap-2 py-1.5 text-sm">
47
+ <Icon
48
+ className={
49
+ a.outcome === "broken"
50
+ ? "text-destructive mt-0.5 h-4 w-4 shrink-0"
51
+ : "text-muted-foreground mt-0.5 h-4 w-4 shrink-0"
52
+ }
53
+ />
54
+ <div className="min-w-0 flex-1">
55
+ <p className="flex flex-wrap justify-between gap-x-3">
56
+ <span className="font-medium">
57
+ {a.toVersion || a.to.slice(0, 7)}
58
+ <span className="text-muted-foreground font-normal">
59
+ {" "}
60
+ · {OUTCOME[a.outcome]}
61
+ {a.auto ? "" : " (by hand)"}
62
+ </span>
63
+ </span>
64
+ <span className="text-muted-foreground text-xs">
65
+ {when(a.finishedAt)}
66
+ </span>
67
+ </p>
68
+ {a.error && a.outcome !== "done" && (
69
+ <p className="text-muted-foreground text-xs break-words">{a.error}</p>
70
+ )}
71
+ </div>
72
+ </li>
73
+ );
74
+ }
75
+
76
+ // Settings > Updates: the switch for automatic updates, how long everything
77
+ // must be idle first, what it's doing now, and how the last ones went.
78
+ export function AutoUpdates({ auto }: { auto: AutoUpdateView }) {
79
+ const save = useAutoUpdateSetting();
80
+ const onError = (e: Error) => toast.error(e.message);
81
+ return (
82
+ <div className="space-y-3">
83
+ <SettingToggle
84
+ title="Install updates automatically"
85
+ detail="Once everything has been idle for a while. Terminals and chats keep running through the restart, and a version that doesn't start cleanly is rolled back."
86
+ checked={auto.enabled}
87
+ locked={auto.locked}
88
+ onChange={(on) => save.mutate({ auto: on }, { onError })}
89
+ />
90
+ {auto.enabled && (
91
+ <div className="flex flex-wrap items-center justify-between gap-2 px-3">
92
+ <label htmlFor="auto-idle" className="text-sm">
93
+ Idle for
94
+ </label>
95
+ <Select
96
+ value={String(auto.idleMinutes)}
97
+ disabled={auto.idleLocked}
98
+ onValueChange={(v) =>
99
+ save.mutate({ idleMinutes: Number(v) }, { onError })
100
+ }
101
+ >
102
+ <SelectTrigger id="auto-idle" className="min-h-11 w-36 md:min-h-0">
103
+ <SelectValue />
104
+ </SelectTrigger>
105
+ <SelectContent>
106
+ {IDLE_MINUTES.map((m) => (
107
+ <SelectItem key={m} value={String(m)}>
108
+ {m} minutes
109
+ </SelectItem>
110
+ ))}
111
+ {!(IDLE_MINUTES as readonly number[]).includes(
112
+ auto.idleMinutes
113
+ ) && (
114
+ <SelectItem value={String(auto.idleMinutes)}>
115
+ {auto.idleMinutes} minutes
116
+ </SelectItem>
117
+ )}
118
+ </SelectContent>
119
+ </Select>
120
+ </div>
121
+ )}
122
+ {auto.status && auto.status.code !== "off" && (
123
+ <p className="text-muted-foreground px-3 text-xs" role="status">
124
+ {auto.status.text}
125
+ </p>
126
+ )}
127
+ {auto.attempts.length > 0 && (
128
+ <div className="px-3">
129
+ <p className="text-muted-foreground text-xs font-medium">
130
+ Recent updates
131
+ </p>
132
+ <ul className="divide-border divide-y">
133
+ {auto.attempts.map((a) => (
134
+ <Attempt key={a.id} a={a} />
135
+ ))}
136
+ </ul>
137
+ </div>
138
+ )}
139
+ </div>
140
+ );
141
+ }
@@ -173,7 +173,7 @@ describe("UpdateBanner", () => {
173
173
  },
174
174
  });
175
175
  await render();
176
- expect(text()).toContain("Building… step 3 of 4");
176
+ expect(text()).toContain("Building… step 3 of 5");
177
177
  expect(button("Dismiss")).toBeUndefined();
178
178
  });
179
179
 
@@ -196,4 +196,42 @@ describe("UpdateBanner", () => {
196
196
  expect(text()).toBe("");
197
197
  expect(localStorage.getItem("agentos.update.dismissedRun")).toBe("r9");
198
198
  });
199
+
200
+ it("says an automatic update happened, with What's New", async () => {
201
+ view = make({
202
+ available: false,
203
+ run: {
204
+ id: "a1",
205
+ phase: "done",
206
+ auto: true,
207
+ from: "a",
208
+ fromVersion: "0.3.1",
209
+ toVersion: "0.3.5",
210
+ startedAt: Date.now() - 60_000,
211
+ finishedAt: Date.now(),
212
+ },
213
+ });
214
+ await render();
215
+ expect(text()).toContain("updated itself to 0.3.5");
216
+ expect(button("See what's new")).toBeDefined();
217
+ });
218
+
219
+ it("says calmly when an automatic update was rolled back", async () => {
220
+ view = make({
221
+ available: false,
222
+ run: {
223
+ id: "a2",
224
+ phase: "rolled_back",
225
+ auto: true,
226
+ from: "a",
227
+ fromVersion: "0.3.1",
228
+ toVersion: "0.3.5",
229
+ startedAt: 1,
230
+ finishedAt: 2,
231
+ },
232
+ });
233
+ await render();
234
+ expect(text()).toContain("went back to 0.3.1");
235
+ expect(document.querySelector('[role="alert"]')).toBeNull();
236
+ });
199
237
  });
@@ -12,16 +12,24 @@ import {
12
12
  type UpdateView,
13
13
  } from "@/lib/updates/banner";
14
14
  import { cn } from "@/lib/utils";
15
+ import { TipHost } from "@/components/Tips";
15
16
  import { WhatsNewHost } from "@/components/WhatsNew";
16
17
  import { whatsNewActions } from "@/stores/whatsNew";
17
18
  import { UpdateActions } from "./UpdateActions";
18
19
 
19
- const STEPS = ["pulling", "installing", "building", "restarting"] as const;
20
+ const STEPS = [
21
+ "pulling",
22
+ "installing",
23
+ "building",
24
+ "restarting",
25
+ "verifying",
26
+ ] as const;
20
27
  const LABEL: Record<(typeof STEPS)[number], string> = {
21
28
  pulling: "Pulling the new version",
22
29
  installing: "Installing dependencies",
23
30
  building: "Building",
24
31
  restarting: "Restarting",
32
+ verifying: "Checking the new version",
25
33
  };
26
34
  const SLOW_MS = 10 * 60_000;
27
35
 
@@ -79,13 +87,24 @@ export function UpdateBannerView({
79
87
  else if (state === "built")
80
88
  text = <>Built {run?.toVersion}. Restart AgentOS to finish updating.</>;
81
89
  else if (failed) text = <>The update didn&apos;t finish. {run?.error}</>;
90
+ else if (state === "updated")
91
+ text = <>AgentOS updated itself to {run?.toVersion}</>;
92
+ else if (state === "rolled_back")
93
+ text = (
94
+ <>
95
+ An update to {run?.toVersion} didn&apos;t start cleanly, so AgentOS went
96
+ back to {run?.fromVersion}. It won&apos;t try that version again.
97
+ </>
98
+ );
82
99
  else {
83
100
  const step = Math.max(
84
101
  0,
85
102
  STEPS.indexOf(run!.phase as (typeof STEPS)[number])
86
103
  );
87
104
  text =
88
- elapsed > SLOW_MS ? (
105
+ run?.phase === "rolling_back" ? (
106
+ <>The new version didn&apos;t start cleanly; going back…</>
107
+ ) : elapsed > SLOW_MS ? (
89
108
  <>Taking longer than expected. Check the update log, then reload.</>
90
109
  ) : (
91
110
  <>
@@ -99,7 +118,7 @@ export function UpdateBannerView({
99
118
  {state === "available" && !view.canUpdate.ok && view.canUpdate.reason && (
100
119
  <p className="text-muted-foreground text-xs">{view.canUpdate.reason}</p>
101
120
  )}
102
- {failed && <Log lines={run?.log ?? []} />}
121
+ {(failed || state === "rolled_back") && <Log lines={run?.log ?? []} />}
103
122
  {notesOpen && latest && (
104
123
  <ul className="text-muted-foreground list-disc space-y-0.5 pl-4 text-xs">
105
124
  {latest.notes.map((n, i) => (
@@ -149,7 +168,7 @@ export function UpdateBannerView({
149
168
  />
150
169
  </button>
151
170
  )}
152
- {state === "available" && (
171
+ {(state === "available" || state === "updated") && (
153
172
  <button
154
173
  type="button"
155
174
  className="text-muted-foreground hover:text-foreground inline-flex min-h-11 items-center text-xs underline md:min-h-0"
@@ -206,10 +225,17 @@ export function UpdateBanner() {
206
225
  dismissedVersion: dismissed.version,
207
226
  dismissedRun: dismissed.run,
208
227
  });
209
- if (state === "hidden" || !view) return <WhatsNewHost />;
228
+ if (state === "hidden" || !view)
229
+ return (
230
+ <>
231
+ <WhatsNewHost />
232
+ <TipHost />
233
+ </>
234
+ );
210
235
  return (
211
236
  <>
212
237
  <WhatsNewHost />
238
+ <TipHost />
213
239
  <UpdateBannerView
214
240
  state={state}
215
241
  view={view}
@@ -4,12 +4,14 @@ import { toast } from "sonner";
4
4
  import { Loader2 } from "lucide-react";
5
5
  import { Button } from "@/components/ui/button";
6
6
  import { SettingToggle } from "@/components/Settings/SettingToggle";
7
+ import { useSetTipsOff, useTips } from "@/data/tips";
7
8
  import {
8
9
  useCheckForUpdates,
9
10
  useUpdateBannerSetting,
10
11
  useUpdates,
11
12
  } from "@/data/updates";
12
13
  import { isActivePhase } from "@/lib/updates/banner";
14
+ import { AutoUpdates } from "./AutoUpdates";
13
15
  import { UpdateActions } from "./UpdateActions";
14
16
 
15
17
  function ago(at: number | null): string {
@@ -32,7 +34,8 @@ function Row({ label, value }: { label: string; value: React.ReactNode }) {
32
34
  );
33
35
  }
34
36
 
35
- // Settings > Updates: what's installed, what's out, and the switch for the banner.
37
+ // Settings > Updates: what's installed, what's out, automatic updates, and
38
+ // the switch for the banner.
36
39
  export function UpdatesSection() {
37
40
  const { data: view, isLoading, error } = useUpdates();
38
41
  const check = useCheckForUpdates();
@@ -93,7 +96,7 @@ export function UpdatesSection() {
93
96
  The last check failed: {view.checkError}
94
97
  </p>
95
98
  )}
96
- {view.run?.phase === "failed" && (
99
+ {view.run?.phase === "failed" && !view.run.auto && (
97
100
  <p className="text-destructive text-sm" role="alert">
98
101
  The last update failed. {view.run.error}
99
102
  </p>
@@ -136,15 +139,33 @@ export function UpdatesSection() {
136
139
  )}
137
140
  </div>
138
141
 
142
+ {view.auto && <AutoUpdates auto={view.auto} />}
143
+
139
144
  <SettingToggle
140
145
  title="Show update banner"
141
- detail="Tell everyone using this AgentOS when a newer version is out. Nothing updates unless someone chooses to."
146
+ detail="Tell everyone using this AgentOS when a newer version is out, and when it updated itself."
142
147
  checked={view.bannerEnabled}
143
148
  locked={view.bannerLocked}
144
149
  onChange={(on) =>
145
150
  banner.mutate(on, { onError: (e) => toast.error(e.message) })
146
151
  }
147
152
  />
153
+ <TipsToggle />
148
154
  </div>
149
155
  );
150
156
  }
157
+
158
+ function TipsToggle() {
159
+ const { data } = useTips();
160
+ const setOff = useSetTipsOff();
161
+ return (
162
+ <SettingToggle
163
+ title="Show Tip of the Day"
164
+ detail="A short tip about something AgentOS can do, behind the lightbulb in the sidebar. It never opens on its own."
165
+ checked={!!data && !data.off}
166
+ onChange={(on) =>
167
+ setOff.mutate(!on, { onError: (e) => toast.error(e.message) })
168
+ }
169
+ />
170
+ );
171
+ }
@@ -6,8 +6,9 @@ import { UpdateBanner } from "@/components/Updates";
6
6
  import { PaneLayout } from "@/components/PaneLayout";
7
7
  import { PaneBarSlotsProvider } from "@/components/Pane/PaneBarSlots";
8
8
  import type { ViewProps } from "./types";
9
- import { QuickSwitcher, StartServerDialog } from "./lazy";
9
+ import { CodeSearchDialog, StartServerDialog } from "./lazy";
10
10
  import { fileOpenActions } from "@/stores/fileOpen";
11
+ import { HistoryButtons } from "@/components/Nav/HistoryButtons";
11
12
  import { useSidebarPinned } from "@/hooks/useSidebarPinned";
12
13
 
13
14
  export function DesktopView({
@@ -57,7 +58,10 @@ export function DesktopView({
57
58
  <div className="min-h-0 flex-1">
58
59
  <PaneBarSlotsProvider
59
60
  leading={
60
- <SidebarToggle isPinned={isPinned} togglePin={togglePin} />
61
+ <>
62
+ <SidebarToggle isPinned={isPinned} togglePin={togglePin} />
63
+ <HistoryButtons />
64
+ </>
61
65
  }
62
66
  trailing={<DesktopAppActions showNew={!isPinned} />}
63
67
  >
@@ -67,16 +71,10 @@ export function DesktopView({
67
71
  </div>
68
72
 
69
73
  {/* Dialogs */}
70
- <QuickSwitcher
71
- sessions={sessions}
74
+ <CodeSearchDialog
72
75
  open={showQuickSwitcher}
73
76
  onOpenChange={setShowQuickSwitcher}
74
- currentSessionId={focusedActiveTab?.sessionId ?? undefined}
75
77
  activeSessionWorkingDir={activeSession?.working_directory ?? undefined}
76
- onSelectSession={(sessionId) => {
77
- const session = sessions.find((s) => s.id === sessionId);
78
- if (session) selectSession(session);
79
- }}
80
78
  onSelectFile={(file, line) => {
81
79
  // Convert relative path to absolute by prepending working directory
82
80
  const absolutePath = activeSession?.working_directory
@@ -6,7 +6,7 @@ import { PaneLayout } from "@/components/PaneLayout";
6
6
  import { SwipeSidebar } from "@/components/mobile/SwipeSidebar";
7
7
  import { UpdateBanner } from "@/components/Updates";
8
8
  import type { ViewProps } from "./types";
9
- import { QuickSwitcher, StartServerDialog } from "./lazy";
9
+ import { CodeSearchDialog, StartServerDialog } from "./lazy";
10
10
  import { fileOpenActions } from "@/stores/fileOpen";
11
11
 
12
12
  export function MobileView({
@@ -66,16 +66,10 @@ export function MobileView({
66
66
  </div>
67
67
 
68
68
  {/* Dialogs */}
69
- <QuickSwitcher
70
- sessions={sessions}
69
+ <CodeSearchDialog
71
70
  open={showQuickSwitcher}
72
71
  onOpenChange={setShowQuickSwitcher}
73
- currentSessionId={focusedActiveTab?.sessionId ?? undefined}
74
72
  activeSessionWorkingDir={activeSession?.working_directory ?? undefined}
75
- onSelectSession={(sessionId) => {
76
- const session = sessions.find((s) => s.id === sessionId);
77
- if (session) attachToSession(session);
78
- }}
79
73
  onSelectFile={(file, line) => {
80
74
  // Convert relative path to absolute by prepending working directory
81
75
  const absolutePath = activeSession?.working_directory
@@ -2,8 +2,8 @@ import dynamic from "next/dynamic";
2
2
 
3
3
  // Dialogs both views hold closed until asked for: their code loads after the
4
4
  // first paint rather than with it.
5
- export const QuickSwitcher = dynamic(
6
- () => import("@/components/QuickSwitcher").then((m) => m.QuickSwitcher),
5
+ export const CodeSearchDialog = dynamic(
6
+ () => import("@/components/CodeSearchDialog").then((m) => m.CodeSearchDialog),
7
7
  { ssr: false }
8
8
  );
9
9
  export const StartServerDialog = dynamic(