@shipfox/client-logs 27.0.0 → 29.0.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipfox/client-logs",
3
3
  "license": "MIT",
4
- "version": "27.0.0",
4
+ "version": "29.0.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/ShipfoxHQ/shipfox.git",
@@ -20,7 +20,7 @@
20
20
  "@swc/helpers": "^0.5.17",
21
21
  "@shipfox/api-logs-dto": "17.0.0",
22
22
  "@shipfox/client-api": "6.0.1",
23
- "@shipfox/react-ui": "2.3.1"
23
+ "@shipfox/react-ui": "2.3.3"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "@tanstack/react-query": "^5.101.0",
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import {Icon} from '@shipfox/react-ui/icon';
3
+ import {Icon, type IconName} from '@shipfox/react-ui/icon';
4
4
  import {
5
5
  LogContent,
6
6
  LogDisclosure,
@@ -67,198 +67,280 @@ function AgentSessionRowView({
67
67
 
68
68
  switch (row.kind) {
69
69
  case 'message':
70
- return (
71
- <LogRow
72
- lineNumber={null}
73
- timestamp={new Date(row.timestamp)}
74
- indent={indent}
75
- tone={row.terminalFailure ? 'error' : 'default'}
76
- data-log-terminal-failure={row.terminalFailure ? 'true' : undefined}
77
- >
78
- <LogContent className="text-foreground-contrast-primary">
79
- <span className="flex min-w-0 items-start gap-inline">
80
- <MessageIcon role={row.role} terminalFailure={row.terminalFailure} />
81
- <span className="flex min-w-0 flex-1 flex-col gap-tight">
82
- <span className="flex min-w-0 items-center gap-inline">
83
- <MessageRoleLabel label={row.label} terminalFailure={row.terminalFailure} />
84
- <RowMetadata meta={row.meta} className="ml-auto flex-none" />
85
- </span>
86
- <span className="block min-w-0">
87
- <PreviewText text={row.text} />
88
- </span>
89
- </span>
90
- </span>
91
- </LogContent>
92
- </LogRow>
93
- );
70
+ return <SessionMessageRow row={row} indent={indent} />;
94
71
  case 'thinking':
72
+ return <SessionThinkingRow row={row} indent={indent} disclosure={disclosureProps} />;
73
+ case 'tool-call':
95
74
  return (
96
- <LogDisclosure indent={indent} {...disclosureProps}>
97
- <LogDisclosureTrigger
98
- summary={wordSummary(row.text)}
99
- timestamp={new Date(row.timestamp)}
100
- className="text-foreground-contrast-secondary"
101
- >
102
- thinking
103
- </LogDisclosureTrigger>
104
- <LogDisclosureContent className="text-foreground-contrast-secondary">
105
- <LogContent className="text-foreground-contrast-secondary">
106
- <PreviewText text={row.text} />
107
- </LogContent>
108
- </LogDisclosureContent>
109
- </LogDisclosure>
110
- );
111
- case 'tool-call': {
112
- const awaitingResult = row.id != null && !resolvedToolCallIds.has(row.id);
113
- return (
114
- <LogDisclosure indent={indent} {...disclosureProps}>
115
- <LogDisclosureTrigger
116
- timestamp={new Date(row.timestamp)}
117
- summary={compactPreview(row.summary ?? row.input)}
118
- trailing={
119
- awaitingResult ? (
120
- <span className="inline-flex items-center gap-tight">
121
- <Icon
122
- name="loader4Line"
123
- className="size-12 motion-safe:animate-spin"
124
- aria-hidden="true"
125
- />
126
- awaiting result
127
- </span>
128
- ) : null
129
- }
130
- >
131
- <span className="inline-flex min-w-0 items-center gap-inline">
132
- <Icon name="terminalBoxLine" className="size-14 flex-none" aria-hidden="true" />
133
- <span className="truncate">tool {row.name}</span>
134
- </span>
135
- </LogDisclosureTrigger>
136
- <LogDisclosureContent>
137
- {row.summary != null ? (
138
- <>
139
- <LogContent>
140
- <PreviewText text={row.summary} />
141
- </LogContent>
142
- <LogContent variant="code">
143
- <PreviewText text={row.input} />
144
- </LogContent>
145
- </>
146
- ) : (
147
- <LogContent variant="code">
148
- <PreviewText text={row.input} />
149
- </LogContent>
150
- )}
151
- </LogDisclosureContent>
152
- </LogDisclosure>
75
+ <SessionToolCallRow
76
+ row={row}
77
+ indent={indent}
78
+ disclosure={disclosureProps}
79
+ resolvedToolCallIds={resolvedToolCallIds}
80
+ />
153
81
  );
154
- }
155
- case 'tool-result': {
156
- const toolName =
157
- row.toolName === 'tool'
158
- ? ((row.toolCallId != null ? toolCallNames.get(row.toolCallId) : undefined) ??
159
- '(unmatched)')
160
- : row.toolName;
82
+ case 'tool-result':
161
83
  return (
162
- <LogDisclosure indent={indent} {...disclosureProps}>
163
- <LogDisclosureTrigger
164
- timestamp={new Date(row.timestamp)}
165
- summary={compactPreview(row.output)}
166
- trailing={
167
- <span
168
- className={cn(
169
- 'inline-flex items-center gap-tight',
170
- row.isError ? 'text-tag-error-icon' : 'text-foreground-contrast-secondary',
171
- )}
172
- >
173
- <Icon
174
- name={row.isError ? 'closeCircleLine' : 'checkLine'}
175
- className="size-12"
176
- aria-hidden="true"
177
- />
178
- {row.isError ? 'error' : 'ok'}
179
- </span>
180
- }
181
- >
182
- <span className="inline-flex min-w-0 items-center gap-inline">
183
- <Icon name="terminalWindowLine" className="size-14 flex-none" aria-hidden="true" />
184
- <span className="truncate">result {toolName}</span>
185
- </span>
186
- </LogDisclosureTrigger>
187
- <LogDisclosureContent>
188
- <LogContent variant="code" className="text-foreground-contrast-primary">
189
- <PreviewText text={row.output} />
190
- </LogContent>
191
- </LogDisclosureContent>
192
- </LogDisclosure>
193
- );
194
- }
195
- case 'lifecycle':
196
- return (
197
- <LogRow
198
- lineNumber={null}
199
- timestamp={new Date(row.timestamp)}
84
+ <SessionToolResultRow
85
+ row={row}
200
86
  indent={indent}
201
- tone={row.tone}
202
- data-log-terminal-failure={row.terminalFailure ? 'true' : undefined}
203
- >
204
- <LogContent className="text-foreground-contrast-secondary">
205
- <span className="inline-flex w-full items-center gap-inline">
206
- <Icon name="informationLine" className="size-14 flex-none" aria-hidden="true" />
207
- <span className="min-w-0">
208
- <span className="font-medium">{row.label}</span>
209
- {row.detail != null ? (
210
- <>
211
- {' · '}
212
- <span className="text-foreground-contrast-secondary">{row.detail}</span>
213
- </>
214
- ) : null}
215
- </span>
216
- <span
217
- aria-hidden="true"
218
- className="h-px flex-1 border-t border-dashed border-current opacity-30"
219
- />
220
- <RowMetadata meta={row.meta} />
221
- </span>
222
- </LogContent>
223
- </LogRow>
87
+ disclosure={disclosureProps}
88
+ toolCallNames={toolCallNames}
89
+ />
224
90
  );
91
+ case 'lifecycle':
92
+ return <SessionLifecycleRow row={row} indent={indent} />;
225
93
  case 'raw':
226
- return (
227
- <LogDisclosure indent={indent} {...disclosureProps}>
228
- <LogDisclosureTrigger
229
- timestamp={new Date(row.timestamp)}
230
- summary={compactPreview(row.raw)}
231
- className="text-foreground-contrast-primary"
232
- >
233
- <span className="inline-flex min-w-0 items-center gap-inline">
94
+ return <SessionRawRow row={row} indent={indent} disclosure={disclosureProps} />;
95
+ default:
96
+ return assertNever(row);
97
+ }
98
+ }
99
+
100
+ type DisclosureState = {open: boolean; onOpenChange: (open: boolean) => void};
101
+
102
+ function SessionMessageRow({
103
+ row,
104
+ indent,
105
+ }: {
106
+ row: Extract<SessionViewRow, {kind: 'message'}>;
107
+ indent: number;
108
+ }) {
109
+ return (
110
+ <LogRow
111
+ lineNumber={null}
112
+ timestamp={new Date(row.timestamp)}
113
+ indent={indent}
114
+ tone={row.terminalFailure ? 'error' : 'default'}
115
+ data-log-terminal-failure={row.terminalFailure ? 'true' : undefined}
116
+ >
117
+ <LogContent className="text-foreground-contrast-primary">
118
+ <span className="flex min-w-0 items-start gap-inline">
119
+ <MessageIcon role={row.role} terminalFailure={row.terminalFailure} />
120
+ <span className="flex min-w-0 flex-1 flex-col gap-tight">
121
+ <span className="flex min-w-0 items-center gap-inline">
122
+ <MessageRoleLabel label={row.label} terminalFailure={row.terminalFailure} />
123
+ <RowMetadata meta={row.meta} className="ml-auto flex-none" />
124
+ </span>
125
+ <span className="block min-w-0">
126
+ <PreviewText text={row.text} />
127
+ </span>
128
+ </span>
129
+ </span>
130
+ </LogContent>
131
+ </LogRow>
132
+ );
133
+ }
134
+
135
+ function SessionThinkingRow({
136
+ row,
137
+ indent,
138
+ disclosure,
139
+ }: {
140
+ row: Extract<SessionViewRow, {kind: 'thinking'}>;
141
+ indent: number;
142
+ disclosure: DisclosureState;
143
+ }) {
144
+ return (
145
+ <LogDisclosure indent={indent} {...disclosure}>
146
+ <LogDisclosureTrigger
147
+ summary={wordSummary(row.text)}
148
+ timestamp={new Date(row.timestamp)}
149
+ className="text-foreground-contrast-secondary"
150
+ >
151
+ thinking
152
+ </LogDisclosureTrigger>
153
+ <LogDisclosureContent className="text-foreground-contrast-secondary">
154
+ <LogContent className="text-foreground-contrast-secondary">
155
+ <PreviewText text={row.text} />
156
+ </LogContent>
157
+ </LogDisclosureContent>
158
+ </LogDisclosure>
159
+ );
160
+ }
161
+
162
+ function SessionToolCallRow({
163
+ row,
164
+ indent,
165
+ disclosure,
166
+ resolvedToolCallIds,
167
+ }: {
168
+ row: Extract<SessionViewRow, {kind: 'tool-call'}>;
169
+ indent: number;
170
+ disclosure: DisclosureState;
171
+ resolvedToolCallIds: ReadonlySet<string>;
172
+ }) {
173
+ const awaitingResult = row.id != null && !resolvedToolCallIds.has(row.id);
174
+ return (
175
+ <LogDisclosure indent={indent} {...disclosure}>
176
+ <LogDisclosureTrigger
177
+ timestamp={new Date(row.timestamp)}
178
+ summary={compactPreview(row.summary ?? row.input)}
179
+ trailing={
180
+ awaitingResult ? (
181
+ <span className="inline-flex items-center gap-tight">
234
182
  <Icon
235
- name="errorWarningLine"
236
- className="size-14 flex-none text-tag-warning-icon"
183
+ name="loader4Line"
184
+ className="size-12 motion-safe:animate-spin"
237
185
  aria-hidden="true"
238
186
  />
239
- <span className="truncate">{row.label}</span>
187
+ awaiting result
240
188
  </span>
241
- </LogDisclosureTrigger>
242
- <LogDisclosureContent>
189
+ ) : null
190
+ }
191
+ >
192
+ <span className="inline-flex min-w-0 items-center gap-inline">
193
+ <Icon name="terminalBoxLine" className="size-14 flex-none" aria-hidden="true" />
194
+ <span className="truncate">tool {row.name}</span>
195
+ </span>
196
+ </LogDisclosureTrigger>
197
+ <LogDisclosureContent>
198
+ {row.summary != null ? (
199
+ <>
200
+ <LogContent>
201
+ <PreviewText text={row.summary} />
202
+ </LogContent>
243
203
  <LogContent variant="code">
244
- <PreviewText text={row.raw} />
204
+ <PreviewText text={row.input} />
245
205
  </LogContent>
246
- </LogDisclosureContent>
247
- </LogDisclosure>
248
- );
249
- default:
250
- return assertNever(row);
251
- }
206
+ </>
207
+ ) : (
208
+ <LogContent variant="code">
209
+ <PreviewText text={row.input} />
210
+ </LogContent>
211
+ )}
212
+ </LogDisclosureContent>
213
+ </LogDisclosure>
214
+ );
215
+ }
216
+
217
+ function SessionToolResultRow({
218
+ row,
219
+ indent,
220
+ disclosure,
221
+ toolCallNames,
222
+ }: {
223
+ row: Extract<SessionViewRow, {kind: 'tool-result'}>;
224
+ indent: number;
225
+ disclosure: DisclosureState;
226
+ toolCallNames: ReadonlyMap<string, string>;
227
+ }) {
228
+ const toolName =
229
+ row.toolName === 'tool'
230
+ ? ((row.toolCallId != null ? toolCallNames.get(row.toolCallId) : undefined) ?? '(unmatched)')
231
+ : row.toolName;
232
+ return (
233
+ <LogDisclosure indent={indent} {...disclosure}>
234
+ <LogDisclosureTrigger
235
+ timestamp={new Date(row.timestamp)}
236
+ summary={compactPreview(row.output)}
237
+ trailing={
238
+ <span
239
+ className={cn(
240
+ 'inline-flex items-center gap-tight',
241
+ row.isError ? 'text-tag-error-icon' : 'text-foreground-contrast-secondary',
242
+ )}
243
+ >
244
+ <Icon
245
+ name={row.isError ? 'closeCircleLine' : 'checkLine'}
246
+ className="size-12"
247
+ aria-hidden="true"
248
+ />
249
+ {row.isError ? 'error' : 'ok'}
250
+ </span>
251
+ }
252
+ >
253
+ <span className="inline-flex min-w-0 items-center gap-inline">
254
+ <Icon name="terminalWindowLine" className="size-14 flex-none" aria-hidden="true" />
255
+ <span className="truncate">result {toolName}</span>
256
+ </span>
257
+ </LogDisclosureTrigger>
258
+ <LogDisclosureContent>
259
+ <LogContent variant="code" className="text-foreground-contrast-primary">
260
+ <PreviewText text={row.output} />
261
+ </LogContent>
262
+ </LogDisclosureContent>
263
+ </LogDisclosure>
264
+ );
265
+ }
266
+
267
+ function SessionLifecycleRow({
268
+ row,
269
+ indent,
270
+ }: {
271
+ row: Extract<SessionViewRow, {kind: 'lifecycle'}>;
272
+ indent: number;
273
+ }) {
274
+ return (
275
+ <LogRow
276
+ lineNumber={null}
277
+ timestamp={new Date(row.timestamp)}
278
+ indent={indent}
279
+ tone={row.tone}
280
+ data-log-terminal-failure={row.terminalFailure ? 'true' : undefined}
281
+ >
282
+ <LogContent className="text-foreground-contrast-secondary">
283
+ <span className="inline-flex w-full items-center gap-inline">
284
+ <Icon name="informationLine" className="size-14 flex-none" aria-hidden="true" />
285
+ <span className="min-w-0">
286
+ <span className="font-medium">{row.label}</span>
287
+ {row.detail != null ? (
288
+ <>
289
+ {' · '}
290
+ <span className="text-foreground-contrast-secondary">{row.detail}</span>
291
+ </>
292
+ ) : null}
293
+ </span>
294
+ <span
295
+ aria-hidden="true"
296
+ className="h-px flex-1 border-t border-dashed border-current opacity-30"
297
+ />
298
+ <RowMetadata meta={row.meta} />
299
+ </span>
300
+ </LogContent>
301
+ </LogRow>
302
+ );
303
+ }
304
+
305
+ function SessionRawRow({
306
+ row,
307
+ indent,
308
+ disclosure,
309
+ }: {
310
+ row: Extract<SessionViewRow, {kind: 'raw'}>;
311
+ indent: number;
312
+ disclosure: DisclosureState;
313
+ }) {
314
+ return (
315
+ <LogDisclosure indent={indent} {...disclosure}>
316
+ <LogDisclosureTrigger
317
+ timestamp={new Date(row.timestamp)}
318
+ summary={compactPreview(row.raw)}
319
+ className="text-foreground-contrast-primary"
320
+ >
321
+ <span className="inline-flex min-w-0 items-center gap-inline">
322
+ <Icon
323
+ name="errorWarningLine"
324
+ className="size-14 flex-none text-tag-warning-icon"
325
+ aria-hidden="true"
326
+ />
327
+ <span className="truncate">{row.label}</span>
328
+ </span>
329
+ </LogDisclosureTrigger>
330
+ <LogDisclosureContent>
331
+ <LogContent variant="code">
332
+ <PreviewText text={row.raw} />
333
+ </LogContent>
334
+ </LogDisclosureContent>
335
+ </LogDisclosure>
336
+ );
252
337
  }
253
338
 
254
339
  function MessageIcon({role, terminalFailure}: {role: string; terminalFailure: boolean}) {
255
- const name = terminalFailure
256
- ? 'closeCircleLine'
257
- : role === 'user'
258
- ? 'userLine'
259
- : role === 'assistant'
260
- ? 'robot2Line'
261
- : 'message2Line';
340
+ let name: IconName = 'message2Line';
341
+ if (terminalFailure) name = 'closeCircleLine';
342
+ else if (role === 'user') name = 'userLine';
343
+ else if (role === 'assistant') name = 'robot2Line';
262
344
 
263
345
  return (
264
346
  <Icon
@@ -70,11 +70,13 @@ export function LogView({
70
70
  const resolvedToolCalls = useMemo(() => collectResolvedToolCalls(tree.nodes), [tree.nodes]);
71
71
  const noOutputState = normalizedSearch ? null : getNoOutputState(tree, emptyState);
72
72
  const anchorRecordCount = records.length;
73
- const searchStatus = normalizedSearch
74
- ? visibleNodes.length === 0
75
- ? `No log lines match “${deferredSearch.trim()}”.`
76
- : `Log search updated for “${deferredSearch.trim()}”.`
77
- : null;
73
+ let searchStatus: string | null = null;
74
+ if (normalizedSearch) {
75
+ searchStatus =
76
+ visibleNodes.length === 0
77
+ ? `No log lines match “${deferredSearch.trim()}”.`
78
+ : `Log search updated for “${deferredSearch.trim()}”.`;
79
+ }
78
80
 
79
81
  useEffect(() => {
80
82
  if (!anchorToFailure) return;