@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
@@ -167,6 +167,12 @@
167
167
  import { dragCancel, dragEnd, dragMove, dragStart, type DragState } from '../video/band/drag';
168
168
  import { createBandLayerCache } from '../video/band/memo';
169
169
  import { BAND_KEYS, PLAYER_KEYS, chapterJump, FRAME_STEP_SEC } from '../video/keys';
170
+ import {
171
+ highlightRowHtml,
172
+ lineScrubTarget,
173
+ pickMatch,
174
+ transcriptWheelDecision
175
+ } from '../video/transcriptSearch';
170
176
  import {
171
177
  adMaxStep,
172
178
  adSkipTarget,
@@ -887,6 +893,18 @@
887
893
  let transcriptVisible = $state(true);
888
894
  let keysOpen = $state(false);
889
895
 
896
+ // ── Transcript search (jatui-xmtnv) ────────────────────────────────
897
+ // `/` opens a search box over the transcript pane; matches highlight in
898
+ // place, Enter jumps to the next match (Shift+Enter back), Esc closes.
899
+ // The page omnibar's own `/` yields while focus is inside the player:
900
+ // the claiming keydown's propagation is stopped, and the search box
901
+ // stops its own keydowns so typing never leaks into the player map or
902
+ // the page.
903
+ let searchOpen = $state(false);
904
+ let searchQuery = $state('');
905
+ let searchMatchIdx = $state(-1);
906
+ let searchInputEl: HTMLInputElement | undefined = $state();
907
+
890
908
  let zoomTimer: ReturnType<typeof setTimeout> | null = null;
891
909
  let osdTimer: ReturnType<typeof setTimeout> | null = null;
892
910
 
@@ -1182,6 +1200,23 @@
1182
1200
  const sortedSegments = $derived(sortSegments(transcript));
1183
1201
  const sortedBookmarks = $derived(sortBookmarks(bookmarks));
1184
1202
 
1203
+ // ── Transcript search deriveds (jatui-xmtnv) ─────────────────────────
1204
+ /** Live only while the box is open with a non-empty query. */
1205
+ const searchActive = $derived(searchOpen && searchQuery.trim().length > 0);
1206
+ /** Matching rows in the pane's own play order — count and jump order
1207
+ * agree with what's on screen. */
1208
+ const searchMatches = $derived.by(() => {
1209
+ if (!searchActive) return [];
1210
+ const q = searchQuery.trim().toLowerCase();
1211
+ return sortedSegments.filter((s) => s.text.toLowerCase().includes(q));
1212
+ });
1213
+ /** The match the Enter-cursor is on — wears the distinct "current" mark. */
1214
+ const searchCurrentId = $derived(
1215
+ searchMatchIdx >= 0 && searchMatchIdx < searchMatches.length
1216
+ ? searchMatches[searchMatchIdx].id
1217
+ : null
1218
+ );
1219
+
1185
1220
  /** Peaks normalised to 0–1 against their own max, so 0–1, 0–99 and 0–255 inputs all work. */
1186
1221
  const peaks = $derived.by(() => {
1187
1222
  const raw = waveform?.peaks;
@@ -2809,7 +2844,11 @@
2809
2844
  return;
2810
2845
  }
2811
2846
  if (e.shiftKey) {
2812
- const delta = e.deltaY < 0 ? 5 : -5;
2847
+ // Chrome on Linux/Windows and macOS turn Shift+wheel into a
2848
+ // HORIZONTAL scroll, so deltaY is 0 and the value sits in deltaX.
2849
+ // Use the effective delta to drive the seek step direction.
2850
+ const d = e.deltaY !== 0 ? e.deltaY : e.deltaX;
2851
+ const delta = d < 0 ? 5 : -5;
2813
2852
  seekTo(currentTime + delta);
2814
2853
  flashOsd(`${delta > 0 ? '+' : ''}${delta}s · ${formatTime(currentTime + delta, useHours)}`);
2815
2854
  return;
@@ -2858,6 +2897,22 @@
2858
2897
  return;
2859
2898
  }
2860
2899
 
2900
+ // The transcript search (jatui-xmtnv) owns Escape while it's open —
2901
+ // even when focus has left its input — and re-focuses on `/`; every
2902
+ // other key waits so typing never drives the player.
2903
+ if (searchOpen) {
2904
+ if (e.key === 'Escape') {
2905
+ e.preventDefault();
2906
+ e.stopPropagation();
2907
+ closeSearch();
2908
+ } else if (e.key === '/') {
2909
+ e.preventDefault();
2910
+ e.stopPropagation(); // the omnibar yields while focus is inside the player
2911
+ searchInputEl?.focus();
2912
+ }
2913
+ return;
2914
+ }
2915
+
2861
2916
  switch (e.key) {
2862
2917
  case '?':
2863
2918
  e.preventDefault();
@@ -2968,6 +3023,16 @@
2968
3023
  flashOsd(transcriptVisible ? 'Transcript shown' : 'Transcript hidden');
2969
3024
  }
2970
3025
  return;
3026
+ case '/':
3027
+ // Transcript search (jatui-xmtnv). The omnibar yields: focus is
3028
+ // inside the player, so this keydown never reaches the page's
3029
+ // own `/` handler (stopPropagation below the player root).
3030
+ if (sortedSegments.length > 0) {
3031
+ e.preventDefault();
3032
+ e.stopPropagation();
3033
+ openSearch();
3034
+ }
3035
+ return;
2971
3036
  case 'n':
2972
3037
  case 'N':
2973
3038
  if (night !== undefined) {
@@ -3106,6 +3171,88 @@
3106
3171
  seekTo(seg.startSec);
3107
3172
  }
3108
3173
 
3174
+ // ── Transcript search (jatui-xmtnv) ───────────────────────────────────
3175
+ /** `/`: open the box (showing the transcript if it was hidden) and hand
3176
+ * it focus once it mounts — the effect below lands it. */
3177
+ function openSearch() {
3178
+ if (sortedSegments.length === 0) return;
3179
+ transcriptVisible = true;
3180
+ searchOpen = true;
3181
+ searchMatchIdx = -1;
3182
+ }
3183
+ /** Esc / the × button: close, clear, and give the keys back to the map. */
3184
+ function closeSearch() {
3185
+ searchOpen = false;
3186
+ searchQuery = '';
3187
+ searchMatchIdx = -1;
3188
+ rootEl?.focus();
3189
+ }
3190
+ /** Enter (next) / Shift+Enter (previous), wrapping. With no cursor yet a
3191
+ * search opened mid-talk starts from the match at/after the playhead. */
3192
+ function jumpSearch(direction: 1 | -1) {
3193
+ const matches = searchMatches;
3194
+ if (matches.length === 0) return;
3195
+ searchMatchIdx = pickMatch(
3196
+ matches.map((m) => m.startSec),
3197
+ currentTime,
3198
+ direction,
3199
+ searchMatchIdx
3200
+ );
3201
+ seekTo(matches[searchMatchIdx].startSec);
3202
+ }
3203
+ /** The box owns its keys — never leak into the player map or the page
3204
+ * omnibar while it's open (typing `k` must not pause, `/` must not
3205
+ * re-open the omnibar). */
3206
+ function onSearchKeydown(e: KeyboardEvent) {
3207
+ e.stopPropagation();
3208
+ if (e.key === 'Enter') {
3209
+ e.preventDefault();
3210
+ jumpSearch(e.shiftKey ? -1 : 1);
3211
+ } else if (e.key === 'Escape') {
3212
+ e.preventDefault();
3213
+ closeSearch();
3214
+ }
3215
+ }
3216
+ // Focus + select the box when it opens (the input mounts with it).
3217
+ $effect(() => {
3218
+ if (searchOpen && searchInputEl) {
3219
+ searchInputEl.focus();
3220
+ searchInputEl.select();
3221
+ }
3222
+ });
3223
+
3224
+ // ── Transcript wheel scrub (jatui-xmtnv) ─────────────────────────────────
3225
+ // A plain wheel over the transcript pane scrolls it natively — exactly as
3226
+ // before (the reader's own scroll, follow-intent logic untouched —
3227
+ // jatui-ap4oz/ee2ty); preventDefault is NEVER taken for it, so the page is
3228
+ // never trapped. Shift+wheel steps PLAYBACK one line at a time (extension
3229
+ // library.js:8580 parity, matching Shift+wheel over the video = seek): the
3230
+ // pure decision in video/transcriptSearch.ts accumulates deltas so one
3231
+ // notch is one line — a mouse wheel's ~100px notches step exactly once
3232
+ // each and a trackpad's stream of small deltas never flies through the
3233
+ // transcript. Seeking makes the stepped line active, so the pane follows
3234
+ // it (the follow's own scrolls are never intent).
3235
+ let lineWheelAccum = 0;
3236
+ function onTranscriptWheel(e: WheelEvent) {
3237
+ const d = e.deltaY !== 0 ? e.deltaY : e.deltaX;
3238
+ const decision = transcriptWheelDecision(
3239
+ e.shiftKey,
3240
+ sortedSegments.length > 0,
3241
+ d,
3242
+ lineWheelAccum
3243
+ );
3244
+ if (!decision.claimed) return; // plain wheel = native pane scroll
3245
+ e.preventDefault();
3246
+ lineWheelAccum = decision.nextAccum;
3247
+ if (!decision.shouldStep) return;
3248
+ const target = lineScrubTarget(
3249
+ currentTime,
3250
+ sortedSegments.map((s) => s.startSec),
3251
+ decision.direction
3252
+ );
3253
+ if (target != null) seekTo(target);
3254
+ }
3255
+
3109
3256
  // No active line (seeked past the transcript) ⇒ nothing to jump to.
3110
3257
  $effect(() => {
3111
3258
  if (!activeSegment && followPaused) untrack(() => (followPaused = false));
@@ -3795,10 +3942,42 @@
3795
3942
  style:--vp-transcript-max={transcriptMaxHeight}
3796
3943
  style:--vp-transcript-side-max={!transcriptMaxHeight && mainHeight > 0 ? `${mainHeight}px` : undefined}
3797
3944
  >
3945
+ {#if searchOpen}
3946
+ <!-- Transcript search (jatui-xmtnv): `/` opened it. Enter jumps to
3947
+ the next match (Shift+Enter back), Esc closes; the count is
3948
+ aria-live so a screen reader hears match N of M. -->
3949
+ <div class="vp-search">
3950
+ <input
3951
+ class="vp-search-input"
3952
+ type="text"
3953
+ placeholder="Search transcript"
3954
+ aria-label="Search transcript"
3955
+ spellcheck="false"
3956
+ bind:value={searchQuery}
3957
+ bind:this={searchInputEl}
3958
+ oninput={() => (searchMatchIdx = -1)}
3959
+ onkeydown={onSearchKeydown}
3960
+ />
3961
+ <span class="vp-search-count" aria-live="polite">
3962
+ {searchMatches.length === 0
3963
+ ? (searchActive ? 'No matches' : '')
3964
+ : searchMatchIdx >= 0
3965
+ ? `${searchMatchIdx + 1} / ${searchMatches.length}`
3966
+ : `${searchMatches.length}`}
3967
+ </span>
3968
+ <button
3969
+ type="button"
3970
+ class="vp-search-close"
3971
+ aria-label="Close search"
3972
+ onclick={closeSearch}
3973
+ >×</button>
3974
+ </div>
3975
+ {/if}
3798
3976
  <div
3799
3977
  class="vp-transcript"
3800
3978
  use:transcriptPaneAction
3801
3979
  onscroll={onTranscriptScroll}
3980
+ onwheel={onTranscriptWheel}
3802
3981
  >
3803
3982
  {#if transWindow}
3804
3983
  <!-- Windowed: only the rows near the viewport exist in the DOM.
@@ -3823,7 +4002,15 @@
3823
4002
  {seg.speakerName ?? 'Unknown speaker'}
3824
4003
  </span>
3825
4004
  {/if}
3826
- <span class="vp-row-text">{seg.text}</span>
4005
+ <span class="vp-row-text">
4006
+ {#if searchActive}
4007
+ <!-- Search marks are safe HTML: highlightRowHtml escapes
4008
+ everything but its own <mark> tags (jatui-xmtnv). -->
4009
+ {@html highlightRowHtml(seg.text, searchQuery, searchCurrentId === seg.id)}
4010
+ {:else}
4011
+ {seg.text}
4012
+ {/if}
4013
+ </span>
3827
4014
  </button>
3828
4015
  {/each}
3829
4016
  <div class="vp-spacer" style="height: {transWindow.padBottom}px" aria-hidden="true"></div>
@@ -3846,7 +4033,13 @@
3846
4033
  {seg.speakerName ?? 'Unknown speaker'}
3847
4034
  </span>
3848
4035
  {/if}
3849
- <span class="vp-row-text">{seg.text}</span>
4036
+ <span class="vp-row-text">
4037
+ {#if searchActive}
4038
+ {@html highlightRowHtml(seg.text, searchQuery, searchCurrentId === seg.id)}
4039
+ {:else}
4040
+ {seg.text}
4041
+ {/if}
4042
+ </span>
3850
4043
  </button>
3851
4044
  {/each}
3852
4045
  {/if}
@@ -4909,6 +5102,79 @@
4909
5102
  }
4910
5103
  }
4911
5104
 
5105
+ /* Transcript search (jatui-xmtnv): a slim bar over the pane's top edge.
5106
+ Focus ring on the input only; marks tint with the primary token, and
5107
+ the cursor's match is filled so "which match am I on" is legible at a
5108
+ glance. */
5109
+ .vp-search {
5110
+ position: absolute;
5111
+ top: 0;
5112
+ left: 0;
5113
+ right: 0;
5114
+ z-index: 3;
5115
+ display: flex;
5116
+ align-items: center;
5117
+ gap: var(--jatui-space-sm, 4px);
5118
+ padding: var(--jatui-space-sm, 4px) var(--jatui-space-md, 6px);
5119
+ background: var(--color-base-100);
5120
+ border: var(--jatui-border-w, 1px) solid
5121
+ color-mix(in oklch, var(--color-primary) 45%, transparent);
5122
+ border-radius: var(--jatui-radius-box, 0.5rem);
5123
+ }
5124
+
5125
+ .vp-search-input {
5126
+ flex: 1 1 auto;
5127
+ min-width: 0;
5128
+ background: none;
5129
+ border: none;
5130
+ outline: none;
5131
+ color: var(--color-base-content);
5132
+ font-size: 0.8125rem;
5133
+ padding: 0;
5134
+ }
5135
+
5136
+ .vp-search-input::placeholder {
5137
+ color: color-mix(in oklch, var(--color-base-content) 45%, transparent);
5138
+ }
5139
+
5140
+ .vp-search-count {
5141
+ flex: none;
5142
+ font-family: ui-monospace, monospace;
5143
+ font-size: 0.66rem;
5144
+ color: color-mix(in oklch, var(--color-base-content) 55%, transparent);
5145
+ }
5146
+
5147
+ .vp-search-close {
5148
+ flex: none;
5149
+ background: none;
5150
+ border: none;
5151
+ color: var(--color-base-content);
5152
+ cursor: pointer;
5153
+ padding: 0 var(--jatui-space-sm, 4px);
5154
+ font-size: 0.9rem;
5155
+ line-height: 1;
5156
+ border-radius: var(--jatui-radius-field, 0.25rem);
5157
+ }
5158
+
5159
+ .vp-search-close:hover {
5160
+ background: color-mix(in oklch, var(--color-base-content) 8%, transparent);
5161
+ }
5162
+
5163
+ /* Match highlight inside a transcript row — an <mark> tag, so UA styling
5164
+ is overridden with the token surface. The cursor's match is filled with
5165
+ the primary token; the rest is a softer tint. */
5166
+ .vp-row-text :global(mark.vp-search-mark) {
5167
+ background: color-mix(in oklch, var(--color-primary) 20%, transparent);
5168
+ color: inherit;
5169
+ border-radius: 2px;
5170
+ padding: 0 1px;
5171
+ }
5172
+
5173
+ .vp-row-text :global(mark.vp-search-mark.current) {
5174
+ background: var(--color-primary);
5175
+ color: var(--color-primary-content);
5176
+ }
5177
+
4912
5178
  /* Keyboard help overlay — the `?` surface. Keycaps first, description after;
4913
5179
  token-only colours, same chrome family as the hover card. */
4914
5180
  .vp-keys-backdrop {
@@ -0,0 +1,217 @@
1
+ <!--
2
+ AgentLog — a streaming agent transcript that draws itself down the page:
3
+ rows arrive one at a time, each growing in (height, a brief blur and a small
4
+ lift, a soft fading clip edge) while a curved tree guide strokes alongside
5
+ them, plus an optional "working" line at the tail.
6
+
7
+ This is the machinery only; what a row says is yours. Two ways to fill it:
8
+
9
+ items + row snippet <AgentLog items={visible} key={(s) => s.id}>
10
+ {#snippet row(step)}{step.label}{/snippet}
11
+ </AgentLog>
12
+ children <AgentLog>
13
+ <AgentLogRow first>…</AgentLogRow>
14
+ <AgentLogRow last>…</AgentLogRow>
15
+ </AgentLog>
16
+
17
+ Pacing is separate: slice your items with createRevealTicker (timer or
18
+ real-event driven). Rows already present on the log's first render rest;
19
+ only rows added afterwards animate (pass `appear` to animate those too).
20
+
21
+ Adapted from BoardUI's agent-log (WorkingRow, GuideBridge, the overall
22
+ structure): MIT License — Copyright (c) 2026 Mertcan Dundar Esmergul (BoardUI)
23
+ The working row sits deliberately OFF the guide: the tree records what the
24
+ agent did, and that one line has not happened yet.
25
+ -->
26
+ <script lang="ts" generics="T">
27
+ import { onMount, type Snippet } from 'svelte';
28
+ import AgentThinking from './AgentThinking.svelte';
29
+ import AgentLogRow from './AgentLogRow.svelte';
30
+ import { getAgentLogContext, setAgentLogContext } from './agentLog';
31
+ import { prefersReducedMotion } from '../../motion/level';
32
+
33
+ interface Props {
34
+ /** Rows to render (already sliced to what should be visible). */
35
+ items?: T[];
36
+ /** Row content for `items`. */
37
+ row?: Snippet<[item: T, index: number]>;
38
+ /** Stable key per item. Default: index. */
39
+ key?: (item: T, index: number) => string | number;
40
+ /** Extra classes per row (items mode). */
41
+ rowClass?: (item: T, index: number) => string;
42
+ /** Manual rows (AgentLogRow) instead of `items`. */
43
+ children?: Snippet;
44
+ /**
45
+ * Working line at the tail (AgentThinking "stars"): a label, or
46
+ * false/null/'' for none.
47
+ */
48
+ working?: string | false | null;
49
+ /** Elapsed timer on the working line. */
50
+ workingTimer?: boolean;
51
+ /** When the work started (epoch ms) — keeps the working timer honest across re-mounts. */
52
+ workingStartedAt?: number;
53
+ /**
54
+ * Draw the tree guide. Default true.
55
+ */
56
+ guide?: boolean;
57
+ /**
58
+ * Nested list: draw a stretch of trunk this many px ABOVE the first row
59
+ * (from the parent row's glyph down to this list) and make the first branch
60
+ * wait for it, so the two levels read as one line. 0 = none.
61
+ */
62
+ bridge?: number;
63
+ /**
64
+ * Animate rows present on the first render. Default: inherit — a log
65
+ * mounted inside a row that is itself growing in animates; a top-level log
66
+ * does not.
67
+ */
68
+ appear?: boolean;
69
+ /**
70
+ * Announce additions to screen readers (role="log", polite). Turn off for a
71
+ * log nested inside another live region.
72
+ */
73
+ live?: boolean;
74
+ /** Accessible name for the log. */
75
+ ariaLabel?: string;
76
+ /** aria-busy while work is in progress. Default: a working line is shown. */
77
+ busy?: boolean;
78
+ class?: string;
79
+ listClass?: string;
80
+ }
81
+
82
+ let {
83
+ items,
84
+ row,
85
+ key,
86
+ rowClass,
87
+ children,
88
+ working = null,
89
+ workingTimer = false,
90
+ workingStartedAt,
91
+ guide = true,
92
+ bridge = 0,
93
+ appear,
94
+ live = true,
95
+ ariaLabel,
96
+ busy,
97
+ class: className = '',
98
+ listClass = ''
99
+ }: Props = $props();
100
+
101
+ const parent = getAgentLogContext();
102
+ // Plain (non-reactive) flag: each row reads it once, when it mounts.
103
+ // svelte-ignore state_referenced_locally
104
+ const flag = { live: appear ?? parent?.isLive() ?? false };
105
+ const reduced = prefersReducedMotion();
106
+
107
+ setAgentLogContext({
108
+ isLive: () => flag.live,
109
+ get bridged() {
110
+ return bridge > 0;
111
+ }
112
+ });
113
+
114
+ onMount(() => {
115
+ flag.live = true;
116
+ });
117
+
118
+ const keyOf = (item: T, i: number) => (key ? key(item, i) : i);
119
+ const showWorking = $derived(!!working);
120
+ </script>
121
+
122
+ <div
123
+ class="agent-log {className}"
124
+ role={live ? 'log' : undefined}
125
+ aria-live={live ? 'polite' : undefined}
126
+ aria-relevant={live ? 'additions' : undefined}
127
+ aria-label={ariaLabel}
128
+ aria-busy={busy ?? showWorking}
129
+ >
130
+ {#if bridge > 0 && guide && ((items && items.length > 0) || children)}
131
+ <span
132
+ class="al-bridge"
133
+ class:is-drawing={!reduced && flag.live}
134
+ style:top="-{bridge}px"
135
+ style:height="{bridge}px"
136
+ aria-hidden="true"
137
+ ></span>
138
+ {/if}
139
+ <ol class="al-list {listClass}" class:has-bridge={bridge > 0} role="list">
140
+ {#if items && row}
141
+ {#each items as item, i (keyOf(item, i))}
142
+ <AgentLogRow
143
+ first={i === 0}
144
+ last={i === items.length - 1}
145
+ {guide}
146
+ class={rowClass?.(item, i) ?? ''}
147
+ >
148
+ {@render row(item, i)}
149
+ </AgentLogRow>
150
+ {/each}
151
+ {:else if children}
152
+ {@render children()}
153
+ {/if}
154
+ </ol>
155
+
156
+ {#if showWorking}
157
+ <AgentLogRow as="div" guide={false} class="al-working">
158
+ <div class="al-working-inner">
159
+ <AgentThinking
160
+ variant="stars"
161
+ size="sm"
162
+ label={working || 'Working'}
163
+ showTimer={workingTimer}
164
+ startedAt={workingStartedAt}
165
+ />
166
+ </div>
167
+ </AgentLogRow>
168
+ {/if}
169
+ </div>
170
+
171
+ <style>
172
+ .agent-log {
173
+ position: relative;
174
+ min-width: 0;
175
+ }
176
+ .al-list {
177
+ position: relative;
178
+ margin: 0;
179
+ padding: 0;
180
+ list-style: none;
181
+ /* rows must touch: each row's trunk meets the next row's branch */
182
+ display: flex;
183
+ flex-direction: column;
184
+ gap: 0;
185
+ }
186
+
187
+ /* From the parent row's glyph down into this list's first branch. */
188
+ .al-bridge {
189
+ pointer-events: none;
190
+ position: absolute;
191
+ inset-inline-start: 0;
192
+ width: 1px;
193
+ background: color-mix(in oklab, var(--color-base-content) 24%, transparent);
194
+ transform-origin: top;
195
+ }
196
+ .al-bridge.is-drawing {
197
+ animation: al-bridge calc(var(--motion-fast, 150ms) * 0.8) linear both;
198
+ }
199
+ @keyframes al-bridge {
200
+ from {
201
+ transform: scaleY(0);
202
+ }
203
+ to {
204
+ transform: scaleY(1);
205
+ }
206
+ }
207
+
208
+ .al-working-inner {
209
+ padding-block: 0.25rem;
210
+ }
211
+
212
+ @media (prefers-reduced-motion: reduce) {
213
+ .al-bridge.is-drawing {
214
+ animation: none;
215
+ }
216
+ }
217
+ </style>
@@ -0,0 +1,72 @@
1
+ import { type Snippet } from 'svelte';
2
+ declare function $$render<T>(): {
3
+ props: {
4
+ /** Rows to render (already sliced to what should be visible). */
5
+ items?: T[];
6
+ /** Row content for `items`. */
7
+ row?: Snippet<[item: T, index: number]>;
8
+ /** Stable key per item. Default: index. */
9
+ key?: (item: T, index: number) => string | number;
10
+ /** Extra classes per row (items mode). */
11
+ rowClass?: (item: T, index: number) => string;
12
+ /** Manual rows (AgentLogRow) instead of `items`. */
13
+ children?: Snippet;
14
+ /**
15
+ * Working line at the tail (AgentThinking "stars"): a label, or
16
+ * false/null/'' for none.
17
+ */
18
+ working?: string | false | null;
19
+ /** Elapsed timer on the working line. */
20
+ workingTimer?: boolean;
21
+ /** When the work started (epoch ms) — keeps the working timer honest across re-mounts. */
22
+ workingStartedAt?: number;
23
+ /**
24
+ * Draw the tree guide. Default true.
25
+ */
26
+ guide?: boolean;
27
+ /**
28
+ * Nested list: draw a stretch of trunk this many px ABOVE the first row
29
+ * (from the parent row's glyph down to this list) and make the first branch
30
+ * wait for it, so the two levels read as one line. 0 = none.
31
+ */
32
+ bridge?: number;
33
+ /**
34
+ * Animate rows present on the first render. Default: inherit — a log
35
+ * mounted inside a row that is itself growing in animates; a top-level log
36
+ * does not.
37
+ */
38
+ appear?: boolean;
39
+ /**
40
+ * Announce additions to screen readers (role="log", polite). Turn off for a
41
+ * log nested inside another live region.
42
+ */
43
+ live?: boolean;
44
+ /** Accessible name for the log. */
45
+ ariaLabel?: string;
46
+ /** aria-busy while work is in progress. Default: a working line is shown. */
47
+ busy?: boolean;
48
+ class?: string;
49
+ listClass?: string;
50
+ };
51
+ exports: {};
52
+ bindings: "";
53
+ slots: {};
54
+ events: {};
55
+ };
56
+ declare class __sveltets_Render<T> {
57
+ props(): ReturnType<typeof $$render<T>>['props'];
58
+ events(): ReturnType<typeof $$render<T>>['events'];
59
+ slots(): ReturnType<typeof $$render<T>>['slots'];
60
+ bindings(): "";
61
+ exports(): {};
62
+ }
63
+ interface $$IsomorphicComponent {
64
+ new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
65
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
66
+ } & ReturnType<__sveltets_Render<T>['exports']>;
67
+ <T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
68
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
69
+ }
70
+ declare const AgentLog: $$IsomorphicComponent;
71
+ type AgentLog<T> = InstanceType<typeof AgentLog<T>>;
72
+ export default AgentLog;