@saadnvd1/agent-os 0.3.770 → 0.3.775

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 (52) hide show
  1. package/app/api/sessions/[id]/artifacts/[artifactId]/compare/route.ts +41 -0
  2. package/app/api/shares/preview/route.ts +6 -1
  3. package/app/api/shares/route.test.ts +51 -0
  4. package/app/page.tsx +1 -0
  5. package/components/Artifacts/ArtifactActions.tsx +27 -3
  6. package/components/Artifacts/ArtifactPreview.tsx +27 -12
  7. package/components/Artifacts/Compare/DiffView.tsx +116 -0
  8. package/components/Artifacts/Compare/SideBySide.tsx +60 -0
  9. package/components/Artifacts/Compare/VersionPicker.tsx +47 -0
  10. package/components/Artifacts/Compare/index.tsx +191 -0
  11. package/components/Chat/Messages.tsx +6 -0
  12. package/components/Chat/ShareFromHere.tsx +39 -0
  13. package/components/CommandPalette/CommandPalette.tsx +5 -4
  14. package/components/CommandPalette/CommandsBody.tsx +97 -131
  15. package/components/CommandPalette/PaletteRows.tsx +79 -0
  16. package/components/CommandPalette/PaletteTabs.tsx +51 -0
  17. package/components/CommandPalette/conversations/ConversationSearch.tsx +13 -11
  18. package/components/CommandPalette/useAppCommands.ts +14 -0
  19. package/components/CommandPalette/useConversationRows.ts +55 -0
  20. package/components/SessionList/useSessionSwitching.ts +2 -2
  21. package/components/Share/MessageList.tsx +87 -0
  22. package/components/Share/RangePicker.tsx +59 -0
  23. package/components/Share/ShareDialog.tsx +113 -52
  24. package/data/session-artifacts/keys.ts +2 -0
  25. package/data/session-artifacts/queries.ts +25 -0
  26. package/data/shares/queries.ts +11 -0
  27. package/hooks/usePaletteCommands.ts +4 -1
  28. package/lib/artifacts/actions.ts +11 -1
  29. package/lib/artifacts/compare/compareVersions.ts +66 -0
  30. package/lib/artifacts/compare/diff.ts +178 -0
  31. package/lib/artifacts/compare/extractText.ts +64 -0
  32. package/lib/artifacts/compare/index.ts +11 -0
  33. package/lib/artifacts/compare/types.ts +44 -0
  34. package/lib/artifacts/match.ts +64 -0
  35. package/lib/artifacts/store.ts +39 -10
  36. package/lib/artifacts/tools.ts +9 -4
  37. package/lib/artifacts/versions.ts +2 -1
  38. package/lib/palette/registry.ts +71 -17
  39. package/lib/palette/sections.ts +143 -0
  40. package/lib/palette/tabs.ts +21 -0
  41. package/lib/palette/usage.ts +81 -0
  42. package/lib/security/demo.ts +3 -0
  43. package/lib/share/markdown.ts +3 -0
  44. package/lib/share/range.ts +189 -0
  45. package/lib/share/render.tsx +3 -0
  46. package/lib/share/service.ts +64 -19
  47. package/lib/share/snapshot.ts +39 -2
  48. package/lib/share/styles.ts +2 -0
  49. package/lib/share/view.ts +11 -0
  50. package/package.json +1 -1
  51. package/stores/palette.ts +13 -11
  52. package/stores/shareUi.ts +6 -1
@@ -16,7 +16,11 @@ import {
16
16
  renderChatSnapshot,
17
17
  } from "./render";
18
18
  import { artifactToMarkdown, chatToMarkdown } from "./markdown";
19
- import { buildArtifactSnapshot, buildChatSnapshot } from "./snapshot";
19
+ import {
20
+ RangeGone,
21
+ buildArtifactSnapshot,
22
+ buildChatSnapshot,
23
+ } from "./snapshot";
20
24
 
21
25
  export interface ShareRequest {
22
26
  kind: "chat" | "artifact";
@@ -25,6 +29,9 @@ export interface ShareRequest {
25
29
  includeTools: boolean;
26
30
  /** Messages the reader left out in the preview. */
27
31
  dropIds?: string[];
32
+ /** The part of the chat shared: from this message to that one, ends included. */
33
+ fromId?: string;
34
+ toId?: string;
28
35
  follow?: "latest";
29
36
  sharedAt: string;
30
37
  }
@@ -34,6 +41,7 @@ export interface ItemRow {
34
41
  type: string;
35
42
  tools: boolean;
36
43
  excerpt: string;
44
+ createdAt?: number;
37
45
  }
38
46
 
39
47
  export interface Page {
@@ -87,6 +95,15 @@ export function parseRequest(raw: unknown, sharedAt?: unknown): ShareRequest {
87
95
  throw new BadShare("dropIds must be a list of ids", 400);
88
96
  dropIds = b.dropIds as string[];
89
97
  }
98
+ const end = (key: "fromId" | "toId") => {
99
+ const v = b[key];
100
+ if (v === undefined) return undefined;
101
+ if (typeof v !== "string" || !v || v.length > 200)
102
+ throw new BadShare(`${key} must be a message id`, 400);
103
+ return v;
104
+ };
105
+ const fromId = end("fromId");
106
+ const toId = end("toId");
90
107
  if (b.follow !== undefined && b.follow !== "latest")
91
108
  throw new BadShare("follow must be latest", 400);
92
109
  if (b.follow && kind !== "artifact")
@@ -100,11 +117,41 @@ export function parseRequest(raw: unknown, sharedAt?: unknown): ShareRequest {
100
117
  artifactId,
101
118
  includeTools: b.includeTools === true,
102
119
  dropIds,
120
+ fromId,
121
+ toId,
103
122
  follow: b.follow === "latest" ? "latest" : undefined,
104
123
  sharedAt: at,
105
124
  };
106
125
  }
107
126
 
127
+ const rowOf = ({ id, type, tools, excerpt, createdAt }: ItemRow): ItemRow => ({
128
+ id,
129
+ type,
130
+ tools,
131
+ excerpt,
132
+ createdAt,
133
+ });
134
+
135
+ function chatSession(sessionId: string) {
136
+ let session;
137
+ try {
138
+ session = getSession(sessionId);
139
+ } catch {
140
+ throw new BadShare("No such chat", 404);
141
+ }
142
+ if (session.view !== "chat")
143
+ throw new BadShare("Only chats can be shared", 400);
144
+ }
145
+
146
+ /** Every shareable message of a chat, tool rows included, in order. */
147
+ export async function listChatItems(req: ShareRequest): Promise<ItemRow[]> {
148
+ chatSession(req.sessionId!);
149
+ const all = await buildChatSnapshot(req.sessionId!, shareContext(), {
150
+ includeTools: true,
151
+ });
152
+ return all.items.map(rowOf);
153
+ }
154
+
108
155
  const sha256 = (s: string) => createHash("sha256").update(s).digest("hex");
109
156
 
110
157
  /** Binds both parts: neither can change without the hash changing. */
@@ -120,29 +167,27 @@ export async function buildPage(req: ShareRequest): Promise<Page> {
120
167
  let items: ItemRow[] = [];
121
168
 
122
169
  if (req.kind === "chat") {
123
- let session;
124
- try {
125
- session = getSession(req.sessionId!);
126
- } catch {
127
- throw new BadShare("No such chat", 404);
128
- }
129
- if (session.view !== "chat")
130
- throw new BadShare("Only chats can be shared", 400);
170
+ chatSession(req.sessionId!);
131
171
  const all = await buildChatSnapshot(req.sessionId!, ctx, {
132
172
  includeTools: req.includeTools,
133
173
  });
134
- items = all.items.map(({ id, type, tools, excerpt }) => ({
135
- id,
136
- type,
137
- tools,
138
- excerpt,
139
- }));
140
- const snap = req.dropIds?.length
141
- ? await buildChatSnapshot(req.sessionId!, ctx, {
174
+ items = all.items.map(rowOf);
175
+ const narrowed =
176
+ req.dropIds?.length || req.fromId !== undefined || req.toId !== undefined;
177
+ let snap = all;
178
+ if (narrowed) {
179
+ try {
180
+ snap = await buildChatSnapshot(req.sessionId!, ctx, {
142
181
  includeTools: req.includeTools,
143
182
  dropIds: req.dropIds,
144
- })
145
- : all;
183
+ fromId: req.fromId,
184
+ toId: req.toId,
185
+ });
186
+ } catch (err) {
187
+ if (err instanceof RangeGone) throw new BadShare(err.message, 409);
188
+ throw err;
189
+ }
190
+ }
146
191
  if (!snap.items.length)
147
192
  throw new BadShare("Nothing to share: every message is left out", 400);
148
193
  html = renderChatSnapshot(snap, { sharedAt: req.sharedAt });
@@ -15,6 +15,7 @@ import {
15
15
  type Hits,
16
16
  type ShareContext,
17
17
  } from "./redact";
18
+ import { windowOf } from "./range";
18
19
 
19
20
  export type SnapshotType =
20
21
  "user" | "assistant" | "plan" | "error" | "divider" | "artifact" | "detail";
@@ -32,6 +33,7 @@ export interface SnapshotItem {
32
33
  excerpt: string;
33
34
  /** Hidden unless "include tool calls and thinking" is on. */
34
35
  tools: boolean;
36
+ createdAt?: number;
35
37
  }
36
38
 
37
39
  export interface Snapshot {
@@ -39,9 +41,19 @@ export interface Snapshot {
39
41
  items: SnapshotItem[];
40
42
  /** Whether the tools group was left out, to say so on the page. */
41
43
  toolsHidden: boolean;
44
+ /** Messages before / after the shared range were left out, to say so. */
45
+ cutBefore?: boolean;
46
+ cutAfter?: boolean;
42
47
  hits: Hits;
43
48
  }
44
49
 
50
+ /** The range names a message that is no longer in the chat. */
51
+ export class RangeGone extends Error {
52
+ constructor() {
53
+ super("That part of the chat changed; pick the range again.");
54
+ }
55
+ }
56
+
45
57
  export const MAX_TOOL_INPUT = 4_000;
46
58
  export const MAX_TOOL_OUTPUT = 20_000;
47
59
  export const HIDDEN_FILE = "[contents of a secret file hidden]";
@@ -189,7 +201,12 @@ export function toRaw(item: ChatItem): Raw | null {
189
201
  export async function buildChatSnapshot(
190
202
  sessionId: string,
191
203
  ctx: ShareContext,
192
- opts: { includeTools: boolean; dropIds?: string[] }
204
+ opts: {
205
+ includeTools: boolean;
206
+ dropIds?: string[];
207
+ fromId?: string;
208
+ toId?: string;
209
+ }
193
210
  ): Promise<Snapshot> {
194
211
  const session = getSession(sessionId);
195
212
  const dropped = new Set(opts.dropIds ?? []);
@@ -201,13 +218,31 @@ export async function buildChatSnapshot(
201
218
  };
202
219
 
203
220
  const items: SnapshotItem[] = [];
204
- for (const item of visible(listItems(sessionId))) {
221
+ const all = visible(listItems(sessionId));
222
+ let window: { start: number; end: number } | null = null;
223
+ if (opts.fromId !== undefined || opts.toId !== undefined) {
224
+ window = windowOf(
225
+ all.map((i) => i.id),
226
+ opts.fromId,
227
+ opts.toId
228
+ );
229
+ if (!window) throw new RangeGone();
230
+ }
231
+ let cutBefore = false;
232
+ let cutAfter = false;
233
+ for (const [at, item] of all.entries()) {
205
234
  const raw = toRaw(item);
206
235
  if (!raw) continue;
207
236
  if (raw.tools && !opts.includeTools) continue;
237
+ if (window && (at < window.start || at > window.end)) {
238
+ if (at < window.start) cutBefore = true;
239
+ else cutAfter = true;
240
+ continue;
241
+ }
208
242
  if (dropped.has(raw.id)) continue;
209
243
  const row: SnapshotItem = {
210
244
  ...raw,
245
+ createdAt: item.createdAt,
211
246
  label: raw.label === undefined ? undefined : take(raw.label),
212
247
  text: take(raw.text),
213
248
  excerpt: "",
@@ -225,6 +260,8 @@ export async function buildChatSnapshot(
225
260
  title: take(session.name).slice(0, 120) || "Chat",
226
261
  items,
227
262
  toolsHidden: !opts.includeTools,
263
+ cutBefore,
264
+ cutAfter,
228
265
  hits,
229
266
  };
230
267
  }
@@ -56,6 +56,8 @@ h1.title{margin:0 0 4px;font-size:20px;line-height:1.3}
56
56
  .plan>.kicker{margin-top:12px}
57
57
  .kicker{font-size:12px;font-weight:600;color:hsl(var(--muted-foreground));text-transform:uppercase;letter-spacing:.04em}
58
58
  .error{color:hsl(var(--destructive));border:1px solid hsl(var(--destructive)/.4);border-radius:10px;padding:8px 14px;white-space:pre-wrap;overflow-wrap:anywhere}
59
+ .cut{display:flex;align-items:center;gap:12px;color:hsl(var(--muted-foreground));font-size:12px;font-style:italic}
60
+ .cut::before,.cut::after{content:"";flex:1;border-top:1px dashed hsl(var(--border))}
59
61
  .divider{display:flex;align-items:center;gap:12px;color:hsl(var(--muted-foreground));font-size:12px}
60
62
  .divider::before,.divider::after{content:"";flex:1;border-top:1px solid hsl(var(--border))}
61
63
  details.tool{border:1px solid hsl(var(--border));border-radius:10px;background:hsl(var(--card))}
package/lib/share/view.ts CHANGED
@@ -15,6 +15,11 @@ export interface PreviewRequest extends ShareTarget {
15
15
  includeTools: boolean;
16
16
  /** Messages left out of the share. */
17
17
  dropIds?: string[];
18
+ /** Only the messages from this one to `toId`, both included; either end open when absent. */
19
+ fromId?: string;
20
+ toId?: string;
21
+ /** Just the list of messages, no page: what the range picker needs first. */
22
+ listOnly?: boolean;
18
23
  }
19
24
 
20
25
  export interface PreviewItem {
@@ -22,11 +27,17 @@ export interface PreviewItem {
22
27
  type: string;
23
28
  tools: boolean;
24
29
  excerpt: string;
30
+ createdAt?: number;
25
31
  }
26
32
 
27
33
  /** Download formats a server can make; the client lists what it is told. */
28
34
  export type ShareFormat = "pdf" | "html" | "md";
29
35
 
36
+ export interface MessageList {
37
+ /** Every shareable message in order, tool rows included. */
38
+ items: PreviewItem[];
39
+ }
40
+
30
41
  export interface SharePreview {
31
42
  html: string;
32
43
  markdown: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.770",
3
+ "version": "0.3.775",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"
package/stores/palette.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import { proxy, ref } from "valtio";
2
- import { PaletteRegistry, type PaletteCommand } from "@/lib/palette/registry";
2
+ import {
3
+ PaletteRegistry,
4
+ type PaletteCommand,
5
+ type PaletteTab,
6
+ } from "@/lib/palette/registry";
3
7
 
4
8
  // A one-off choice in the palette (which project, which machine), in place
5
9
  // of its commands. ⌘1-9 picks the first nine.
@@ -8,20 +12,18 @@ export interface PalettePicker {
8
12
  items: PaletteCommand[];
9
13
  }
10
14
 
11
- export type PaletteMode = "commands" | "conversations";
12
-
13
15
  export const paletteUi = proxy<{
14
16
  open: boolean;
15
17
  picker: PalettePicker | null;
16
- mode: PaletteMode;
17
- // What was typed when the mode changed, so the other mode starts with it.
18
+ tab: PaletteTab;
19
+ // What was typed when the tab changed, so a body that mounts starts with it.
18
20
  query: string;
19
- }>({ open: false, picker: null, mode: "commands", query: "" });
21
+ }>({ open: false, picker: null, tab: "all", query: "" });
20
22
 
21
23
  export const paletteActions = {
22
24
  open: () => {
23
25
  paletteUi.picker = null;
24
- paletteUi.mode = "commands";
26
+ paletteUi.tab = "all";
25
27
  paletteUi.query = "";
26
28
  paletteUi.open = true;
27
29
  },
@@ -29,17 +31,17 @@ export const paletteActions = {
29
31
  paletteUi.open = open;
30
32
  if (!open) {
31
33
  paletteUi.picker = null;
32
- paletteUi.mode = "commands";
34
+ paletteUi.tab = "all";
33
35
  paletteUi.query = "";
34
36
  }
35
37
  },
36
- setMode: (mode: PaletteMode, query = "") => {
38
+ setTab: (tab: PaletteTab, query = "") => {
37
39
  paletteUi.picker = null;
38
- paletteUi.mode = mode;
40
+ paletteUi.tab = tab;
39
41
  paletteUi.query = query;
40
42
  },
41
43
  pick: (placeholder: string, items: PaletteCommand[]) => {
42
- paletteUi.mode = "commands";
44
+ paletteUi.tab = "all";
43
45
  paletteUi.picker = ref({
44
46
  placeholder,
45
47
  items: items.map((c, i) => (i < 9 ? { ...c, hint: `⌘${i + 1}` } : c)),
package/stores/shareUi.ts CHANGED
@@ -5,7 +5,12 @@ import type { ShareTarget } from "@/data/shares";
5
5
  // menu, the pane or the artifact card.
6
6
  // A page's version, when it has more than one, is named in the title; the
7
7
  // share is of that exact version.
8
- export type ShareUiTarget = ShareTarget & { title: string; version?: number };
8
+ // A chat share can start from one message ("Share from here").
9
+ export type ShareUiTarget = ShareTarget & {
10
+ title: string;
11
+ version?: number;
12
+ fromId?: string;
13
+ };
9
14
 
10
15
  export const shareUi = proxy<{
11
16
  target: ShareUiTarget | null;