remote-codex 0.1.10 → 0.11.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 (40) hide show
  1. package/apps/supervisor-api/dist/index.js +11159 -27875
  2. package/apps/supervisor-web/dist/assets/{highlighted-body-OFNGDK62-CyMcatlD.js → highlighted-body-OFNGDK62-ChrwAL9u.js} +1 -1
  3. package/apps/supervisor-web/dist/assets/index-DHf2HOXx.js +381 -0
  4. package/apps/supervisor-web/dist/assets/index-DpWxXCgt.css +32 -0
  5. package/apps/supervisor-web/dist/assets/{xterm-DbYWMNQ0.js → xterm-D4sevve4.js} +1 -1
  6. package/apps/supervisor-web/dist/index.html +2 -2
  7. package/package.json +2 -3
  8. package/packages/agent-runtime/src/index.ts +4 -0
  9. package/packages/agent-runtime/src/management-errors.ts +11 -0
  10. package/packages/agent-runtime/src/model-pricing.ts +312 -0
  11. package/packages/agent-runtime/src/registry.ts +19 -4
  12. package/packages/agent-runtime/src/runtime-errors.ts +97 -0
  13. package/packages/agent-runtime/src/types.ts +36 -3
  14. package/packages/agent-runtime/src/unavailable-runtime.ts +169 -0
  15. package/packages/claude/src/runtimeAdapter.test.ts +95 -6
  16. package/packages/claude/src/runtimeAdapter.ts +421 -65
  17. package/packages/codex/src/historyItems.test.ts +110 -0
  18. package/packages/codex/src/historyItems.ts +96 -15
  19. package/packages/codex/src/hookHistory.test.ts +59 -0
  20. package/packages/codex/src/index.ts +7 -0
  21. package/packages/codex/src/local-session-store.ts +390 -0
  22. package/packages/codex/src/management/codex-management-service.ts +454 -0
  23. package/packages/codex/src/management/codexHostConfig.test.ts +88 -0
  24. package/packages/codex/src/management/codexHostConfig.ts +188 -0
  25. package/packages/codex/src/management/errors.ts +20 -0
  26. package/packages/codex/src/modelPricing.test.ts +184 -0
  27. package/packages/codex/src/modelPricing.ts +9 -0
  28. package/packages/codex/src/runtime-errors.test.ts +72 -0
  29. package/packages/codex/src/runtime-errors.ts +37 -0
  30. package/packages/codex/src/runtimeAdapter.ts +15 -0
  31. package/packages/codex/src/thread-title.ts +1 -0
  32. package/packages/opencode/src/historyItems.test.ts +504 -0
  33. package/packages/opencode/src/historyItems.ts +896 -0
  34. package/packages/opencode/src/index.ts +2 -0
  35. package/packages/opencode/src/runtimeAdapter.test.ts +1355 -0
  36. package/packages/opencode/src/runtimeAdapter.ts +1469 -0
  37. package/packages/shared/src/agent-providers.ts +56 -0
  38. package/packages/shared/src/index.ts +170 -35
  39. package/apps/supervisor-web/dist/assets/index-BlAhoIuq.js +0 -379
  40. package/apps/supervisor-web/dist/assets/index-DI0NRNgr.css +0 -32
@@ -0,0 +1,896 @@
1
+ import path from 'node:path';
2
+
3
+ import type {
4
+ AgentHistoryItem,
5
+ AgentTurn,
6
+ } from '../../agent-runtime/src/index';
7
+
8
+ export interface OpenCodeHistoryItemMappingOptions {
9
+ workspacePath?: string | null;
10
+ }
11
+
12
+ export interface OpenCodePlanUpdate {
13
+ explanation: string | null;
14
+ plan: Array<{ step: string; status: string }>;
15
+ }
16
+
17
+ function isRecord(value: unknown): value is Record<string, unknown> {
18
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
19
+ }
20
+
21
+ function stringValue(value: unknown): string | null {
22
+ return typeof value === 'string' && value.trim() ? value : null;
23
+ }
24
+
25
+ function numberValue(value: unknown): number | null {
26
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
27
+ }
28
+
29
+ function normalizedToolName(toolName: string) {
30
+ return toolName.replace(/[\s_-]+/g, '').toLowerCase();
31
+ }
32
+
33
+ function compactJson(value: unknown): string {
34
+ try {
35
+ return JSON.stringify(value, null, 2);
36
+ } catch {
37
+ return String(value);
38
+ }
39
+ }
40
+
41
+ function isoFromMs(value: unknown) {
42
+ const time = numberValue(value);
43
+ if (time === null) {
44
+ return null;
45
+ }
46
+ return new Date(time).toISOString();
47
+ }
48
+
49
+ function toolContentText(content: unknown) {
50
+ if (!Array.isArray(content)) {
51
+ return '';
52
+ }
53
+ return content
54
+ .map((entry) => {
55
+ if (!isRecord(entry)) {
56
+ return '';
57
+ }
58
+ if (typeof entry.text === 'string') {
59
+ return entry.text;
60
+ }
61
+ if (typeof entry.url === 'string') {
62
+ return entry.url;
63
+ }
64
+ return '';
65
+ })
66
+ .filter(Boolean)
67
+ .join('\n');
68
+ }
69
+
70
+ function toolStateDetail(state: Record<string, unknown>) {
71
+ const parts = ['State:', compactJson(state)];
72
+ const output = stringValue(state.output) ?? toolContentText(state.content);
73
+ if (output) {
74
+ parts.push('', 'Output:', output);
75
+ }
76
+ const error = isRecord(state.error)
77
+ ? stringValue(state.error.message) ?? compactJson(state.error)
78
+ : stringValue(state.error);
79
+ if (error) {
80
+ parts.push('', 'Error:', error);
81
+ }
82
+ return parts.join('\n');
83
+ }
84
+
85
+ function toolStateStatus(state: Record<string, unknown>) {
86
+ const status = stringValue(state.status);
87
+ if (status === 'pending' || status === 'running') {
88
+ return 'running';
89
+ }
90
+ if (status === 'error') {
91
+ return 'failed';
92
+ }
93
+ return 'completed';
94
+ }
95
+
96
+ function toolSummary(input: unknown) {
97
+ if (!isRecord(input)) {
98
+ return stringValue(input) ?? compactJson(input);
99
+ }
100
+ return (
101
+ stringValue(input.description) ??
102
+ stringValue(input.command) ??
103
+ stringValue(input.cmd) ??
104
+ stringValue(input.filePath) ??
105
+ stringValue(input.file_path) ??
106
+ stringValue(input.path) ??
107
+ stringValue(input.file) ??
108
+ stringValue(input.relativePath) ??
109
+ stringValue(input.relative_path) ??
110
+ stringValue(input.pattern) ??
111
+ stringValue(input.query) ??
112
+ stringValue(input.url) ??
113
+ compactJson(input)
114
+ );
115
+ }
116
+
117
+ function commandSummary(input: unknown, state: Record<string, unknown>) {
118
+ const command = isRecord(input)
119
+ ? stringValue(input.command) ?? stringValue(input.cmd)
120
+ : null;
121
+ if (command) {
122
+ return command;
123
+ }
124
+ const raw = stringValue(state.raw);
125
+ if (raw) {
126
+ return raw;
127
+ }
128
+ return toolSummary(input);
129
+ }
130
+
131
+ function firstStringValue(record: Record<string, unknown>, keys: string[]) {
132
+ for (const key of keys) {
133
+ const value = stringValue(record[key]);
134
+ if (value) {
135
+ return value;
136
+ }
137
+ }
138
+ return null;
139
+ }
140
+
141
+ function filePathFromInput(input: unknown) {
142
+ if (!isRecord(input)) {
143
+ return null;
144
+ }
145
+ return firstStringValue(input, [
146
+ 'filePath',
147
+ 'file_path',
148
+ 'path',
149
+ 'file',
150
+ 'relativePath',
151
+ 'relative_path',
152
+ 'target',
153
+ ]);
154
+ }
155
+
156
+ function displayPath(pathValue: string | null, options: OpenCodeHistoryItemMappingOptions) {
157
+ if (!pathValue) {
158
+ return null;
159
+ }
160
+ if (!path.isAbsolute(pathValue) || !options.workspacePath) {
161
+ return pathValue;
162
+ }
163
+
164
+ const root = path.resolve(options.workspacePath);
165
+ const absolutePath = path.resolve(pathValue);
166
+ const relativePath = path.relative(root, absolutePath);
167
+ if (
168
+ !relativePath ||
169
+ relativePath.startsWith('..') ||
170
+ path.isAbsolute(relativePath)
171
+ ) {
172
+ return pathValue;
173
+ }
174
+ return relativePath;
175
+ }
176
+
177
+ function toolIsLowInformationPatch(
178
+ normalized: string,
179
+ state: Record<string, unknown>,
180
+ input: unknown,
181
+ patchText: string | null,
182
+ path: string | null,
183
+ metadataStats: ReturnType<typeof fileChangeStatsFromMetadata>,
184
+ ) {
185
+ if (normalized !== 'applypatch' && normalized !== 'patch') {
186
+ return false;
187
+ }
188
+ if (toolStateStatus(state) !== 'running') {
189
+ return false;
190
+ }
191
+ if (path || patchText || metadataStats || stringValue(state.output)) {
192
+ return false;
193
+ }
194
+ return !isRecord(input) || Object.keys(input).length === 0;
195
+ }
196
+
197
+ function countUnifiedDiffStats(diffText: string | null) {
198
+ if (!diffText) {
199
+ return null;
200
+ }
201
+ let addedLines = 0;
202
+ let removedLines = 0;
203
+ for (const line of diffText.split(/\r?\n/)) {
204
+ if (line.startsWith('+++') || line.startsWith('---')) {
205
+ continue;
206
+ }
207
+ if (line.startsWith('+')) {
208
+ addedLines += 1;
209
+ } else if (line.startsWith('-')) {
210
+ removedLines += 1;
211
+ }
212
+ }
213
+ return { addedLines, removedLines };
214
+ }
215
+
216
+ function extractPathFromPatchText(patchText: string | null) {
217
+ if (!patchText) {
218
+ return null;
219
+ }
220
+ for (const line of patchText.split(/\r?\n/)) {
221
+ const match = line.match(/^\*\*\* (?:Update|Add|Delete) File: (.+)$/);
222
+ if (match?.[1]) {
223
+ return match[1].trim();
224
+ }
225
+ }
226
+ return null;
227
+ }
228
+
229
+ function fileChangeStatsFromMetadata(metadata: unknown) {
230
+ if (!isRecord(metadata) || !Array.isArray(metadata.files)) {
231
+ return null;
232
+ }
233
+ const files = metadata.files.filter(isRecord);
234
+ if (files.length === 0) {
235
+ return null;
236
+ }
237
+ const paths = files
238
+ .map((file) => (
239
+ stringValue(file.filePath) ??
240
+ stringValue(file.path) ??
241
+ stringValue(file.relativePath)
242
+ ))
243
+ .filter((path): path is string => Boolean(path));
244
+ const addedLines = files.reduce((total, file) => (
245
+ total +
246
+ (numberValue(file.additions) ??
247
+ numberValue(file.addedLines) ??
248
+ numberValue(file.added) ??
249
+ 0)
250
+ ), 0);
251
+ const removedLines = files.reduce((total, file) => (
252
+ total +
253
+ (numberValue(file.deletions) ??
254
+ numberValue(file.removedLines) ??
255
+ numberValue(file.removed) ??
256
+ 0)
257
+ ), 0);
258
+ return {
259
+ changedFiles: files.length,
260
+ path: paths[0] ?? null,
261
+ previewText: paths.length > 1
262
+ ? `${paths.length} changed files`
263
+ : paths[0] ?? `${files.length} changed file${files.length === 1 ? '' : 's'}`,
264
+ addedLines,
265
+ removedLines,
266
+ };
267
+ }
268
+
269
+ function todoSummary(input: unknown, output: string | null) {
270
+ const todos = isRecord(input) && Array.isArray(input.todos)
271
+ ? input.todos
272
+ : null;
273
+ if (!todos) {
274
+ return output ?? toolSummary(input);
275
+ }
276
+ const counts = new Map<string, number>();
277
+ todos.forEach((todo) => {
278
+ const status = isRecord(todo) ? stringValue(todo.status) ?? 'unknown' : 'unknown';
279
+ counts.set(status, (counts.get(status) ?? 0) + 1);
280
+ });
281
+ return Array.from(counts.entries())
282
+ .map(([status, count]) => `${count} ${status}`)
283
+ .join(', ');
284
+ }
285
+
286
+ function todoRecordsFromValue(value: unknown): unknown[] | null {
287
+ if (Array.isArray(value)) {
288
+ return value;
289
+ }
290
+ if (isRecord(value) && Array.isArray(value.todos)) {
291
+ return value.todos;
292
+ }
293
+ if (typeof value === 'string' && value.trim()) {
294
+ try {
295
+ const parsed = JSON.parse(value) as unknown;
296
+ return todoRecordsFromValue(parsed);
297
+ } catch {
298
+ return null;
299
+ }
300
+ }
301
+ return null;
302
+ }
303
+
304
+ function planUpdateFromTodoTool(tool: Record<string, unknown>): OpenCodePlanUpdate | null {
305
+ const name = stringValue(tool.name) ?? stringValue(tool.tool) ?? '';
306
+ const normalized = normalizedToolName(name);
307
+ if (normalized !== 'todowrite' && normalized !== 'todo' && normalized !== 'todos') {
308
+ return null;
309
+ }
310
+
311
+ const state = isRecord(tool.state) ? tool.state : {};
312
+ const todos =
313
+ todoRecordsFromValue(state.input) ??
314
+ (isRecord(state.metadata) ? todoRecordsFromValue(state.metadata) : null) ??
315
+ todoRecordsFromValue(state.output);
316
+ if (!todos || todos.length === 0) {
317
+ return null;
318
+ }
319
+
320
+ const plan = todos
321
+ .map((todo) => {
322
+ if (!isRecord(todo)) {
323
+ return null;
324
+ }
325
+ const step = stringValue(todo.content) ?? stringValue(todo.step) ?? stringValue(todo.title);
326
+ if (!step) {
327
+ return null;
328
+ }
329
+ return {
330
+ step,
331
+ status: stringValue(todo.status) ?? 'pending',
332
+ };
333
+ })
334
+ .filter((step): step is { step: string; status: string } => Boolean(step));
335
+
336
+ return plan.length > 0
337
+ ? {
338
+ explanation: null,
339
+ plan,
340
+ }
341
+ : null;
342
+ }
343
+
344
+ function normalizeLegacyPart(part: unknown): unknown {
345
+ if (!isRecord(part)) {
346
+ return null;
347
+ }
348
+ const type = stringValue(part.type);
349
+ if (type !== 'tool') {
350
+ return part;
351
+ }
352
+ return {
353
+ ...part,
354
+ name: stringValue(part.name) ?? stringValue(part.tool) ?? 'Tool',
355
+ };
356
+ }
357
+
358
+ function normalizeLegacyParts(parts: unknown[], fallbackText: string | null) {
359
+ const normalized = parts.map(normalizeLegacyPart).filter(Boolean);
360
+ if (normalized.length > 0 || !fallbackText) {
361
+ return normalized;
362
+ }
363
+ return [{ type: 'text', text: fallbackText }];
364
+ }
365
+
366
+ function normalizeLegacyMessage(message: unknown): unknown {
367
+ if (!isRecord(message) || !isRecord(message.info) || !Array.isArray(message.parts)) {
368
+ return message;
369
+ }
370
+ const info = message.info;
371
+ const role = stringValue(info.role) ?? stringValue(info.type);
372
+ const id = stringValue(info.id) ?? crypto.randomUUID();
373
+ const time = isRecord(info.time) ? info.time : undefined;
374
+
375
+ if (role === 'user') {
376
+ const text = message.parts
377
+ .map((part) => (isRecord(part) && stringValue(part.text) ? part.text : null))
378
+ .filter((text): text is string => Boolean(text))
379
+ .join('\n');
380
+ return {
381
+ id,
382
+ type: 'user',
383
+ time,
384
+ text,
385
+ };
386
+ }
387
+
388
+ if (role !== 'assistant') {
389
+ return message;
390
+ }
391
+
392
+ const model = isRecord(info.model)
393
+ ? {
394
+ id: stringValue(info.model.id) ?? stringValue(info.model.modelID) ?? stringValue(info.modelID) ?? 'unknown',
395
+ providerID: stringValue(info.model.providerID) ?? stringValue(info.providerID) ?? 'unknown',
396
+ variant: stringValue(info.model.variant) ?? stringValue(info.variant) ?? 'default',
397
+ }
398
+ : {
399
+ id: stringValue(info.modelID) ?? 'unknown',
400
+ providerID: stringValue(info.providerID) ?? 'unknown',
401
+ variant: stringValue(info.variant) ?? 'default',
402
+ };
403
+
404
+ return {
405
+ id,
406
+ type: 'assistant',
407
+ time,
408
+ agent: stringValue(info.agent) ?? 'build',
409
+ model,
410
+ content: normalizeLegacyParts(message.parts, stringValue(info.text)),
411
+ error: info.error,
412
+ cost: numberValue(info.cost) ?? undefined,
413
+ tokens: isRecord(info.tokens) ? info.tokens : undefined,
414
+ finish: stringValue(info.finish) ?? undefined,
415
+ };
416
+ }
417
+
418
+ function mapAssistantTool(
419
+ messageId: string,
420
+ tool: Record<string, unknown>,
421
+ options: OpenCodeHistoryItemMappingOptions,
422
+ ): AgentHistoryItem | null {
423
+ const id = stringValue(tool.id) ?? `${messageId}:tool`;
424
+ const name = stringValue(tool.name) ?? stringValue(tool.tool) ?? 'Tool';
425
+ const state = isRecord(tool.state) ? tool.state : {};
426
+ const input = state.input;
427
+ const summary = toolSummary(input);
428
+ const detailText = [`Tool: ${name}`, '', 'Input:', compactJson(input), '', toolStateDetail(state)]
429
+ .filter(Boolean)
430
+ .join('\n');
431
+ const normalized = normalizedToolName(name);
432
+
433
+ if (normalized === 'bash' || normalized === 'shell') {
434
+ const command = commandSummary(input, state);
435
+ return {
436
+ id,
437
+ kind: 'commandExecution',
438
+ text: command || name,
439
+ previewText: command || name,
440
+ detailText,
441
+ status: toolStateStatus(state),
442
+ };
443
+ }
444
+
445
+ if ([
446
+ 'edit',
447
+ 'multiedit',
448
+ 'write',
449
+ 'notebookedit',
450
+ 'applypatch',
451
+ 'patch',
452
+ ].includes(normalized)) {
453
+ const metadataStats = fileChangeStatsFromMetadata(state.metadata);
454
+ const patchText = isRecord(input)
455
+ ? stringValue(input.patchText) ??
456
+ stringValue(input.patch) ??
457
+ stringValue(input.diff)
458
+ : null;
459
+ const path = metadataStats?.path ?? filePathFromInput(input) ?? extractPathFromPatchText(patchText);
460
+ if (toolIsLowInformationPatch(normalized, state, input, patchText, path, metadataStats)) {
461
+ return null;
462
+ }
463
+ const output = stringValue(state.output);
464
+ const diffStats = countUnifiedDiffStats(patchText);
465
+ const displayFilePath = displayPath(path, options);
466
+ return {
467
+ id,
468
+ kind: 'fileChange',
469
+ text: metadataStats
470
+ ? metadataStats.changedFiles > 1
471
+ ? `${metadataStats.changedFiles} changed files`
472
+ : displayFilePath ?? metadataStats.previewText
473
+ : displayFilePath ?? output ?? summary ?? name,
474
+ previewText: metadataStats
475
+ ? metadataStats.changedFiles > 1
476
+ ? `${metadataStats.changedFiles} changed files`
477
+ : displayFilePath ?? metadataStats.previewText
478
+ : displayFilePath
479
+ ? `${name}: ${displayFilePath}`
480
+ : output ?? summary ?? name,
481
+ detailText,
482
+ changedFiles: metadataStats?.changedFiles ?? (path ? 1 : null),
483
+ addedLines: metadataStats?.addedLines ?? diffStats?.addedLines ?? null,
484
+ removedLines: metadataStats?.removedLines ?? diffStats?.removedLines ?? null,
485
+ status: toolStateStatus(state),
486
+ };
487
+ }
488
+
489
+ if (['read', 'grep', 'glob', 'list', 'ls', 'bashoutput'].includes(normalized)) {
490
+ const path = filePathFromInput(input);
491
+ const text = displayPath(path, options) ?? summary ?? name;
492
+ return {
493
+ id,
494
+ kind: 'fileRead',
495
+ text,
496
+ previewText: text,
497
+ detailText,
498
+ status: toolStateStatus(state),
499
+ };
500
+ }
501
+
502
+ if (normalized.includes('web')) {
503
+ return {
504
+ id,
505
+ kind: 'webSearch',
506
+ text: summary || name,
507
+ previewText: summary || name,
508
+ detailText,
509
+ status: toolStateStatus(state),
510
+ };
511
+ }
512
+
513
+ if (normalized === 'todowrite' || normalized === 'todo' || normalized === 'todos') {
514
+ return null;
515
+ }
516
+
517
+ if (normalized === 'task' || normalized === 'agent') {
518
+ return {
519
+ id,
520
+ kind: 'agentToolCall',
521
+ text: summary ? `${name}: ${summary}` : name,
522
+ previewText: name,
523
+ detailText,
524
+ status: toolStateStatus(state),
525
+ };
526
+ }
527
+
528
+ if (normalized === 'skill') {
529
+ const skill = isRecord(input) ? stringValue(input.skill) ?? stringValue(input.name) : null;
530
+ return {
531
+ id,
532
+ kind: 'skillToolCall',
533
+ text: skill ? `Skill: ${skill}` : summary ? `${name}: ${summary}` : name,
534
+ previewText: skill ?? name,
535
+ detailText,
536
+ status: toolStateStatus(state),
537
+ };
538
+ }
539
+
540
+ return {
541
+ id,
542
+ kind: 'toolCall',
543
+ text: summary ? `${name}: ${summary}` : name,
544
+ previewText: name,
545
+ detailText,
546
+ status: toolStateStatus(state),
547
+ };
548
+ }
549
+
550
+ function openCodeMessageToPlanUpdate(message: unknown): OpenCodePlanUpdate | null {
551
+ const normalizedMessage = normalizeLegacyMessage(message);
552
+ if (!isRecord(normalizedMessage)) {
553
+ return null;
554
+ }
555
+ const content = Array.isArray(normalizedMessage.content)
556
+ ? normalizedMessage.content
557
+ : [];
558
+ for (const part of content) {
559
+ if (!isRecord(part) || stringValue(part.type) !== 'tool') {
560
+ continue;
561
+ }
562
+ const planUpdate = planUpdateFromTodoTool(part);
563
+ if (planUpdate) {
564
+ return planUpdate;
565
+ }
566
+ }
567
+ return null;
568
+ }
569
+
570
+ export function openCodeMessagesToPlanUpdate(messages: unknown[]): OpenCodePlanUpdate | null {
571
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
572
+ const planUpdate = openCodeMessageToPlanUpdate(messages[index]);
573
+ if (planUpdate) {
574
+ return planUpdate;
575
+ }
576
+ }
577
+ return null;
578
+ }
579
+
580
+ function mapAssistantPart(
581
+ messageId: string,
582
+ part: Record<string, unknown>,
583
+ index: number,
584
+ options: OpenCodeHistoryItemMappingOptions,
585
+ ): AgentHistoryItem | null {
586
+ const partId = stringValue(part.id) ?? `${messageId}:${stringValue(part.type) ?? 'part'}:${index}`;
587
+ const partType = stringValue(part.type);
588
+
589
+ if (partType === 'text') {
590
+ const text = stringValue(part.text);
591
+ return text
592
+ ? {
593
+ id: `${messageId}:text:${index}`,
594
+ kind: 'agentMessage',
595
+ text,
596
+ }
597
+ : null;
598
+ }
599
+
600
+ if (partType === 'reasoning') {
601
+ const text = stringValue(part.text);
602
+ return text
603
+ ? {
604
+ id: partId,
605
+ kind: 'reasoning',
606
+ text,
607
+ }
608
+ : null;
609
+ }
610
+
611
+ if (partType === 'tool') {
612
+ return mapAssistantTool(messageId, part, options);
613
+ }
614
+
615
+ if (partType === 'file') {
616
+ const sourcePath = isRecord(part.source) ? stringValue(part.source.path) : null;
617
+ const filename = displayPath(sourcePath, options) ??
618
+ stringValue(part.filename) ??
619
+ stringValue(part.url) ??
620
+ 'Attached file';
621
+ const sourceText = isRecord(part.source) && isRecord(part.source.text)
622
+ ? stringValue(part.source.text.value)
623
+ : null;
624
+ return {
625
+ id: partId,
626
+ kind: 'fileRead',
627
+ text: filename,
628
+ previewText: filename,
629
+ detailText: sourceText ?? compactJson(part),
630
+ status: 'completed',
631
+ };
632
+ }
633
+
634
+ if (partType === 'patch') {
635
+ const files = Array.isArray(part.files)
636
+ ? part.files.filter((file): file is string => typeof file === 'string')
637
+ : [];
638
+ const displayFiles = files.map((file) => displayPath(file, options) ?? file);
639
+ return {
640
+ id: partId,
641
+ kind: 'fileChange',
642
+ text: displayFiles.length > 0 ? displayFiles.join('\n') : stringValue(part.hash) ?? 'Patch',
643
+ previewText: files.length > 0 ? `${files.length} changed file${files.length === 1 ? '' : 's'}` : 'Patch',
644
+ detailText: compactJson(part),
645
+ changedFiles: files.length || null,
646
+ addedLines: null,
647
+ removedLines: null,
648
+ status: 'completed',
649
+ };
650
+ }
651
+
652
+ if (partType === 'step-finish') {
653
+ return null;
654
+ }
655
+
656
+ if (partType === 'step-start') {
657
+ return null;
658
+ }
659
+
660
+ if (partType === 'snapshot') {
661
+ return {
662
+ id: partId,
663
+ kind: 'other',
664
+ text: stringValue(part.snapshot) ?? 'Snapshot',
665
+ previewText: 'Snapshot',
666
+ status: 'completed',
667
+ };
668
+ }
669
+
670
+ if (partType === 'agent') {
671
+ const name = stringValue(part.name) ?? 'Agent';
672
+ const source = isRecord(part.source) ? stringValue(part.source.value) : null;
673
+ return {
674
+ id: partId,
675
+ kind: 'agentToolCall',
676
+ text: source ? `${name}: ${source}` : name,
677
+ previewText: name,
678
+ detailText: compactJson(part),
679
+ status: 'completed',
680
+ };
681
+ }
682
+
683
+ if (partType === 'subtask') {
684
+ const description = stringValue(part.description);
685
+ const prompt = stringValue(part.prompt);
686
+ return {
687
+ id: partId,
688
+ kind: 'agentToolCall',
689
+ text: description ?? prompt ?? 'Subtask',
690
+ previewText: stringValue(part.agent) ?? 'Subtask',
691
+ detailText: compactJson(part),
692
+ status: 'completed',
693
+ };
694
+ }
695
+
696
+ if (partType === 'retry') {
697
+ const error = isRecord(part.error)
698
+ ? stringValue(part.error.message) ?? compactJson(part.error)
699
+ : stringValue(part.error);
700
+ return {
701
+ id: partId,
702
+ kind: 'other',
703
+ text: error ? `Retry ${numberValue(part.attempt) ?? ''}: ${error}` : `Retry ${numberValue(part.attempt) ?? ''}`.trim(),
704
+ previewText: 'Retry',
705
+ detailText: compactJson(part),
706
+ status: 'failed',
707
+ };
708
+ }
709
+
710
+ if (partType === 'compaction') {
711
+ return {
712
+ id: partId,
713
+ kind: 'contextCompaction',
714
+ text: part.auto === true ? 'Context compacted automatically' : 'Context compacted',
715
+ previewText: 'Context compacted',
716
+ };
717
+ }
718
+
719
+ return {
720
+ id: partId,
721
+ kind: 'other',
722
+ text: compactJson(part),
723
+ previewText: partType ?? 'OpenCode part',
724
+ };
725
+ }
726
+
727
+ export function openCodeMessageToHistoryItems(
728
+ message: unknown,
729
+ options: OpenCodeHistoryItemMappingOptions = {},
730
+ ): AgentHistoryItem[] {
731
+ message = normalizeLegacyMessage(message);
732
+ if (!isRecord(message)) {
733
+ return [];
734
+ }
735
+ const id = stringValue(message.id) ?? crypto.randomUUID();
736
+ const type = stringValue(message.type);
737
+ if (type === 'user') {
738
+ return [{
739
+ id,
740
+ kind: 'userMessage',
741
+ text: stringValue(message.text) ?? '',
742
+ }];
743
+ }
744
+ if (type === 'synthetic') {
745
+ return [{
746
+ id,
747
+ kind: 'other',
748
+ text: stringValue(message.text) ?? 'Synthetic message',
749
+ previewText: 'Synthetic message',
750
+ }];
751
+ }
752
+ if (type === 'shell') {
753
+ const command = stringValue(message.command) ?? 'Shell command';
754
+ return [{
755
+ id,
756
+ kind: 'commandExecution',
757
+ text: command,
758
+ previewText: command,
759
+ detailText: stringValue(message.output) ?? null,
760
+ status: isRecord(message.time) && typeof message.time.completed === 'number'
761
+ ? 'completed'
762
+ : 'running',
763
+ }];
764
+ }
765
+ if (type === 'agent-switched') {
766
+ return [{
767
+ id,
768
+ kind: 'other',
769
+ text: `Agent switched to ${stringValue(message.agent) ?? 'unknown'}`,
770
+ previewText: 'Agent switched',
771
+ }];
772
+ }
773
+ if (type === 'model-switched') {
774
+ const model = isRecord(message.model)
775
+ ? [stringValue(message.model.providerID), stringValue(message.model.id)]
776
+ .filter(Boolean)
777
+ .join('/')
778
+ : 'unknown';
779
+ return [{
780
+ id,
781
+ kind: 'other',
782
+ text: `Model switched to ${model}`,
783
+ previewText: 'Model switched',
784
+ }];
785
+ }
786
+ if (type === 'compaction') {
787
+ return [{
788
+ id,
789
+ kind: 'contextCompaction',
790
+ text: stringValue(message.summary) ?? 'Context compacted',
791
+ previewText: `${stringValue(message.reason) ?? 'manual'} compaction`,
792
+ }];
793
+ }
794
+ if (type !== 'assistant') {
795
+ return [{
796
+ id,
797
+ kind: 'other',
798
+ text: compactJson(message),
799
+ previewText: type ?? 'OpenCode message',
800
+ }];
801
+ }
802
+
803
+ const items: AgentHistoryItem[] = [];
804
+ const content = Array.isArray(message.content) ? message.content : [];
805
+ for (const [index, part] of content.entries()) {
806
+ if (!isRecord(part)) {
807
+ continue;
808
+ }
809
+ const item = mapAssistantPart(id, part, index, options);
810
+ if (item) {
811
+ items.push(item);
812
+ }
813
+ }
814
+
815
+ const error = isRecord(message.error)
816
+ ? stringValue(message.error.message) ?? compactJson(message.error)
817
+ : null;
818
+ if (error) {
819
+ items.push({
820
+ id: `${id}:error`,
821
+ kind: 'other',
822
+ text: error,
823
+ previewText: 'Assistant error',
824
+ status: 'failed',
825
+ });
826
+ }
827
+ return items;
828
+ }
829
+
830
+ export function openCodeMessagesToTurns(
831
+ messages: unknown[],
832
+ options: OpenCodeHistoryItemMappingOptions = {},
833
+ ): AgentTurn[] {
834
+ const turns: AgentTurn[] = [];
835
+ let current: {
836
+ providerTurnId: string;
837
+ startedAt: string | null;
838
+ items: AgentHistoryItem[];
839
+ error: string | null;
840
+ hasAssistantResult: boolean;
841
+ } | null = null;
842
+
843
+ const flush = () => {
844
+ if (!current || current.items.length === 0) {
845
+ return;
846
+ }
847
+ turns.push({
848
+ providerTurnId: current.providerTurnId,
849
+ startedAt: current.startedAt,
850
+ status: current.error ? 'failed' : current.hasAssistantResult ? 'completed' : 'inProgress',
851
+ error: current.error ? { message: current.error } : null,
852
+ items: current.items,
853
+ });
854
+ current = null;
855
+ };
856
+
857
+ for (const message of messages) {
858
+ const normalizedMessage = normalizeLegacyMessage(message);
859
+ if (!isRecord(normalizedMessage)) {
860
+ continue;
861
+ }
862
+ const id = stringValue(normalizedMessage.id) ?? crypto.randomUUID();
863
+ const type = stringValue(normalizedMessage.type);
864
+ const createdAt = isRecord(normalizedMessage.time) ? isoFromMs(normalizedMessage.time.created) : null;
865
+ if (type === 'user') {
866
+ flush();
867
+ current = {
868
+ providerTurnId: `opencode-turn-${id}`,
869
+ startedAt: createdAt,
870
+ items: openCodeMessageToHistoryItems(normalizedMessage, options),
871
+ error: null,
872
+ hasAssistantResult: false,
873
+ };
874
+ continue;
875
+ }
876
+ if (!current) {
877
+ current = {
878
+ providerTurnId: `opencode-turn-${id}`,
879
+ startedAt: createdAt,
880
+ items: [],
881
+ error: null,
882
+ hasAssistantResult: false,
883
+ };
884
+ }
885
+ const items = openCodeMessageToHistoryItems(normalizedMessage, options);
886
+ current.items.push(...items);
887
+ if (items.some((item) => item.kind !== 'userMessage' && item.kind !== 'other')) {
888
+ current.hasAssistantResult = true;
889
+ }
890
+ if (isRecord(normalizedMessage.error)) {
891
+ current.error = stringValue(normalizedMessage.error.message) ?? compactJson(normalizedMessage.error);
892
+ }
893
+ }
894
+ flush();
895
+ return turns;
896
+ }