@workerdeck/ui 0.15.0 → 0.17.0

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 (71) hide show
  1. package/README.md +60 -0
  2. package/build/{SessionPanel-J2U8v88q.d.mts → SessionPanel-CnNYEX80.d.mts} +170 -21
  3. package/build/{SessionPanel-DI1NO4l8.mjs → SessionPanel-DMPhsNlW.mjs} +4759 -1483
  4. package/build/SessionPanel-DMPhsNlW.mjs.map +1 -0
  5. package/build/{format-ljc3lKpA.d.mts → format-DfI_je9S.d.mts} +1 -1
  6. package/build/format.d.mts +39 -4
  7. package/build/format.mjs +2 -118
  8. package/build/index.d.mts +493 -45
  9. package/build/index.mjs +343 -17
  10. package/build/index.mjs.map +1 -1
  11. package/build/status-Ydzi7n6j.mjs +143 -0
  12. package/build/status-Ydzi7n6j.mjs.map +1 -0
  13. package/build/workspace.d.mts +9 -1
  14. package/build/workspace.mjs +108 -5
  15. package/build/workspace.mjs.map +1 -1
  16. package/package.json +16 -7
  17. package/src/components/agent/Composer.tsx +189 -89
  18. package/src/components/agent/Conversation.tsx +12 -12
  19. package/src/components/agent/FileCard.tsx +0 -26
  20. package/src/components/agent/FileTree.tsx +9 -8
  21. package/src/components/agent/Loader.tsx +22 -72
  22. package/src/components/agent/Message.tsx +11 -46
  23. package/src/components/agent/PermissionPrompt.tsx +0 -92
  24. package/src/components/agent/ProjectIcon.tsx +119 -0
  25. package/src/components/agent/QuestionPrompt.tsx +0 -122
  26. package/src/components/agent/Reasoning.tsx +5 -19
  27. package/src/components/agent/Response.tsx +1 -132
  28. package/src/components/agent/SessionBrowser.tsx +84 -3
  29. package/src/components/agent/SessionPanel.tsx +249 -28
  30. package/src/components/agent/SessionWorkspace.tsx +29 -0
  31. package/src/components/agent/StatusBar.tsx +20 -4
  32. package/src/components/agent/ToolCallCard.tsx +85 -112
  33. package/src/components/agent/Transcript.tsx +780 -203
  34. package/src/components/agent/UsageDialog.tsx +20 -106
  35. package/src/components/agent/UsageMeters.tsx +133 -0
  36. package/src/components/agent/pulse.tsx +3 -2
  37. package/src/components/agent/tool-result-fetch.tsx +36 -0
  38. package/src/components/agent/tool-result-image.tsx +209 -0
  39. package/src/components/agent/transcript-rows.ts +173 -0
  40. package/src/components/agent/transcript-variant.tsx +29 -51
  41. package/src/components/agent/use-height-epoch.ts +60 -0
  42. package/src/components/agent/use-path-links.ts +147 -0
  43. package/src/components/agent/use-transcript-jumps.ts +190 -0
  44. package/src/components/prompt-area/cursor-helpers.ts +65 -0
  45. package/src/components/prompt-area/use-prompt-area.ts +16 -10
  46. package/src/components/terminal/PermissionPrompt.tsx +119 -0
  47. package/src/components/terminal/QuestionPrompt.tsx +322 -0
  48. package/src/components/terminal/StatusLine.tsx +159 -0
  49. package/src/components/terminal/TerminalTranscript.tsx +225 -0
  50. package/src/components/terminal/affordances.tsx +118 -0
  51. package/src/components/terminal/blocks.ts +232 -0
  52. package/src/components/terminal/diff.tsx +130 -0
  53. package/src/components/terminal/height.ts +770 -0
  54. package/src/components/terminal/image-box.ts +53 -0
  55. package/src/components/terminal/items.tsx +486 -0
  56. package/src/components/terminal/markdown.tsx +191 -0
  57. package/src/components/terminal/press.tsx +120 -0
  58. package/src/components/terminal/prompt.tsx +343 -0
  59. package/src/components/terminal/result-preview.ts +86 -0
  60. package/src/components/terminal/row.tsx +132 -0
  61. package/src/components/terminal/scrubber.tsx +784 -0
  62. package/src/components/terminal/surface.tsx +80 -0
  63. package/src/components/terminal/tool-run.ts +224 -0
  64. package/src/index.ts +36 -1
  65. package/src/lib/status.ts +59 -3
  66. package/src/lib/tool-icon.ts +14 -0
  67. package/src/styles/terminal.css +1081 -0
  68. package/src/styles/theme.css +41 -0
  69. package/build/SessionPanel-DI1NO4l8.mjs.map +0 -1
  70. package/build/format.mjs.map +0 -1
  71. package/src/components/agent/line-prompt.tsx +0 -249
package/build/index.mjs CHANGED
@@ -1,11 +1,12 @@
1
- import { $ as Tip, A as SkillsDialog, B as commandTrigger, C as isMutatingTool, Ct as Button, D as permissionModeChoices, E as PermissionModeSelect, F as skillPrompt, G as plainTextToSegments, H as mentionTrigger, I as TranscriptDensityProvider, J as Splitter, K as segmentsToPlainText, L as TranscriptVariantProvider, M as HostFilesDialog, N as ContextDialog, O as permissionModeMeta, P as Composer, Q as copyText, R as useTranscriptDensity, S as Response, St as badgeVariants, T as PERMISSION_MODES, Tt as cn, U as usePromptAreaState, V as hashtagTrigger, W as PromptArea, X as Spinner, Y as CodeBlock, Z as CopyButton, _ as SessionInfoDialog, _t as SelectItemText, a as SessionEmptyState, at as DialogContent, b as parseUserQuestions, bt as Input, c as Message, ct as DialogTrigger, d as FileCard, dt as MenuItem, et as TooltipContent, f as Conversation, ft as MenuSeparator, g as STATUS_META, gt as SelectItem, h as StatusBar, ht as SelectContent, i as ToolCallCard, it as DialogClose, j as McpDialog, k as ModelSelect, l as MessageContent, lt as Menu, m as ConversationScrollButton, mt as Select, n as UsageDialog, nt as Dialog, o as Reasoning, ot as DialogHeader, p as ConversationContent, pt as MenuTrigger, q as ProgressRing, r as Transcript, rt as DialogBody, s as PromptTokenText, st as DialogRow, t as SessionPanel, tt as TooltipProvider, u as Loader, ut as MenuContent, v as QUESTION_BEHAVIORS, vt as SelectTrigger, w as toolIcon, wt as buttonVariants, x as PermissionPrompt, xt as Badge, y as QuestionPrompt, yt as SelectValue, z as useTranscriptVariant } from "./SessionPanel-DI1NO4l8.mjs";
1
+ import { $ as TranscriptDensityProvider, A as PermissionModeSelect, At as MenuTrigger, B as TerminalSurface, Bt as Button, C as TerminalQuestionPrompt, Ct as DialogHeader, D as parseUserQuestions, Dt as MenuContent, E as QuestionPrompt, Et as Menu, F as McpDialog, Ft as SelectTrigger, G as Blank, H as TerminalDiff, Ht as cn, I as HostFilesDialog, It as SelectValue, J as CopyAction, K as Ink, L as ContextDialog, Lt as Input, M as permissionModeMeta, Mt as SelectContent, N as ModelSelect, Nt as SelectItem, O as PermissionPrompt, Ot as MenuItem, P as SkillsDialog, Pt as SelectItemText, Q as toolIcon, R as Composer, Rt as Badge, S as SessionInfoDialog, St as DialogContent, T as QUESTION_BEHAVIORS, Tt as DialogTrigger, U as previewPatch, V as TerminalMarkdown, Vt as buttonVariants, W as Band, X as useAffordances, Y as WithActions, Z as isMutatingTool, _ as Conversation, _t as TooltipContent, a as Transcript, at as mentionTrigger, b as StatusBar, bt as DialogBody, c as ToolCallCard, ct as plainTextToSegments, d as Response, dt as Splitter, et as TranscriptVariantProvider, f as PromptTokenText, ft as CodeBlock, g as FileCard, gt as Tip, h as Loader, ht as copyText, i as useMinuteClock, it as hashtagTrigger, j as permissionModeChoices, jt as Select, k as PERMISSION_MODES, kt as MenuSeparator, l as SessionEmptyState, lt as segmentsToPlainText, m as MessageContent, mt as CopyButton, n as UsageDialog, nt as useTranscriptVariant, o as TerminalItemView, ot as usePromptAreaState, p as Message, pt as Spinner, q as Row, r as UsageMeters, rt as commandTrigger, s as TerminalTranscript, st as PromptArea, t as SessionPanel, tt as useTranscriptDensity, u as Reasoning, ut as ProgressRing, v as ConversationContent, vt as TooltipProvider, w as TerminalPermissionPrompt, wt as DialogRow, x as STATUS_META, xt as DialogClose, y as ConversationScrollButton, yt as Dialog, z as skillPrompt, zt as badgeVariants } from "./SessionPanel-DMPhsNlW.mjs";
2
2
  import { a as formatDuration, c as formatRelativeTime, d as rateLimitWindowSeconds, f as toolInputPreview, i as formatCountdown, l as formatTokens, n as formatBytes, o as formatRateLimitWindow, r as formatCost, s as formatRateLimitWindowLong, t as formatAgoPrecise, u as friendlyModel } from "./format-DqR56Y8l.mjs";
3
+ import { a as statusPresentation, c as windowLabel, i as modelLabel, o as tightestWindow, r as meterSeverity, t as contextSeverity } from "./status-Ydzi7n6j.mjs";
3
4
  import { forwardRef, useEffect, useMemo, useRef, useState } from "react";
4
- import { jsx, jsxs } from "react/jsx-runtime";
5
- import { BellRing, CircleAlert, CircleSlash, Layers, Moon, PauseCircle, Pencil, Search, SearchX, Trash2, X } from "lucide-react";
5
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
+ import { Anchor, Atom, Beaker, BellRing, Bike, Binary, Blocks, Bot, Box, Brain, Briefcase, Brush, Bug, Building2, Cable, Camera, Car, Cat, CircleAlert, CircleSlash, CircuitBoard, Cloud, Code, Coffee, Cog, Compass, Container, Cpu, CreditCard, Database, Diamond, Dog, Feather, Film, Flame, FlaskConical, Folder, Gamepad2, Gauge, Gem, Ghost, Gift, GitBranch, Globe, GraduationCap, Hammer, Hexagon, House, Image, Key, Landmark, Laptop, Layers, Leaf, Library, Lightbulb, Lock, Mail, Map as Map$1, MessageCircle, Mic, Monitor, Moon, Mountain, Music, Network, Newspaper, Notebook, Orbit, Package, Palette, PauseCircle, PenTool, Pencil, Phone, PiggyBank, Plane, Plug, Puzzle, Radar, Radio, Receipt, Rocket, Ruler, Satellite, Scale, Scissors, Search, SearchX, Server, Shield, Ship, ShoppingCart, Signal, Smartphone, Sparkles, Sprout, Square, Star, Sun, Table, Tag, Target, Telescope, Terminal, TestTube, Trash2, TreePine, Trophy, Truck, Umbrella, Users, Wallet, Wand, Waves, Webhook, Wifi, Wrench, X, Zap } from "lucide-react";
6
7
  import { AlertDialog as AlertDialog$1 } from "@base-ui/react/alert-dialog";
7
8
  import { Toaster as Toaster$1, toast } from "sonner";
8
- import { STATE_LABELS, STATE_ORDER, adaptersOf, clearFilters, filterRows, groupRows, hasFacetFilter, sessionLabel, subsetSummary } from "@workerdeck/protocol";
9
+ import { STATE_LABELS, STATE_ORDER, adaptersOf, clearFilters, filterRows, groupRows, hasFacetFilter, projectLabel, projectsOf, sessionLabel, subsetSummary } from "@workerdeck/protocol";
9
10
  //#region src/components/ui/Card.tsx
10
11
  function Card({ className, ...props }) {
11
12
  return /* @__PURE__ */ jsx("div", {
@@ -139,6 +140,108 @@ function getChipsByTrigger(segments, trigger) {
139
140
  return segments.filter((seg) => seg.type === "chip" && seg.trigger === trigger);
140
141
  }
141
142
  //#endregion
143
+ //#region src/components/terminal/StatusLine.tsx
144
+ /**
145
+ * The session's readings, as one line.
146
+ *
147
+ * The styled bar puts each of these in its own affordance — a progress ring, a
148
+ * badge, a tooltip — which is right for a dashboard and wrong here for the usual
149
+ * reason: a terminal has one line height, and five widgets of five different
150
+ * heights is five different rhythms in a row that is supposed to be quiet. So
151
+ * they are words, `·`-separated, exactly as the CLI's own status line writes
152
+ * them.
153
+ *
154
+ * The readings themselves come from the same helpers the styled bar uses
155
+ * (`lib/status.ts`), and that matters more than it looks: `statusPresentation`
156
+ * is where the rule lives that **a dropped socket outranks the session status**,
157
+ * because a status held over a dead socket is a stale reading being presented as
158
+ * a live one. Re-deriving that here would be a second answer to a question that
159
+ * already has one.
160
+ */
161
+ /** Severity → tone. The 80/95 thresholds are `lib/status.ts`'s, not new ones. */
162
+ const SEVERITY_TONE = {
163
+ none: "dim",
164
+ warning: "yellow",
165
+ error: "red"
166
+ };
167
+ /** One reading. A button only when it leads somewhere — a segment that looks
168
+ * pressable and does nothing is worse than plain text. */
169
+ function Reading({ tone, onPress, label, children }) {
170
+ if (!onPress) return /* @__PURE__ */ jsx(Ink, {
171
+ tone,
172
+ children
173
+ });
174
+ return /* @__PURE__ */ jsx("button", {
175
+ type: "button",
176
+ className: "term-reading",
177
+ onClick: onPress,
178
+ title: label,
179
+ children: /* @__PURE__ */ jsx(Ink, {
180
+ tone,
181
+ children
182
+ })
183
+ });
184
+ }
185
+ function TerminalStatusLine({ state, rateLimits, connection, onOpenStatus, onOpenContext, onOpenUsage }) {
186
+ const presentation = statusPresentation({
187
+ status: state.status,
188
+ connection
189
+ });
190
+ const usage = state.contextUsage;
191
+ const window = tightestWindow(rateLimits ?? state.rateLimits);
192
+ const model = modelLabel({
193
+ model: state.model,
194
+ models: state.models ?? []
195
+ });
196
+ const parts = [/* @__PURE__ */ jsx(Reading, {
197
+ tone: SEVERITY_TONE[presentation.severity],
198
+ onPress: onOpenStatus,
199
+ label: "Session status",
200
+ children: presentation.label
201
+ }, "status")];
202
+ if (usage) parts.push(/* @__PURE__ */ jsxs(Reading, {
203
+ tone: SEVERITY_TONE[contextSeverity(usage)],
204
+ onPress: onOpenContext,
205
+ label: "Context window",
206
+ children: [
207
+ Math.round(usage.percentage),
208
+ "% ctx (",
209
+ formatTokens(usage.totalTokens),
210
+ ")"
211
+ ]
212
+ }, "context"));
213
+ if (window) parts.push(/* @__PURE__ */ jsxs(Reading, {
214
+ tone: SEVERITY_TONE[meterSeverity(window.info.utilization)],
215
+ onPress: onOpenUsage,
216
+ label: "Plan usage",
217
+ children: [
218
+ Math.round(window.info.utilization ?? 0),
219
+ "% ",
220
+ windowLabel(window.key)
221
+ ]
222
+ }, "usage"));
223
+ if (state.totalCostUsd > 0) parts.push(/* @__PURE__ */ jsx(Ink, {
224
+ tone: "faint",
225
+ children: formatCost(state.totalCostUsd)
226
+ }, "cost"));
227
+ if (model) parts.push(/* @__PURE__ */ jsx(Ink, {
228
+ tone: "faint",
229
+ children: model
230
+ }, "model"));
231
+ if (state.permissionMode && state.permissionMode !== "default") parts.push(/* @__PURE__ */ jsx(Ink, {
232
+ tone: "yellow",
233
+ children: state.permissionMode
234
+ }, "mode"));
235
+ return /* @__PURE__ */ jsx(Row, {
236
+ "data-slot": "status-line",
237
+ tone: "dim",
238
+ children: parts.map((part, index) => /* @__PURE__ */ jsxs("span", { children: [index > 0 ? /* @__PURE__ */ jsx(Ink, {
239
+ tone: "faint",
240
+ children: " · "
241
+ }) : null, part] }, index))
242
+ });
243
+ }
244
+ //#endregion
142
245
  //#region src/components/agent/SessionList.tsx
143
246
  function SessionListItem({ session, active, onSelect, onDelete }) {
144
247
  const meta = STATUS_META[session.status];
@@ -208,6 +311,179 @@ function SessionList({ sessions, activeId, onSelect, onDelete, emptyText = "No s
208
311
  });
209
312
  }
210
313
  //#endregion
314
+ //#region src/components/agent/ProjectIcon.tsx
315
+ /**
316
+ * A project's icon — the render side of protocol's `ProjectIcon`, for a list
317
+ * row or a group header.
318
+ *
319
+ * **The glyph arm is a curated set, and that is the design rather than a
320
+ * shortcut.** The gateway validates a glyph name by *shape* only (lucide's
321
+ * lowercase-kebab convention) and explicitly declines to grow an icon catalog,
322
+ * so the contract already says a client must fall back on a name it does not
323
+ * know.
324
+ *
325
+ * The alternative was measured rather than argued, against the VS Code
326
+ * sidebar's own bundle: `sidebar.js` is **31 KB** with one glyph, **77 KB**
327
+ * with the 110 below (~418 bytes each), and **927 KB** with a
328
+ * `import * as` over lucide's ~1,600 — which is what it costs, because a
329
+ * namespace import defeats tree-shaking by construction. (`DynamicIcon` is the
330
+ * third option and worse in a different currency: ~1,600 chunk files shipped
331
+ * inside the `.vsix`.) Twelve times the bundle to render a name nobody has
332
+ * declared yet is not a trade worth making; 46 KB to cover the names people
333
+ * actually use is.
334
+ *
335
+ * The set is chosen to cover what people call a project; an unlisted-but-valid
336
+ * name draws {@link Folder}, which is what the row drew before this feature
337
+ * existed. **Grow it freely** — each addition is ~400 bytes and the fallback
338
+ * means a missing one is a shrug, not a hole.
339
+ *
340
+ * **The image arm draws nothing until its bytes arrive**, and takes them as a
341
+ * resolved `src` rather than fetching: the wire carries only an address
342
+ * (media type, size, content hash), and *who* fetches it differs per client —
343
+ * a VS Code webview cannot reach a gateway at all and must be handed a data
344
+ * URL by its extension host, while a browser can fetch and `createObjectURL`.
345
+ * A component that fetched would be wrong on one of them. Absent `src` renders
346
+ * nothing rather than a placeholder box: an icon is decoration beside a name
347
+ * that is already there, and a box that becomes a picture a beat later is more
348
+ * movement than the picture is worth.
349
+ *
350
+ * One thing an image icon cannot do, and a glyph can: **take the row's
351
+ * colour.** An `<img>`-embedded SVG is its own document, so `currentColor` does
352
+ * not reach it and its own `prefers-color-scheme` resolves against the *OS*
353
+ * rather than the host's theme (measured: it does resolve, which is the
354
+ * surprise — so a monochrome mark tuned for both schemes is right on a dark OS
355
+ * in a dark editor and wrong on a light OS in a dark one). A repo that wants a
356
+ * mark that always matches its row should declare a glyph; one that wants its
357
+ * brand should declare the image and accept that it is a picture.
358
+ */
359
+ const GLYPHS = {
360
+ anchor: Anchor,
361
+ atom: Atom,
362
+ beaker: Beaker,
363
+ bike: Bike,
364
+ binary: Binary,
365
+ blocks: Blocks,
366
+ bot: Bot,
367
+ box: Box,
368
+ brain: Brain,
369
+ briefcase: Briefcase,
370
+ brush: Brush,
371
+ bug: Bug,
372
+ "building-2": Building2,
373
+ cable: Cable,
374
+ camera: Camera,
375
+ car: Car,
376
+ cat: Cat,
377
+ "circuit-board": CircuitBoard,
378
+ cloud: Cloud,
379
+ code: Code,
380
+ coffee: Coffee,
381
+ cog: Cog,
382
+ compass: Compass,
383
+ container: Container,
384
+ cpu: Cpu,
385
+ "credit-card": CreditCard,
386
+ database: Database,
387
+ diamond: Diamond,
388
+ dog: Dog,
389
+ feather: Feather,
390
+ film: Film,
391
+ flame: Flame,
392
+ "flask-conical": FlaskConical,
393
+ folder: Folder,
394
+ "gamepad-2": Gamepad2,
395
+ gauge: Gauge,
396
+ gem: Gem,
397
+ ghost: Ghost,
398
+ gift: Gift,
399
+ "git-branch": GitBranch,
400
+ globe: Globe,
401
+ "graduation-cap": GraduationCap,
402
+ hammer: Hammer,
403
+ hexagon: Hexagon,
404
+ house: House,
405
+ image: Image,
406
+ key: Key,
407
+ landmark: Landmark,
408
+ laptop: Laptop,
409
+ layers: Layers,
410
+ leaf: Leaf,
411
+ library: Library,
412
+ lightbulb: Lightbulb,
413
+ lock: Lock,
414
+ mail: Mail,
415
+ map: Map$1,
416
+ "message-circle": MessageCircle,
417
+ mic: Mic,
418
+ monitor: Monitor,
419
+ moon: Moon,
420
+ mountain: Mountain,
421
+ music: Music,
422
+ network: Network,
423
+ newspaper: Newspaper,
424
+ notebook: Notebook,
425
+ orbit: Orbit,
426
+ package: Package,
427
+ palette: Palette,
428
+ "pen-tool": PenTool,
429
+ phone: Phone,
430
+ "piggy-bank": PiggyBank,
431
+ plane: Plane,
432
+ plug: Plug,
433
+ puzzle: Puzzle,
434
+ radar: Radar,
435
+ radio: Radio,
436
+ receipt: Receipt,
437
+ rocket: Rocket,
438
+ ruler: Ruler,
439
+ satellite: Satellite,
440
+ scale: Scale,
441
+ scissors: Scissors,
442
+ server: Server,
443
+ shield: Shield,
444
+ ship: Ship,
445
+ "shopping-cart": ShoppingCart,
446
+ signal: Signal,
447
+ smartphone: Smartphone,
448
+ sparkles: Sparkles,
449
+ sprout: Sprout,
450
+ square: Square,
451
+ star: Star,
452
+ sun: Sun,
453
+ table: Table,
454
+ tag: Tag,
455
+ target: Target,
456
+ telescope: Telescope,
457
+ terminal: Terminal,
458
+ "test-tube": TestTube,
459
+ "tree-pine": TreePine,
460
+ trophy: Trophy,
461
+ truck: Truck,
462
+ umbrella: Umbrella,
463
+ users: Users,
464
+ wallet: Wallet,
465
+ wand: Wand,
466
+ waves: Waves,
467
+ webhook: Webhook,
468
+ wifi: Wifi,
469
+ wrench: Wrench,
470
+ zap: Zap
471
+ };
472
+ function ProjectIcon({ icon, src, name, className }) {
473
+ if (!icon) return null;
474
+ if (icon.type === "image") {
475
+ if (!src) return null;
476
+ return /* @__PURE__ */ jsx("img", {
477
+ src,
478
+ alt: "",
479
+ "aria-hidden": true,
480
+ title: name,
481
+ className: cn("inline-block size-3 shrink-0 object-contain", className)
482
+ });
483
+ }
484
+ return /* @__PURE__ */ jsx(GLYPHS[icon.name] ?? Folder, { className: cn("inline-block size-3 shrink-0", className) });
485
+ }
486
+ //#endregion
211
487
  //#region src/components/agent/SessionBrowser.tsx
212
488
  /**
213
489
  * How a list row is drawn, in one place, so `SidebarRow` in `web` matches this
@@ -228,7 +504,7 @@ function SessionList({ sessions, activeId, onSelect, onDelete, emptyText = "No s
228
504
  function rowShapeClass(active) {
229
505
  return cn("px-2 py-1.5 hover:bg-row-hover", active ? "mr-1 ml-0 rounded-r-md border-l-4 border-l-accent" : "mx-1 rounded-md");
230
506
  }
231
- function SessionBrowser({ rows, config, onConfigChange, scope, activeId, onSelect, onDelete, onRename, emptyState, showControls = true, className }) {
507
+ function SessionBrowser({ rows, config, onConfigChange, scope, activeId, onSelect, onDelete, onRename, emptyState, showControls = true, projectIcons, className }) {
232
508
  const visible = useMemo(() => filterRows(rows, config, scope), [
233
509
  rows,
234
510
  config,
@@ -237,6 +513,7 @@ function SessionBrowser({ rows, config, onConfigChange, scope, activeId, onSelec
237
513
  const groups = useMemo(() => groupRows(visible, config), [visible, config]);
238
514
  const subset = subsetSummary(config, scope, visible.length, rows.length);
239
515
  const adapters = useMemo(() => adaptersOf(rows), [rows]);
516
+ const projects = useMemo(() => projectsOf(rows), [rows]);
240
517
  const gateways = useMemo(() => {
241
518
  const seen = /* @__PURE__ */ new Map();
242
519
  for (const row of rows) seen.set(row.hostId, row.hostName);
@@ -302,6 +579,18 @@ function SessionBrowser({ rows, config, onConfigChange, scope, activeId, onSelec
302
579
  onChange: (gateways) => set({ gateways })
303
580
  })
304
581
  }) : null,
582
+ projects.length > 1 ? /* @__PURE__ */ jsx(FilterRow, {
583
+ label: "Project",
584
+ children: /* @__PURE__ */ jsx(FacetSelect, {
585
+ label: "Project",
586
+ value: config.projects ?? [],
587
+ options: projects.map((p) => ({
588
+ value: p.key,
589
+ label: p.label
590
+ })),
591
+ onChange: (next) => set({ projects: next })
592
+ })
593
+ }) : null,
305
594
  /* @__PURE__ */ jsx(FilterRow, {
306
595
  label: "Group",
307
596
  children: /* @__PURE__ */ jsx(OneOfSelect, {
@@ -320,6 +609,10 @@ function SessionBrowser({ rows, config, onConfigChange, scope, activeId, onSelec
320
609
  value: "adapter",
321
610
  label: "By engine"
322
611
  },
612
+ ...projects.length > 1 ? [{
613
+ value: "project",
614
+ label: "By project"
615
+ }] : [],
323
616
  ...gateways.length > 1 ? [{
324
617
  value: "gateway",
325
618
  label: "By gateway"
@@ -346,6 +639,10 @@ function SessionBrowser({ rows, config, onConfigChange, scope, activeId, onSelec
346
639
  value: "state",
347
640
  label: "State"
348
641
  },
642
+ ...projects.length > 1 ? [{
643
+ value: "project",
644
+ label: "Project"
645
+ }] : [],
349
646
  ...gateways.length > 1 ? [{
350
647
  value: "gateway",
351
648
  label: "Gateway"
@@ -388,18 +685,28 @@ function SessionBrowser({ rows, config, onConfigChange, scope, activeId, onSelec
388
685
  children: groups.map((group) => /* @__PURE__ */ jsxs("div", {
389
686
  className: "flex flex-col gap-1",
390
687
  children: [config.groupBy !== "none" && group.label ? /* @__PURE__ */ jsxs("div", {
391
- className: "flex items-baseline gap-2 px-3 text-label font-medium text-fg-4",
392
- children: [/* @__PURE__ */ jsx("span", {
393
- className: "uppercase tracking-wide",
394
- children: group.label
395
- }), /* @__PURE__ */ jsx("span", {
396
- className: "text-fg-4/70",
397
- children: group.rows.length
398
- })]
688
+ className: "flex items-center gap-2 px-3 text-label font-medium text-fg-4",
689
+ children: [
690
+ config.groupBy === "project" ? /* @__PURE__ */ jsx(ProjectIcon, {
691
+ icon: group.rows[0]?.info.project?.icon,
692
+ src: iconSrcOf(group.rows[0], projectIcons),
693
+ name: group.label
694
+ }) : null,
695
+ /* @__PURE__ */ jsx("span", {
696
+ className: "uppercase tracking-wide",
697
+ children: group.label
698
+ }),
699
+ /* @__PURE__ */ jsx("span", {
700
+ className: "text-fg-4/70",
701
+ children: group.rows.length
702
+ })
703
+ ]
399
704
  }) : null, group.rows.map((row) => /* @__PURE__ */ jsx(SessionRowItem, {
400
705
  row,
401
706
  active: row.info.id === activeId,
402
707
  showGateway: gateways.length > 1,
708
+ showProject: config.groupBy !== "project",
709
+ projectIcons,
403
710
  onSelect,
404
711
  onDelete,
405
712
  onRename
@@ -409,17 +716,27 @@ function SessionBrowser({ rows, config, onConfigChange, scope, activeId, onSelec
409
716
  ]
410
717
  });
411
718
  }
412
- function SessionRowItem({ row, active, showGateway, onSelect, onDelete, onRename }) {
719
+ /** The bytes for a row's project icon, if it has an image one and the caller has
720
+ * fetched it yet. Shared by the row and its group header so the two cannot draw
721
+ * different pictures for one project. */
722
+ function iconSrcOf(row, icons) {
723
+ const icon = row?.info.project?.icon;
724
+ return icon?.type === "image" ? icons?.[icon.hash] : void 0;
725
+ }
726
+ function SessionRowItem({ row, active, showGateway, showProject = true, projectIcons, onSelect, onDelete, onRename }) {
413
727
  const { info } = row;
414
728
  const [editing, setEditing] = useState(false);
415
729
  const folder = info.cwd.split("/").filter(Boolean).pop() ?? info.cwd;
730
+ const project = showProject ? projectLabel(row) : folder;
731
+ const projectIcon = showProject ? info.project?.icon : void 0;
416
732
  const details = [
417
733
  showGateway ? row.hostName : void 0,
418
734
  friendlyModel(info.model),
419
- folder,
735
+ project,
420
736
  info.profile ? `@${info.profile}` : void 0,
421
737
  formatCost(info.totalCostUsd)
422
738
  ].filter(Boolean);
739
+ const cut = details.indexOf(project);
423
740
  return /* @__PURE__ */ jsxs("div", {
424
741
  "data-slot": "session-row",
425
742
  "data-active": active || void 0,
@@ -458,7 +775,16 @@ function SessionRowItem({ row, active, showGateway, onSelect, onDelete, onRename
458
775
  type: "button",
459
776
  tabIndex: -1,
460
777
  className: "min-w-0 flex-1 truncate text-left font-mono outline-none",
461
- children: details.join(" · ")
778
+ children: cut < 0 ? details.join(" · ") : /* @__PURE__ */ jsxs(Fragment$1, { children: [
779
+ details.slice(0, cut).map((part) => `${part} · `),
780
+ /* @__PURE__ */ jsx(ProjectIcon, {
781
+ icon: projectIcon,
782
+ src: iconSrcOf(row, projectIcons),
783
+ name: project,
784
+ className: "mr-1 align-[-0.2em]"
785
+ }),
786
+ details.slice(cut).join(" · ")
787
+ ] })
462
788
  }),
463
789
  onRename && !editing ? /* @__PURE__ */ jsx(Button, {
464
790
  variant: "ghost",
@@ -667,6 +993,6 @@ function EngineIcon({ engine, model, className }) {
667
993
  });
668
994
  }
669
995
  //#endregion
670
- export { AlertDialog, AlertDialogClose, AlertDialogContent, AlertDialogDescription, AlertDialogTitle, AlertDialogTrigger, Badge, Button, Card, CardContent, CardHeader, CardTitle, CodeBlock, Composer, ContextDialog, Conversation, ConversationContent, ConversationScrollButton, CopyButton, Dialog, DialogBody, DialogClose, DialogContent, DialogHeader, DialogRow, DialogTrigger, Empty, EmptyKey, EngineIcon, FileCard, HostFilesDialog, Input, Loader, McpDialog, Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger, Message, MessageContent, ModelSelect, PERMISSION_MODES, PermissionModeSelect, PermissionPrompt, ProgressRing, PromptArea, PromptTokenText, QUESTION_BEHAVIORS, QuestionPrompt, Reasoning, Response, STATUS_META, Select, SelectContent, SelectItem, SelectItemText, SelectTrigger, SelectValue, SessionBrowser, SessionEmptyState, SessionInfoDialog, SessionList, SessionListItem, SessionPanel, SessionStatusIcon, SkillsDialog, Spinner, Splitter, StatusBar, Textarea, Tip, Toaster, ToolCallCard, TooltipContent, TooltipProvider, Transcript, TranscriptDensityProvider, TranscriptVariantProvider, UsageDialog, badgeVariants, buttonVariants, cn, commandTrigger, copyText, formatAgoPrecise, formatBytes, formatCost, formatCountdown, formatDuration, formatRateLimitWindow, formatRateLimitWindowLong, formatRelativeTime, formatTokens, friendlyModel, getChipsByTrigger, hashtagTrigger, isMutatingTool, isSegmentsEmpty, mentionTrigger, parseUserQuestions, permissionModeChoices, permissionModeMeta, plainTextToSegments, rateLimitWindowSeconds, rowShapeClass, segmentsToPlainText, skillPrompt, toast, toolIcon, toolInputPreview, usePromptAreaState, useTranscriptDensity, useTranscriptVariant };
996
+ export { AlertDialog, AlertDialogClose, AlertDialogContent, AlertDialogDescription, AlertDialogTitle, AlertDialogTrigger, Badge, Band, Blank, Button, Card, CardContent, CardHeader, CardTitle, CodeBlock, Composer, ContextDialog, Conversation, ConversationContent, ConversationScrollButton, CopyAction, CopyButton, Dialog, DialogBody, DialogClose, DialogContent, DialogHeader, DialogRow, DialogTrigger, Empty, EmptyKey, EngineIcon, FileCard, HostFilesDialog, Ink, Input, Loader, McpDialog, Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger, Message, MessageContent, ModelSelect, PERMISSION_MODES, PermissionModeSelect, PermissionPrompt, ProgressRing, ProjectIcon, PromptArea, PromptTokenText, QUESTION_BEHAVIORS, QuestionPrompt, Reasoning, Response, Row, STATUS_META, Select, SelectContent, SelectItem, SelectItemText, SelectTrigger, SelectValue, SessionBrowser, SessionEmptyState, SessionInfoDialog, SessionList, SessionListItem, SessionPanel, SessionStatusIcon, SkillsDialog, Spinner, Splitter, StatusBar, TerminalDiff, TerminalItemView, TerminalMarkdown, TerminalPermissionPrompt, TerminalQuestionPrompt, TerminalStatusLine, TerminalSurface, TerminalTranscript, Textarea, Tip, Toaster, ToolCallCard, TooltipContent, TooltipProvider, Transcript, TranscriptDensityProvider, TranscriptVariantProvider, UsageDialog, UsageMeters, WithActions, badgeVariants, buttonVariants, cn, commandTrigger, copyText, engineMark, formatAgoPrecise, formatBytes, formatCost, formatCountdown, formatDuration, formatRateLimitWindow, formatRateLimitWindowLong, formatRelativeTime, formatTokens, friendlyModel, getChipsByTrigger, hashtagTrigger, isMutatingTool, isSegmentsEmpty, mentionTrigger, parseUserQuestions, permissionModeChoices, permissionModeMeta, plainTextToSegments, previewPatch, rateLimitWindowSeconds, rowShapeClass, segmentsToPlainText, skillPrompt, toast, toolIcon, toolInputPreview, useAffordances, useMinuteClock, usePromptAreaState, useTranscriptDensity, useTranscriptVariant };
671
997
 
672
998
  //# sourceMappingURL=index.mjs.map