@larkup/workspace-app 0.2.3 → 0.2.7

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.
@@ -1 +1 @@
1
- {"version":3,"file":"chat-workspace.d.ts","sourceRoot":"","sources":["../../../src/components/chat/chat-workspace.tsx"],"names":[],"mappings":"AAiPA,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,+BAQjE"}
1
+ {"version":3,"file":"chat-workspace.d.ts","sourceRoot":"","sources":["../../../src/components/chat/chat-workspace.tsx"],"names":[],"mappings":"AAkOA,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,+BAQjE"}
@@ -1 +1 @@
1
- {"version":3,"file":"message-item.d.ts","sourceRoot":"","sources":["../../../src/components/chat/message-item.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAEpC,OAAO,0BAA0B,CAAC;AAuBlC,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEhG,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,MAAM,EACN,WAAW,EACX,aAAa,EACb,QAAQ,EACR,sBAA8B,EAC9B,UAAU,EACV,MAAM,EACN,UAAU,GACX,EAAE;IACD,OAAO,EAAE,SAAS,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,aAAa,CAAC,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,8EAA8E;IAC9E,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,UAAU,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACxD,yHAAyH;IACzH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,GAAG,SAAS,KAAK,IAAI,CAAC;CAChF,+BAilBA;AAipBD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA4FnD"}
1
+ {"version":3,"file":"message-item.d.ts","sourceRoot":"","sources":["../../../src/components/chat/message-item.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAEpC,OAAO,0BAA0B,CAAC;AAuBlC,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEhG,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,MAAM,EACN,WAAW,EACX,aAAa,EACb,QAAQ,EACR,sBAA8B,EAC9B,UAAU,EACV,MAAM,EACN,UAAU,GACX,EAAE;IACD,OAAO,EAAE,SAAS,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,aAAa,CAAC,EAAE,QAAQ,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,8EAA8E;IAC9E,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,UAAU,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACxD,yHAAyH;IACzH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,GAAG,SAAS,KAAK,IAAI,CAAC;CAChF,+BAklBA;AAqpBD,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA4FnD"}
@@ -2,6 +2,7 @@ export interface DataTableConfig {
2
2
  columns: string[];
3
3
  rows: Record<string, any>[];
4
4
  totalRows: number;
5
+ truncated?: boolean;
5
6
  aggregationResults?: Record<string, number>;
6
7
  }
7
8
  export interface ChatDataTableProps {
@@ -1 +1 @@
1
- {"version":3,"file":"chat-data-table.d.ts","sourceRoot":"","sources":["../../../../src/components/chat/tools/chat-data-table.tsx"],"names":[],"mappings":"AA6BA,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,eAAe,CAAC;IACxB;8DAC0D;IAC1D,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA0DD,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,OAAe,EAAE,EAAE,kBAAkB,+BAiR5E"}
1
+ {"version":3,"file":"chat-data-table.d.ts","sourceRoot":"","sources":["../../../../src/components/chat/tools/chat-data-table.tsx"],"names":[],"mappings":"AA6BA,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7C;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,eAAe,CAAC;IACxB;8DAC0D;IAC1D,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA0DD,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,OAAe,EAAE,EAAE,kBAAkB,+BAuR5E"}
@@ -1 +1 @@
1
- {"version":3,"file":"chat-media-preview.d.ts","sourceRoot":"","sources":["../../../../src/components/chat/tools/chat-media-preview.tsx"],"names":[],"mappings":"AAMA;;;;;;;GAOG;AAEH,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,SAAS,EACT,OAAO,GACR,EAAE;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,eAAe,CAAC;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,sCAwQA;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,CAAC,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,MAAM,GACf,MAAM,GAAG,SAAS,CAyCpB;AAED,2EAA2E;AAC3E,wBAAgB,mBAAmB,CACjC,SAAS,CAAC,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,MAAM,GACf,MAAM,GAAG,SAAS,CAsBpB;AAqDD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,EAAE,CAqB7D"}
1
+ {"version":3,"file":"chat-media-preview.d.ts","sourceRoot":"","sources":["../../../../src/components/chat/tools/chat-media-preview.tsx"],"names":[],"mappings":"AAMA;;;;;;;GAOG;AAEH,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,SAAS,EACT,OAAO,GACR,EAAE;IACD,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,eAAe,CAAC;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,sCAuRA;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,CAAC,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,MAAM,GACf,MAAM,GAAG,SAAS,CAyCpB;AAED,2EAA2E;AAC3E,wBAAgB,mBAAmB,CACjC,SAAS,CAAC,EAAE,MAAM,EAClB,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,MAAM,GACf,MAAM,GAAG,SAAS,CAsBpB;AAqDD,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,EAAE,CAqB7D"}
@@ -0,0 +1,2 @@
1
+ export declare function compactToolResult(value: any): any;
2
+ //# sourceMappingURL=message-compaction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-compaction.d.ts","sourceRoot":"","sources":["../../../src/lib/chat/message-compaction.ts"],"names":[],"mappings":"AAAA,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,CAwBjD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@larkup/workspace-app",
3
- "version": "0.2.3",
3
+ "version": "0.2.7",
4
4
  "type": "module",
5
5
  "description": "Canonical Larkup chat, data and settings pages with host adapters",
6
6
  "files": [
@@ -556,9 +556,9 @@
556
556
  "@larkup/vector-stores": "^0.1.27",
557
557
  "@larkup/sandbox": "^0.1.8",
558
558
  "next": "16.3.8",
559
+ "@larkup/integrations": "0.2.1",
559
560
  "@larkup/tool-doc-editor": "0.3.2",
560
- "@larkup/workspace-ui": "0.2.0",
561
- "@larkup/integrations": "0.2.1"
561
+ "@larkup/workspace-ui": "0.2.0"
562
562
  },
563
563
  "peerDependencies": {
564
564
  "react": ">=19",
@@ -4,6 +4,7 @@ import { workspaceUrl, workspaceFetch as fetch } from '../../host';
4
4
  import { useState, useRef, useEffect, useMemo, useCallback } from 'react';
5
5
  import { useChat } from '@ai-sdk/react';
6
6
  import { DefaultChatTransport } from 'ai';
7
+ import { compactToolResult } from '../../lib/chat/message-compaction';
7
8
  import type { RagConfig } from '@larkup/core/types';
8
9
  import {
9
10
  ArrowUp,
@@ -88,22 +89,6 @@ const fetcher = async <T,>(url: string): Promise<T> => {
88
89
  return payload as T;
89
90
  };
90
91
 
91
- function compactToolResult(value: any): any {
92
- if (!value || typeof value !== 'object') return value;
93
- if (value.type === 'json' && 'value' in value) {
94
- return { ...value, value: compactToolResult(value.value) };
95
- }
96
-
97
- const compact = { ...value };
98
- if (Array.isArray(compact.rows)) compact.rows = compact.rows.slice(0, 50);
99
- if (Array.isArray(compact.data)) compact.data = compact.data.slice(0, 100);
100
- if (typeof compact.stdout === 'string') compact.stdout = compact.stdout.slice(0, 4_000);
101
- if (typeof compact.stderr === 'string') compact.stderr = compact.stderr.slice(0, 1_000);
102
- if (typeof compact.fileBase64 === 'string') delete compact.fileBase64;
103
- if (Array.isArray(compact.pages)) delete compact.pages;
104
- return compact;
105
- }
106
-
107
92
  /**
108
93
  * Compact messages for the LLM context to prevent massive payloads (like chart data,
109
94
  * table rows, and document base64s) from bloating the request and exceeding token limits.
@@ -519,6 +519,7 @@ export function MessageItem({
519
519
  return {
520
520
  toolCallId,
521
521
  percent: smoothPendingToolProgress(pendingToolStartedAt.current[toolCallId], progressClock),
522
+ indeterminate: true,
522
523
  label: behavior.pendingLabel || 'Working…',
523
524
  message: behavior.pendingLabel || 'Working…',
524
525
  };
@@ -902,6 +903,7 @@ function renderToolPart(
902
903
  toolProgress?: {
903
904
  toolCallId: string;
904
905
  percent: number;
906
+ indeterminate?: boolean;
905
907
  label: string;
906
908
  message?: string;
907
909
  } | null,
@@ -929,7 +931,9 @@ function renderToolPart(
929
931
  // Past the point where the remaining work has a measurable size, a
930
932
  // percentage is a claim we cannot back. Drop it and shuttle the bar
931
933
  // instead, so the wait still reads as active rather than stalled.
932
- const indeterminate = liveProgress ? isIndeterminateProgress(liveProgress.percent) : false;
934
+ const indeterminate = liveProgress
935
+ ? liveProgress.indeterminate === true || isIndeterminateProgress(liveProgress.percent)
936
+ : false;
933
937
 
934
938
  return (
935
939
  <div key={index} className="flex min-w-0 flex-col gap-1.5 py-3">
@@ -1045,6 +1049,7 @@ function renderToolPart(
1045
1049
  columns: output.columns ?? [],
1046
1050
  rows: output.rows ?? [],
1047
1051
  totalRows: output.totalRows ?? 0,
1052
+ truncated: output.truncated,
1048
1053
  aggregationResults: output.aggregationResults,
1049
1054
  };
1050
1055
  if (tableConfig.rows.length === 0 && !tableConfig.aggregationResults) return null;
@@ -31,6 +31,7 @@ export interface DataTableConfig {
31
31
  columns: string[];
32
32
  rows: Record<string, any>[];
33
33
  totalRows: number;
34
+ truncated?: boolean;
34
35
  aggregationResults?: Record<string, number>;
35
36
  }
36
37
 
@@ -104,6 +105,7 @@ export function ChatDataTable({ config, compact = false }: ChatDataTableProps) {
104
105
  );
105
106
  const { columns, rows } = normalized;
106
107
  const { totalRows, aggregationResults } = config;
108
+ const isPartialResult = config.truncated === true || totalRows > rows.length;
107
109
  const [sortCol, setSortCol] = useState<string | null>(null);
108
110
  const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc');
109
111
  const [page, setPage] = useState(0);
@@ -173,7 +175,7 @@ export function ChatDataTable({ config, compact = false }: ChatDataTableProps) {
173
175
  }
174
176
 
175
177
  if (compact && !isExpanded) {
176
- const count = totalRows || rows.length;
178
+ const count = rows.length;
177
179
  return (
178
180
  <button
179
181
  type="button"
@@ -183,7 +185,9 @@ export function ChatDataTable({ config, compact = false }: ChatDataTableProps) {
183
185
  <Table2 className="size-3.5" />
184
186
  <span>Data checked</span>
185
187
  <span className="rounded-full bg-secondary px-1.5 py-0.5 text-[10px] tabular-nums text-foreground">
186
- {count.toLocaleString()} {count === 1 ? 'row' : 'rows'}
188
+ {count.toLocaleString()}
189
+ {isPartialResult ? ` of ${totalRows.toLocaleString()}` : ''}{' '}
190
+ {totalRows === 1 ? 'row' : 'rows'}
187
191
  </span>
188
192
  </button>
189
193
  );
@@ -318,8 +322,11 @@ export function ChatDataTable({ config, compact = false }: ChatDataTableProps) {
318
322
  <div className="flex items-center justify-between border-t border-border/30 px-3 py-2">
319
323
  <div className="flex items-center gap-2">
320
324
  <span className="text-[11px] text-muted-foreground">
321
- {filteredRows.length !== rows.length ? `${filteredRows.length} matched of ` : ''}
322
- {totalRows.toLocaleString()} rows
325
+ {filteredRows.length !== rows.length
326
+ ? `${filteredRows.length.toLocaleString()} matched in ${rows.length.toLocaleString()} returned rows`
327
+ : isPartialResult
328
+ ? `${rows.length.toLocaleString()} shown of ${totalRows.toLocaleString()} matching rows`
329
+ : `${rows.length.toLocaleString()} rows`}
323
330
  </span>
324
331
  <div className="h-3 w-px bg-border/40" />
325
332
  <div className="flex items-center gap-1.5">
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useEffect, useMemo, useRef, useState } from 'react';
3
+ import { useEffect, useRef, useState } from 'react';
4
4
  import { Image as ImageIcon, Film, AudioLines, Maximize2, X } from 'lucide-react';
5
5
  import { workspaceUrl } from '../../../host';
6
6
 
@@ -32,6 +32,7 @@ export function ChatMediaPreview({
32
32
  }) {
33
33
  const [expanded, setExpanded] = useState(false);
34
34
  const [imageUnavailable, setImageUnavailable] = useState(false);
35
+ const [videoUnavailable, setVideoUnavailable] = useState(false);
35
36
  const videoRef = useRef<HTMLVideoElement>(null);
36
37
  // Media is rendered only from a server-validated presentMedia result. Do
37
38
  // not invent a URL from an identifier emitted in model text: it could point
@@ -42,10 +43,6 @@ export function ChatMediaPreview({
42
43
  assetUrl && startSecs !== undefined
43
44
  ? `${assetUrl.split('#')[0]}#t=${startSecs}${endSecs !== undefined ? `,${endSecs}` : ''}`
44
45
  : assetUrl;
45
- const providerEmbedUrl = useMemo(
46
- () => (mediaType === 'video' ? getProviderEmbedUrl(sourceUrl, startSecs, endSecs) : undefined),
47
- [mediaType, sourceUrl, startSecs, endSecs],
48
- );
49
46
  const hasClipEnd =
50
47
  mediaType === 'video' &&
51
48
  Number.isFinite(startSecs) &&
@@ -133,35 +130,53 @@ export function ChatMediaPreview({
133
130
  }
134
131
 
135
132
  if (mediaType === 'video') {
133
+ if (videoUnavailable) {
134
+ return (
135
+ <div className="my-2 w-full max-w-2xl overflow-hidden rounded-lg border border-border bg-card">
136
+ <div className="flex aspect-video items-center justify-center bg-muted/30 px-6 text-center">
137
+ <div>
138
+ <Film className="mx-auto mb-2 size-6 text-muted-foreground" />
139
+ <p className="text-sm font-medium text-foreground">Preview unavailable</p>
140
+ <p className="mt-1 text-xs text-muted-foreground">
141
+ Open the original source to watch this cited moment.
142
+ </p>
143
+ </div>
144
+ </div>
145
+ <MediaCitationFooter
146
+ icon={<Film className="size-3 text-muted-foreground" />}
147
+ fileName={fileName}
148
+ sourceUrl={timestampedMediaUrl(sourceUrl, startSecs, endSecs)}
149
+ timestamp={
150
+ startSecs !== undefined
151
+ ? `${formatTimestamp(startSecs)}${
152
+ endSecs !== undefined ? ` – ${formatTimestamp(endSecs)}` : ''
153
+ }`
154
+ : undefined
155
+ }
156
+ />
157
+ </div>
158
+ );
159
+ }
136
160
  return (
137
161
  <div className="my-2 w-full max-w-2xl overflow-hidden rounded-lg border border-border bg-card">
138
- {providerEmbedUrl ? (
139
- <iframe
140
- src={providerEmbedUrl}
141
- title={fileName ? `Watch ${fileName}` : 'Video preview'}
142
- className="aspect-video w-full bg-black"
143
- allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
144
- allowFullScreen
145
- />
146
- ) : (
147
- <video
148
- ref={videoRef}
149
- src={playbackUrl}
150
- controls
151
- playsInline
152
- preload="metadata"
153
- className="max-h-90 w-full bg-black object-contain"
154
- onTimeUpdate={(event) => {
155
- // Media-fragment end times are not enforced consistently by
156
- // browsers. Stop local playback at the cited evidence boundary
157
- // so a supporting clip never continues into unrelated footage.
158
- if (hasClipEnd && event.currentTarget.currentTime >= (endSecs as number)) {
159
- event.currentTarget.pause();
160
- event.currentTarget.currentTime = endSecs as number;
161
- }
162
- }}
163
- />
164
- )}
162
+ <video
163
+ ref={videoRef}
164
+ src={playbackUrl}
165
+ controls
166
+ playsInline
167
+ preload="metadata"
168
+ className="max-h-90 w-full bg-black object-contain"
169
+ onError={() => setVideoUnavailable(true)}
170
+ onTimeUpdate={(event) => {
171
+ // Media-fragment end times are not enforced consistently by
172
+ // browsers. Stop local playback at the cited evidence boundary
173
+ // so a supporting clip never continues into unrelated footage.
174
+ if (hasClipEnd && event.currentTarget.currentTime >= (endSecs as number)) {
175
+ event.currentTarget.pause();
176
+ event.currentTarget.currentTime = endSecs as number;
177
+ }
178
+ }}
179
+ />
165
180
  <MediaCitationFooter
166
181
  icon={<Film className="size-3 text-muted-foreground" />}
167
182
  fileName={fileName}
@@ -0,0 +1,25 @@
1
+ export function compactToolResult(value: any): any {
2
+ if (!value || typeof value !== 'object') return value;
3
+ if (value.type === 'json' && 'value' in value) {
4
+ return { ...value, value: compactToolResult(value.value) };
5
+ }
6
+
7
+ const compact = { ...value };
8
+ if (Array.isArray(compact.rows)) compact.rows = compact.rows.slice(0, 50);
9
+ if (Array.isArray(compact.data)) compact.data = compact.data.slice(0, 100);
10
+ if (typeof compact.stdout === 'string') compact.stdout = compact.stdout.slice(0, 4_000);
11
+ if (typeof compact.stderr === 'string') compact.stderr = compact.stderr.slice(0, 1_000);
12
+ if (typeof compact.fileBase64 === 'string') delete compact.fileBase64;
13
+ if (Array.isArray(compact.pages)) {
14
+ compact.pages = compact.pages.slice(0, 3).flatMap((page: any) => {
15
+ if (!page || typeof page !== 'object' || !Number.isInteger(page.pageNumber)) return [];
16
+ return [
17
+ {
18
+ pageNumber: page.pageNumber,
19
+ ...(typeof page.previewUrl === 'string' ? { previewUrl: page.previewUrl } : {}),
20
+ },
21
+ ];
22
+ });
23
+ }
24
+ return compact;
25
+ }