@joewinke/jatui 0.38.5 → 0.39.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 (67) hide show
  1. package/CLAUDE.md +32 -1
  2. package/THIRD_PARTY_NOTICES.md +51 -0
  3. package/dist/components/BulkActionBar.svelte +32 -2
  4. package/dist/components/BulkActionBar.svelte.d.ts +5 -1
  5. package/dist/components/FloatingActionBar.svelte +102 -34
  6. package/dist/components/FloatingActionBar.svelte.d.ts +9 -0
  7. package/dist/components/MediaExplorer.svelte +249 -0
  8. package/dist/components/MediaExplorer.svelte.d.ts +37 -0
  9. package/dist/components/NotificationCenter.svelte +481 -0
  10. package/dist/components/NotificationCenter.svelte.d.ts +29 -0
  11. package/dist/components/SearchDropdown.svelte +54 -8
  12. package/dist/components/SearchDropdown.svelte.d.ts +12 -0
  13. package/dist/components/VideoPlayer.svelte +269 -3
  14. package/dist/components/agent/AgentLog.svelte +217 -0
  15. package/dist/components/agent/AgentLog.svelte.d.ts +72 -0
  16. package/dist/components/agent/AgentLogRow.svelte +281 -0
  17. package/dist/components/agent/AgentLogRow.svelte.d.ts +21 -0
  18. package/dist/components/agent/AgentTaskList.svelte +607 -0
  19. package/dist/components/agent/AgentTaskList.svelte.d.ts +40 -0
  20. package/dist/components/agent/AgentThinking.svelte +280 -0
  21. package/dist/components/agent/AgentThinking.svelte.d.ts +22 -0
  22. package/dist/components/agent/ComposerGlow.svelte +265 -0
  23. package/dist/components/agent/ComposerGlow.svelte.d.ts +18 -0
  24. package/dist/components/agent/ContextMeter.svelte +277 -0
  25. package/dist/components/agent/ContextMeter.svelte.d.ts +21 -0
  26. package/dist/components/agent/Questionnaire.svelte +767 -0
  27. package/dist/components/agent/Questionnaire.svelte.d.ts +29 -0
  28. package/dist/components/agent/ShimmerText.svelte +100 -0
  29. package/dist/components/agent/ShimmerText.svelte.d.ts +19 -0
  30. package/dist/components/agent/StreamedText.svelte +135 -0
  31. package/dist/components/agent/StreamedText.svelte.d.ts +13 -0
  32. package/dist/components/agent/agentLog.d.ts +22 -0
  33. package/dist/components/agent/agentLog.js +26 -0
  34. package/dist/components/agent/agentTaskList.d.ts +90 -0
  35. package/dist/components/agent/agentTaskList.js +93 -0
  36. package/dist/components/agent/agentThinking.d.ts +46 -0
  37. package/dist/components/agent/agentThinking.js +78 -0
  38. package/dist/components/agent/contextMeter.d.ts +97 -0
  39. package/dist/components/agent/contextMeter.js +156 -0
  40. package/dist/components/agent/questionnaire.d.ts +104 -0
  41. package/dist/components/agent/questionnaire.js +225 -0
  42. package/dist/components/agent/revealCore.d.ts +74 -0
  43. package/dist/components/agent/revealCore.js +160 -0
  44. package/dist/components/agent/revealTicker.d.ts +34 -0
  45. package/dist/components/agent/revealTicker.js +73 -0
  46. package/dist/components/agent/streamedText.d.ts +28 -0
  47. package/dist/components/agent/streamedText.js +59 -0
  48. package/dist/components/messaging/MessageInput.svelte +34 -4
  49. package/dist/components/messaging/MessageInput.svelte.d.ts +7 -0
  50. package/dist/components/messaging/MessageItem.svelte +2 -2
  51. package/dist/components/notificationCenter.d.ts +65 -0
  52. package/dist/components/notificationCenter.js +110 -0
  53. package/dist/components/progress/Steps.svelte +269 -5
  54. package/dist/components/progress/Steps.svelte.d.ts +16 -1
  55. package/dist/components/progress/stepsSummary.d.ts +14 -0
  56. package/dist/components/progress/stepsSummary.js +32 -0
  57. package/dist/index.d.ts +34 -0
  58. package/dist/index.js +29 -0
  59. package/dist/selection/clickSelection.d.ts +89 -0
  60. package/dist/selection/clickSelection.js +152 -0
  61. package/dist/selection/createClickSelection.d.ts +79 -0
  62. package/dist/selection/createClickSelection.js +134 -0
  63. package/dist/video/keys.d.ts +5 -5
  64. package/dist/video/keys.js +9 -5
  65. package/dist/video/transcriptSearch.d.ts +80 -0
  66. package/dist/video/transcriptSearch.js +153 -0
  67. package/package.json +3 -2
@@ -0,0 +1,29 @@
1
+ import { type QuestionnaireAnswers, type QuestionnaireQuestion } from './questionnaire';
2
+ interface Props {
3
+ /** AskUserQuestion-shaped questions. */
4
+ questions: QuestionnaireQuestion[];
5
+ /** Answers aligned with `questions` (bindable). */
6
+ answers?: QuestionnaireAnswers;
7
+ /** Current question index (bindable). */
8
+ step?: number;
9
+ /** Show the free-text "Other" row on every question. */
10
+ allowOther?: boolean;
11
+ otherLabel?: string;
12
+ otherPlaceholder?: string;
13
+ /** Auto-advance delay (ms) after a single-select pick; `false` disables. */
14
+ advanceDelay?: number | false;
15
+ submitLabel?: string;
16
+ nextLabel?: string;
17
+ previousLabel?: string;
18
+ /** Card heading, e.g. "A few questions before I start". */
19
+ title?: string;
20
+ /** Done stays disabled until every question has an answer. */
21
+ requireAll?: boolean;
22
+ disabled?: boolean;
23
+ onChange?: (answers: QuestionnaireAnswers) => void;
24
+ onComplete?: (answers: QuestionnaireAnswers) => void;
25
+ class?: string;
26
+ }
27
+ declare const Questionnaire: import("svelte").Component<Props, {}, "step" | "answers">;
28
+ type Questionnaire = ReturnType<typeof Questionnaire>;
29
+ export default Questionnaire;
@@ -0,0 +1,100 @@
1
+ <!--
2
+ ShimmerText — a highlight that travels across a line of text ("Thinking",
3
+ "Searching the docs", a running task title). One tone in, every stop derived
4
+ from it with color-mix, so any token colour shimmers consistently.
5
+
6
+ The single-tone gradient idea comes from BoardUI's agent-thinking label:
7
+ MIT License — Copyright (c) 2026 Mertcan Dundar Esmergul (BoardUI)
8
+
9
+ Decorative loop: marked .jatui-motion-ambient and turned off (plain text in
10
+ the tone colour, no gradient left behind) under data-motion="calm" and
11
+ prefers-reduced-motion. `active={false}` does the same on demand.
12
+ -->
13
+ <script lang="ts">
14
+ import type { Snippet } from 'svelte';
15
+ import { TONE_COLOR, type AgentThinkingTone } from './agentThinking';
16
+
17
+ interface Props {
18
+ /** The text. Or pass children for inline markup. */
19
+ text?: string;
20
+ /** Token tone, or any CSS colour (e.g. 'var(--color-info)'). */
21
+ tone?: AgentThinkingTone | (string & {});
22
+ /** One sweep, ms. */
23
+ duration?: number;
24
+ /** false = plain text in the tone colour (the shimmer stops). */
25
+ active?: boolean;
26
+ /** Element to render. */
27
+ as?: 'span' | 'div' | 'p' | 'strong';
28
+ class?: string;
29
+ children?: Snippet;
30
+ }
31
+
32
+ let {
33
+ text,
34
+ tone = 'default',
35
+ duration = 2600,
36
+ active = true,
37
+ as = 'span',
38
+ class: className = '',
39
+ children
40
+ }: Props = $props();
41
+
42
+ const color = $derived(TONE_COLOR[tone as AgentThinkingTone] ?? tone);
43
+ </script>
44
+
45
+ <svelte:element
46
+ this={as}
47
+ class="jatui-shimmer {className}"
48
+ class:jatui-motion-ambient={active}
49
+ class:is-active={active}
50
+ style:--jatui-shimmer-tone={color}
51
+ style:--jatui-shimmer-duration="{duration}ms"
52
+ >
53
+ {#if children}{@render children()}{:else}{text}{/if}
54
+ </svelte:element>
55
+
56
+ <style>
57
+ .jatui-shimmer {
58
+ color: var(--jatui-shimmer-tone);
59
+ }
60
+
61
+ .jatui-shimmer.is-active {
62
+ color: transparent;
63
+ background-image: linear-gradient(
64
+ 100deg,
65
+ color-mix(in oklab, var(--jatui-shimmer-tone) 55%, transparent) 30%,
66
+ var(--jatui-shimmer-tone) 50%,
67
+ color-mix(in oklab, var(--jatui-shimmer-tone) 55%, transparent) 70%
68
+ );
69
+ background-size: 300% 100%;
70
+ background-position: 200% center;
71
+ -webkit-background-clip: text;
72
+ background-clip: text;
73
+ animation: jatui-shimmer-sweep var(--jatui-shimmer-duration) linear infinite;
74
+ }
75
+
76
+ @keyframes jatui-shimmer-sweep {
77
+ from {
78
+ background-position: 200% center;
79
+ }
80
+ to {
81
+ background-position: -100% center;
82
+ }
83
+ }
84
+
85
+ /* Calm / reduced motion: solid text, not a frozen half-dim gradient. */
86
+ :global([data-motion='calm']) .jatui-shimmer.is-active,
87
+ .jatui-shimmer.is-active:global([data-motion='calm']) {
88
+ color: var(--jatui-shimmer-tone);
89
+ background-image: none;
90
+ animation: none;
91
+ }
92
+
93
+ @media (prefers-reduced-motion: reduce) {
94
+ .jatui-shimmer.is-active {
95
+ color: var(--jatui-shimmer-tone);
96
+ background-image: none;
97
+ animation: none;
98
+ }
99
+ }
100
+ </style>
@@ -0,0 +1,19 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type AgentThinkingTone } from './agentThinking';
3
+ interface Props {
4
+ /** The text. Or pass children for inline markup. */
5
+ text?: string;
6
+ /** Token tone, or any CSS colour (e.g. 'var(--color-info)'). */
7
+ tone?: AgentThinkingTone | (string & {});
8
+ /** One sweep, ms. */
9
+ duration?: number;
10
+ /** false = plain text in the tone colour (the shimmer stops). */
11
+ active?: boolean;
12
+ /** Element to render. */
13
+ as?: 'span' | 'div' | 'p' | 'strong';
14
+ class?: string;
15
+ children?: Snippet;
16
+ }
17
+ declare const ShimmerText: import("svelte").Component<Props, {}, "">;
18
+ type ShimmerText = ReturnType<typeof ShimmerText>;
19
+ export default ShimmerText;
@@ -0,0 +1,135 @@
1
+ <!--
2
+ StreamedText — streamed assistant text that reveals per BLOCK (paragraph or
3
+ line), never per token or word.
4
+
5
+ Why blocks: a token arrives as a word fragment; a fade/blur on that fragment
6
+ then mutates into the finished word with no transition, which reads as
7
+ stutter. Here each block mounts once (keyed by its index — the splitter in
8
+ ./streamedText.ts is append-only, so earlier blocks never change), plays a
9
+ short fade + un-blur on the motion tokens, and from then on only GROWS as
10
+ text arrives. Blocks already present on first render (history) don't
11
+ animate; under prefers-reduced-motion the reveal is instant.
12
+ Idea adapted from BoardUI's agent-chat StreamedText
13
+ (MIT License — Copyright (c) 2026 Mertcan Dundar Esmergul (BoardUI)).
14
+
15
+ Props:
16
+ text the accumulated text so far
17
+ streaming shows a subtle caret after the last block while true
18
+ split 'paragraph' (blank-line separated, fences kept whole; default)
19
+ or 'line'
20
+ as root element (default 'div')
21
+ renderBlock Snippet<[text, index]> to render each block yourself (e.g.
22
+ markdown → HTML). The caret is then appended after your block.
23
+
24
+ ─── Recipe: when to show the thinking row ──────────────────────────────────
25
+ Show <AgentThinking> until the reply's first TEXT part exists — not until the
26
+ request leaves the "submitted" state. Reasoning models flip a chat transport
27
+ to "streaming" as soon as reasoning/tool chunks arrive, so keying the
28
+ indicator off status === 'submitted' hides it while the transcript is still
29
+ empty, leaving a blank gap:
30
+
31
+ const answer = $derived(message.parts.filter((p) => p.type === 'text').map((p) => p.text).join(''))
32
+ {#if busy && !answer}<AgentThinking label="Thinking" />{/if}
33
+ {#if answer}<StreamedText text={answer} streaming={busy} />{/if}
34
+ -->
35
+ <script lang="ts">
36
+ import type { Snippet } from 'svelte';
37
+ import { resolveMotionTiming } from '../../motion/transitions';
38
+ import { splitStreamedText, type StreamedTextSplit } from './streamedText';
39
+
40
+ interface Props {
41
+ text: string;
42
+ streaming?: boolean;
43
+ split?: StreamedTextSplit;
44
+ as?: string;
45
+ class?: string;
46
+ renderBlock?: Snippet<[string, number]>;
47
+ }
48
+
49
+ let {
50
+ text,
51
+ streaming = false,
52
+ split = 'paragraph',
53
+ as = 'div',
54
+ class: className = '',
55
+ renderBlock
56
+ }: Props = $props();
57
+
58
+ const blocks = $derived(splitStreamedText(text ?? '', split));
59
+
60
+ /** Block entrance: opacity + a 4px blur + a small rise, on the motion tokens. */
61
+ function blockIn(node: Element) {
62
+ const { delay, duration, easing } = resolveMotionTiming(node, {}, {
63
+ duration: 'move',
64
+ easing: 'glide'
65
+ });
66
+ return {
67
+ delay,
68
+ duration: duration * 1.4,
69
+ easing,
70
+ css: (t: number, u: number) =>
71
+ `opacity: ${t}; filter: blur(${(u * 4).toFixed(2)}px); transform: translateY(${(u * 4).toFixed(2)}px);`
72
+ };
73
+ }
74
+ </script>
75
+
76
+ <svelte:element this={as} class="streamed-text {className}" aria-busy={streaming ? 'true' : undefined}>
77
+ {#each blocks as block, i (block.key)}
78
+ {@const live = streaming && i === blocks.length - 1}
79
+ {#if renderBlock}
80
+ <div class="streamed-text-block" in:blockIn>
81
+ {@render renderBlock(block.text, i)}{#if live}<span class="streamed-text-caret jatui-motion-ambient" aria-hidden="true"></span>{/if}
82
+ </div>
83
+ {:else}
84
+ <p class="streamed-text-block streamed-text-plain" in:blockIn>
85
+ {block.text}{#if live}<span class="streamed-text-caret jatui-motion-ambient" aria-hidden="true"></span>{/if}
86
+ </p>
87
+ {/if}
88
+ {/each}
89
+ {#if streaming && blocks.length === 0}
90
+ <span class="streamed-text-caret jatui-motion-ambient" aria-hidden="true"></span>
91
+ {/if}
92
+ </svelte:element>
93
+
94
+ <style>
95
+ .streamed-text {
96
+ display: flex;
97
+ flex-direction: column;
98
+ gap: 0.75rem;
99
+ min-width: 0;
100
+ }
101
+
102
+ .streamed-text-block {
103
+ min-width: 0;
104
+ overflow-wrap: anywhere;
105
+ }
106
+
107
+ .streamed-text-plain {
108
+ margin: 0;
109
+ white-space: pre-wrap;
110
+ }
111
+
112
+ .streamed-text-caret {
113
+ display: inline-block;
114
+ width: 0.5em;
115
+ height: 1em;
116
+ margin-left: 0.15em;
117
+ vertical-align: -0.15em;
118
+ border-radius: 1px;
119
+ background: color-mix(in oklch, var(--color-base-content) 45%, transparent);
120
+ animation: streamed-text-caret 1.1s steps(1, end) infinite;
121
+ }
122
+
123
+ @keyframes streamed-text-caret {
124
+ 50% {
125
+ opacity: 0;
126
+ }
127
+ }
128
+
129
+ /* Static caret under reduced motion (the global floor also stops it). */
130
+ @media (prefers-reduced-motion: reduce) {
131
+ .streamed-text-caret {
132
+ animation: none;
133
+ }
134
+ }
135
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type StreamedTextSplit } from './streamedText';
3
+ interface Props {
4
+ text: string;
5
+ streaming?: boolean;
6
+ split?: StreamedTextSplit;
7
+ as?: string;
8
+ class?: string;
9
+ renderBlock?: Snippet<[string, number]>;
10
+ }
11
+ declare const StreamedText: import("svelte").Component<Props, {}, "">;
12
+ type StreamedText = ReturnType<typeof StreamedText>;
13
+ export default StreamedText;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The elbow sits a fixed distance from the row's top, not at 50%, so a row
3
+ * whose label wraps still branches at its first line: half a 20px line plus
4
+ * the row's 4px top padding.
5
+ */
6
+ export declare const BRANCH_Y = 14;
7
+ export declare const BRANCH_RADIUS = 6;
8
+ export declare const BRANCH_WIDTH = 12;
9
+ /** Down the trunk, round the corner, out to the label. */
10
+ export declare const BRANCH_PATH = "M0.5 0 V8 Q0.5 14 6.5 14 H11.5";
11
+ export interface AgentLogContext {
12
+ /**
13
+ * Has the log finished its first render? Rows present on first paint (a
14
+ * history view, a re-opened group) render at rest; rows added afterwards
15
+ * grow in. Plain value, not reactive: each row reads it once, at mount.
16
+ */
17
+ isLive(): boolean;
18
+ /** The log draws a bridge above its first row (nested lists). */
19
+ bridged: boolean;
20
+ }
21
+ export declare function setAgentLogContext(ctx: AgentLogContext): void;
22
+ export declare function getAgentLogContext(): AgentLogContext | undefined;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shared geometry + context for AgentLog / AgentLogRow.
3
+ *
4
+ * Tree-guide geometry adapted from BoardUI's agent-log:
5
+ * MIT License — Copyright (c) 2026 Mertcan Dundar Esmergul (BoardUI)
6
+ * (fixed elbow height so wrapped labels branch at their first line; branch
7
+ * drawn as one path, trunk as a 1px bar scaling from its top.)
8
+ */
9
+ import { getContext, setContext } from 'svelte';
10
+ /**
11
+ * The elbow sits a fixed distance from the row's top, not at 50%, so a row
12
+ * whose label wraps still branches at its first line: half a 20px line plus
13
+ * the row's 4px top padding.
14
+ */
15
+ export const BRANCH_Y = 14;
16
+ export const BRANCH_RADIUS = 6;
17
+ export const BRANCH_WIDTH = 12;
18
+ /** Down the trunk, round the corner, out to the label. */
19
+ export const BRANCH_PATH = `M0.5 0 V${BRANCH_Y - BRANCH_RADIUS} Q0.5 ${BRANCH_Y} ${0.5 + BRANCH_RADIUS} ${BRANCH_Y} H11.5`;
20
+ const KEY = Symbol('jatui-agent-log');
21
+ export function setAgentLogContext(ctx) {
22
+ setContext(KEY, ctx);
23
+ }
24
+ export function getAgentLogContext() {
25
+ return getContext(KEY);
26
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Pure model behind AgentTaskList: unit counting (a task header and each of
3
+ * its steps are one reveal unit), what is visible at a given reveal count,
4
+ * the status shown, the fold policy, and the source-mark stack. No DOM —
5
+ * tests/unit/agentTaskList.test.ts runs it in node.
6
+ *
7
+ * Our own design (clean-room); the reveal pacing it feeds is RevealCore.
8
+ */
9
+ import type { Component } from 'svelte';
10
+ export type AgentTaskStatus = 'pending' | 'running' | 'done' | 'error';
11
+ /** A resource a step touched — a file, a table, a URL. */
12
+ export interface AgentTaskChip {
13
+ label: string;
14
+ /** An icon component taking `{ size, class }` (e.g. lucide-svelte), or a short text glyph. */
15
+ icon?: Component<{
16
+ size?: number | string;
17
+ class?: string;
18
+ }> | string;
19
+ /** Makes the chip a link. */
20
+ href?: string;
21
+ }
22
+ /** A page a research step read. */
23
+ export interface AgentTaskSource {
24
+ title: string;
25
+ href?: string;
26
+ /** Shown in the hover card; derived from `href` when omitted. */
27
+ domain?: string;
28
+ /** Host-supplied icon URL (e.g. your favicon proxy). Falls back to a letter tile. */
29
+ favicon?: string;
30
+ }
31
+ export interface AgentTaskStep {
32
+ id?: string;
33
+ label: string;
34
+ /** Trailing detail, e.g. "5 results" or "212 ms". */
35
+ meta?: string;
36
+ chips?: AgentTaskChip[];
37
+ /** Research step: branches into a "Sources" row of site marks. */
38
+ sources?: AgentTaskSource[];
39
+ }
40
+ export interface AgentTask {
41
+ id: string;
42
+ label: string;
43
+ status: AgentTaskStatus;
44
+ steps: AgentTaskStep[];
45
+ }
46
+ /** `true` folds each task as it finishes; `'all'` folds every task together at the end. */
47
+ export type CollapsePolicy = boolean | 'all';
48
+ export interface TaskReveal {
49
+ /** Unit index of this task's header. */
50
+ offset: number;
51
+ /** The header has been revealed. */
52
+ visible: boolean;
53
+ /** How many of its steps are revealed. */
54
+ stepsVisible: number;
55
+ /** Header and every step are revealed. */
56
+ allRevealed: boolean;
57
+ /** Some unit after this task is revealed (the log has moved on). */
58
+ passed: boolean;
59
+ }
60
+ export declare function countUnits(tasks: readonly AgentTask[]): number;
61
+ export declare function layoutTaskReveal(tasks: readonly AgentTask[], count: number): TaskReveal[];
62
+ /**
63
+ * The status to show. A task reported done/error still reads as running
64
+ * while its steps are being revealed, so a replayed (timer-driven) log never
65
+ * shows a check mark above steps that haven't appeared yet.
66
+ */
67
+ export declare function displayStatus(task: AgentTask, r: TaskReveal): AgentTaskStatus;
68
+ /** Every unit is revealed and no task is still pending or running. */
69
+ export declare function isListComplete(tasks: readonly AgentTask[], revealed: number): boolean;
70
+ /**
71
+ * Should the policy fold this task? Only finished-and-done tasks fold — a
72
+ * failed task stays open so the error is visible.
73
+ * true once the log has moved past it (or the list is complete)
74
+ * 'all' only when the whole list is complete
75
+ */
76
+ export declare function foldedByPolicy(task: AgentTask, r: TaskReveal, policy: CollapsePolicy | undefined, listComplete: boolean): boolean;
77
+ /** Open = the user's choice when they made one, else the policy. */
78
+ export declare function isTaskOpen(folded: boolean, override: boolean | undefined): boolean;
79
+ export declare const DEFAULT_MAX_SOURCES = 6;
80
+ /** Up to `max` marks; anything past that collapses into one "+N" mark. */
81
+ export declare function sourceStack<S>(sources: readonly S[], max?: number): {
82
+ visible: S[];
83
+ overflow: S[];
84
+ };
85
+ export declare function sourceDomain(s: AgentTaskSource): string;
86
+ /** Letter-tile fallback: first letter of the domain, else the title. */
87
+ export declare function sourceInitial(s: AgentTaskSource): string;
88
+ /** Hover / accessible text: "Title — domain". */
89
+ export declare function sourceLabel(s: AgentTaskSource): string;
90
+ export declare function stepKey(step: AgentTaskStep, index: number): string;
@@ -0,0 +1,93 @@
1
+ export function countUnits(tasks) {
2
+ let n = 0;
3
+ for (const t of tasks)
4
+ n += 1 + t.steps.length;
5
+ return n;
6
+ }
7
+ export function layoutTaskReveal(tasks, count) {
8
+ const out = [];
9
+ const revealed = Math.max(0, Math.min(count, countUnits(tasks)));
10
+ let offset = 0;
11
+ for (const t of tasks) {
12
+ const units = 1 + t.steps.length;
13
+ const shown = Math.max(0, Math.min(revealed - offset, units));
14
+ out.push({
15
+ offset,
16
+ visible: shown > 0,
17
+ stepsVisible: Math.max(0, shown - 1),
18
+ allRevealed: shown >= units,
19
+ passed: revealed > offset + units
20
+ });
21
+ offset += units;
22
+ }
23
+ return out;
24
+ }
25
+ /**
26
+ * The status to show. A task reported done/error still reads as running
27
+ * while its steps are being revealed, so a replayed (timer-driven) log never
28
+ * shows a check mark above steps that haven't appeared yet.
29
+ */
30
+ export function displayStatus(task, r) {
31
+ if ((task.status === 'done' || task.status === 'error') && !r.allRevealed) {
32
+ return r.visible ? 'running' : 'pending';
33
+ }
34
+ return task.status;
35
+ }
36
+ /** Every unit is revealed and no task is still pending or running. */
37
+ export function isListComplete(tasks, revealed) {
38
+ const total = countUnits(tasks);
39
+ if (total === 0 || revealed < total)
40
+ return false;
41
+ return tasks.every((t) => t.status === 'done' || t.status === 'error');
42
+ }
43
+ /**
44
+ * Should the policy fold this task? Only finished-and-done tasks fold — a
45
+ * failed task stays open so the error is visible.
46
+ * true once the log has moved past it (or the list is complete)
47
+ * 'all' only when the whole list is complete
48
+ */
49
+ export function foldedByPolicy(task, r, policy, listComplete) {
50
+ if (!policy)
51
+ return false;
52
+ if (displayStatus(task, r) !== 'done')
53
+ return false;
54
+ if (policy === 'all')
55
+ return listComplete;
56
+ return r.passed || listComplete;
57
+ }
58
+ /** Open = the user's choice when they made one, else the policy. */
59
+ export function isTaskOpen(folded, override) {
60
+ return override ?? !folded;
61
+ }
62
+ export const DEFAULT_MAX_SOURCES = 6;
63
+ /** Up to `max` marks; anything past that collapses into one "+N" mark. */
64
+ export function sourceStack(sources, max = DEFAULT_MAX_SOURCES) {
65
+ const cap = Math.max(1, Math.floor(max));
66
+ return { visible: sources.slice(0, cap), overflow: sources.slice(cap) };
67
+ }
68
+ export function sourceDomain(s) {
69
+ if (s.domain)
70
+ return s.domain;
71
+ if (!s.href)
72
+ return '';
73
+ try {
74
+ return new URL(s.href).hostname.replace(/^www\./, '');
75
+ }
76
+ catch {
77
+ return '';
78
+ }
79
+ }
80
+ /** Letter-tile fallback: first letter of the domain, else the title. */
81
+ export function sourceInitial(s) {
82
+ const base = sourceDomain(s) || s.title || '?';
83
+ const ch = base.trim().charAt(0);
84
+ return ch ? ch.toUpperCase() : '?';
85
+ }
86
+ /** Hover / accessible text: "Title — domain". */
87
+ export function sourceLabel(s) {
88
+ const d = sourceDomain(s);
89
+ return d && d !== s.title ? `${s.title} — ${d}` : s.title;
90
+ }
91
+ export function stepKey(step, index) {
92
+ return step.id ?? `#${index}`;
93
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Pure helpers behind AgentThinking (and anything else that wants the same
3
+ * "agent is working" vocabulary): the dot-grid comet math and the elapsed
4
+ * timer format. No DOM, so tests/unit/agentThinking.test.ts runs them in node.
5
+ *
6
+ * The dot-grid comet (scalar per cell, trail 0.3, `** 1.5` falloff, 0.12 floor,
7
+ * the static seed frame) is adapted from BoardUI's agent-thinking:
8
+ * MIT License — Copyright (c) 2026 Mertcan Dundar Esmergul (BoardUI)
9
+ * BoardUI re-renders every 80 ms from a setInterval; here the same curve is
10
+ * baked into one CSS @keyframes and each cell is offset by animation-delay,
11
+ * so nothing runs on the main thread.
12
+ */
13
+ export type AgentThinkingVariant = 'wave' | 'spin' | 'stars' | 'infinity';
14
+ export type AgentThinkingTone = 'subtle' | 'default' | 'strong' | 'primary';
15
+ export declare const DOT_GRID = 3;
16
+ /** One lap of the wave/spin pattern, ms (BoardUI: 8 steps × 80 ms). */
17
+ export declare const DOT_PERIOD_MS = 640;
18
+ export declare const DOT_TRAIL = 0.3;
19
+ export declare const DOT_MIN_OPACITY = 0.12;
20
+ /**
21
+ * Resting frame: what renders on the server, on first paint and under reduced
22
+ * motion — a frozen wavefront, never a blank box.
23
+ */
24
+ export declare const DOT_SEED: readonly number[];
25
+ /**
26
+ * Where a cell sits along the pattern's direction of travel, in [0, 1).
27
+ * Wave: diagonal top-left → bottom-right, compressed below 1 so the wrap reads
28
+ * as the front leaving the grid and re-entering. Spin: angle around the centre.
29
+ */
30
+ export declare function dotScalar(variant: 'wave' | 'spin', col: number, row: number): number;
31
+ /** Opacity of a cell `behind` (0..1 of a lap) the bright head. */
32
+ export declare function cometOpacity(behind: number): number;
33
+ /**
34
+ * The CSS animation-delay (ms, ≤ 0) that puts a cell's peak at the right
35
+ * point of the shared lap: negative so every cell is mid-cycle from frame one.
36
+ */
37
+ export declare function dotDelayMs(variant: 'wave' | 'spin', index: number): number;
38
+ /**
39
+ * Elapsed time for the timer: `0.0s` … `59.9s`, then `1m 05s`, then `1h 02m`.
40
+ * Fixed-width-friendly (rendered with tabular-nums), never negative.
41
+ */
42
+ export declare function formatThinkingTime(ms: number): string;
43
+ /** Default tone per variant — sparkles read heavier, so they start subtle. */
44
+ export declare const VARIANT_TONE: Record<AgentThinkingVariant, AgentThinkingTone>;
45
+ /** Tone → CSS colour, token-only. */
46
+ export declare const TONE_COLOR: Record<AgentThinkingTone, string>;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Pure helpers behind AgentThinking (and anything else that wants the same
3
+ * "agent is working" vocabulary): the dot-grid comet math and the elapsed
4
+ * timer format. No DOM, so tests/unit/agentThinking.test.ts runs them in node.
5
+ *
6
+ * The dot-grid comet (scalar per cell, trail 0.3, `** 1.5` falloff, 0.12 floor,
7
+ * the static seed frame) is adapted from BoardUI's agent-thinking:
8
+ * MIT License — Copyright (c) 2026 Mertcan Dundar Esmergul (BoardUI)
9
+ * BoardUI re-renders every 80 ms from a setInterval; here the same curve is
10
+ * baked into one CSS @keyframes and each cell is offset by animation-delay,
11
+ * so nothing runs on the main thread.
12
+ */
13
+ export const DOT_GRID = 3;
14
+ /** One lap of the wave/spin pattern, ms (BoardUI: 8 steps × 80 ms). */
15
+ export const DOT_PERIOD_MS = 640;
16
+ export const DOT_TRAIL = 0.3;
17
+ export const DOT_MIN_OPACITY = 0.12;
18
+ /**
19
+ * Resting frame: what renders on the server, on first paint and under reduced
20
+ * motion — a frozen wavefront, never a blank box.
21
+ */
22
+ export const DOT_SEED = [0.55, 0.3, 0.15, 0.85, 0.55, 0.3, 1, 0.85, 0.55];
23
+ /**
24
+ * Where a cell sits along the pattern's direction of travel, in [0, 1).
25
+ * Wave: diagonal top-left → bottom-right, compressed below 1 so the wrap reads
26
+ * as the front leaving the grid and re-entering. Spin: angle around the centre.
27
+ */
28
+ export function dotScalar(variant, col, row) {
29
+ const m = DOT_GRID - 1;
30
+ if (variant === 'wave')
31
+ return ((col + row) / (2 * m)) * (DOT_GRID / (DOT_GRID + 1));
32
+ const c = m / 2;
33
+ return (Math.atan2(row - c, col - c) / (2 * Math.PI) + 1) % 1;
34
+ }
35
+ /** Opacity of a cell `behind` (0..1 of a lap) the bright head. */
36
+ export function cometOpacity(behind) {
37
+ const b = ((behind % 1) + 1) % 1;
38
+ const lit = Math.max(0, 1 - b / DOT_TRAIL) ** 1.5;
39
+ return DOT_MIN_OPACITY + (1 - DOT_MIN_OPACITY) * lit;
40
+ }
41
+ /**
42
+ * The CSS animation-delay (ms, ≤ 0) that puts a cell's peak at the right
43
+ * point of the shared lap: negative so every cell is mid-cycle from frame one.
44
+ */
45
+ export function dotDelayMs(variant, index) {
46
+ const s = dotScalar(variant, index % DOT_GRID, Math.floor(index / DOT_GRID));
47
+ return Math.round((s - 1) * DOT_PERIOD_MS);
48
+ }
49
+ /**
50
+ * Elapsed time for the timer: `0.0s` … `59.9s`, then `1m 05s`, then `1h 02m`.
51
+ * Fixed-width-friendly (rendered with tabular-nums), never negative.
52
+ */
53
+ export function formatThinkingTime(ms) {
54
+ const t = Math.max(0, Number.isFinite(ms) ? ms : 0);
55
+ if (t < 60_000)
56
+ return `${(Math.floor(t / 100) / 10).toFixed(1)}s`;
57
+ const totalS = Math.floor(t / 1000);
58
+ if (totalS < 3600) {
59
+ const m = Math.floor(totalS / 60);
60
+ return `${m}m ${String(totalS % 60).padStart(2, '0')}s`;
61
+ }
62
+ const h = Math.floor(totalS / 3600);
63
+ return `${h}h ${String(Math.floor((totalS % 3600) / 60)).padStart(2, '0')}m`;
64
+ }
65
+ /** Default tone per variant — sparkles read heavier, so they start subtle. */
66
+ export const VARIANT_TONE = {
67
+ wave: 'default',
68
+ spin: 'default',
69
+ stars: 'subtle',
70
+ infinity: 'default'
71
+ };
72
+ /** Tone → CSS colour, token-only. */
73
+ export const TONE_COLOR = {
74
+ subtle: 'color-mix(in oklab, var(--color-base-content) 50%, transparent)',
75
+ default: 'color-mix(in oklab, var(--color-base-content) 72%, transparent)',
76
+ strong: 'var(--color-base-content)',
77
+ primary: 'var(--color-primary)'
78
+ };