@shipfox/client-logs 0.2.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 (80) hide show
  1. package/.storybook/main.ts +1 -0
  2. package/.storybook/preview.css +9 -0
  3. package/.storybook/preview.tsx +56 -0
  4. package/.swcrc +42 -0
  5. package/.turbo/turbo-build.log +2 -0
  6. package/.turbo/turbo-type$colon$emit.log +1 -0
  7. package/.turbo/turbo-type.log +1 -0
  8. package/CHANGELOG.md +93 -0
  9. package/LICENSE +21 -0
  10. package/dist/components/agent-session-rows.d.ts +8 -0
  11. package/dist/components/agent-session-rows.d.ts.map +1 -0
  12. package/dist/components/agent-session-rows.js +374 -0
  13. package/dist/components/agent-session-rows.js.map +1 -0
  14. package/dist/components/index.d.ts +5 -0
  15. package/dist/components/index.d.ts.map +1 -0
  16. package/dist/components/index.js +6 -0
  17. package/dist/components/index.js.map +1 -0
  18. package/dist/components/log-group.d.ts +11 -0
  19. package/dist/components/log-group.d.ts.map +1 -0
  20. package/dist/components/log-group.js +57 -0
  21. package/dist/components/log-group.js.map +1 -0
  22. package/dist/components/log-view.d.ts +20 -0
  23. package/dist/components/log-view.d.ts.map +1 -0
  24. package/dist/components/log-view.js +243 -0
  25. package/dist/components/log-view.js.map +1 -0
  26. package/dist/components/output-log-row.d.ts +9 -0
  27. package/dist/components/output-log-row.d.ts.map +1 -0
  28. package/dist/components/output-log-row.js +24 -0
  29. package/dist/components/output-log-row.js.map +1 -0
  30. package/dist/components/system-markers.d.ts +21 -0
  31. package/dist/components/system-markers.d.ts.map +1 -0
  32. package/dist/components/system-markers.js +113 -0
  33. package/dist/components/system-markers.js.map +1 -0
  34. package/dist/core/log-read.d.ts +33 -0
  35. package/dist/core/log-read.d.ts.map +1 -0
  36. package/dist/core/log-read.js +49 -0
  37. package/dist/core/log-read.js.map +1 -0
  38. package/dist/core/log-tree.d.ts +85 -0
  39. package/dist/core/log-tree.d.ts.map +1 -0
  40. package/dist/core/log-tree.js +139 -0
  41. package/dist/core/log-tree.js.map +1 -0
  42. package/dist/env.d.js +2 -0
  43. package/dist/env.d.js.map +1 -0
  44. package/dist/hooks/api/step-logs.d.ts +24 -0
  45. package/dist/hooks/api/step-logs.d.ts.map +1 -0
  46. package/dist/hooks/api/step-logs.js +129 -0
  47. package/dist/hooks/api/step-logs.js.map +1 -0
  48. package/dist/index.d.ts +5 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +5 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/tsconfig.test.tsbuildinfo +1 -0
  53. package/package.json +84 -0
  54. package/src/components/agent-session-rows.tsx +341 -0
  55. package/src/components/index.ts +15 -0
  56. package/src/components/log-group.stories.tsx +204 -0
  57. package/src/components/log-group.tsx +54 -0
  58. package/src/components/log-view.stories.tsx +584 -0
  59. package/src/components/log-view.test.tsx +266 -0
  60. package/src/components/log-view.tsx +281 -0
  61. package/src/components/output-log-row.stories.tsx +76 -0
  62. package/src/components/output-log-row.tsx +35 -0
  63. package/src/components/system-markers.stories.tsx +46 -0
  64. package/src/components/system-markers.tsx +148 -0
  65. package/src/core/log-read.test.ts +219 -0
  66. package/src/core/log-read.ts +79 -0
  67. package/src/core/log-tree.test.ts +380 -0
  68. package/src/core/log-tree.ts +200 -0
  69. package/src/env.d.ts +7 -0
  70. package/src/hooks/api/step-logs-query.test.tsx +283 -0
  71. package/src/hooks/api/step-logs.test.ts +66 -0
  72. package/src/hooks/api/step-logs.ts +166 -0
  73. package/src/index.ts +18 -0
  74. package/test/setup.ts +3 -0
  75. package/tsconfig.build.json +9 -0
  76. package/tsconfig.build.tsbuildinfo +1 -0
  77. package/tsconfig.json +3 -0
  78. package/tsconfig.test.json +8 -0
  79. package/vercel.json +8 -0
  80. package/vitest.config.ts +70 -0
@@ -0,0 +1,584 @@
1
+ import type {LogRecord, SessionViewRow} from '@shipfox/api-logs-dto';
2
+ import type {Meta, StoryObj} from '@storybook/react';
3
+ import {LogView, LogViewSkeleton} from './log-view.js';
4
+
5
+ const ESC = String.fromCharCode(27);
6
+ const origin = new Date('2026-06-23T10:00:00.000Z').getTime();
7
+ const at = (offsetSeconds: number) => origin + offsetSeconds * 1000;
8
+
9
+ const out = (data: string, offset: number, stream: 'stdout' | 'stderr' = 'stdout'): LogRecord => ({
10
+ v: 1,
11
+ ts: at(offset),
12
+ type: 'output',
13
+ stream,
14
+ data,
15
+ });
16
+ const session = (row: SessionViewRow, offset: number): LogRecord => ({
17
+ v: 1,
18
+ ts: at(offset),
19
+ type: 'agent_session',
20
+ row: {...row, timestamp: at(offset)},
21
+ });
22
+ const groupStart = (
23
+ groupId: string,
24
+ name: string,
25
+ offset: number,
26
+ parentGroupId: string | null = null,
27
+ ): LogRecord => ({
28
+ v: 1,
29
+ ts: at(offset),
30
+ type: 'group_start',
31
+ group_id: groupId,
32
+ parent_group_id: parentGroupId,
33
+ name,
34
+ });
35
+ const groupEnd = (groupId: string, offset: number): LogRecord => ({
36
+ v: 1,
37
+ ts: at(offset),
38
+ type: 'group_end',
39
+ group_id: groupId,
40
+ });
41
+
42
+ const showcaseRecords: LogRecord[] = [
43
+ out('$ pnpm build && pnpm test\n', 0),
44
+ groupStart('g1', 'Install dependencies', 1),
45
+ out('Resolving packages...\n', 2),
46
+ out('Linking 318 packages\n', 3),
47
+ groupEnd('g1', 4),
48
+ groupStart('g2', 'Build', 5),
49
+ out(`${ESC}[32m✓${ESC}[0m built ${ESC}[34m1284${ESC}[0m modules\n`, 6),
50
+ out('warn: deprecated glob@7, upgrade to glob@10\n', 7, 'stderr'),
51
+ groupEnd('g2', 8),
52
+ groupStart('g3', 'Test', 9),
53
+ out('running 42 tests\n', 10),
54
+ out('FAIL client.test.ts > retries on 503\n', 11, 'stderr'),
55
+ groupEnd('g3', 12),
56
+ {v: 1, ts: at(13), type: 'gap', dropped_bytes: 2048},
57
+ {v: 1, ts: at(14), type: 'end', total_bytes: 15_360},
58
+ ];
59
+
60
+ // A pipeline nested three levels deep: Deploy > Build > Compile, and Deploy >
61
+ // Test > {unit, e2e}. The e2e leaf writes to stderr, so its error bubbles up to
62
+ // Test and the top-level pipeline (visible as an inset bar when those groups are
63
+ // collapsed); the Build branch stays clean. Closed inner groups carry a duration.
64
+ const nestedRecords: LogRecord[] = [
65
+ groupStart('g1', 'Deploy pipeline', 0),
66
+ out('$ ./deploy.sh\n', 0.1),
67
+ groupStart('g2', 'Build', 1, 'g1'),
68
+ out('resolving workspace graph\n', 1.2),
69
+ groupStart('g3', 'Compile @app/web', 2, 'g2'),
70
+ out(`${ESC}[32m✓${ESC}[0m built ${ESC}[34m842${ESC}[0m modules\n`, 3),
71
+ groupEnd('g3', 4),
72
+ groupStart('g4', 'Compile @app/api', 4.2, 'g2'),
73
+ out('tsc --build\n', 5),
74
+ out('✓ 1.1k files emitted\n', 6),
75
+ groupEnd('g4', 7),
76
+ groupEnd('g2', 7.5),
77
+ groupStart('g5', 'Test', 8, 'g1'),
78
+ groupStart('g6', 'unit', 8.2, 'g5'),
79
+ out('running 128 tests\n', 9),
80
+ out('✓ 128 passed\n', 10),
81
+ groupEnd('g6', 11),
82
+ groupStart('g7', 'e2e', 11.2, 'g5'),
83
+ out('running 12 specs\n', 12),
84
+ out('FAIL checkout.spec.ts > applies coupon at checkout\n', 13, 'stderr'),
85
+ groupEnd('g7', 14),
86
+ groupEnd('g5', 14.5),
87
+ groupEnd('g1', 15),
88
+ {v: 1, ts: at(15.2), type: 'end', total_bytes: 9_216},
89
+ ];
90
+
91
+ const unifiedAgentRecords: LogRecord[] = [
92
+ session(
93
+ {
94
+ kind: 'message',
95
+ timestamp: 0,
96
+ role: 'user',
97
+ label: 'user',
98
+ meta: [],
99
+ text: 'Update the auth form error handling.',
100
+ terminalFailure: false,
101
+ },
102
+ 0,
103
+ ),
104
+ session(
105
+ {
106
+ kind: 'message',
107
+ timestamp: 0,
108
+ role: 'assistant',
109
+ label: 'assistant',
110
+ meta: [{label: 'model', value: 'gpt-5-codex'}],
111
+ text: 'I will inspect the form and the existing tests first.',
112
+ terminalFailure: false,
113
+ },
114
+ 1,
115
+ ),
116
+ session(
117
+ {
118
+ kind: 'thinking',
119
+ timestamp: 0,
120
+ text: 'The likely risk is server errors being written to the wrong form meta slot.',
121
+ },
122
+ 1.1,
123
+ ),
124
+ session(
125
+ {
126
+ kind: 'tool-call',
127
+ timestamp: 0,
128
+ id: 'call-1',
129
+ name: 'read_file',
130
+ input: JSON.stringify({path: 'src/login-form.tsx'}, null, 2),
131
+ },
132
+ 1.2,
133
+ ),
134
+ out('$ pnpm --filter @shipfox/client-auth test\n', 2),
135
+ session(
136
+ {
137
+ kind: 'tool-result',
138
+ timestamp: 0,
139
+ toolCallId: 'call-1',
140
+ toolName: 'read_file',
141
+ output: 'export function LoginForm() { /* ... */ }',
142
+ isError: false,
143
+ },
144
+ 3,
145
+ ),
146
+ session(
147
+ {
148
+ kind: 'lifecycle',
149
+ timestamp: 0,
150
+ label: 'Model changed',
151
+ detail: 'gpt-5-codex',
152
+ meta: [{label: 'provider', value: 'openai'}],
153
+ tone: 'default',
154
+ terminalFailure: false,
155
+ },
156
+ 4,
157
+ ),
158
+ {v: 1, ts: at(5), type: 'end', total_bytes: 4096},
159
+ ];
160
+
161
+ const awaitingAgentRecords: LogRecord[] = [
162
+ session(
163
+ {
164
+ kind: 'tool-call',
165
+ timestamp: 0,
166
+ id: 'call-2',
167
+ name: 'run_tests',
168
+ input: JSON.stringify({filter: '@shipfox/client-logs'}, null, 2),
169
+ },
170
+ 0,
171
+ ),
172
+ out('running tests...\n', 1),
173
+ ];
174
+
175
+ const failedAgentRecords: LogRecord[] = [
176
+ out('$ pnpm test\n', 0),
177
+ session(
178
+ {
179
+ kind: 'message',
180
+ timestamp: 0,
181
+ role: 'assistant',
182
+ label: 'assistant',
183
+ meta: [],
184
+ text: 'The run cannot continue because the harness aborted.',
185
+ terminalFailure: true,
186
+ },
187
+ 1,
188
+ ),
189
+ ];
190
+
191
+ const allAgentSessionTypeRecords: LogRecord[] = [
192
+ session(
193
+ {
194
+ kind: 'lifecycle',
195
+ timestamp: 0,
196
+ label: 'Session started',
197
+ detail: 'session-2026-06-23',
198
+ meta: [{label: 'cwd', value: '/workspace/platform'}],
199
+ tone: 'default',
200
+ terminalFailure: false,
201
+ },
202
+ 0,
203
+ ),
204
+ session(
205
+ {
206
+ kind: 'lifecycle',
207
+ timestamp: 0,
208
+ label: 'Session info',
209
+ detail: 'Restored 14 messages from prior context.',
210
+ meta: [],
211
+ tone: 'default',
212
+ terminalFailure: false,
213
+ },
214
+ 1,
215
+ ),
216
+ session(
217
+ {
218
+ kind: 'lifecycle',
219
+ timestamp: 0,
220
+ label: 'Review setup',
221
+ detail: 'entry-review',
222
+ meta: [],
223
+ tone: 'default',
224
+ terminalFailure: false,
225
+ },
226
+ 2,
227
+ ),
228
+ session(
229
+ {
230
+ kind: 'lifecycle',
231
+ timestamp: 0,
232
+ label: 'Thinking level changed',
233
+ detail: 'high',
234
+ meta: [],
235
+ tone: 'default',
236
+ terminalFailure: false,
237
+ },
238
+ 3,
239
+ ),
240
+ session(
241
+ {
242
+ kind: 'lifecycle',
243
+ timestamp: 0,
244
+ label: 'Model changed',
245
+ detail: 'gpt-5-codex',
246
+ meta: [{label: 'provider', value: 'openai'}],
247
+ tone: 'default',
248
+ terminalFailure: false,
249
+ },
250
+ 4,
251
+ ),
252
+ session(
253
+ {
254
+ kind: 'message',
255
+ timestamp: 0,
256
+ role: 'user',
257
+ label: 'user',
258
+ meta: [{label: 'attachment', value: 'image/png'}],
259
+ text: 'Review the failed workflow attempt and patch the tests.',
260
+ terminalFailure: false,
261
+ },
262
+ 5,
263
+ ),
264
+ session(
265
+ {
266
+ kind: 'message',
267
+ timestamp: 0,
268
+ role: 'assistant',
269
+ label: 'assistant',
270
+ meta: [
271
+ {label: 'model', value: 'gpt-5-codex'},
272
+ {label: 'provider', value: 'openai'},
273
+ ],
274
+ text: 'I will inspect the failure anchor and the log renderer.',
275
+ terminalFailure: false,
276
+ },
277
+ 6,
278
+ ),
279
+ session(
280
+ {
281
+ kind: 'thinking',
282
+ timestamp: 0,
283
+ text: 'The UI needs to preserve the terminal assistant message while still showing the tool activity inline.',
284
+ },
285
+ 6.1,
286
+ ),
287
+ session(
288
+ {
289
+ kind: 'tool-call',
290
+ timestamp: 0,
291
+ id: 'call-read',
292
+ name: 'read_file',
293
+ input: JSON.stringify({path: 'libs/client/logs/src/components/log-view.tsx'}, null, 2),
294
+ },
295
+ 6.2,
296
+ ),
297
+ session(
298
+ {
299
+ kind: 'tool-result',
300
+ timestamp: 0,
301
+ toolCallId: 'call-read',
302
+ toolName: 'read_file',
303
+ output: '<AgentSessionRows rows={[node.record.row]} />',
304
+ isError: false,
305
+ },
306
+ 7,
307
+ ),
308
+ session(
309
+ {
310
+ kind: 'tool-result',
311
+ timestamp: 0,
312
+ toolCallId: 'call-test',
313
+ toolName: 'run_tests',
314
+ output: 'FAIL log-view.test.tsx > renders tool results',
315
+ isError: true,
316
+ },
317
+ 8,
318
+ ),
319
+ session(
320
+ {
321
+ kind: 'message',
322
+ timestamp: 0,
323
+ role: 'system',
324
+ label: 'branch summary',
325
+ meta: [{label: 'from', value: 'entry-review'}],
326
+ text: 'Kept the renderer scoped to canonical agent_session rows and log-tree ordering.',
327
+ terminalFailure: false,
328
+ },
329
+ 9,
330
+ ),
331
+ session(
332
+ {
333
+ kind: 'message',
334
+ timestamp: 0,
335
+ role: 'system',
336
+ label: 'compaction',
337
+ meta: [{label: 'tokens', value: '42000'}],
338
+ text: 'Previous context summarized into 3 decisions.',
339
+ terminalFailure: false,
340
+ },
341
+ 10,
342
+ ),
343
+ session(
344
+ {
345
+ kind: 'message',
346
+ timestamp: 0,
347
+ role: 'system',
348
+ label: 'custom',
349
+ meta: [{label: 'type', value: 'review'}],
350
+ text: '{"verdict":"coverage looks complete"}',
351
+ terminalFailure: false,
352
+ },
353
+ 11,
354
+ ),
355
+ session(
356
+ {
357
+ kind: 'message',
358
+ timestamp: 0,
359
+ role: 'system',
360
+ label: 'operator note',
361
+ meta: [],
362
+ text: 'Retry after the fixture update.',
363
+ terminalFailure: false,
364
+ },
365
+ 12,
366
+ ),
367
+ session(
368
+ {
369
+ kind: 'message',
370
+ timestamp: 0,
371
+ role: 'assistant',
372
+ label: 'assistant',
373
+ meta: [{label: 'error', value: 'Harness aborted before the retry finished.'}],
374
+ text: 'I cannot continue because the test harness aborted.',
375
+ terminalFailure: true,
376
+ },
377
+ 13,
378
+ ),
379
+ session(
380
+ {
381
+ kind: 'message',
382
+ timestamp: 0,
383
+ role: 'tool',
384
+ label: 'bash execution',
385
+ meta: [
386
+ {label: 'command', value: 'pnpm test'},
387
+ {label: 'exit', value: '1'},
388
+ {label: 'truncated', value: 'true'},
389
+ {label: 'full output', value: '/tmp/shipfox-agent-output.log', inline: false},
390
+ ],
391
+ text: 'FAIL log-view.test.tsx > renders tool results',
392
+ terminalFailure: false,
393
+ },
394
+ 14,
395
+ ),
396
+ session(
397
+ {
398
+ kind: 'message',
399
+ timestamp: 0,
400
+ role: 'custom',
401
+ label: 'status',
402
+ meta: [],
403
+ text: 'Extension status update.',
404
+ terminalFailure: false,
405
+ },
406
+ 15,
407
+ ),
408
+ session(
409
+ {
410
+ kind: 'raw',
411
+ timestamp: 0,
412
+ label: 'Unknown session entry: future_entry',
413
+ raw: '{"type":"future_entry","payload":{"feature":"new-session-event"}}',
414
+ },
415
+ 16,
416
+ ),
417
+ session({kind: 'raw', timestamp: 0, label: 'Malformed session entry', raw: '{not-json'}, 17),
418
+ {v: 1, ts: at(20), type: 'end', total_bytes: 12_288},
419
+ ];
420
+
421
+ const meta = {
422
+ title: 'Logs/LogView',
423
+ component: LogView,
424
+ parameters: {layout: 'padded'},
425
+ tags: ['autodocs'],
426
+ argTypes: {
427
+ timestamps: {control: 'inline-radio', options: ['off', 'rel', 'abs']},
428
+ wrap: {control: 'boolean'},
429
+ showLineNumbers: {control: 'boolean'},
430
+ },
431
+ args: {
432
+ timestamps: 'off',
433
+ wrap: false,
434
+ showLineNumbers: true,
435
+ },
436
+ } satisfies Meta<typeof LogView>;
437
+
438
+ export default meta;
439
+ type Story = StoryObj<typeof meta>;
440
+
441
+ export const Playground: Story = {
442
+ render: (args) => (
443
+ <div className="max-w-3xl">
444
+ <LogView {...args} records={showcaseRecords} />
445
+ </div>
446
+ ),
447
+ };
448
+
449
+ /**
450
+ * Groups nested three levels deep (Deploy > Build > Compile, Deploy > Test >
451
+ * unit/e2e). Each level indents; the failing `e2e` leaf bubbles its error up to
452
+ * `Test` and `Deploy pipeline`. Collapse any group to see its "N lines" summary,
453
+ * duration, and (for a branch with a failure) the inset error bar.
454
+ */
455
+ export const NestedGroups: Story = {
456
+ args: {defaultGroupsOpen: true},
457
+ render: (args) => (
458
+ <div className="max-w-3xl">
459
+ <LogView {...args} records={nestedRecords} />
460
+ </div>
461
+ ),
462
+ };
463
+
464
+ export const ClosedEmpty: Story = {
465
+ args: {showLineNumbers: true},
466
+ render: (args) => (
467
+ <div className="max-w-3xl">
468
+ <LogView {...args} records={[]} />
469
+ </div>
470
+ ),
471
+ };
472
+
473
+ export const PendingEmpty: Story = {
474
+ args: {showLineNumbers: true, emptyState: 'pending'},
475
+ render: (args) => (
476
+ <div className="max-w-3xl">
477
+ <LogView {...args} records={[]} />
478
+ </div>
479
+ ),
480
+ };
481
+
482
+ export const MinimalOutput: Story = {
483
+ args: {showLineNumbers: true},
484
+ render: (args) => (
485
+ <div className="max-w-3xl">
486
+ <LogView {...args} records={[{v: 1, ts: at(1), type: 'end', total_bytes: 0}]} />
487
+ </div>
488
+ ),
489
+ };
490
+
491
+ export const UnifiedAgentSession: Story = {
492
+ args: {showLineNumbers: true},
493
+ render: (args) => (
494
+ <div className="max-w-3xl">
495
+ <LogView {...args} records={unifiedAgentRecords} />
496
+ </div>
497
+ ),
498
+ };
499
+
500
+ export const AllAgentSessionTypes: Story = {
501
+ args: {showLineNumbers: true, anchorToFailure: true},
502
+ render: (args) => (
503
+ <div className="max-w-3xl">
504
+ <LogView {...args} records={allAgentSessionTypeRecords} />
505
+ </div>
506
+ ),
507
+ };
508
+
509
+ export const RunningAgentToolCall: Story = {
510
+ args: {showLineNumbers: true, emptyState: 'pending'},
511
+ render: (args) => (
512
+ <div className="max-w-3xl">
513
+ <LogView {...args} records={awaitingAgentRecords} />
514
+ </div>
515
+ ),
516
+ };
517
+
518
+ export const FailedAgentSession: Story = {
519
+ args: {showLineNumbers: true, anchorToFailure: true},
520
+ render: (args) => (
521
+ <div className="max-w-3xl">
522
+ <LogView {...args} records={failedAgentRecords} />
523
+ </div>
524
+ ),
525
+ };
526
+
527
+ export const UnknownAgentEntry: Story = {
528
+ args: {showLineNumbers: true},
529
+ render: (args) => (
530
+ <div className="max-w-3xl">
531
+ <LogView
532
+ {...args}
533
+ records={[
534
+ session(
535
+ {
536
+ kind: 'raw',
537
+ timestamp: 0,
538
+ label: 'Unknown session entry: future_entry',
539
+ raw: '{"type":"future_entry","payload":{"value":true}}',
540
+ },
541
+ 0,
542
+ ),
543
+ ]}
544
+ />
545
+ </div>
546
+ ),
547
+ };
548
+
549
+ export const LargeAgentPayload: Story = {
550
+ args: {showLineNumbers: true},
551
+ render: (args) => (
552
+ <div className="max-w-3xl">
553
+ <LogView
554
+ {...args}
555
+ records={[
556
+ session(
557
+ {
558
+ kind: 'message',
559
+ timestamp: 0,
560
+ role: 'assistant',
561
+ label: 'assistant',
562
+ meta: [],
563
+ text: 'Large payload '.repeat(180),
564
+ terminalFailure: false,
565
+ },
566
+ 0,
567
+ ),
568
+ ]}
569
+ />
570
+ </div>
571
+ ),
572
+ };
573
+
574
+ export const LoadingSkeleton: Story = {
575
+ render: (args) => (
576
+ <div className="max-w-3xl">
577
+ <LogViewSkeleton
578
+ timestamps={args.timestamps}
579
+ wrap={args.wrap}
580
+ showLineNumbers={args.showLineNumbers}
581
+ />
582
+ </div>
583
+ ),
584
+ };