@heyramzi/ui 2.8.0 → 2.9.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 (115) hide show
  1. package/COMPONENTS.md +1 -1
  2. package/README.md +1 -1
  3. package/package.json +58 -74
  4. package/src/agents/vendor/draw.ts +4 -4
  5. package/src/canvas/headless/canvas-cleanup.test.ts +1 -1
  6. package/src/canvas/ops/ops.ts +1 -1
  7. package/src/charts/area-chart.svelte +11 -1
  8. package/src/charts/bar-chart.svelte +18 -3
  9. package/src/charts/line-chart.svelte +11 -1
  10. package/src/components/data-table/data-table.ts +4 -5
  11. package/src/components/data-table/pagination.svelte +5 -3
  12. package/src/components/docs/on-this-page.svelte +82 -5
  13. package/src/components/docs/scroll-spy.svelte.ts +6 -1
  14. package/src/components/list-page/detail-header.svelte +7 -3
  15. package/src/components/list-page/empty-state.svelte +3 -1
  16. package/src/components/list-page/filter-popover.svelte +2 -2
  17. package/src/components/list-page/rail-row.svelte +5 -2
  18. package/src/components/mark/mark.svelte +2 -1
  19. package/src/components/section-card/section-card.svelte +3 -1
  20. package/src/components/section-header/section-header.svelte +5 -5
  21. package/src/components/segmented/segment.svelte +1 -1
  22. package/src/components/table-view/table-view.svelte +6 -1
  23. package/src/components/tags-input/tags-input.svelte +1 -1
  24. package/src/components/toolbar/toolbar.svelte +1 -1
  25. package/src/editor/anchors.ts +28 -0
  26. package/src/editor/doc-blocks.ts +168 -0
  27. package/src/editor/doc-page.css +389 -0
  28. package/src/editor/editor.svelte +47 -5
  29. package/src/editor/extensions.ts +26 -2
  30. package/src/editor/markdown.ts +27 -2
  31. package/src/editor/slash-items.ts +18 -0
  32. package/src/icons/trades.ts +1 -1
  33. package/src/inbox/README.md +6 -4
  34. package/src/inbox/inbox.test.ts +1 -1
  35. package/src/inbox/index.ts +2 -2
  36. package/src/inbox/logic.ts +29 -5
  37. package/src/inbox/thread-inbox.svelte +219 -92
  38. package/src/inbox/types.ts +27 -1
  39. package/src/lib/initials.test.ts +25 -0
  40. package/src/lib/initials.ts +29 -0
  41. package/src/modules.test.ts +4 -64
  42. package/src/panel/README.md +1 -1
  43. package/src/record/README.md +1 -1
  44. package/src/record/info-list.svelte +5 -4
  45. package/src/record/next-up.svelte +1 -1
  46. package/src/record/project-list.svelte +1 -1
  47. package/src/record/steps-rail.svelte +3 -3
  48. package/src/shadcn/badge/badge.svelte +1 -1
  49. package/src/shadcn/card/card-title.svelte +1 -5
  50. package/src/shadcn/empty/empty-description.svelte +1 -1
  51. package/src/shadcn/progress/progress.svelte +8 -1
  52. package/src/shadcn/table/table-cell.svelte +10 -2
  53. package/src/status/README.md +1 -1
  54. package/src/thread/README.md +2 -0
  55. package/src/thread/index.ts +9 -1
  56. package/src/thread/logic.ts +33 -3
  57. package/src/thread/thread-body.svelte +37 -0
  58. package/src/thread/thread-events.svelte +30 -0
  59. package/src/thread/thread.svelte +74 -59
  60. package/src/thread/types.ts +21 -0
  61. package/src/{tasks → work}/README.md +11 -8
  62. package/src/{tasks → work}/detail/task-thread.svelte +8 -15
  63. package/src/work/docs/doc-editor-page.svelte +194 -0
  64. package/src/work/docs/doc-menu.svelte +333 -0
  65. package/src/work/docs/docs-demo.svelte +227 -0
  66. package/src/work/docs/edited-chip.svelte +125 -0
  67. package/src/work/docs/edited.ts +54 -0
  68. package/src/work/docs/index.ts +38 -0
  69. package/src/work/docs/labels.ts +70 -0
  70. package/src/work/docs/menu.ts +127 -0
  71. package/src/work/docs/outline.ts +60 -0
  72. package/src/{tasks → work}/gantt/gantt-group-row.svelte +1 -1
  73. package/src/{tasks → work}/inbox/inbox-card.svelte +113 -50
  74. package/src/work/inbox/inbox-logic.ts +98 -0
  75. package/src/work/inbox/inbox-trace.svelte +58 -0
  76. package/src/{tasks → work}/inbox/inbox-types.ts +2 -0
  77. package/src/{tasks → work}/index.ts +0 -10
  78. package/src/{tasks → work}/kanban-board.svelte +1 -1
  79. package/src/{tasks → work}/task-list.svelte +1 -1
  80. package/src/{tasks → work}/time/time-tracker.svelte +2 -5
  81. package/src/tasks/inbox/inbox-logic.ts +0 -21
  82. package/src/tasks/inbox/inbox-trace.svelte +0 -51
  83. /package/src/{tasks → work}/ambient.d.ts +0 -0
  84. /package/src/{tasks → work}/board-order.ts +0 -0
  85. /package/src/{tasks → work}/dates.ts +0 -0
  86. /package/src/{tasks → work}/demo.svelte +0 -0
  87. /package/src/{tasks → work}/detail/detail-demo.svelte +0 -0
  88. /package/src/{tasks → work}/detail/detail-types.ts +0 -0
  89. /package/src/{tasks → work}/detail/task-chip.svelte +0 -0
  90. /package/src/{tasks → work}/detail/task-detail.svelte +0 -0
  91. /package/src/{tasks → work}/detail/task-property.svelte +0 -0
  92. /package/src/{tasks → work}/gantt/gantt-bar.svelte +0 -0
  93. /package/src/{tasks → work}/gantt/gantt-grid.svelte +0 -0
  94. /package/src/{tasks → work}/gantt/gantt-header.svelte +0 -0
  95. /package/src/{tasks → work}/gantt/gantt-task-row.svelte +0 -0
  96. /package/src/{tasks → work}/gantt/gantt.css +0 -0
  97. /package/src/{tasks → work}/gantt/gantt.test.ts +0 -0
  98. /package/src/{tasks → work}/gantt/logic.test.ts +0 -0
  99. /package/src/{tasks → work}/gantt/logic.ts +0 -0
  100. /package/src/{tasks → work}/inbox/inbox-draft.svelte +0 -0
  101. /package/src/{tasks → work}/inbox/inbox-statuses.ts +0 -0
  102. /package/src/{tasks → work}/inbox/index.ts +0 -0
  103. /package/src/{tasks → work}/kanban-column.svelte +0 -0
  104. /package/src/{tasks → work}/kanban-drag.ts +0 -0
  105. /package/src/{tasks → work}/kanban-types.ts +0 -0
  106. /package/src/{tasks → work}/presets/sales.ts +0 -0
  107. /package/src/{tasks → work}/reorder.ts +0 -0
  108. /package/src/{tasks → work}/task-gantt.svelte +0 -0
  109. /package/src/{tasks → work}/task-types.ts +0 -0
  110. /package/src/{tasks → work}/time/duration.ts +0 -0
  111. /package/src/{tasks → work}/time/time-chip.svelte +0 -0
  112. /package/src/{tasks → work}/time/time-dot.svelte +0 -0
  113. /package/src/{tasks → work}/time/time-ring.svelte +0 -0
  114. /package/src/{tasks → work}/time/time-types.ts +0 -0
  115. /package/src/{tasks → work}/use-kanban-dnd.svelte.ts +0 -0
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The outline of a document: which headings it lists, and the anchor each one gets.
3
+ * Pure, so the editor's anchors and the outline's ids can never disagree.
4
+ */
5
+
6
+ import { anchorIds } from "../../editor/anchors.js";
7
+
8
+ export { anchorIds, slugify } from "../../editor/anchors.js";
9
+
10
+ export type OutlineLevel = 2 | 3;
11
+
12
+ export type OutlineHeading = { id: string; label: string; level: OutlineLevel };
13
+
14
+ type DocNode = {
15
+ type?: string;
16
+ attrs?: Record<string, unknown>;
17
+ content?: DocNode[];
18
+ text?: string;
19
+ };
20
+
21
+ function textOf(node: DocNode): string {
22
+ if (node.type === "text") return node.text ?? "";
23
+ return (node.content ?? []).map(textOf).join("");
24
+ }
25
+
26
+ export type FoundHeading = { id: string; label: string; level: number };
27
+
28
+ /** Every heading in document order, at any depth, with its anchor. */
29
+ export function headingsOf(doc: unknown): FoundHeading[] {
30
+ const found: { label: string; level: number }[] = [];
31
+ const walk = (node: DocNode) => {
32
+ if (node.type === "heading") {
33
+ const level = typeof node.attrs?.level === "number" ? node.attrs.level : 1;
34
+ found.push({ label: textOf(node).trim(), level });
35
+ return;
36
+ }
37
+ for (const child of node.content ?? []) walk(child);
38
+ };
39
+ if (doc && typeof doc === "object") walk(doc as DocNode);
40
+ const ids = anchorIds(found.map((heading) => heading.label));
41
+ return found.map((heading, i) => ({ id: ids[i], label: heading.label, level: heading.level }));
42
+ }
43
+
44
+ /**
45
+ * What the outline lists: H2 and H3 with a label. H1 and H4 are ignored, and a
46
+ * document with fewer than two entries gets no outline at all.
47
+ */
48
+ export function outlineOf(headings: FoundHeading[]): OutlineHeading[] {
49
+ const items = headings.filter(
50
+ (heading): heading is FoundHeading & { level: OutlineLevel } =>
51
+ (heading.level === 2 || heading.level === 3) && heading.label !== "",
52
+ );
53
+ if (items.length < 2) return [];
54
+ return items.map(({ id, label, level }) => ({ id, label, level }));
55
+ }
56
+
57
+ /** The outline of a Tiptap document. */
58
+ export function outlineOfDoc(doc: unknown): OutlineHeading[] {
59
+ return outlineOf(headingsOf(doc));
60
+ }
@@ -28,7 +28,7 @@
28
28
  <span
29
29
  class="grid h-5 min-w-5 place-items-center rounded-sm bg-foreground/5 px-1 text-xs font-medium text-muted-foreground tabular-nums"
30
30
  >
31
- {row.count}
31
+ {row.count.toLocaleString()}
32
32
  </span>
33
33
  </div>
34
34
  </div>
@@ -9,7 +9,14 @@
9
9
  import StatusDot from "../../status/status-dot.svelte";
10
10
  import { TONE_TEXT } from "../../lib/tone.js";
11
11
  import InboxDraftEditor from "./inbox-draft.svelte";
12
- import { canUndo, isOpen, startDraft } from "./inbox-logic.js";
12
+ import {
13
+ canUndo,
14
+ cardFacts,
15
+ freshReasoning,
16
+ isOpen,
17
+ startDraft,
18
+ whenOf,
19
+ } from "./inbox-logic.js";
13
20
  import {
14
21
  SOURCE_ICON,
15
22
  SOURCE_LABEL,
@@ -29,15 +36,38 @@
29
36
  onverdict: (verdict: InboxVerdict) => void;
30
37
  /** Replaces the summary lines, for a host that renders its own proposal. */
31
38
  renderProposal?: Snippet<[InboxItem]>;
39
+ /** How the source time reads. Defaults to a relative time, never a raw ISO string. */
40
+ when?: (at: string) => string;
32
41
  }
33
42
 
34
- let { item, selected = false, onselect, onverdict, renderProposal }: Props = $props();
43
+ const FACT =
44
+ "relative inline-flex items-center ps-3 before:absolute before:start-1 before:opacity-40 before:content-['·']";
45
+
46
+ let {
47
+ item,
48
+ selected = false,
49
+ onselect,
50
+ onverdict,
51
+ renderProposal,
52
+ when = whenOf,
53
+ }: Props = $props();
35
54
 
36
55
  let drafts = $derived(item.proposal.drafts ?? []);
37
56
  let open = $derived(isOpen(item));
38
57
  let level = $derived(confidenceLevel(item.confidence.score));
39
58
  let tone = $derived(inboxConfidence.tone(level));
40
59
  let SourceIcon = $derived(SOURCE_ICON[item.trace.source.type]);
60
+ let facts = $derived(cardFacts(item));
61
+ let time = $derived(when(item.trace.source.at));
62
+ let why = $derived(freshReasoning(item, facts));
63
+ // WHY not the source link alone: a trace holding only "Open source" was a disclosure to open
64
+ // for a link the host already shows.
65
+ let hasTrace = $derived(item.trace.evidence.length > 0 || !!why);
66
+ let expanded = $state(false);
67
+ let longCommand = $derived(
68
+ !!facts.command &&
69
+ (facts.command.text.length > 240 || facts.command.text.split("\n").length > 3),
70
+ );
41
71
 
42
72
  let pickedDraft = $state<string>();
43
73
  let edits = $state<InboxDraftEdits>({});
@@ -167,64 +197,99 @@
167
197
  <header class="flex items-start gap-2.5">
168
198
  <StatusDot statuses={inboxStates} status={item.state} class="mt-0.5" />
169
199
  <span class="sr-only">{inboxStates.label(item.state)}.</span>
170
- <div class="flex min-w-0 flex-1 flex-col gap-1">
200
+ <div class="flex min-w-0 flex-1 flex-col gap-2">
171
201
  <div class="flex flex-wrap items-center gap-x-2 gap-y-1">
172
202
  <h3 class="text-sm leading-5 font-medium text-balance">{item.title}</h3>
173
- <StatusBadge statuses={inboxKinds} status={item.kind} />
203
+ {#if item.kind !== "action"}
204
+ <StatusBadge statuses={inboxKinds} status={item.kind} />
205
+ {/if}
174
206
  {#if !open}
175
207
  <StatusBadge statuses={inboxStates} status={item.state} />
176
208
  {/if}
177
209
  </div>
210
+
178
211
  {#if renderProposal}
179
212
  {@render renderProposal(item)}
180
213
  {:else}
181
- <p class="text-muted-foreground text-sm text-pretty">{item.proposal.summary}</p>
182
- {#if item.proposal.detail}
183
- <p class="text-muted-foreground text-sm text-pretty">{item.proposal.detail}</p>
214
+ {#if facts.command}
215
+ <div class="flex flex-col items-start gap-1">
216
+ <!-- The padding sits on a wrapper: on the clamped box itself, the 4th line showed through it. -->
217
+ <div class="bg-muted/50 w-full rounded-md px-2.5 py-2">
218
+ <pre
219
+ class={cn(
220
+ "text-foreground font-mono text-xs leading-5 break-words whitespace-pre-wrap",
221
+ !expanded && "line-clamp-3",
222
+ )}><span class="text-muted-foreground select-none"
223
+ >{facts.command.tool} </span>{facts.command.text}</pre>
224
+ </div>
225
+ {#if longCommand}
226
+ <button
227
+ type="button"
228
+ class="text-muted-foreground hover:text-foreground text-xs underline-offset-2 hover:underline"
229
+ aria-expanded={expanded}
230
+ onclick={() => (expanded = !expanded)}
231
+ >
232
+ {expanded ? "Show less" : "Show all"}
233
+ </button>
234
+ {/if}
235
+ </div>
184
236
  {/if}
237
+ {#each facts.lines as line, i (i)}
238
+ <p class="text-muted-foreground text-sm text-pretty">{line}</p>
239
+ {/each}
185
240
  {/if}
186
- </div>
187
- </header>
188
241
 
189
- <div class="text-muted-foreground flex flex-wrap items-center gap-x-4 gap-y-1 pl-6 text-xs">
190
- <span class="inline-flex items-center gap-1.5">
191
- <SourceIcon size={14} aria-hidden="true" />
192
- {SOURCE_LABEL[item.trace.source.type]}, {item.trace.source.at}
193
- </span>
194
- <span class={cn("inline-flex items-center gap-1.5 tabular-nums", TONE_TEXT[tone])}>
195
- <StatusDot
196
- statuses={inboxConfidence}
197
- status={level}
198
- progress={item.confidence.score}
199
- size={12}
200
- animate={false}
201
- />
202
- {Math.round(item.confidence.score * 100)}% sure
203
- </span>
204
- {#if item.agent}
205
- <span class="inline-flex items-center gap-1.5">
206
- <AgentAvatar
207
- name={item.agent.name}
208
- shape={item.agent.shape}
209
- color={item.agent.color}
210
- state={item.state === "working" ? "working" : "default"}
211
- size={16}
212
- paused={item.state !== "working"}
213
- interactive={false}
214
- aria-hidden
215
- />
216
- {item.agent.name}
217
- </span>
218
- {/if}
219
- </div>
242
+ <!-- Each dot hangs off the fact it leads, in a gutter the wrapper clips, so a wrapped line
243
+ never starts with a dot. The same pattern as DetailHeader's meta line. -->
244
+ <div class="overflow-hidden">
245
+ <p class="text-muted-foreground -ms-3 flex flex-wrap items-center gap-y-1 text-xs">
246
+ {#if item.agent}
247
+ <span class={cn(FACT, "gap-1.5")}>
248
+ <AgentAvatar
249
+ name={item.agent.name}
250
+ shape={item.agent.shape}
251
+ color={item.agent.color}
252
+ state={item.state === "working" ? "working" : "default"}
253
+ size={16}
254
+ paused={item.state !== "working"}
255
+ interactive={false}
256
+ aria-hidden
257
+ />
258
+ {item.agent.name}
259
+ </span>
260
+ {:else}
261
+ <span class={cn(FACT, "gap-1.5")}>
262
+ <SourceIcon size={14} aria-hidden="true" />
263
+ {SOURCE_LABEL[item.trace.source.type]}
264
+ </span>
265
+ {/if}
266
+ {#if time}
267
+ <span class={cn(FACT, "tabular-nums")}>{time}</span>
268
+ {/if}
269
+ {#if level !== "high"}
270
+ <span class={cn(FACT, "gap-1.5 tabular-nums", TONE_TEXT[tone])}>
271
+ <StatusDot
272
+ statuses={inboxConfidence}
273
+ status={level}
274
+ progress={item.confidence.score}
275
+ size={12}
276
+ animate={false}
277
+ />
278
+ {Math.round(item.confidence.score * 100)}% sure
279
+ </span>
280
+ {/if}
281
+ </p>
282
+ </div>
220
283
 
221
- {#if item.confidence.missing && level !== "high" && open}
222
- <p class={cn("pl-6 text-sm", TONE_TEXT[tone])}>Missing: {item.confidence.missing}</p>
223
- {/if}
284
+ {#if item.confidence.missing && level !== "high" && open}
285
+ <p class={cn("text-sm", TONE_TEXT[tone])}>Missing: {item.confidence.missing}</p>
286
+ {/if}
224
287
 
225
- <div class="pl-6">
226
- <InboxTraceView trace={item.trace} />
227
- </div>
288
+ {#if hasTrace}
289
+ <InboxTraceView trace={item.trace} reasoning={why} agent={item.agent?.name} />
290
+ {/if}
291
+ </div>
292
+ </header>
228
293
 
229
294
  {#if item.state === "working"}
230
295
  <div class="flex flex-col gap-1.5 pl-6">
@@ -272,11 +337,9 @@
272
337
  <div class="flex flex-wrap items-center gap-2 pl-6">
273
338
  <Button size="sm" onclick={approve} aria-keyshortcuts="a">{sendLabel}</Button>
274
339
  {#if !drafts.some((d) => d.outcome === "decline")}
275
- <Button size="sm" variant="destructive" onclick={decline} aria-keyshortcuts="d"
276
- >Decline</Button
277
- >
340
+ <Button size="sm" variant="ghost" onclick={decline} aria-keyshortcuts="d">Decline</Button>
278
341
  {/if}
279
- <Button size="sm" variant="outline" onclick={steer} aria-keyshortcuts="s">Steer</Button>
342
+ <Button size="sm" variant="ghost" onclick={steer} aria-keyshortcuts="s">Steer</Button>
280
343
  {#if drafts.length}
281
344
  <Button size="sm" variant="ghost" onclick={copy} class="ml-auto" aria-live="polite">
282
345
  {copied ? "Copied" : "Copy"}
@@ -0,0 +1,98 @@
1
+ import type { InboxDraft, InboxItem } from "./inbox-types.js";
2
+
3
+ /** The draft to open on: the agent's pick, else the first. */
4
+ export function startDraft(drafts: InboxDraft[], recommended?: string): InboxDraft | undefined {
5
+ return drafts.find((draft) => draft.templateId === recommended) ?? drafts[0];
6
+ }
7
+
8
+ /** A person can act on an item only while it waits for them. */
9
+ export function isOpen(item: InboxItem): boolean {
10
+ return item.tier === "ask" && item.state === "proposed";
11
+ }
12
+
13
+ export function canUndo(item: InboxItem): boolean {
14
+ return item.tier === "auto" && item.state === "done";
15
+ }
16
+
17
+ /** Lowercase, one space between words, no trailing ellipsis: what two copies of a line share. */
18
+ function plain(text: string): string {
19
+ return text
20
+ .replace(/(…|\.{3})\s*$/, "")
21
+ .replace(/\s+/g, " ")
22
+ .trim()
23
+ .toLowerCase();
24
+ }
25
+
26
+ /** The lines of a card that say something the title and the command don't. */
27
+ export type CardFacts = {
28
+ lines: string[];
29
+ command?: { tool: string; text: string };
30
+ };
31
+
32
+ /**
33
+ * Each fact once: a line that repeats the title or the command, even clipped, is dropped, and
34
+ * one that starts with the title keeps only the rest.
35
+ */
36
+ export function cardFacts(item: InboxItem): CardFacts {
37
+ const { command, summary, detail } = item.proposal;
38
+ const shown = [plain(item.title)];
39
+ if (command) shown.push(plain(`${command.tool}: ${command.text}`), plain(command.text));
40
+ const lines: string[] = [];
41
+ for (const raw of [summary, detail]) {
42
+ let line = raw?.trim() ?? "";
43
+ const title = item.title.trim();
44
+ if (line.toLowerCase().startsWith(title.toLowerCase()) && line.length > title.length) {
45
+ line = line.slice(title.length).replace(/^[\s:.,;-]+/, "");
46
+ }
47
+ const key = plain(line);
48
+ if (!key || shown.some((seen) => seen.includes(key) || key.includes(seen))) continue;
49
+ shown.push(key);
50
+ lines.push(line);
51
+ }
52
+ return { lines, command };
53
+ }
54
+
55
+ /** The reasoning of a trace when no line of the card already says it. */
56
+ export function freshReasoning(item: InboxItem, facts: CardFacts): string {
57
+ const reasoning = item.trace.reasoning.trim();
58
+ const key = plain(reasoning);
59
+ if (!key) return "";
60
+ const said = [item.title, item.proposal.summary, item.proposal.detail ?? "", ...facts.lines]
61
+ .map(plain)
62
+ .filter(Boolean);
63
+ return said.some((line) => line.includes(key) || key.includes(line)) ? "" : reasoning;
64
+ }
65
+
66
+ const ISO = /^\d{4}-\d{2}-\d{2}T/;
67
+ const UNITS: [number, Intl.RelativeTimeFormatUnit][] = [
68
+ [604_800, "week"],
69
+ [86_400, "day"],
70
+ [3_600, "hour"],
71
+ [60, "minute"],
72
+ ];
73
+
74
+ /**
75
+ * A time as a person says it: "3 hr. ago", "Oct 12". A host-formatted string stays as written,
76
+ * and a raw ISO string never reaches the screen.
77
+ */
78
+ export function whenOf(at: string, now: number = Date.now(), locale = "en-GB"): string {
79
+ if (!ISO.test(at)) return at;
80
+ const time = Date.parse(at);
81
+ if (Number.isNaN(time)) return "";
82
+ const seconds = Math.round((time - now) / 1000);
83
+ const age = Math.abs(seconds);
84
+ if (age >= 4 * 604_800) {
85
+ return new Intl.DateTimeFormat(locale, { month: "short", day: "numeric" }).format(time);
86
+ }
87
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto", style: "short" });
88
+ for (const [size, unit] of UNITS) {
89
+ if (age >= size) return rtf.format(Math.round(seconds / size), unit);
90
+ }
91
+ return rtf.format(0, "second");
92
+ }
93
+
94
+ /** True when a key press belongs to a text field and so isn't a shortcut. */
95
+ export function isTyping(target: EventTarget | null): boolean {
96
+ if (!(target instanceof HTMLElement)) return false;
97
+ return target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName);
98
+ }
@@ -0,0 +1,58 @@
1
+ <script lang="ts">
2
+ import { Reasoning, ReasoningContent, ReasoningTrigger } from "../../ai/reasoning/index.js";
3
+ import type { InboxTrace } from "./inbox-types.js";
4
+
5
+ interface Props {
6
+ trace: InboxTrace;
7
+ /** The reasoning no other line of the card says. Empty when the card already shows it. */
8
+ reasoning?: string;
9
+ /** The name the card's meta line shows, so the source label doesn't repeat it. */
10
+ agent?: string;
11
+ }
12
+
13
+ let { trace, reasoning = "", agent }: Props = $props();
14
+
15
+ let count = $derived(trace.evidence.length);
16
+ let label = $derived(
17
+ trace.source.label.trim() === agent?.trim() ? "" : trace.source.label.trim(),
18
+ );
19
+ </script>
20
+
21
+ <!-- WHY the shared Reasoning and not a local disclosure: it is already the
22
+ collapsed-by-default "how the agent got here" pattern, with the same chevron. -->
23
+ <Reasoning>
24
+ <ReasoningTrigger class="w-auto">
25
+ <span>Trace{count > 0 ? ` · ${count} ${count === 1 ? "quote" : "quotes"}` : ""}</span>
26
+ </ReasoningTrigger>
27
+ <ReasoningContent>
28
+ <div class="flex flex-col gap-3">
29
+ {#if label || trace.source.href}
30
+ <p class="text-foreground">
31
+ {label}
32
+ {#if trace.source.href}
33
+ <a href={trace.source.href} class="text-foreground underline underline-offset-2"
34
+ >Open source</a
35
+ >
36
+ {/if}
37
+ </p>
38
+ {/if}
39
+ {#if count > 0}
40
+ <ul class="flex flex-col gap-2">
41
+ {#each trace.evidence as evidence, i (i)}
42
+ <li>
43
+ <blockquote class="text-foreground">&ldquo;{evidence.quote}&rdquo;</blockquote>
44
+ {#if evidence.by || evidence.at}
45
+ <span class="text-xs tabular-nums"
46
+ >{[evidence.by, evidence.at].filter(Boolean).join(", ")}</span
47
+ >
48
+ {/if}
49
+ </li>
50
+ {/each}
51
+ </ul>
52
+ {/if}
53
+ {#if reasoning}
54
+ <p><span class="text-foreground font-medium">Why:</span> {reasoning}</p>
55
+ {/if}
56
+ </div>
57
+ </ReasoningContent>
58
+ </Reasoning>
@@ -28,6 +28,8 @@ export type InboxDraft = {
28
28
  export type InboxProposal = {
29
29
  summary: string;
30
30
  detail?: string;
31
+ /** The exact call to run, drawn once in monospace. Summary and detail that repeat it are dropped. */
32
+ command?: { tool: string; text: string };
31
33
  drafts?: InboxDraft[];
32
34
  /** The draft to open on. Defaults to the first. */
33
35
  recommended?: string;
@@ -13,16 +13,6 @@ export type {
13
13
  KanbanMove,
14
14
  KanbanSortKey,
15
15
  } from "./kanban-types.js";
16
- export { StatusBadge, StatusDot, StatusIcon, defineStatuses } from "../status/index.js";
17
- export type {
18
- StatusDef,
19
- StatusDotVariant,
20
- StatusId,
21
- StatusMap,
22
- Statuses,
23
- } from "../status/index.js";
24
- export { SidePanel } from "../panel/index.js";
25
- export type { PanelStat, SidePanelWidth } from "../panel/index.js";
26
16
  export { default as TaskList } from "./task-list.svelte";
27
17
  export { default as TaskGantt } from "./task-gantt.svelte";
28
18
  export { addDays, dayNumber, isoFromDay, todayIso } from "./dates.js";
@@ -484,7 +484,7 @@
484
484
  <span class="truncate whitespace-nowrap">{column.label}</span>
485
485
  {#if count > 0}
486
486
  <span class="ms-auto text-xs text-muted-foreground/60 tabular-nums"
487
- >{count}</span
487
+ >{count.toLocaleString()}</span
488
488
  >
489
489
  {/if}
490
490
  {/if}
@@ -254,7 +254,7 @@
254
254
  <span
255
255
  class="grid h-5 min-w-5 place-items-center rounded-sm bg-foreground/5 px-1 text-xs font-medium text-muted-foreground tabular-nums"
256
256
  >
257
- {count}
257
+ {count.toLocaleString()}
258
258
  </span>
259
259
  {/if}
260
260
  {#if actions}
@@ -1,6 +1,7 @@
1
1
  <script lang="ts" generics="T">
2
2
  import type { Snippet } from "svelte";
3
3
  import { cn } from "../../lib/utils.js";
4
+ import { initial } from "../../lib/initials.js";
4
5
  import { TONE_COLOR, TONE_TEXT } from "../../lib/tone.js";
5
6
  import * as Popover from "../../shadcn/popover/index.js";
6
7
  import {
@@ -206,10 +207,6 @@
206
207
  deleting = null;
207
208
  }
208
209
  }
209
-
210
- function initial(name: string): string {
211
- return name.trim().charAt(0).toLocaleUpperCase() || "?";
212
- }
213
210
  </script>
214
211
 
215
212
  <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
@@ -468,7 +465,7 @@
468
465
  }
469
466
 
470
467
  .time-toggle:active {
471
- transform: scale(0.94);
468
+ transform: scale(0.96);
472
469
  }
473
470
 
474
471
  @media (any-pointer: coarse) {
@@ -1,21 +0,0 @@
1
- import type { InboxDraft, InboxItem } from "./inbox-types.js";
2
-
3
- /** The draft to open on: the agent's pick, else the first. */
4
- export function startDraft(drafts: InboxDraft[], recommended?: string): InboxDraft | undefined {
5
- return drafts.find((draft) => draft.templateId === recommended) ?? drafts[0];
6
- }
7
-
8
- /** A person can act on an item only while it waits for them. */
9
- export function isOpen(item: InboxItem): boolean {
10
- return item.tier === "ask" && item.state === "proposed";
11
- }
12
-
13
- export function canUndo(item: InboxItem): boolean {
14
- return item.tier === "auto" && item.state === "done";
15
- }
16
-
17
- /** True when a key press belongs to a text field and so isn't a shortcut. */
18
- export function isTyping(target: EventTarget | null): boolean {
19
- if (!(target instanceof HTMLElement)) return false;
20
- return target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName);
21
- }
@@ -1,51 +0,0 @@
1
- <script lang="ts">
2
- import { Reasoning, ReasoningContent, ReasoningTrigger } from "../../ai/reasoning/index.js";
3
- import { SOURCE_ICON, SOURCE_LABEL } from "./inbox-statuses.js";
4
- import type { InboxTrace } from "./inbox-types.js";
5
-
6
- interface Props {
7
- trace: InboxTrace;
8
- }
9
-
10
- let { trace }: Props = $props();
11
-
12
- let count = $derived(trace.evidence.length);
13
- let SourceIcon = $derived(SOURCE_ICON[trace.source.type]);
14
- </script>
15
-
16
- <!-- WHY the shared Reasoning and not a local disclosure: it is already the
17
- collapsed-by-default "how the agent got here" pattern, with the same chevron. -->
18
- <Reasoning>
19
- <ReasoningTrigger class="w-auto">
20
- <SourceIcon size={14} class="shrink-0" aria-hidden="true" />
21
- <span>
22
- Trace: {SOURCE_LABEL[trace.source.type]}, {trace.source.at}, {count}
23
- {count === 1 ? "quote" : "quotes"}
24
- </span>
25
- </ReasoningTrigger>
26
- <ReasoningContent>
27
- <div class="flex flex-col gap-3">
28
- <p class="text-foreground">
29
- {trace.source.label}
30
- {#if trace.source.href}
31
- <a href={trace.source.href} class="text-foreground ml-1 underline underline-offset-2"
32
- >Open source</a
33
- >
34
- {/if}
35
- </p>
36
- <ul class="flex flex-col gap-2">
37
- {#each trace.evidence as evidence, i (i)}
38
- <li>
39
- <blockquote class="text-foreground">&ldquo;{evidence.quote}&rdquo;</blockquote>
40
- {#if evidence.by || evidence.at}
41
- <span class="text-xs tabular-nums"
42
- >{[evidence.by, evidence.at].filter(Boolean).join(", ")}</span
43
- >
44
- {/if}
45
- </li>
46
- {/each}
47
- </ul>
48
- <p><span class="text-foreground font-medium">Why:</span> {trace.reasoning}</p>
49
- </div>
50
- </ReasoningContent>
51
- </Reasoning>
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes