@saadnvd1/agent-os 0.3.792 → 0.3.799
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/busy/route.ts +2 -2
- package/app/api/tips/route.test.ts +56 -0
- package/app/api/tips/route.ts +45 -0
- package/app/api/updates/health/route.ts +22 -0
- package/app/api/updates/route.test.ts +53 -0
- package/app/api/updates/settings/route.ts +37 -10
- package/components/Chat/ChatPanel.tsx +28 -0
- package/components/Chat/needs-more-history.test.ts +28 -0
- package/components/Chat/needs-more-history.ts +14 -0
- package/components/Recipes/TimerFields.tsx +2 -1
- package/components/Schedules/ScheduleForm.tsx +1 -1
- package/components/Settings/SettingsDialog.tsx +1 -1
- package/components/SidebarFooter.tsx +2 -0
- package/components/Tips/TipButton.tsx +39 -0
- package/components/Tips/TipDialog.tsx +148 -0
- package/components/Tips/TipHost.tsx +14 -0
- package/components/Tips/TipMedia.tsx +71 -0
- package/components/Tips/index.ts +2 -0
- package/components/Updates/AutoUpdates.tsx +141 -0
- package/components/Updates/UpdateBanner.test.ts +39 -1
- package/components/Updates/UpdateBanner.tsx +31 -5
- package/components/Updates/UpdatesSection.tsx +24 -3
- package/content/tips.json +456 -0
- package/content/whats-new.json +32 -0
- package/data/tips/index.ts +49 -0
- package/data/updates/index.ts +5 -0
- package/hooks/useReloadWhenUpdated.ts +5 -1
- package/lib/busy.ts +23 -1
- package/lib/chat/drivers/copilot-approvals.ts +2 -0
- package/lib/chat/drivers/copilot-ask.ts +232 -0
- package/lib/chat/drivers/copilot-mapper.ts +9 -0
- package/lib/chat/drivers/copilot.ts +18 -4
- package/lib/db/migrations.ts +21 -0
- package/lib/recipes/timer.ts +3 -1
- package/lib/schedules/cron.ts +14 -5
- package/lib/schedules/store.ts +3 -2
- package/lib/security/demo.ts +2 -0
- package/lib/tasks/finish.ts +5 -5
- package/lib/tips/conditions.ts +38 -0
- package/lib/tips/index.ts +2 -0
- package/lib/tips/load.ts +26 -0
- package/lib/tips/media.ts +101 -0
- package/lib/tips/pick.ts +35 -0
- package/lib/tips/schema.ts +82 -0
- package/lib/tips/state.ts +26 -0
- package/lib/tips/view.ts +60 -0
- package/lib/updates/auto.ts +149 -0
- package/lib/updates/banner.ts +78 -3
- package/lib/updates/check.ts +64 -8
- package/lib/updates/history.ts +99 -0
- package/lib/updates/run.ts +334 -23
- package/lib/updates/settings.ts +66 -0
- package/lib/updates/view.ts +14 -3
- package/package.json +2 -2
- package/public/tips/.gitkeep +0 -0
- package/public/tips/chat-slash-menu-poster.webp +0 -0
- package/public/tips/chat-slash-menu.mp4 +0 -0
- package/scripts/autodeploy +4 -0
- package/scripts/autodeploy.test.ts +12 -0
- package/scripts/docs-links.test.ts +1 -25
- package/scripts/lib/commands.sh +32 -1
- package/scripts/lib/deps-swap.sh +8 -6
- package/scripts/lib/heading-slugs.ts +26 -0
- package/scripts/lib/update-verify.mjs +90 -0
- package/scripts/redeploy +31 -2
- package/scripts/redeploy.test.ts +94 -3
- package/scripts/tips-lint.test.ts +77 -0
- package/scripts/tips-media.ts +31 -0
- package/scripts/update-e2e.test.ts +215 -0
- package/scripts/update-recover.mjs +130 -0
- package/scripts/update-recover.test.ts +144 -0
- package/scripts/update-run.mjs +182 -40
- package/scripts/update-run.test.ts +208 -2
- package/server.ts +3 -2
- package/stores/tips.ts +14 -0
|
@@ -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,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
|
|
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 = [
|
|
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'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't start cleanly, so AgentOS went
|
|
96
|
+
back to {run?.fromVersion}. It won'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
|
-
|
|
105
|
+
run?.phase === "rolling_back" ? (
|
|
106
|
+
<>The new version didn'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)
|
|
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,
|
|
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
|
|
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
|
+
}
|