@saadnvd1/agent-os 0.3.782 → 0.3.790

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 (72) hide show
  1. package/app/api/hosts/[id]/agents/route.ts +6 -4
  2. package/app/api/machine/route.ts +5 -3
  3. package/app/api/whats-new/route.ts +47 -0
  4. package/components/Chat/ChatPanel.tsx +2 -0
  5. package/components/Chat/ModelPicker.tsx +82 -42
  6. package/components/Chat/composer/ComposerBody.tsx +3 -0
  7. package/components/Chat/composer/Toolbar.tsx +3 -0
  8. package/components/Draft/AgentModelChip.test.ts +1 -0
  9. package/components/Draft/AgentModelChip.tsx +36 -93
  10. package/components/Draft/Chip.tsx +27 -0
  11. package/components/Draft/DraftChips.tsx +5 -1
  12. package/components/Draft/DraftPanel.tsx +0 -21
  13. package/components/Draft/catalogBlocker.ts +19 -0
  14. package/components/Draft/picker/AgentModelPicker.tsx +152 -0
  15. package/components/Draft/picker/AgentRail.tsx +145 -0
  16. package/components/Draft/picker/ModelList.tsx +136 -0
  17. package/components/Draft/picker/ModelNote.tsx +15 -0
  18. package/components/Draft/picker/PickerSheet.tsx +61 -0
  19. package/components/Draft/picker/SameAsLast.tsx +52 -0
  20. package/components/Draft/picker/SearchField.tsx +48 -0
  21. package/components/Draft/picker/picker.test.ts +152 -0
  22. package/components/Draft/picker/sections.test.ts +115 -0
  23. package/components/Draft/picker/sections.ts +121 -0
  24. package/components/Draft/picker/useListKeys.ts +54 -0
  25. package/components/Draft/picker/useModelSearch.ts +66 -0
  26. package/components/SidebarFooter.tsx +5 -1
  27. package/components/Updates/UpdateBanner.test.ts +1 -1
  28. package/components/Updates/UpdateBanner.tsx +28 -14
  29. package/components/WhatsNew/WhatsNewButton.tsx +33 -0
  30. package/components/WhatsNew/WhatsNewDialog.tsx +169 -0
  31. package/components/WhatsNew/WhatsNewHost.tsx +17 -0
  32. package/components/WhatsNew/WhatsNewRows.tsx +62 -0
  33. package/components/WhatsNew/index.ts +2 -0
  34. package/components/ui/dialog.tsx +12 -1
  35. package/content/whats-new.json +1782 -0
  36. package/data/chat/useChat.ts +3 -0
  37. package/data/machines/index.ts +13 -4
  38. package/data/machines/keys.ts +2 -1
  39. package/data/whats-new/index.ts +44 -0
  40. package/hooks/useWhatsNewSeen.ts +50 -0
  41. package/lib/chat/drivers/copilot.ts +9 -6
  42. package/lib/chat/drivers/opencode.ts +1 -1
  43. package/lib/chat/drivers/pi.ts +1 -1
  44. package/lib/chat/events.ts +6 -0
  45. package/lib/chat/settings.ts +2 -0
  46. package/lib/machines/catalog.ts +5 -1
  47. package/lib/machines/recent.ts +31 -0
  48. package/lib/machines/remote.ts +28 -9
  49. package/lib/machines/report.ts +7 -2
  50. package/lib/machines/types.ts +12 -0
  51. package/lib/orchestrator/review-prompt.ts +2 -0
  52. package/lib/security/demo.ts +2 -0
  53. package/lib/tasks/brief.ts +1 -0
  54. package/lib/whats-new/ahead.ts +28 -0
  55. package/lib/whats-new/areas.ts +76 -0
  56. package/lib/whats-new/claude.ts +35 -0
  57. package/lib/whats-new/collect.ts +68 -0
  58. package/lib/whats-new/generate.ts +107 -0
  59. package/lib/whats-new/lint.ts +76 -0
  60. package/lib/whats-new/load.ts +59 -0
  61. package/lib/whats-new/rows.ts +79 -0
  62. package/lib/whats-new/schema.ts +41 -0
  63. package/lib/whats-new/summarize.ts +91 -0
  64. package/lib/whats-new/unread.ts +10 -0
  65. package/package.json +2 -1
  66. package/scripts/autodeploy +33 -10
  67. package/scripts/autodeploy.test.ts +92 -2
  68. package/scripts/publish-detached +20 -0
  69. package/scripts/whats-new.ts +118 -0
  70. package/stores/whatsNew.ts +16 -0
  71. package/components/Draft/draftModels.test.ts +0 -31
  72. package/components/Draft/draftModels.ts +0 -17
@@ -0,0 +1,169 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import { Sparkles, X } from "lucide-react";
5
+ import { Button } from "@/components/ui/button";
6
+ import {
7
+ Dialog,
8
+ DialogClose,
9
+ DialogContent,
10
+ DialogDescription,
11
+ DialogTitle,
12
+ } from "@/components/ui/dialog";
13
+ import { ShimmeringLoader } from "@/components/ui/skeleton";
14
+ import {
15
+ useWhatsNewAhead,
16
+ useWhatsNewHead,
17
+ useWhatsNewList,
18
+ } from "@/data/whats-new";
19
+ import { useUpdates } from "@/data/updates";
20
+ import { useMediaQuery } from "@/hooks/useMediaQuery";
21
+ import { whatsNewStore } from "@/stores/whatsNew";
22
+ import { useWhatsNewSeen } from "@/hooks/useWhatsNewSeen";
23
+ import { buildRows, type Row } from "@/lib/whats-new/rows";
24
+ import { unreadCount } from "@/lib/whats-new/unread";
25
+ import { WhatsNewRow } from "./WhatsNewRows";
26
+
27
+ function Empty() {
28
+ return (
29
+ <div className="text-muted-foreground flex flex-1 flex-col items-center justify-center gap-2 px-8 py-12 text-center">
30
+ <span className="bg-muted grid size-11 place-items-center rounded-xl">
31
+ <Sparkles className="size-5" />
32
+ </span>
33
+ <h3 className="text-foreground text-[15px] font-semibold">
34
+ Nothing to show yet
35
+ </h3>
36
+ <p className="max-w-[17.5rem] text-[13px]">
37
+ Changes appear here after your next update.
38
+ </p>
39
+ </div>
40
+ );
41
+ }
42
+
43
+ export function WhatsNewDialog({
44
+ open,
45
+ onOpenChange,
46
+ }: {
47
+ open: boolean;
48
+ onOpenChange: (open: boolean) => void;
49
+ }) {
50
+ const wide = useMediaQuery("(min-width: 768px)");
51
+ const { data: head } = useWhatsNewHead();
52
+ const { seen, markSeen } = useWhatsNewSeen(head);
53
+ const { data: updates } = useUpdates();
54
+ const waiting = !!updates?.available;
55
+ const list = useWhatsNewList(open);
56
+ const ahead = useWhatsNewAhead(open && waiting);
57
+ const [previewing, setPreviewing] = useState(whatsNewStore.preview);
58
+ // Where the last visit ended, read once as the dialog opens: opening marks
59
+ // everything seen, and the divider has to stay where it was.
60
+ const [baseline] = useState(seen);
61
+ useEffect(() => {
62
+ markSeen();
63
+ // Once, on open; `seen` changes as a result of this.
64
+ // eslint-disable-next-line react-hooks/exhaustive-deps
65
+ }, []);
66
+
67
+ const entries = useMemo(
68
+ () => list.data?.pages.flatMap((p) => p.entries) ?? [],
69
+ [list.data]
70
+ );
71
+ const rows = useMemo<Row[]>(() => {
72
+ const own = buildRows(entries, baseline);
73
+ const coming = previewing && ahead.data?.length ? ahead.data : [];
74
+ return coming.length
75
+ ? [
76
+ { type: "heading", key: "ahead", label: "Arrives with the update" },
77
+ ...buildRows(coming, null),
78
+ { type: "heading", key: "installed", label: "Already installed" },
79
+ ...own,
80
+ ]
81
+ : own;
82
+ }, [entries, baseline, previewing, ahead.data]);
83
+
84
+ const unread =
85
+ head && baseline !== null ? unreadCount(head.recent, baseline) : 0;
86
+ const version = updates?.current?.version;
87
+ const aheadCount = ahead.data?.length ?? 0;
88
+
89
+ return (
90
+ <Dialog open={open} onOpenChange={onOpenChange}>
91
+ <DialogContent
92
+ showCloseButton={false}
93
+ placement={wide ? "center" : "sheet"}
94
+ className="flex max-h-[85dvh] flex-col gap-0 overflow-hidden p-0 max-md:h-[85dvh] sm:max-w-[34rem]"
95
+ >
96
+ <div className="bg-muted-foreground/40 mx-auto mt-2 h-1 w-9 rounded-full md:hidden" />
97
+ <div className="border-border/50 flex items-center gap-2 border-b px-4 py-3">
98
+ <div className="min-w-0 flex-1">
99
+ <DialogTitle>What&apos;s New</DialogTitle>
100
+ <DialogDescription className="mt-1 text-xs">
101
+ {[
102
+ version && `AgentOS ${version}`,
103
+ unread > 0
104
+ ? `${unread} new since you last looked`
105
+ : "You're up to date",
106
+ ]
107
+ .filter(Boolean)
108
+ .join(" · ")}
109
+ </DialogDescription>
110
+ </div>
111
+ <DialogClose asChild>
112
+ <Button
113
+ variant="ghost"
114
+ size="icon-sm"
115
+ aria-label="Close"
116
+ className="-mr-2 size-11 shrink-0 md:size-8"
117
+ >
118
+ <X className="size-4" />
119
+ </Button>
120
+ </DialogClose>
121
+ </div>
122
+
123
+ <div className="min-h-0 flex-1 overflow-y-auto pb-2">
124
+ {waiting && aheadCount > 0 && (
125
+ <div className="bg-primary/10 mx-3 mt-3 flex items-center gap-3 rounded-lg px-3 py-2 text-[12.5px]">
126
+ <span className="flex-1">
127
+ <b className="font-semibold">Update available.</b> {aheadCount}{" "}
128
+ more {aheadCount === 1 ? "change is" : "changes are"} waiting.
129
+ </span>
130
+ <button
131
+ type="button"
132
+ aria-pressed={previewing}
133
+ onClick={() => setPreviewing(!previewing)}
134
+ className="text-primary -my-1 min-h-11 px-2 font-semibold md:min-h-8"
135
+ >
136
+ {previewing ? "Hide" : "Preview"}
137
+ </button>
138
+ </div>
139
+ )}
140
+ {list.isPending ? (
141
+ <div className="space-y-3 p-4" aria-busy>
142
+ {[0, 1, 2, 3].map((i) => (
143
+ <ShimmeringLoader
144
+ key={i}
145
+ delayIndex={i}
146
+ className="h-9 w-full"
147
+ />
148
+ ))}
149
+ </div>
150
+ ) : rows.length === 0 ? (
151
+ <Empty />
152
+ ) : (
153
+ rows.map((row) => <WhatsNewRow key={row.key} row={row} />)
154
+ )}
155
+ {list.hasNextPage && (
156
+ <Button
157
+ variant="secondary"
158
+ className="mx-4 my-2 min-h-11 w-[calc(100%-2rem)]"
159
+ disabled={list.isFetchingNextPage}
160
+ onClick={() => list.fetchNextPage()}
161
+ >
162
+ Show older changes
163
+ </Button>
164
+ )}
165
+ </div>
166
+ </DialogContent>
167
+ </Dialog>
168
+ );
169
+ }
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ import { useSnapshot } from "valtio";
4
+ import { whatsNewActions, whatsNewStore } from "@/stores/whatsNew";
5
+ import { WhatsNewDialog } from "./WhatsNewDialog";
6
+
7
+ /** Mounted once, near the top of the app: the footer row and the update banner both open it. */
8
+ export function WhatsNewHost() {
9
+ const { open } = useSnapshot(whatsNewStore);
10
+ if (!open) return null;
11
+ return (
12
+ <WhatsNewDialog
13
+ open
14
+ onOpenChange={(next) => !next && whatsNewActions.close()}
15
+ />
16
+ );
17
+ }
@@ -0,0 +1,62 @@
1
+ import { StatusPill } from "@/components/ui/status-pill";
2
+ import type { Row } from "@/lib/whats-new/rows";
3
+ import { KIND_LABEL, type Kind } from "@/lib/whats-new/schema";
4
+
5
+ const TONE = { new: "primary", improved: "muted", fixed: "green" } as const;
6
+
7
+ export function KindChip({ kind }: { kind: Kind }) {
8
+ return (
9
+ <StatusPill
10
+ tone={TONE[kind]}
11
+ className="label-mono mt-0.5 w-[4.5rem] shrink-0 justify-center py-1 tracking-wider"
12
+ >
13
+ {KIND_LABEL[kind]}
14
+ </StatusPill>
15
+ );
16
+ }
17
+
18
+ export function WhatsNewRow({ row }: { row: Row }) {
19
+ if (row.type === "day")
20
+ return (
21
+ <h3 className="label-mono text-muted-foreground bg-popover sticky top-0 z-10 px-4 pt-4 pb-1.5">
22
+ {row.label}
23
+ </h3>
24
+ );
25
+ if (row.type === "heading")
26
+ return (
27
+ <h3 className="label-mono text-primary px-4 pt-4 pb-1.5">{row.label}</h3>
28
+ );
29
+ if (row.type === "divider")
30
+ return (
31
+ <div className="text-primary my-2 flex items-center gap-3 px-4">
32
+ <span className="bg-primary/30 h-px flex-1" />
33
+ <span className="label-mono">{row.label}</span>
34
+ <span className="bg-primary/30 h-px flex-1" />
35
+ </div>
36
+ );
37
+ const { entry } = row;
38
+ return (
39
+ <div className="flex items-start gap-3 px-4 py-2">
40
+ <KindChip kind={entry.kind} />
41
+ <div className="min-w-0 flex-1">
42
+ <p className="text-[13.5px] leading-snug">{entry.text}</p>
43
+ {entry.note && (
44
+ <p className="text-muted-foreground mt-0.5 text-xs">{entry.note}</p>
45
+ )}
46
+ </div>
47
+ {entry.url ? (
48
+ <a
49
+ href={entry.url}
50
+ target="_blank"
51
+ rel="noreferrer noopener"
52
+ aria-label={`Pull request ${entry.id}`}
53
+ className="text-muted-foreground hover:text-foreground -my-2 -mr-2 inline-flex min-h-11 min-w-11 items-center justify-center text-[11px] md:min-h-8 md:min-w-8"
54
+ >
55
+ #{entry.id}
56
+ </a>
57
+ ) : (
58
+ <span className="text-muted-foreground text-[11px]">#{entry.id}</span>
59
+ )}
60
+ </div>
61
+ );
62
+ }
@@ -0,0 +1,2 @@
1
+ export { WhatsNewButton } from "./WhatsNewButton";
2
+ export { WhatsNewHost } from "./WhatsNewHost";
@@ -46,13 +46,23 @@ function DialogOverlay({
46
46
  );
47
47
  }
48
48
 
49
+ const PLACEMENT = {
50
+ center:
51
+ "top-[50%] left-[50%] grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-2xl p-6 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:max-w-lg",
52
+ // Rises from the bottom edge on a phone; the caller sets its height.
53
+ sheet:
54
+ "inset-x-0 bottom-0 flex w-full flex-col rounded-t-2xl pb-[env(safe-area-inset-bottom)] data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
55
+ } as const;
56
+
49
57
  function DialogContent({
50
58
  className,
51
59
  children,
52
60
  showCloseButton = true,
61
+ placement = "center",
53
62
  ...props
54
63
  }: React.ComponentProps<typeof DialogPrimitive.Content> & {
55
64
  showCloseButton?: boolean;
65
+ placement?: keyof typeof PLACEMENT;
56
66
  }) {
57
67
  return (
58
68
  <DialogPortal data-slot="dialog-portal">
@@ -60,7 +70,8 @@ function DialogContent({
60
70
  <DialogPrimitive.Content
61
71
  data-slot="dialog-content"
62
72
  className={cn(
63
- "popover-surface data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-2xl p-6 duration-200 outline-none sm:max-w-lg",
73
+ "popover-surface data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed z-50 duration-200 outline-none",
74
+ PLACEMENT[placement],
64
75
  className
65
76
  )}
66
77
  {...props}