@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.
- package/COMPONENTS.md +1 -1
- package/README.md +1 -1
- package/package.json +58 -74
- package/src/agents/vendor/draw.ts +4 -4
- package/src/canvas/headless/canvas-cleanup.test.ts +1 -1
- package/src/canvas/ops/ops.ts +1 -1
- package/src/charts/area-chart.svelte +11 -1
- package/src/charts/bar-chart.svelte +18 -3
- package/src/charts/line-chart.svelte +11 -1
- package/src/components/data-table/data-table.ts +4 -5
- package/src/components/data-table/pagination.svelte +5 -3
- package/src/components/docs/on-this-page.svelte +82 -5
- package/src/components/docs/scroll-spy.svelte.ts +6 -1
- package/src/components/list-page/detail-header.svelte +7 -3
- package/src/components/list-page/empty-state.svelte +3 -1
- package/src/components/list-page/filter-popover.svelte +2 -2
- package/src/components/list-page/rail-row.svelte +5 -2
- package/src/components/mark/mark.svelte +2 -1
- package/src/components/section-card/section-card.svelte +3 -1
- package/src/components/section-header/section-header.svelte +5 -5
- package/src/components/segmented/segment.svelte +1 -1
- package/src/components/table-view/table-view.svelte +6 -1
- package/src/components/tags-input/tags-input.svelte +1 -1
- package/src/components/toolbar/toolbar.svelte +1 -1
- package/src/editor/anchors.ts +28 -0
- package/src/editor/doc-blocks.ts +168 -0
- package/src/editor/doc-page.css +389 -0
- package/src/editor/editor.svelte +47 -5
- package/src/editor/extensions.ts +26 -2
- package/src/editor/markdown.ts +27 -2
- package/src/editor/slash-items.ts +18 -0
- package/src/icons/trades.ts +1 -1
- package/src/inbox/README.md +6 -4
- package/src/inbox/inbox.test.ts +1 -1
- package/src/inbox/index.ts +2 -2
- package/src/inbox/logic.ts +29 -5
- package/src/inbox/thread-inbox.svelte +219 -92
- package/src/inbox/types.ts +27 -1
- package/src/lib/initials.test.ts +25 -0
- package/src/lib/initials.ts +29 -0
- package/src/modules.test.ts +4 -64
- package/src/panel/README.md +1 -1
- package/src/record/README.md +1 -1
- package/src/record/info-list.svelte +5 -4
- package/src/record/next-up.svelte +1 -1
- package/src/record/project-list.svelte +1 -1
- package/src/record/steps-rail.svelte +3 -3
- package/src/shadcn/badge/badge.svelte +1 -1
- package/src/shadcn/card/card-title.svelte +1 -5
- package/src/shadcn/empty/empty-description.svelte +1 -1
- package/src/shadcn/progress/progress.svelte +8 -1
- package/src/shadcn/table/table-cell.svelte +10 -2
- package/src/status/README.md +1 -1
- package/src/thread/README.md +2 -0
- package/src/thread/index.ts +9 -1
- package/src/thread/logic.ts +33 -3
- package/src/thread/thread-body.svelte +37 -0
- package/src/thread/thread-events.svelte +30 -0
- package/src/thread/thread.svelte +74 -59
- package/src/thread/types.ts +21 -0
- package/src/{tasks → work}/README.md +11 -8
- package/src/{tasks → work}/detail/task-thread.svelte +8 -15
- package/src/work/docs/doc-editor-page.svelte +194 -0
- package/src/work/docs/doc-menu.svelte +333 -0
- package/src/work/docs/docs-demo.svelte +227 -0
- package/src/work/docs/edited-chip.svelte +125 -0
- package/src/work/docs/edited.ts +54 -0
- package/src/work/docs/index.ts +38 -0
- package/src/work/docs/labels.ts +70 -0
- package/src/work/docs/menu.ts +127 -0
- package/src/work/docs/outline.ts +60 -0
- package/src/{tasks → work}/gantt/gantt-group-row.svelte +1 -1
- package/src/{tasks → work}/inbox/inbox-card.svelte +113 -50
- package/src/work/inbox/inbox-logic.ts +98 -0
- package/src/work/inbox/inbox-trace.svelte +58 -0
- package/src/{tasks → work}/inbox/inbox-types.ts +2 -0
- package/src/{tasks → work}/index.ts +0 -10
- package/src/{tasks → work}/kanban-board.svelte +1 -1
- package/src/{tasks → work}/task-list.svelte +1 -1
- package/src/{tasks → work}/time/time-tracker.svelte +2 -5
- package/src/tasks/inbox/inbox-logic.ts +0 -21
- package/src/tasks/inbox/inbox-trace.svelte +0 -51
- /package/src/{tasks → work}/ambient.d.ts +0 -0
- /package/src/{tasks → work}/board-order.ts +0 -0
- /package/src/{tasks → work}/dates.ts +0 -0
- /package/src/{tasks → work}/demo.svelte +0 -0
- /package/src/{tasks → work}/detail/detail-demo.svelte +0 -0
- /package/src/{tasks → work}/detail/detail-types.ts +0 -0
- /package/src/{tasks → work}/detail/task-chip.svelte +0 -0
- /package/src/{tasks → work}/detail/task-detail.svelte +0 -0
- /package/src/{tasks → work}/detail/task-property.svelte +0 -0
- /package/src/{tasks → work}/gantt/gantt-bar.svelte +0 -0
- /package/src/{tasks → work}/gantt/gantt-grid.svelte +0 -0
- /package/src/{tasks → work}/gantt/gantt-header.svelte +0 -0
- /package/src/{tasks → work}/gantt/gantt-task-row.svelte +0 -0
- /package/src/{tasks → work}/gantt/gantt.css +0 -0
- /package/src/{tasks → work}/gantt/gantt.test.ts +0 -0
- /package/src/{tasks → work}/gantt/logic.test.ts +0 -0
- /package/src/{tasks → work}/gantt/logic.ts +0 -0
- /package/src/{tasks → work}/inbox/inbox-draft.svelte +0 -0
- /package/src/{tasks → work}/inbox/inbox-statuses.ts +0 -0
- /package/src/{tasks → work}/inbox/index.ts +0 -0
- /package/src/{tasks → work}/kanban-column.svelte +0 -0
- /package/src/{tasks → work}/kanban-drag.ts +0 -0
- /package/src/{tasks → work}/kanban-types.ts +0 -0
- /package/src/{tasks → work}/presets/sales.ts +0 -0
- /package/src/{tasks → work}/reorder.ts +0 -0
- /package/src/{tasks → work}/task-gantt.svelte +0 -0
- /package/src/{tasks → work}/task-types.ts +0 -0
- /package/src/{tasks → work}/time/duration.ts +0 -0
- /package/src/{tasks → work}/time/time-chip.svelte +0 -0
- /package/src/{tasks → work}/time/time-dot.svelte +0 -0
- /package/src/{tasks → work}/time/time-ring.svelte +0 -0
- /package/src/{tasks → work}/time/time-types.ts +0 -0
- /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
|
+
}
|
|
@@ -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 {
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
<
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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="
|
|
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="
|
|
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">“{evidence.quote}”</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";
|
|
@@ -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.
|
|
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">“{evidence.quote}”</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
|