@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.
- package/CLAUDE.md +32 -1
- package/THIRD_PARTY_NOTICES.md +51 -0
- package/dist/components/BulkActionBar.svelte +32 -2
- package/dist/components/BulkActionBar.svelte.d.ts +5 -1
- package/dist/components/FloatingActionBar.svelte +102 -34
- package/dist/components/FloatingActionBar.svelte.d.ts +9 -0
- package/dist/components/MediaExplorer.svelte +249 -0
- package/dist/components/MediaExplorer.svelte.d.ts +37 -0
- package/dist/components/NotificationCenter.svelte +481 -0
- package/dist/components/NotificationCenter.svelte.d.ts +29 -0
- package/dist/components/SearchDropdown.svelte +54 -8
- package/dist/components/SearchDropdown.svelte.d.ts +12 -0
- package/dist/components/VideoPlayer.svelte +269 -3
- package/dist/components/agent/AgentLog.svelte +217 -0
- package/dist/components/agent/AgentLog.svelte.d.ts +72 -0
- package/dist/components/agent/AgentLogRow.svelte +281 -0
- package/dist/components/agent/AgentLogRow.svelte.d.ts +21 -0
- package/dist/components/agent/AgentTaskList.svelte +607 -0
- package/dist/components/agent/AgentTaskList.svelte.d.ts +40 -0
- package/dist/components/agent/AgentThinking.svelte +280 -0
- package/dist/components/agent/AgentThinking.svelte.d.ts +22 -0
- package/dist/components/agent/ComposerGlow.svelte +265 -0
- package/dist/components/agent/ComposerGlow.svelte.d.ts +18 -0
- package/dist/components/agent/ContextMeter.svelte +277 -0
- package/dist/components/agent/ContextMeter.svelte.d.ts +21 -0
- package/dist/components/agent/Questionnaire.svelte +767 -0
- package/dist/components/agent/Questionnaire.svelte.d.ts +29 -0
- package/dist/components/agent/ShimmerText.svelte +100 -0
- package/dist/components/agent/ShimmerText.svelte.d.ts +19 -0
- package/dist/components/agent/StreamedText.svelte +135 -0
- package/dist/components/agent/StreamedText.svelte.d.ts +13 -0
- package/dist/components/agent/agentLog.d.ts +22 -0
- package/dist/components/agent/agentLog.js +26 -0
- package/dist/components/agent/agentTaskList.d.ts +90 -0
- package/dist/components/agent/agentTaskList.js +93 -0
- package/dist/components/agent/agentThinking.d.ts +46 -0
- package/dist/components/agent/agentThinking.js +78 -0
- package/dist/components/agent/contextMeter.d.ts +97 -0
- package/dist/components/agent/contextMeter.js +156 -0
- package/dist/components/agent/questionnaire.d.ts +104 -0
- package/dist/components/agent/questionnaire.js +225 -0
- package/dist/components/agent/revealCore.d.ts +74 -0
- package/dist/components/agent/revealCore.js +160 -0
- package/dist/components/agent/revealTicker.d.ts +34 -0
- package/dist/components/agent/revealTicker.js +73 -0
- package/dist/components/agent/streamedText.d.ts +28 -0
- package/dist/components/agent/streamedText.js +59 -0
- package/dist/components/messaging/MessageInput.svelte +34 -4
- package/dist/components/messaging/MessageInput.svelte.d.ts +7 -0
- package/dist/components/messaging/MessageItem.svelte +2 -2
- package/dist/components/notificationCenter.d.ts +65 -0
- package/dist/components/notificationCenter.js +110 -0
- package/dist/components/progress/Steps.svelte +269 -5
- package/dist/components/progress/Steps.svelte.d.ts +16 -1
- package/dist/components/progress/stepsSummary.d.ts +14 -0
- package/dist/components/progress/stepsSummary.js +32 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +29 -0
- package/dist/selection/clickSelection.d.ts +89 -0
- package/dist/selection/clickSelection.js +152 -0
- package/dist/selection/createClickSelection.d.ts +79 -0
- package/dist/selection/createClickSelection.js +134 -0
- package/dist/video/keys.d.ts +5 -5
- package/dist/video/keys.js +9 -5
- package/dist/video/transcriptSearch.d.ts +80 -0
- package/dist/video/transcriptSearch.js +153 -0
- 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
|
-
|
|
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">
|
|
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">
|
|
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;
|