@joewinke/jatui 0.24.0 → 0.25.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.
|
@@ -34,6 +34,8 @@ export interface TranscriptItem {
|
|
|
34
34
|
srcLines: number;
|
|
35
35
|
/** id of the signal node that owns this item (assigned after parse) */
|
|
36
36
|
signalId: string | null;
|
|
37
|
+
/** ISO write time of the first source message (null on line-parsed/live items) */
|
|
38
|
+
timestamp?: string | null;
|
|
37
39
|
}
|
|
38
40
|
export interface FileTouchedEntry {
|
|
39
41
|
path: string;
|
|
@@ -65,11 +67,46 @@ export interface SignalNode {
|
|
|
65
67
|
/** Net lines changed across all diffs for this block */
|
|
66
68
|
linesAdded: number;
|
|
67
69
|
linesRemoved: number;
|
|
70
|
+
/**
|
|
71
|
+
* Original signal JSON payload (full data for rich card rendering). Always
|
|
72
|
+
* set by the builders below; optional so SignalNodes constructed by hand
|
|
73
|
+
* (pre-0.25 callers) still type-check.
|
|
74
|
+
*/
|
|
75
|
+
payload?: Record<string, unknown> | null;
|
|
76
|
+
/**
|
|
77
|
+
* True when this node represents the OPEN, in-progress turn — either a
|
|
78
|
+
* server-marked open block (`WorkBlockClient.open === true`) or the
|
|
79
|
+
* synthesized live node owning the trailing items past the last block.
|
|
80
|
+
* SessionNavigator uses this to render a ● live affordance. Optional so
|
|
81
|
+
* existing SignalNode consumers are unaffected.
|
|
82
|
+
*/
|
|
83
|
+
isLive?: boolean;
|
|
68
84
|
}
|
|
69
85
|
export interface LinkedSession {
|
|
70
86
|
items: TranscriptItem[];
|
|
71
87
|
signals: SignalNode[];
|
|
72
88
|
}
|
|
89
|
+
/** Tone drives the marker's hue; deliberately NOT a session-state key. */
|
|
90
|
+
export type MarkerTone = 'success' | 'error' | 'warn' | 'neutral' | 'info';
|
|
91
|
+
/** What a card detector declares when its event deserves a rail marker. */
|
|
92
|
+
export interface TimelineMarkerSpec {
|
|
93
|
+
/** glyph shown on the rail — keep it non-emoji so state glyphs stay distinct */
|
|
94
|
+
glyph: string;
|
|
95
|
+
/** short rail label, e.g. "subtask completed" */
|
|
96
|
+
label: string;
|
|
97
|
+
/** longer text for the title/tooltip */
|
|
98
|
+
detail?: string;
|
|
99
|
+
tone: MarkerTone;
|
|
100
|
+
}
|
|
101
|
+
/** A content event promoted onto the signal timeline, anchored to its row. */
|
|
102
|
+
export interface TimelineMarker extends TimelineMarkerSpec {
|
|
103
|
+
/** rail id — "mrk-<itemId>" */
|
|
104
|
+
id: string;
|
|
105
|
+
/** transcript item this marker anchors to (its `[data-row]`) */
|
|
106
|
+
itemId: number;
|
|
107
|
+
/** signal owning that item — clicking a marker inks this signal's ribbon */
|
|
108
|
+
signalId: string | null;
|
|
109
|
+
}
|
|
73
110
|
export declare function rawTypeToState(raw: string): string;
|
|
74
111
|
/** Build the linked model from rendered transcript lines. */
|
|
75
112
|
export declare function buildLinkedSession(lines: string[]): LinkedSession;
|
|
@@ -99,6 +136,14 @@ export interface WorkBlockClient {
|
|
|
99
136
|
}>;
|
|
100
137
|
linesAdded?: number;
|
|
101
138
|
linesRemoved?: number;
|
|
139
|
+
/**
|
|
140
|
+
* True when the server marks this block as the OPEN in-progress turn
|
|
141
|
+
* (populated by the navigator/sessionWork layer — sibling task jat-difet).
|
|
142
|
+
* Read defensively: may be undefined on blocks from older payloads. When
|
|
143
|
+
* true on the last block, that block's node becomes the live node (it
|
|
144
|
+
* absorbs any trailing items instead of synthesizing a separate one).
|
|
145
|
+
*/
|
|
146
|
+
open?: boolean;
|
|
102
147
|
}
|
|
103
148
|
export interface WorkTranscriptItemClient {
|
|
104
149
|
id: number;
|
|
@@ -107,6 +152,7 @@ export interface WorkTranscriptItemClient {
|
|
|
107
152
|
detail: string[];
|
|
108
153
|
srcLines: number;
|
|
109
154
|
blockId: string | null;
|
|
155
|
+
timestamp?: string | null;
|
|
110
156
|
}
|
|
111
157
|
/**
|
|
112
158
|
* Build a LinkedSession from the WorkBlock substrate (B1).
|
|
@@ -41,10 +41,36 @@ function parseSignalBody(body) {
|
|
|
41
41
|
if (!m)
|
|
42
42
|
return null;
|
|
43
43
|
let label = '';
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
let payload = null;
|
|
45
|
+
// Try to extract and parse the JSON payload from the signal call
|
|
46
|
+
const jsonMatch = body.match(/jat-signal\s+\w+\s+'(\{[\s\S]*?\})'\s*$/) ||
|
|
47
|
+
body.match(/jat-signal\s+\w+\s+"(\{[\s\S]*?\})"\s*$/) ||
|
|
48
|
+
body.match(/jat-signal\s+\w+\s+(\{[\s\S]*?\})\s*$/);
|
|
49
|
+
if (jsonMatch) {
|
|
50
|
+
try {
|
|
51
|
+
payload = JSON.parse(jsonMatch[1]);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// ignore parse errors — payload stays null
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (payload) {
|
|
58
|
+
// Extract label from full payload
|
|
59
|
+
const p = payload;
|
|
60
|
+
const candidate = (Array.isArray(p.summary) ? p.summary[0] : undefined) ??
|
|
61
|
+
(typeof p.approach === 'string' ? p.approach : undefined) ??
|
|
62
|
+
(typeof p.taskTitle === 'string' ? p.taskTitle : undefined) ??
|
|
63
|
+
(typeof p.question === 'string' ? p.question : undefined) ??
|
|
64
|
+
(typeof p.currentStep === 'string' ? p.currentStep : undefined);
|
|
65
|
+
if (candidate)
|
|
66
|
+
label = candidate.slice(0, 90);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
const pick = body.match(/"(?:approach|question|reviewFocus|currentStep|summary|reason|taskTitle)"\s*:\s*"([^"]{3,90})/);
|
|
70
|
+
if (pick)
|
|
71
|
+
label = pick[1];
|
|
72
|
+
}
|
|
73
|
+
return { rawType: m[1], label, payload };
|
|
48
74
|
}
|
|
49
75
|
/** Build the linked model from rendered transcript lines. */
|
|
50
76
|
export function buildLinkedSession(lines) {
|
|
@@ -64,7 +90,14 @@ export function buildLinkedSession(lines) {
|
|
|
64
90
|
} // blank ends a block
|
|
65
91
|
if (line.startsWith('› ')) {
|
|
66
92
|
flush();
|
|
67
|
-
|
|
93
|
+
const headline = line.slice(2);
|
|
94
|
+
// Claude Code's TUI draws a blank "› " input box while idle between
|
|
95
|
+
// turns; a capture that lands mid-frame leaks that empty prompt line
|
|
96
|
+
// into the transcript. Treat it like a blank line (below) rather than
|
|
97
|
+
// a zero-content prompt item — it never carries real content.
|
|
98
|
+
if (!headline.trim())
|
|
99
|
+
continue;
|
|
100
|
+
cur = { kind: 'prompt', headline, detail: [], srcLines: 1 };
|
|
68
101
|
continue;
|
|
69
102
|
}
|
|
70
103
|
if (line.startsWith('⏺ ')) {
|
|
@@ -73,7 +106,7 @@ export function buildLinkedSession(lines) {
|
|
|
73
106
|
if (sig) {
|
|
74
107
|
// signal emission = boundary; consume it, don't render as an item
|
|
75
108
|
flush();
|
|
76
|
-
emits.push({ afterIdx: items.length - 1, rawType: sig.rawType, label: sig.label });
|
|
109
|
+
emits.push({ afterIdx: items.length - 1, rawType: sig.rawType, label: sig.label, payload: sig.payload });
|
|
77
110
|
continue;
|
|
78
111
|
}
|
|
79
112
|
flush();
|
|
@@ -113,13 +146,33 @@ export function buildLinkedSession(lines) {
|
|
|
113
146
|
}
|
|
114
147
|
flush();
|
|
115
148
|
// Build signal nodes from emission boundaries.
|
|
149
|
+
// Ownership is FORWARD: each signal owns the items that come AFTER it, up to
|
|
150
|
+
// (but not including) the next signal's emission point. The first signal also
|
|
151
|
+
// absorbs any preamble items that appear before it.
|
|
152
|
+
//
|
|
153
|
+
// Let B[i] = emits[i].afterIdx = last item index before signal[i].
|
|
154
|
+
//
|
|
155
|
+
// Partition (non-overlapping):
|
|
156
|
+
// sig[0] → [0 .. B[1]] preamble + first forward region
|
|
157
|
+
// sig[i>0] → [B[i]+1 .. B[i+1]] region after sig[i], before sig[i+1]
|
|
158
|
+
// sig[last] → [B[last]+1 .. N-1] region after last signal
|
|
159
|
+
//
|
|
160
|
+
// This ensures output printed AFTER a signal emission (e.g. the READY FOR
|
|
161
|
+
// REVIEW banner after `jat-signal review`) is attributed to that signal, not
|
|
162
|
+
// the subsequent one.
|
|
116
163
|
const signals = [];
|
|
117
|
-
|
|
164
|
+
const N = items.length;
|
|
118
165
|
emits.forEach((e, i) => {
|
|
119
|
-
|
|
120
|
-
|
|
166
|
+
// rowStart: first signal starts at 0 (absorbs preamble items before it);
|
|
167
|
+
// later signals start at the item immediately after this signal's emission.
|
|
168
|
+
const rowStart = i === 0 ? 0 : e.afterIdx + 1;
|
|
169
|
+
// naturalEnd: extends to the next signal's pre-boundary, or to the last item.
|
|
170
|
+
const naturalEnd = i + 1 < emits.length ? emits[i + 1].afterIdx : N - 1;
|
|
171
|
+
// rowEnd: capped at N-1 (never out of bounds); may be < rowStart when this
|
|
172
|
+
// signal has no items after it (e.g. last line is the signal itself).
|
|
173
|
+
const rowEnd = Math.min(naturalEnd, N - 1);
|
|
121
174
|
let lineCount = 0;
|
|
122
|
-
for (let k = rowStart; k <= rowEnd && k <
|
|
175
|
+
for (let k = rowStart; k <= rowEnd && k < N; k++)
|
|
123
176
|
lineCount += items[k].srcLines;
|
|
124
177
|
signals.push({
|
|
125
178
|
id: `sig-${i}`,
|
|
@@ -135,9 +188,9 @@ export function buildLinkedSession(lines) {
|
|
|
135
188
|
filesTouched: [],
|
|
136
189
|
commits: [],
|
|
137
190
|
linesAdded: 0,
|
|
138
|
-
linesRemoved: 0
|
|
191
|
+
linesRemoved: 0,
|
|
192
|
+
payload: e.payload
|
|
139
193
|
});
|
|
140
|
-
prevBoundary = e.afterIdx;
|
|
141
194
|
});
|
|
142
195
|
for (const sig of signals) {
|
|
143
196
|
for (let k = sig.rowStart; k <= sig.rowEnd && k < items.length; k++) {
|
|
@@ -168,9 +221,11 @@ export function buildLinkedSessionFromWork(blocks, items) {
|
|
|
168
221
|
headline: it.headline,
|
|
169
222
|
detail: it.detail,
|
|
170
223
|
srcLines: it.srcLines,
|
|
171
|
-
signalId: null // filled in below
|
|
224
|
+
signalId: null, // filled in below
|
|
225
|
+
timestamp: it.timestamp ?? null
|
|
172
226
|
}));
|
|
173
|
-
// Map WorkBlock → SignalNode
|
|
227
|
+
// Map WorkBlock → SignalNode. `isLive` is seeded from the server's open flag
|
|
228
|
+
// (read defensively — jat-difet adds it; may be undefined on older payloads).
|
|
174
229
|
const signals = blocks.map((blk) => ({
|
|
175
230
|
id: blk.id,
|
|
176
231
|
state: rawTypeToState(blk.signal.type),
|
|
@@ -185,11 +240,80 @@ export function buildLinkedSessionFromWork(blocks, items) {
|
|
|
185
240
|
filesTouched: blk.filesTouched,
|
|
186
241
|
commits: blk.commits ?? [],
|
|
187
242
|
linesAdded: blk.linesAdded ?? 0,
|
|
188
|
-
linesRemoved: blk.linesRemoved ?? 0
|
|
243
|
+
linesRemoved: blk.linesRemoved ?? 0,
|
|
244
|
+
payload: blk.signal.payload,
|
|
245
|
+
isLive: blk.open === true
|
|
189
246
|
}));
|
|
190
|
-
|
|
247
|
+
const N = transcriptItems.length;
|
|
248
|
+
// Helper: synthesize the live node owning the open in-progress turn.
|
|
249
|
+
const makeLiveNode = (start, end, label) => {
|
|
250
|
+
let lineCount = 0;
|
|
251
|
+
for (let k = start; k <= end && k < N; k++)
|
|
252
|
+
lineCount += transcriptItems[k].srcLines;
|
|
253
|
+
return {
|
|
254
|
+
id: 'sig-live',
|
|
255
|
+
state: rawTypeToState('working'),
|
|
256
|
+
rawType: 'working',
|
|
257
|
+
label,
|
|
258
|
+
rowStart: start,
|
|
259
|
+
rowEnd: end,
|
|
260
|
+
lineCount: Math.max(1, lineCount),
|
|
261
|
+
blockId: null,
|
|
262
|
+
summary: null,
|
|
263
|
+
filesTouched: [],
|
|
264
|
+
commits: [],
|
|
265
|
+
linesAdded: 0,
|
|
266
|
+
linesRemoved: 0,
|
|
267
|
+
payload: null,
|
|
268
|
+
isLive: true
|
|
269
|
+
};
|
|
270
|
+
};
|
|
271
|
+
if (N > 0) {
|
|
272
|
+
if (signals.length === 0) {
|
|
273
|
+
// No blocks but live items exist → the whole transcript is the open
|
|
274
|
+
// turn. Give it a single live node so nothing is left unsignaled.
|
|
275
|
+
signals.push(makeLiveNode(0, N - 1, 'current'));
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
// (#1 gap-fill) Guarantee a contiguous [0..N-1] partition so EVERY
|
|
279
|
+
// item ends up owning a signal:
|
|
280
|
+
// (a) first signal absorbs any preamble before its range
|
|
281
|
+
signals[0].rowStart = 0;
|
|
282
|
+
// (b) extend each signal's rowEnd to close the gap before the next
|
|
283
|
+
// signal's range (no-op when ranges already touch/overlap)
|
|
284
|
+
for (let i = 0; i < signals.length - 1; i++) {
|
|
285
|
+
const nextStart = signals[i + 1].rowStart;
|
|
286
|
+
if (signals[i].rowEnd < nextStart - 1) {
|
|
287
|
+
signals[i].rowEnd = nextStart - 1;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// (#2 live node) Trailing items past the last block's range are the
|
|
291
|
+
// OPEN in-progress turn — they must NOT be folded into the previous
|
|
292
|
+
// signal (e.g. a 'review'). Honor a server-marked open block first;
|
|
293
|
+
// otherwise synthesize a dedicated live node.
|
|
294
|
+
const lastSig = signals[signals.length - 1];
|
|
295
|
+
const lastBlockEnd = lastSig.rowEnd;
|
|
296
|
+
if (lastSig.isLive) {
|
|
297
|
+
// Server already marked the last block open → IT is the live node;
|
|
298
|
+
// extend it to absorb any trailing items.
|
|
299
|
+
lastSig.rowEnd = N - 1;
|
|
300
|
+
}
|
|
301
|
+
else if (lastBlockEnd < N - 1) {
|
|
302
|
+
// Synthesize a live node owning [lastBlockEnd+1 .. N-1]. Label from
|
|
303
|
+
// the live task title when available, else a neutral 'current'.
|
|
304
|
+
const p = lastSig.payload;
|
|
305
|
+
const title = p && typeof p.taskTitle === 'string' ? p.taskTitle.slice(0, 90) : null;
|
|
306
|
+
signals.push(makeLiveNode(lastBlockEnd + 1, N - 1, title ?? 'current'));
|
|
307
|
+
}
|
|
308
|
+
// else: the last block already reaches N-1 — nothing trailing.
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Assign signalId to transcript items via (gap-filled) ownership ranges.
|
|
312
|
+
// Clamp to bounds; later signals (incl. the live node) win on any overlap.
|
|
191
313
|
for (const sig of signals) {
|
|
192
|
-
|
|
314
|
+
const start = Math.max(0, sig.rowStart);
|
|
315
|
+
const end = Math.min(sig.rowEnd, N - 1);
|
|
316
|
+
for (let k = start; k <= end; k++) {
|
|
193
317
|
transcriptItems[k].signalId = sig.id;
|
|
194
318
|
}
|
|
195
319
|
}
|
|
@@ -202,7 +326,7 @@ export function buildLinkedSessionFromWork(blocks, items) {
|
|
|
202
326
|
*/
|
|
203
327
|
export const SAMPLE_TRANSCRIPT_LINES = [
|
|
204
328
|
'',
|
|
205
|
-
'› /jat
|
|
329
|
+
'› /jat-start GentleCoast jat-7h2k',
|
|
206
330
|
'⏺ Bash(get-current-session-id)',
|
|
207
331
|
' ⎿ 4f9c1a2e-7b3d-4c8a-9e1f-2a6b8c0d4e5f',
|
|
208
332
|
'⏺ Bash(jat-signal starting \'{"agentName":"GentleCoast","taskTitle":"Add password reset flow"}\')',
|
|
@@ -256,7 +380,7 @@ export const SAMPLE_TRANSCRIPT_LINES = [
|
|
|
256
380
|
'',
|
|
257
381
|
'🔍 READY FOR REVIEW: jat-7h2k — password reset flow implemented, tested, rate-limited.',
|
|
258
382
|
'',
|
|
259
|
-
'› /jat
|
|
383
|
+
'› /jat-complete',
|
|
260
384
|
'⏺ Bash(jat-step committing --task jat-7h2k)',
|
|
261
385
|
' ⎿ Committed: feat(jat-7h2k): add password reset flow',
|
|
262
386
|
'⏺ Bash(jat-signal complete \'{"taskId":"jat-7h2k","summary":"Shipped password reset flow"}\')',
|
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export { default as Flythrough } from './components/Flythrough.svelte';
|
|
|
25
25
|
export { default as LessonEmbed } from './components/LessonEmbed.svelte';
|
|
26
26
|
export type { Collection, CollectionCut, CollectionBeat, } from './components/LessonEmbed.svelte';
|
|
27
27
|
export { buildLinkedSession, buildLinkedSessionFromWork, rawTypeToState, sampleLinkedSession, SAMPLE_TRANSCRIPT_LINES, } from './components/session-nav/transcriptModel';
|
|
28
|
-
export type { ItemKind, TranscriptItem, FileTouchedEntry, SignalNode, LinkedSession, WorkBlockClient, WorkTranscriptItemClient, } from './components/session-nav/transcriptModel';
|
|
28
|
+
export type { ItemKind, TranscriptItem, FileTouchedEntry, SignalNode, LinkedSession, WorkBlockClient, WorkTranscriptItemClient, MarkerTone, TimelineMarkerSpec, TimelineMarker, } from './components/session-nav/transcriptModel';
|
|
29
29
|
export { default as UserAvatar } from './components/UserAvatar.svelte';
|
|
30
30
|
export { default as SidebarUserFooter } from './components/SidebarUserFooter.svelte';
|
|
31
31
|
export { default as TaskTypeIcon } from './components/TaskTypeIcon.svelte';
|
package/dist/index.js
CHANGED
|
@@ -44,7 +44,8 @@ export { qrMatrix, qrCanvas } from './video/advisors/qr';
|
|
|
44
44
|
// LessonEmbed renders a demo-assemble collection.json (cut picker + player).
|
|
45
45
|
export { default as Flythrough } from './components/Flythrough.svelte';
|
|
46
46
|
export { default as LessonEmbed } from './components/LessonEmbed.svelte';
|
|
47
|
-
// Data models — Session transcript (from JAT IDE, jat-jj79f.16 G1
|
|
47
|
+
// Data models — Session transcript (from JAT IDE, jat-jj79f.16 G1; resynced
|
|
48
|
+
// with the IDE's payload/timestamp/live-node/F13-marker model in jatui-ucmkz)
|
|
48
49
|
export { buildLinkedSession, buildLinkedSessionFromWork, rawTypeToState, sampleLinkedSession, SAMPLE_TRANSCRIPT_LINES, } from './components/session-nav/transcriptModel';
|
|
49
50
|
// Components — Universal
|
|
50
51
|
export { default as UserAvatar } from './components/UserAvatar.svelte';
|