@joewinke/jatui 0.1.23 → 0.1.24

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 (124) hide show
  1. package/CLAUDE.md +244 -0
  2. package/package.json +23 -10
  3. package/src/lib/actions/collapse.ts +0 -136
  4. package/src/lib/actions/railNav.ts +0 -473
  5. package/src/lib/components/AnnotationLayer.svelte +0 -108
  6. package/src/lib/components/AnnotationPanel.svelte +0 -319
  7. package/src/lib/components/AudioWaveform.svelte +0 -698
  8. package/src/lib/components/AvailabilityModal.svelte +0 -177
  9. package/src/lib/components/AvatarUpload.svelte +0 -234
  10. package/src/lib/components/Badge.svelte +0 -38
  11. package/src/lib/components/BookingForm.svelte +0 -278
  12. package/src/lib/components/BurndownChart.svelte +0 -778
  13. package/src/lib/components/Button.svelte +0 -81
  14. package/src/lib/components/CalendarPicker.svelte +0 -284
  15. package/src/lib/components/Card.svelte +0 -67
  16. package/src/lib/components/CharacterCounter.svelte +0 -82
  17. package/src/lib/components/ChipInput.svelte +0 -609
  18. package/src/lib/components/Collapse.svelte +0 -82
  19. package/src/lib/components/ColorSelector.svelte +0 -167
  20. package/src/lib/components/CommentThread.svelte +0 -773
  21. package/src/lib/components/ConfirmDialog.svelte +0 -348
  22. package/src/lib/components/ConfirmModal.svelte +0 -142
  23. package/src/lib/components/ContextMenu.svelte +0 -188
  24. package/src/lib/components/CountdownTimer.svelte +0 -94
  25. package/src/lib/components/DateRangePicker.svelte +0 -194
  26. package/src/lib/components/Drawer.svelte +0 -143
  27. package/src/lib/components/EmojiPicker.svelte +0 -159
  28. package/src/lib/components/EntityPreviewCard.svelte +0 -104
  29. package/src/lib/components/FileDropzone.svelte +0 -493
  30. package/src/lib/components/FilePicker.svelte +0 -488
  31. package/src/lib/components/FileThumbnail.svelte +0 -80
  32. package/src/lib/components/FilterDropdown.svelte +0 -203
  33. package/src/lib/components/FloatingActionBar.svelte +0 -354
  34. package/src/lib/components/GPSTracker.svelte +0 -202
  35. package/src/lib/components/HunkDiffView.svelte +0 -348
  36. package/src/lib/components/ImageLightbox.svelte +0 -274
  37. package/src/lib/components/ImageUpload.svelte +0 -147
  38. package/src/lib/components/InlineEdit.svelte +0 -291
  39. package/src/lib/components/InputDialog.svelte +0 -327
  40. package/src/lib/components/KeyboardShortcutsOverlay.svelte +0 -296
  41. package/src/lib/components/LazyImage.svelte +0 -123
  42. package/src/lib/components/LinkShortener.svelte +0 -274
  43. package/src/lib/components/LoadingSpinner.svelte +0 -106
  44. package/src/lib/components/LocationMap.svelte +0 -186
  45. package/src/lib/components/MapView.svelte +0 -341
  46. package/src/lib/components/MarkupEditor.svelte +0 -485
  47. package/src/lib/components/MarkupOverlay.svelte +0 -55
  48. package/src/lib/components/MediaWorkbench.svelte +0 -871
  49. package/src/lib/components/MilestoneCard.svelte +0 -98
  50. package/src/lib/components/MilestoneTimeline.svelte +0 -91
  51. package/src/lib/components/Modal.svelte +0 -244
  52. package/src/lib/components/PDFViewer.svelte +0 -105
  53. package/src/lib/components/PdfThumbnail.svelte +0 -63
  54. package/src/lib/components/PhoneInput.svelte +0 -212
  55. package/src/lib/components/ResizableDivider.svelte +0 -307
  56. package/src/lib/components/ResizablePanel.svelte +0 -305
  57. package/src/lib/components/SearchDropdown.svelte +0 -421
  58. package/src/lib/components/SelectInput.svelte +0 -237
  59. package/src/lib/components/SidebarUserFooter.svelte +0 -34
  60. package/src/lib/components/SignaturePad.svelte +0 -175
  61. package/src/lib/components/SmartImageEditor.svelte +0 -720
  62. package/src/lib/components/SortDropdown.svelte +0 -156
  63. package/src/lib/components/Sparkline.svelte +0 -116
  64. package/src/lib/components/SpeechForm.svelte +0 -114
  65. package/src/lib/components/StatusBadge.svelte +0 -157
  66. package/src/lib/components/TaskTypeIcon.svelte +0 -54
  67. package/src/lib/components/TextArea.svelte +0 -162
  68. package/src/lib/components/TextInput.svelte +0 -131
  69. package/src/lib/components/ThemeSelector.svelte +0 -206
  70. package/src/lib/components/TimeSlotPicker.svelte +0 -162
  71. package/src/lib/components/UserAvatar.svelte +0 -72
  72. package/src/lib/components/VariablePicker.svelte +0 -170
  73. package/src/lib/components/VoicePlayer.svelte +0 -422
  74. package/src/lib/components/linked-columns/LinkedColumns.svelte +0 -526
  75. package/src/lib/components/markup.ts +0 -287
  76. package/src/lib/components/messaging/Avatar.svelte +0 -81
  77. package/src/lib/components/messaging/ChannelInfoModal.svelte +0 -163
  78. package/src/lib/components/messaging/ChannelList.svelte +0 -107
  79. package/src/lib/components/messaging/ChannelMemberAvatarStack.svelte +0 -69
  80. package/src/lib/components/messaging/ChannelMembersModal.svelte +0 -182
  81. package/src/lib/components/messaging/CreateChannelModal.svelte +0 -190
  82. package/src/lib/components/messaging/DirectMessageList.svelte +0 -145
  83. package/src/lib/components/messaging/EmojiSelector.svelte +0 -261
  84. package/src/lib/components/messaging/MentionAutocomplete.svelte +0 -193
  85. package/src/lib/components/messaging/MessageAttachment.svelte +0 -273
  86. package/src/lib/components/messaging/MessageAttachmentUpload.svelte +0 -243
  87. package/src/lib/components/messaging/MessageInput.svelte +0 -452
  88. package/src/lib/components/messaging/MessageItem.svelte +0 -341
  89. package/src/lib/components/messaging/MessageThread.svelte +0 -306
  90. package/src/lib/components/messaging/NotificationSettingsModal.svelte +0 -234
  91. package/src/lib/components/messaging/QuotedMessageDisplay.svelte +0 -123
  92. package/src/lib/components/messaging/StartDMModal.svelte +0 -100
  93. package/src/lib/components/messaging/ThreadPanel.svelte +0 -153
  94. package/src/lib/components/pipeline/DESIGN.md +0 -361
  95. package/src/lib/components/pipeline/Pipeline.svelte +0 -391
  96. package/src/lib/components/pipeline/PipelineCard.svelte +0 -94
  97. package/src/lib/components/pipeline/PipelineColumn.svelte +0 -163
  98. package/src/lib/components/replay/ChapterTimeline.svelte +0 -326
  99. package/src/lib/components/session-nav/transcriptModel.ts +0 -352
  100. package/src/lib/data/emojis.ts +0 -1208
  101. package/src/lib/index.ts +0 -405
  102. package/src/lib/stores/confirmDialog.svelte.ts +0 -48
  103. package/src/lib/stores/inputDialog.svelte.ts +0 -51
  104. package/src/lib/styles/rail.css +0 -63
  105. package/src/lib/types/annotation.ts +0 -38
  106. package/src/lib/types/booking.ts +0 -143
  107. package/src/lib/types/comments.ts +0 -97
  108. package/src/lib/types/entityPreview.ts +0 -45
  109. package/src/lib/types/filePicker.ts +0 -16
  110. package/src/lib/types/googleMaps.d.ts +0 -51
  111. package/src/lib/types/maps.ts +0 -43
  112. package/src/lib/types/messaging.ts +0 -459
  113. package/src/lib/types/milestone.ts +0 -22
  114. package/src/lib/types/pipeline.ts +0 -150
  115. package/src/lib/types/smartImageEditor.ts +0 -39
  116. package/src/lib/types/templateVars.ts +0 -36
  117. package/src/lib/utils/currency.ts +0 -20
  118. package/src/lib/utils/daisyuiColors.ts +0 -243
  119. package/src/lib/utils/dateFormatters.ts +0 -196
  120. package/src/lib/utils/googleMapsLoader.ts +0 -84
  121. package/src/lib/utils/mentionParser.ts +0 -188
  122. package/src/lib/utils/phone.ts +0 -80
  123. package/src/lib/utils/phoneFormat.ts +0 -74
  124. package/src/lib/utils/taskUtils.ts +0 -75
@@ -1,326 +0,0 @@
1
- <script lang="ts">
2
- /**
3
- * ChapterTimeline — horizontal chapter-marker scrubber for WorkBlocks.
4
- *
5
- * Renders one chapter marker per WorkBlock, colored by the block's signal
6
- * type. Clicking a marker seeks to that block. The active block is
7
- * highlighted. Keyboard left/right arrows step through chapters.
8
- *
9
- * Extracted from the JAT IDE (jat-jj79f.5 C1) and promoted into jatui
10
- * so JST projects can embed session replay without taking a dependency
11
- * on the full IDE codebase (jat-jj79f.16 G1).
12
- *
13
- * The IDE-specific `SESSION_STATE_VISUALS` import has been replaced with
14
- * an inline `SIGNAL_COLORS` map that covers the same canonical signal types.
15
- */
16
-
17
- export interface ChapterBlock {
18
- id: string;
19
- signal: { type: string; label: string };
20
- itemCount: number;
21
- lineCount: number;
22
- filesTouched: { path: string; how: string }[];
23
- checkpoint: { timestamp: string | null };
24
- summary: string | null;
25
- }
26
-
27
- /** Minimal color descriptors for each canonical signal state. */
28
- interface SignalColor {
29
- accent: string;
30
- bgTint: string;
31
- glow: string;
32
- shortLabel: string;
33
- }
34
-
35
- const SIGNAL_COLORS: Record<string, SignalColor> = {
36
- starting: { accent: 'oklch(0.70 0.16 250)', bgTint: 'oklch(0.70 0.16 250 / 0.10)', glow: 'oklch(0.70 0.16 250 / 0.45)', shortLabel: '🚀 Starting' },
37
- working: { accent: 'oklch(0.75 0.16 85)', bgTint: 'oklch(0.75 0.16 85 / 0.08)', glow: 'oklch(0.75 0.16 85 / 0.45)', shortLabel: '🔧 Working' },
38
- 'needs-input': { accent: 'oklch(0.68 0.19 25)', bgTint: 'oklch(0.68 0.19 25 / 0.08)', glow: 'oklch(0.68 0.19 25 / 0.4)', shortLabel: '❓ Needs Input' },
39
- 'ready-for-review': { accent: 'oklch(0.72 0.16 200)', bgTint: 'oklch(0.72 0.16 200 / 0.08)', glow: 'oklch(0.72 0.16 200 / 0.4)', shortLabel: '🔍 Review' },
40
- completing: { accent: 'oklch(0.72 0.16 170)', bgTint: 'oklch(0.72 0.16 170 / 0.08)', glow: 'oklch(0.72 0.16 170 / 0.4)', shortLabel: '⚙️ Completing' },
41
- completed: { accent: 'oklch(0.66 0.18 145)', bgTint: 'oklch(0.66 0.18 145 / 0.08)', glow: 'oklch(0.66 0.18 145 / 0.4)', shortLabel: '✅ Done' },
42
- paused: { accent: 'oklch(0.65 0.08 250)', bgTint: 'oklch(0.65 0.08 250 / 0.06)', glow: 'oklch(0.65 0.08 250 / 0.25)', shortLabel: '⏸ Paused' },
43
- idle: { accent: 'oklch(0.55 0.03 250)', bgTint: 'oklch(0.55 0.03 250 / 0.04)', glow: 'oklch(0.55 0.03 250 / 0.2)', shortLabel: 'Idle' },
44
- };
45
-
46
- const DEFAULT_COLOR: SignalColor = SIGNAL_COLORS.idle;
47
-
48
- let {
49
- blocks = [],
50
- activeBlockId = null,
51
- onSeek = (_blockId: string) => {},
52
- }: {
53
- blocks: ChapterBlock[];
54
- activeBlockId: string | null;
55
- onSeek?: (blockId: string) => void;
56
- } = $props();
57
-
58
- function rawTypeToState(raw: string): string {
59
- switch (raw) {
60
- case 'needs_input': return 'needs-input';
61
- case 'review': return 'ready-for-review';
62
- case 'complete':
63
- case 'completed': return 'completed';
64
- default: return raw;
65
- }
66
- }
67
-
68
- function visual(signalType: string): SignalColor {
69
- return SIGNAL_COLORS[rawTypeToState(signalType)] ?? DEFAULT_COLOR;
70
- }
71
-
72
- function glyph(signalType: string): string {
73
- const v = visual(signalType);
74
- const first = v.shortLabel.trim().split(/\s+/)[0];
75
- return /\p{Emoji}/u.test(first) ? first : '●';
76
- }
77
-
78
- function formatTimestamp(ts: string | null): string {
79
- if (!ts) return '';
80
- try {
81
- return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
82
- } catch {
83
- return ts;
84
- }
85
- }
86
-
87
- function handleKey(e: KeyboardEvent) {
88
- if (!blocks.length) return;
89
- const cur = blocks.findIndex((b) => b.id === activeBlockId);
90
- if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
91
- e.preventDefault();
92
- const prev = Math.max(0, cur - 1);
93
- onSeek(blocks[prev].id);
94
- } else if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
95
- e.preventDefault();
96
- const next = Math.min(blocks.length - 1, cur + 1);
97
- onSeek(blocks[next].id);
98
- }
99
- }
100
-
101
- // Proportional widths: each chapter segment is proportional to its lineCount.
102
- const totalLines = $derived(blocks.reduce((s, b) => s + Math.max(1, b.lineCount), 0));
103
- function widthPct(b: ChapterBlock): number {
104
- return (Math.max(1, b.lineCount) / (totalLines || 1)) * 100;
105
- }
106
- </script>
107
-
108
- <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
109
- <div
110
- class="chapter-timeline"
111
- role="slider"
112
- tabindex="0"
113
- aria-label="Session chapter timeline"
114
- aria-valuenow={blocks.findIndex((b) => b.id === activeBlockId) + 1}
115
- aria-valuemin={1}
116
- aria-valuemax={blocks.length}
117
- onkeydown={handleKey}
118
- >
119
- {#if blocks.length === 0}
120
- <div class="empty">No chapters — session has no jat-signal boundaries yet.</div>
121
- {:else}
122
- <div class="track">
123
- {#each blocks as block, i (block.id)}
124
- {@const v = visual(block.signal.type)}
125
- {@const isActive = block.id === activeBlockId}
126
- {@const w = widthPct(block)}
127
- <button
128
- class="chapter"
129
- class:active={isActive}
130
- style="
131
- width: {w}%;
132
- min-width: 2rem;
133
- --chapter-accent: {v.accent};
134
- --chapter-tint: {v.bgTint};
135
- --chapter-glow: {v.glow};
136
- "
137
- title="Block {i + 1}: {block.signal.label || block.signal.type}{block.summary ? '\n' + block.summary : ''}{block.checkpoint.timestamp ? '\n' + formatTimestamp(block.checkpoint.timestamp) : ''}"
138
- onclick={() => onSeek(block.id)}
139
- aria-pressed={isActive}
140
- >
141
- <span class="ch-glyph">{glyph(block.signal.type)}</span>
142
- <span class="ch-label">{block.signal.label || block.signal.type}</span>
143
- <span class="ch-meta">{block.itemCount}i</span>
144
- </button>
145
- {/each}
146
- </div>
147
-
148
- <!-- Chapter detail strip: show summary / files / timestamp for active block -->
149
- {#each blocks as block (block.id)}
150
- {#if block.id === activeBlockId}
151
- <div class="ch-detail">
152
- <span class="ch-detail-idx">
153
- Block {blocks.indexOf(block) + 1}/{blocks.length}
154
- </span>
155
- {#if block.summary}
156
- <span class="ch-detail-summary">{block.summary}</span>
157
- {:else}
158
- <span class="ch-detail-summary muted">{block.signal.label || block.signal.type}</span>
159
- {/if}
160
- {#if block.filesTouched.length > 0}
161
- <span class="ch-detail-files">
162
- {#each block.filesTouched as f (f.path)}
163
- <span class="ch-file">{f.path.split('/').pop()}</span>
164
- {/each}
165
- </span>
166
- {/if}
167
- {#if block.checkpoint.timestamp}
168
- <span class="ch-detail-ts">{formatTimestamp(block.checkpoint.timestamp)}</span>
169
- {/if}
170
- </div>
171
- {/if}
172
- {/each}
173
- {/if}
174
- </div>
175
-
176
- <style>
177
- .chapter-timeline {
178
- display: flex;
179
- flex-direction: column;
180
- gap: 0.4rem;
181
- outline: none;
182
- }
183
-
184
- .empty {
185
- font-size: 0.75rem;
186
- color: oklch(0.55 0.02 250);
187
- padding: 0.5rem;
188
- text-align: center;
189
- }
190
-
191
- .track {
192
- display: flex;
193
- height: 2.25rem;
194
- border-radius: 0.5rem;
195
- overflow: hidden;
196
- background: oklch(0.14 0.01 250);
197
- border: 1px solid oklch(0.22 0.02 250);
198
- gap: 1px;
199
- }
200
-
201
- .chapter {
202
- display: flex;
203
- align-items: center;
204
- gap: 0.3rem;
205
- padding: 0 0.5rem;
206
- background: var(--chapter-tint, oklch(0.18 0.01 250));
207
- border: none;
208
- border-right: 1px solid oklch(0.20 0.02 250);
209
- cursor: pointer;
210
- transition: background 0.12s ease, box-shadow 0.12s ease;
211
- overflow: hidden;
212
- white-space: nowrap;
213
- position: relative;
214
- }
215
-
216
- .chapter:last-child {
217
- border-right: none;
218
- }
219
-
220
- .chapter:hover {
221
- background: color-mix(in oklch, var(--chapter-accent) 18%, oklch(0.16 0.01 250));
222
- box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--chapter-accent) 40%, transparent);
223
- }
224
-
225
- .chapter.active {
226
- background: color-mix(in oklch, var(--chapter-accent) 22%, oklch(0.16 0.01 250));
227
- box-shadow:
228
- inset 0 0 0 2px color-mix(in oklch, var(--chapter-accent) 70%, transparent),
229
- 0 0 12px color-mix(in oklch, var(--chapter-glow) 30%, transparent);
230
- }
231
-
232
- .chapter.active::after {
233
- content: '';
234
- position: absolute;
235
- bottom: 0;
236
- left: 0;
237
- right: 0;
238
- height: 2px;
239
- background: var(--chapter-accent);
240
- border-radius: 0 0 0.25rem 0.25rem;
241
- }
242
-
243
- .ch-glyph {
244
- font-size: 0.95rem;
245
- flex-shrink: 0;
246
- }
247
-
248
- .ch-label {
249
- font-size: 0.7rem;
250
- color: oklch(0.78 0.04 250);
251
- overflow: hidden;
252
- text-overflow: ellipsis;
253
- min-width: 0;
254
- }
255
-
256
- .chapter.active .ch-label {
257
- color: oklch(0.92 0.06 250);
258
- font-weight: 600;
259
- }
260
-
261
- .ch-meta {
262
- font-size: 0.6rem;
263
- color: oklch(0.50 0.02 250);
264
- margin-left: auto;
265
- flex-shrink: 0;
266
- font-family: monospace;
267
- }
268
-
269
- /* Detail strip under the track */
270
- .ch-detail {
271
- display: flex;
272
- align-items: center;
273
- gap: 0.6rem;
274
- padding: 0.25rem 0.5rem;
275
- font-size: 0.72rem;
276
- color: oklch(0.70 0.04 250);
277
- background: oklch(0.15 0.01 250);
278
- border-radius: 0.375rem;
279
- border: 1px solid oklch(0.22 0.02 250);
280
- min-height: 1.75rem;
281
- flex-wrap: wrap;
282
- gap-y: 0.15rem;
283
- }
284
-
285
- .ch-detail-idx {
286
- font-size: 0.65rem;
287
- font-family: monospace;
288
- color: oklch(0.50 0.02 250);
289
- flex-shrink: 0;
290
- }
291
-
292
- .ch-detail-summary {
293
- flex: 1;
294
- overflow: hidden;
295
- text-overflow: ellipsis;
296
- white-space: nowrap;
297
- }
298
-
299
- .ch-detail-summary.muted {
300
- color: oklch(0.55 0.02 250);
301
- font-style: italic;
302
- }
303
-
304
- .ch-detail-files {
305
- display: flex;
306
- gap: 0.3rem;
307
- flex-wrap: wrap;
308
- }
309
-
310
- .ch-file {
311
- font-size: 0.64rem;
312
- font-family: monospace;
313
- background: oklch(0.19 0.02 250);
314
- border: 1px solid oklch(0.25 0.02 250);
315
- border-radius: 0.2rem;
316
- padding: 0.1rem 0.35rem;
317
- color: oklch(0.70 0.08 200);
318
- }
319
-
320
- .ch-detail-ts {
321
- font-size: 0.62rem;
322
- font-family: monospace;
323
- color: oklch(0.48 0.02 250);
324
- flex-shrink: 0;
325
- }
326
- </style>
@@ -1,352 +0,0 @@
1
- /**
2
- * transcriptModel.ts — transcript → linked (transcript items ↔ signal nodes) model
3
- * for the session navigator. Extracted from sankey/sankeyData.ts (jat-w8tgz.1).
4
- *
5
- * THE CONCEPT (from ~/Documents/many_to_one.mp4): a two-column linked view like
6
- * an academic article + its citations. LEFT = the session transcript (prose),
7
- * RIGHT = the signal events (the "references"). Curved ribbons connect spans of
8
- * the transcript to the signal they produced, and ink up as you scroll/hover.
9
- *
10
- * THE ANCHORING TRICK: a JAT agent emits state by running `jat-signal <type> …`,
11
- * which appears in Claude's transcript as a Bash tool call. So we scan the
12
- * transcript for those calls and treat each as a signal-emission boundary. Each
13
- * signal then "owns" the run of transcript that led up to it — the genuine
14
- * many-to-one mapping. The jat-signal lines themselves are plumbing, so we
15
- * consume them as boundaries and do NOT render them.
16
- *
17
- * COMPACTION: raw transcript lines are noisy (every tool result, every "file
18
- * updated successfully" notice). We group lines into ITEMS:
19
- * - prompt — a user message (shown in full; it's the human's intent)
20
- * - assistant — the agent's prose (shown in full; it's the narrative)
21
- * - action — a tool call, with its result body tucked into `detail` so the
22
- * transcript shows just the headline by default and the body is
23
- * a drill-down. This is what keeps the left column readable.
24
- */
25
-
26
- export type ItemKind = 'prompt' | 'assistant' | 'action';
27
-
28
- export interface TranscriptItem {
29
- id: number;
30
- kind: ItemKind;
31
- /** the line shown when collapsed (prose, or the tool call) */
32
- headline: string;
33
- /** collapsible body — tool result lines; empty for prompt/assistant */
34
- detail: string[];
35
- /** how many raw transcript lines this item consumed (feeds the line-count badge) */
36
- srcLines: number;
37
- /** id of the signal node that owns this item (assigned after parse) */
38
- signalId: string | null;
39
- }
40
-
41
- export interface FileTouchedEntry {
42
- path: string;
43
- how: 'written' | 'edited' | 'notebook';
44
- }
45
-
46
- export interface SignalNode {
47
- id: string;
48
- /** canonical SESSION_STATE_VISUALS key (e.g. 'working', 'needs-input', 'ready-for-review') */
49
- state: string;
50
- rawType: string;
51
- /** short human label pulled from the signal payload, if any */
52
- label: string;
53
- /** inclusive transcript-item id range this signal owns */
54
- rowStart: number;
55
- rowEnd: number;
56
- /** raw transcript lines feeding this signal — the "many" in many-to-one */
57
- lineCount: number;
58
- // ── WorkBlock substrate fields (B1: populated when built from getSessionWork) ──
59
- /** WorkBlock id (e.g. "blk-000"); null when built from raw transcript lines */
60
- blockId: string | null;
61
- /** One-line LLM summary of this block (A2); null if not yet populated */
62
- summary: string | null;
63
- /** Files touched in this block (A3 file correlation) */
64
- filesTouched: FileTouchedEntry[];
65
- /** Commit SHAs + subjects for this block (A3 diff data) */
66
- commits: Array<{ shaShort: string; subject: string }>;
67
- /** Net lines changed across all diffs for this block */
68
- linesAdded: number;
69
- linesRemoved: number;
70
- }
71
-
72
- export interface LinkedSession {
73
- items: TranscriptItem[];
74
- signals: SignalNode[];
75
- }
76
-
77
- export function rawTypeToState(raw: string): string {
78
- switch (raw) {
79
- case 'needs_input':
80
- return 'needs-input';
81
- case 'review':
82
- return 'ready-for-review';
83
- case 'complete':
84
- case 'completed':
85
- return 'completed';
86
- default:
87
- return raw; // starting, working, completing, paused, idle pass through
88
- }
89
- }
90
-
91
- /** Detect a `jat-signal <type> '<json>'` invocation in a tool-call body. */
92
- function parseSignalBody(body: string): { rawType: string; label: string } | null {
93
- const m = body.match(/jat-signal\s+(starting|working|needs_input|review|completing|complete|completed|paused|idle)\b/);
94
- if (!m) return null;
95
- let label = '';
96
- const pick = body.match(/"(?:approach|question|reviewFocus|currentStep|summary|reason|taskTitle)"\s*:\s*"([^"]{3,90})/);
97
- if (pick) label = pick[1];
98
- return { rawType: m[1], label };
99
- }
100
-
101
- interface OpenItem { kind: ItemKind; headline: string; detail: string[]; srcLines: number }
102
-
103
- /** Build the linked model from rendered transcript lines. */
104
- export function buildLinkedSession(lines: string[]): LinkedSession {
105
- const items: TranscriptItem[] = [];
106
- interface Emit { afterIdx: number; rawType: string; label: string }
107
- const emits: Emit[] = [];
108
-
109
- let cur: OpenItem | null = null;
110
- const flush = () => {
111
- if (cur) {
112
- items.push({ id: items.length, ...cur, signalId: null });
113
- cur = null;
114
- }
115
- };
116
-
117
- for (const line of lines) {
118
- if (!line.trim()) { flush(); continue; } // blank ends a block
119
-
120
- if (line.startsWith('› ')) {
121
- flush();
122
- cur = { kind: 'prompt', headline: line.slice(2), detail: [], srcLines: 1 };
123
- continue;
124
- }
125
-
126
- if (line.startsWith('⏺ ')) {
127
- const body = line.slice(2);
128
- const sig = parseSignalBody(body);
129
- if (sig) {
130
- // signal emission = boundary; consume it, don't render as an item
131
- flush();
132
- emits.push({ afterIdx: items.length - 1, rawType: sig.rawType, label: sig.label });
133
- continue;
134
- }
135
- flush();
136
- cur = { kind: 'action', headline: body, detail: [], srcLines: 1 };
137
- continue;
138
- }
139
-
140
- if (line.startsWith(' ⎿ ')) {
141
- const body = line.slice(4);
142
- if (cur) { cur.detail.push(body); cur.srcLines++; }
143
- continue;
144
- }
145
-
146
- if (line.startsWith(' ')) {
147
- // continuation: tool detail for actions, prose for prompt/assistant
148
- const body = line.trimStart();
149
- if (cur && cur.kind === 'action') { cur.detail.push(body); cur.srcLines++; }
150
- else if (cur) { cur.headline += '\n' + body; cur.srcLines++; }
151
- continue;
152
- }
153
-
154
- // plain assistant prose
155
- if (cur && cur.kind === 'assistant') { cur.headline += '\n' + line; cur.srcLines++; }
156
- else { flush(); cur = { kind: 'assistant', headline: line, detail: [], srcLines: 1 }; }
157
- }
158
- flush();
159
-
160
- // Build signal nodes from emission boundaries.
161
- const signals: SignalNode[] = [];
162
- let prevBoundary = -1;
163
- emits.forEach((e, i) => {
164
- const rowStart = Math.max(0, prevBoundary + 1);
165
- const rowEnd = Math.max(rowStart, e.afterIdx);
166
- let lineCount = 0;
167
- for (let k = rowStart; k <= rowEnd && k < items.length; k++) lineCount += items[k].srcLines;
168
- signals.push({
169
- id: `sig-${i}`,
170
- state: rawTypeToState(e.rawType),
171
- rawType: e.rawType,
172
- label: e.label,
173
- rowStart,
174
- rowEnd,
175
- lineCount: Math.max(1, lineCount),
176
- // No WorkBlock substrate data in the raw-lines path
177
- blockId: null,
178
- summary: null,
179
- filesTouched: [],
180
- commits: [],
181
- linesAdded: 0,
182
- linesRemoved: 0
183
- });
184
- prevBoundary = e.afterIdx;
185
- });
186
-
187
- for (const sig of signals) {
188
- for (let k = sig.rowStart; k <= sig.rowEnd && k < items.length; k++) {
189
- items[k].signalId = sig.id;
190
- }
191
- }
192
-
193
- return { items, signals };
194
- }
195
-
196
- // ── WorkBlock substrate types (B1) ─────────────────────────────────────────
197
- // These mirror the server-side types from sessionWork.ts / sessionWorkDiffs.ts
198
- // but are kept here as lightweight client-side shapes to avoid importing server
199
- // modules into the browser bundle.
200
-
201
- export interface WorkBlockClient {
202
- id: string;
203
- signal: { type: string; label: string; payload: Record<string, unknown> | null };
204
- itemRange: { start: number; end: number };
205
- itemCount: number;
206
- lineCount: number;
207
- summary: string | null;
208
- filesTouched: FileTouchedEntry[];
209
- checkpoint: { promptUuid: string; timestamp: string | null };
210
- /** Optional diff data (A3) — present when endpoint returns enriched data */
211
- commits?: Array<{ shaShort: string; subject: string }>;
212
- linesAdded?: number;
213
- linesRemoved?: number;
214
- }
215
-
216
- export interface WorkTranscriptItemClient {
217
- id: number;
218
- kind: 'prompt' | 'assistant' | 'action';
219
- headline: string;
220
- detail: string[];
221
- srcLines: number;
222
- blockId: string | null;
223
- }
224
-
225
- /**
226
- * Build a LinkedSession from the WorkBlock substrate (B1).
227
- *
228
- * This is the preferred path for live sessions — it uses the richer data from
229
- * `getSessionWork` (A1) including per-block summaries (A2) and file lists (A3)
230
- * instead of re-parsing transcript lines.
231
- *
232
- * The `items` array from the server is used directly as `TranscriptItem[]` since
233
- * both shapes share `id`, `kind`, `headline`, `detail`, `srcLines` with the
234
- * `signalId` replaced by `blockId → signalId` mapping done here.
235
- *
236
- * @param blocks WorkBlock array from `/api/work/[sessionId]/navigator`
237
- * @param items WorkTranscriptItem array from the same endpoint
238
- */
239
- export function buildLinkedSessionFromWork(
240
- blocks: WorkBlockClient[],
241
- items: WorkTranscriptItemClient[]
242
- ): LinkedSession {
243
- // Map WorkTranscriptItem → TranscriptItem (blockId → signalId mapping happens after)
244
- const transcriptItems: TranscriptItem[] = items.map((it) => ({
245
- id: it.id,
246
- kind: it.kind,
247
- headline: it.headline,
248
- detail: it.detail,
249
- srcLines: it.srcLines,
250
- signalId: null // filled in below
251
- }));
252
-
253
- // Map WorkBlock → SignalNode
254
- const signals: SignalNode[] = blocks.map((blk) => ({
255
- id: blk.id,
256
- state: rawTypeToState(blk.signal.type),
257
- rawType: blk.signal.type,
258
- label: blk.signal.label,
259
- rowStart: blk.itemRange.start,
260
- rowEnd: blk.itemRange.end,
261
- lineCount: blk.lineCount,
262
- // WorkBlock substrate fields
263
- blockId: blk.id,
264
- summary: blk.summary,
265
- filesTouched: blk.filesTouched,
266
- commits: blk.commits ?? [],
267
- linesAdded: blk.linesAdded ?? 0,
268
- linesRemoved: blk.linesRemoved ?? 0
269
- }));
270
-
271
- // Assign signalId to transcript items via block ownership
272
- for (const sig of signals) {
273
- for (let k = sig.rowStart; k <= sig.rowEnd && k < transcriptItems.length; k++) {
274
- transcriptItems[k].signalId = sig.id;
275
- }
276
- }
277
-
278
- return { items: transcriptItems, signals };
279
- }
280
-
281
- /**
282
- * A baked, realistic sample so /sankey always demos beautifully even with no live
283
- * session. Authored as raw transcript lines with real `jat-signal` calls, so it
284
- * flows through the EXACT same parser as live data.
285
- */
286
- export const SAMPLE_TRANSCRIPT_LINES: string[] = [
287
- '',
288
- '› /jat:start GentleCoast jat-7h2k',
289
- '⏺ Bash(get-current-session-id)',
290
- ' ⎿ 4f9c1a2e-7b3d-4c8a-9e1f-2a6b8c0d4e5f',
291
- '⏺ Bash(jat-signal starting \'{"agentName":"GentleCoast","taskTitle":"Add password reset flow"}\')',
292
- ' ⎿ Signal: starting (agent: GentleCoast, task: jat-7h2k)',
293
- '',
294
- 'I\'ll implement the password reset flow. Let me first read the existing auth module',
295
- 'to understand the current login + token handling before adding the reset endpoints.',
296
- '⏺ Read(src/lib/auth/login.ts)',
297
- ' ⎿ export async function login(email, password) {',
298
- ' const user = await findUser(email);',
299
- ' … +48 more lines',
300
- '⏺ Read(src/lib/auth/tokens.ts)',
301
- ' ⎿ export function signToken(payload) {',
302
- ' return jwt.sign(payload, SECRET, { expiresIn: \'15m\' });',
303
- ' … +22 more lines',
304
- '⏺ Bash(jat-signal working \'{"taskId":"jat-7h2k","approach":"Add reset-request + reset-confirm endpoints, reuse signToken with a short-lived scope"}\')',
305
- ' ⎿ Signal: working (task: jat-7h2k)',
306
- '',
307
- 'The token helper already supports short expiries. I\'ll add a reset-scoped token',
308
- 'and two endpoints: one to request a reset email, one to confirm with the token.',
309
- '⏺ Write(src/routes/api/auth/reset-request/+server.ts)',
310
- ' ⎿ Wrote 41 lines',
311
- '⏺ Write(src/routes/api/auth/reset-confirm/+server.ts)',
312
- ' ⎿ Wrote 56 lines',
313
- '⏺ Edit(src/lib/auth/tokens.ts)',
314
- ' ⎿ Added signResetToken() with a 30-minute reset scope',
315
- '⏺ Bash(npm run check)',
316
- ' ⎿ svelte-check found 0 errors and 0 warnings',
317
- '',
318
- 'Both endpoints compile cleanly. One open question before I wire the email send:',
319
- 'should reset links expire in 30 minutes, or match the 15-minute access-token window?',
320
- '⏺ Bash(jat-signal needs_input \'{"taskId":"jat-7h2k","question":"Reset link expiry: 30 min or match the 15-min access token?"}\')',
321
- ' ⎿ Signal: needs_input (task: jat-7h2k)',
322
- '',
323
- '› 30 minutes is fine — people read email slowly.',
324
- '',
325
- 'Got it — keeping the 30-minute reset scope. Now wiring the email send through',
326
- 'the existing Resend helper and adding a rate-limit guard so the endpoint can\'t be',
327
- 'used to spam a victim\'s inbox.',
328
- '⏺ Bash(jat-signal working \'{"taskId":"jat-7h2k","approach":"Wire Resend email + add per-email rate limit on reset-request"}\')',
329
- ' ⎿ Signal: working (task: jat-7h2k)',
330
- '⏺ Edit(src/routes/api/auth/reset-request/+server.ts)',
331
- ' ⎿ Sends reset email via sendEmail(); added 3-per-hour rate limit',
332
- '⏺ Bash(npm run test -- auth)',
333
- ' ⎿ 12 passing (auth reset flow covered)',
334
- '',
335
- 'All tests pass. The flow is complete: request → email → confirm → new password.',
336
- 'Summarizing for review.',
337
- '⏺ Bash(jat-signal review \'{"taskId":"jat-7h2k","summary":["Added reset-request + reset-confirm endpoints","30-min reset-scoped token","Resend email + 3/hr rate limit","12 tests passing"]}\')',
338
- ' ⎿ Signal: review (task: jat-7h2k)',
339
- '',
340
- '🔍 READY FOR REVIEW: jat-7h2k — password reset flow implemented, tested, rate-limited.',
341
- '',
342
- '› /jat:complete',
343
- '⏺ Bash(jat-step committing --task jat-7h2k)',
344
- ' ⎿ Committed: feat(jat-7h2k): add password reset flow',
345
- '⏺ Bash(jat-signal complete \'{"taskId":"jat-7h2k","summary":"Shipped password reset flow"}\')',
346
- ' ⎿ Signal: complete (task: jat-7h2k)',
347
- ''
348
- ];
349
-
350
- export function sampleLinkedSession(): LinkedSession {
351
- return buildLinkedSession(SAMPLE_TRANSCRIPT_LINES);
352
- }