@workerdeck/core 0.9.0 → 0.10.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/build/index.d.mts CHANGED
@@ -1118,6 +1118,32 @@ type AppServerWebSearchItem = {
1118
1118
  type: 'webSearch';
1119
1119
  query: string;
1120
1120
  };
1121
+ /**
1122
+ * A picture the model made with codex's built-in `image_gen` tool.
1123
+ *
1124
+ * `savedPath` is an absolute path on the **host** — by default under
1125
+ * `$CODEX_HOME/generated_images/`, or inside the workspace when the model was
1126
+ * told the asset belongs to the project. It is the only reference we get: the
1127
+ * app-server never sends the bytes, and neither do we (the event log carries
1128
+ * references, never base64 — see the protocol's note on attachments).
1129
+ *
1130
+ * `result` is an undocumented free-form string. Treated as untrusted length:
1131
+ * short values are shown, anything long enough to be an encoded image is not.
1132
+ */
1133
+ type AppServerImageGenerationItem = {
1134
+ id: string;
1135
+ type: 'imageGeneration';
1136
+ status: string;
1137
+ revisedPrompt?: string | null;
1138
+ result: string;
1139
+ savedPath?: string;
1140
+ };
1141
+ /** The model *looked at* an image on disk (`path`, host-absolute). */
1142
+ type AppServerImageViewItem = {
1143
+ id: string;
1144
+ type: 'imageView';
1145
+ path: string;
1146
+ };
1121
1147
  /** The user's own message, echoed back as an item — dropped (the runner
1122
1148
  * already emitted its `user_message`). */
1123
1149
  type AppServerUserMessageItem = {
@@ -1125,7 +1151,7 @@ type AppServerUserMessageItem = {
1125
1151
  type: 'userMessage';
1126
1152
  content?: unknown;
1127
1153
  };
1128
- type AppServerItem = AppServerAgentMessageItem | AppServerReasoningItem | AppServerCommandExecutionItem | AppServerFileChangeItem | AppServerMcpToolCallItem | AppServerWebSearchItem | AppServerUserMessageItem;
1154
+ type AppServerItem = AppServerAgentMessageItem | AppServerReasoningItem | AppServerCommandExecutionItem | AppServerFileChangeItem | AppServerMcpToolCallItem | AppServerWebSearchItem | AppServerImageGenerationItem | AppServerImageViewItem | AppServerUserMessageItem;
1129
1155
  /** The `Turn` object of `turn/started` / `turn/completed`. */
1130
1156
  type AppServerTurn = {
1131
1157
  id: string; /** 'inProgress' | 'completed' | 'failed' | 'interrupted' — open. */
@@ -1330,6 +1356,32 @@ declare class CodexRunner implements Runner {
1330
1356
  fail(message: string): void;
1331
1357
  close(reason?: 'client' | 'server' | 'error'): void;
1332
1358
  subscribe(listener: SessionEventListener, afterSeq?: number): () => void;
1359
+ /**
1360
+ * The session's MCP servers, live from the binary.
1361
+ *
1362
+ * Two sources merged, because codex splits them: `mcpServerStatus/list` says
1363
+ * what is configured and what each server exposes (including every tool's
1364
+ * full JSON Schema, which the Agent SDK does not give us), and the
1365
+ * `mcpServer/startupStatus/updated` notifications say which of them are
1366
+ * actually up.
1367
+ *
1368
+ * Answers **before the session has connected**, over a throwaway child, for
1369
+ * the same reason the skill list does: a codex session spawns nothing until
1370
+ * it has work, and a panel that said "no MCP servers configured" until the
1371
+ * first turn would be stating something false about the operator's config.
1372
+ * The request blocks until the servers are enumerated (measured: complete on
1373
+ * the very first call), so there is no half-populated answer to race.
1374
+ *
1375
+ * Resolves undefined only when there is genuinely nothing to say — the
1376
+ * session is closed, or the child could not be spoken to. The route turns
1377
+ * that into a 501.
1378
+ *
1379
+ * **Listing only.** There is no per-server reconnect or toggle on this
1380
+ * transport — hence no `reconnectMcpServer`/`setMcpServerEnabled` here, and
1381
+ * `ENGINE_CAPABILITIES.codex.mcpServerActions: false` so clients render the
1382
+ * panel read-only instead of offering buttons that cannot work.
1383
+ */
1384
+ mcpServers(): Promise<McpServerStatusInfo[] | undefined>;
1333
1385
  }
1334
1386
  //#endregion
1335
1387
  //#region src/engines/codex/process.d.ts
package/build/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  import { createRequire } from "node:module";
2
- import { randomUUID } from "node:crypto";
2
+ import { createHash, randomUUID } from "node:crypto";
3
3
  import { getSessionMessages, listSessions, query } from "@anthropic-ai/claude-agent-sdk";
4
4
  import { ENGINE_CAPABILITIES, PROTOCOL_VERSION } from "@workerdeck/protocol";
5
5
  import { ToolLoopAgent, generateText, isStepCount, tool } from "ai";
6
6
  import { execFile, spawn } from "node:child_process";
7
- import { existsSync, mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
7
+ import { existsSync, mkdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
8
8
  import { createVfs, runScript } from "@workerdeck/sandbox";
9
9
  import { z } from "zod";
10
10
  import { lookup } from "node:dns/promises";
@@ -2987,6 +2987,118 @@ const APPROVAL_POLICY_BY_MODE = {
2987
2987
  * default, so unattended codex sessions land the same way Claude ones do. */
2988
2988
  const DEFAULT_APPROVAL_TIMEOUT_MS = 3e5;
2989
2989
  /**
2990
+ * Tool name for codex's built-in `image_gen`. A stable string because it is a
2991
+ * rendering contract: both clients key an icon (and, where they can reach the
2992
+ * host filesystem, an inline preview) off it.
2993
+ */
2994
+ const CODEX_IMAGE_TOOL = "CodexImageGeneration";
2995
+ /** Longest `result` worth putting in a tool card. The field is free-form and
2996
+ * undocumented; anything past this is assumed to be an encoded image rather
2997
+ * than a sentence, and encoded images do not go in the event log. */
2998
+ const MAX_IMAGE_RESULT_CHARS = 512;
2999
+ const shortResult = (result) => result.length > 0 && result.length <= MAX_IMAGE_RESULT_CHARS && !result.startsWith("data:");
3000
+ /**
3001
+ * `file_produced.fileId` — derived from the path, not minted fresh.
3002
+ *
3003
+ * Two properties fall out of that and both are load-bearing: codex reports the
3004
+ * same `savedPath` on the progress item and again on the completed one, so a
3005
+ * derived id makes the second emission a no-op instead of a duplicate row; and
3006
+ * a session rebuilt from a snapshot re-derives the same ids, so a client's
3007
+ * cached URL still resolves after a park/restore.
3008
+ */
3009
+ function producedFileId(path) {
3010
+ return createHash("sha256").update(path).digest("hex").slice(0, 32);
3011
+ }
3012
+ /** Media type from the extension, for the handful a client renders inline.
3013
+ * Undefined for everything else — the route sniffs, and guessing here is how a
3014
+ * text file ends up labelled `image/png`. */
3015
+ function producedMediaType(path) {
3016
+ return PRODUCED_MEDIA_TYPES[path.slice(path.lastIndexOf(".") + 1).toLowerCase()];
3017
+ }
3018
+ const PRODUCED_MEDIA_TYPES = {
3019
+ png: "image/png",
3020
+ jpg: "image/jpeg",
3021
+ jpeg: "image/jpeg",
3022
+ gif: "image/gif",
3023
+ webp: "image/webp",
3024
+ svg: "image/svg+xml",
3025
+ pdf: "application/pdf"
3026
+ };
3027
+ /**
3028
+ * Codex's `SkillMetadata` as the protocol states it. `interface.shortDescription`
3029
+ * beats the legacy top-level one (codex's own comment says to prefer it), and
3030
+ * `enabled` defaults to true — an entry codex listed without the field is one it
3031
+ * considers live, and defaulting to false would hide working skills.
3032
+ */
3033
+ function skillInfo(skill) {
3034
+ return {
3035
+ name: skill.name,
3036
+ ...skill.description ? { description: skill.description } : {},
3037
+ ...skill.interface?.shortDescription ?? skill.shortDescription ? { shortDescription: skill.interface?.shortDescription ?? skill.shortDescription } : {},
3038
+ ...skill.interface?.displayName ? { displayName: skill.interface.displayName } : {},
3039
+ ...skill.interface?.defaultPrompt ? { defaultPrompt: skill.interface.defaultPrompt } : {},
3040
+ ...skill.scope ? { scope: skill.scope } : {},
3041
+ enabled: skill.enabled !== false
3042
+ };
3043
+ }
3044
+ /**
3045
+ * Codex's MCP status → the protocol's, which is Claude Code's vocabulary
3046
+ * ('connected' | 'failed' | 'needs-auth' | 'pending' | 'disabled').
3047
+ *
3048
+ * Two inputs, and the auth one wins where it applies: a server that started
3049
+ * fine but has no credential is *needs-auth*, not connected, because that is
3050
+ * the thing the operator has to act on. `notLoggedIn` is the only auth value
3051
+ * that means "unusable" — `unsupported` is the normal answer for a stdio server
3052
+ * that has no auth concept at all.
3053
+ *
3054
+ * A server with no startup notification yet is 'pending', not 'connected':
3055
+ * `mcpServerStatus/list` alone only proves it is *configured*.
3056
+ */
3057
+ function mcpStatusOf(authStatus, update, hasTools) {
3058
+ if (update?.status === "failed") return update.failureReason === "reauthenticationRequired" ? "needs-auth" : "failed";
3059
+ if (update?.status === "cancelled") return "failed";
3060
+ if (authStatus === "notLoggedIn") return "needs-auth";
3061
+ if (update?.status === "ready") return "connected";
3062
+ if (hasTools) return "connected";
3063
+ return "pending";
3064
+ }
3065
+ /** One `mcpServerStatus/list` entry as the protocol states it. */
3066
+ function mcpServerInfo(server, update) {
3067
+ const tools = Object.entries(server.tools ?? {}).flatMap(([key, tool]) => {
3068
+ if (!tool) return [];
3069
+ const annotations = tool.annotations;
3070
+ return [{
3071
+ name: tool.name ?? key,
3072
+ ...tool.description ? { description: tool.description } : {},
3073
+ ...tool.inputSchema !== void 0 ? { inputSchema: tool.inputSchema } : {},
3074
+ ...annotations ? { annotations: {
3075
+ ...annotations.readOnlyHint != null ? { readOnly: annotations.readOnlyHint } : {},
3076
+ ...annotations.destructiveHint != null ? { destructive: annotations.destructiveHint } : {},
3077
+ ...annotations.openWorldHint != null ? { openWorld: annotations.openWorldHint } : {}
3078
+ } } : {}
3079
+ }];
3080
+ });
3081
+ return {
3082
+ name: server.name,
3083
+ status: mcpStatusOf(server.authStatus ?? void 0, update, tools.length > 0),
3084
+ ...update?.error ? { error: update.error } : {},
3085
+ ...server.serverInfo?.name ? { serverInfo: {
3086
+ name: server.serverInfo.name,
3087
+ version: server.serverInfo.version ?? ""
3088
+ } } : {},
3089
+ ...tools.length > 0 ? { tools } : {}
3090
+ };
3091
+ }
3092
+ /** What the card shows while the picture is being made, and after. `savedPath`
3093
+ * only exists once it lands — a client keys its preview off it, so it is a
3094
+ * field rather than a sentence in the result text. */
3095
+ function imageGenerationInput(item) {
3096
+ return {
3097
+ ...item.revisedPrompt ? { prompt: item.revisedPrompt } : {},
3098
+ ...item.savedPath ? { savedPath: item.savedPath } : {}
3099
+ };
3100
+ }
3101
+ /**
2990
3102
  * The experimental per-request decision list, normalized to names: a string
2991
3103
  * entry is its own name, a structured entry (`{acceptWithExecpolicyAmendment:
2992
3104
  * …}`) is named by its key. Undefined = the request stated no list and the
@@ -3246,6 +3358,21 @@ var CodexRunner = class {
3246
3358
  /** Set around history replay: {@link #emit} stamps `replay: true` onto the
3247
3359
  * message events the live item mapping produces. */
3248
3360
  #replayingHistory = false;
3361
+ /** Last `skills` payload emitted, serialized — the comparison that keeps a
3362
+ * `skills/changed` storm (the watcher fires per touched file) from filling
3363
+ * the event log with identical lists. */
3364
+ #skillsFingerprint;
3365
+ /** In-flight `skills/list`, so a burst of `skills/changed` makes one call.
3366
+ * The pending promise is reused rather than queued: the request has no
3367
+ * arguments, so a second one would ask the same question. */
3368
+ #skillsRefresh;
3369
+ /** Host paths already announced via `file_produced`, so the same picture
3370
+ * reported on both the progress and the completed item registers once. */
3371
+ #producedPaths = /* @__PURE__ */ new Set();
3372
+ /** Per-server liveness, accumulated from `mcpServer/startupStatus/updated`.
3373
+ * `mcpServerStatus/list` does not carry a status field at all, so without
3374
+ * this every server would read as "configured" and never as up or down. */
3375
+ #mcpStatus = /* @__PURE__ */ new Map();
3249
3376
  constructor(config, id = randomUUID()) {
3250
3377
  const mode = config.permissionMode ?? "default";
3251
3378
  if (!ENGINE_CAPABILITIES.codex.permissionModes.includes(mode)) throw new Error(`permission mode '${mode}' is not supported by the codex engine`);
@@ -3317,8 +3444,62 @@ var CodexRunner = class {
3317
3444
  this.#turnChain = this.#turnChain.then(() => this.#backfillHistory());
3318
3445
  } else this.#setStatus("idle");
3319
3446
  if (this.#config.prompt) this.sendMessage(this.#config.prompt);
3447
+ if (!this.#config.prompt && !this.#config.resume) this.#probeSkills();
3320
3448
  return this.#turnChain;
3321
3449
  }
3450
+ /**
3451
+ * List skills over a **throwaway** connection, for a session with nothing else
3452
+ * to do yet.
3453
+ *
3454
+ * `skills/list` needs a live child but not a thread, so this spawns one, asks,
3455
+ * and closes it — rather than bringing up the session's own child early and
3456
+ * leaving a codex process parked behind every session someone created and
3457
+ * never typed into. The session's real connection re-lists when it arrives;
3458
+ * the fingerprint compare in {@link #refreshSkills} makes that a no-op.
3459
+ *
3460
+ * Entirely best-effort and never awaited: a missing binary, a failed spawn or
3461
+ * a rejected handshake here must not turn a session that has not started into
3462
+ * a session that failed.
3463
+ */
3464
+ async #probeSkills() {
3465
+ let connection;
3466
+ try {
3467
+ connection = await this.#openScratchConnection();
3468
+ if (this.#closed) return;
3469
+ await this.#refreshSkills(connection);
3470
+ } catch {} finally {
3471
+ connection?.close();
3472
+ }
3473
+ }
3474
+ /**
3475
+ * A handshaken child that is **not** the session's — for the questions a
3476
+ * client can ask before the session has anything to run (its skills, its MCP
3477
+ * servers). The caller owns it and must close it.
3478
+ *
3479
+ * No onNotification/onRequest/onClose wiring on purpose: this child answers
3480
+ * one question and goes away, so its notifications are noise and its death is
3481
+ * not the session's problem. The alternative — bringing the session's real
3482
+ * child up early — would park a codex process behind every session someone
3483
+ * created and never typed into.
3484
+ */
3485
+ async #openScratchConnection() {
3486
+ const connection = this.#config.connectFn({ env: this.#childEnv() });
3487
+ try {
3488
+ await connection.request("initialize", {
3489
+ clientInfo: {
3490
+ name: "workerdeck",
3491
+ title: "WorkerDeck",
3492
+ version: `protocol-${PROTOCOL_VERSION}`
3493
+ },
3494
+ capabilities: { experimentalApi: true }
3495
+ });
3496
+ connection.notify("initialized");
3497
+ return connection;
3498
+ } catch (error) {
3499
+ connection.close();
3500
+ throw error;
3501
+ }
3502
+ }
3322
3503
  sendMessage(text, attachments) {
3323
3504
  if (this.#closed) throw new Error("session is closed");
3324
3505
  const input = this.#buildInput(text, attachments ?? []);
@@ -3534,9 +3715,117 @@ var CodexRunner = class {
3534
3715
  };
3535
3716
  this.#threadLoaded = true;
3536
3717
  }
3718
+ this.#refreshSkills(connection);
3537
3719
  return connection;
3538
3720
  }
3539
3721
  /**
3722
+ * Re-read `skills/list` and publish it, if it changed.
3723
+ *
3724
+ * **`cwds` is passed explicitly, and must be.** The schema documents the empty
3725
+ * case as "the current session working directory", which reads like the
3726
+ * thread's — it is not. Measured against 0.146.0: with no `cwds`, and *after*
3727
+ * a `thread/start` carrying this session's cwd, the response comes back keyed
3728
+ * to the app-server child's own process directory (for WorkerDeck, wherever
3729
+ * the gateway was launched) and reports no repo-scoped skills at all. So a
3730
+ * project's own `.codex/skills/**` were invisible until this argument existed.
3731
+ *
3732
+ * Best-effort throughout. A binary too old to know the method, a broken
3733
+ * manifest, a child that died mid-call — none of that is worth failing a
3734
+ * session over, and the panel simply stays absent.
3735
+ */
3736
+ async #refreshSkills(connection) {
3737
+ if (this.#skillsRefresh) return this.#skillsRefresh;
3738
+ const run = (async () => {
3739
+ try {
3740
+ const result = await connection.request("skills/list", { cwds: [this.#config.cwd] });
3741
+ if (this.#closed) return;
3742
+ const entries = Array.isArray(result?.data) ? result.data : [];
3743
+ const seen = /* @__PURE__ */ new Set();
3744
+ const skills = [];
3745
+ for (const entry of entries) for (const skill of entry?.skills ?? []) {
3746
+ if (typeof skill?.name !== "string" || seen.has(skill.name)) continue;
3747
+ seen.add(skill.name);
3748
+ skills.push(skillInfo(skill));
3749
+ }
3750
+ skills.sort((a, b) => a.name.localeCompare(b.name));
3751
+ const fingerprint = JSON.stringify(skills);
3752
+ if (fingerprint === this.#skillsFingerprint) return;
3753
+ this.#skillsFingerprint = fingerprint;
3754
+ this.#emit({
3755
+ type: "skills",
3756
+ skills
3757
+ });
3758
+ } catch {} finally {
3759
+ this.#skillsRefresh = void 0;
3760
+ }
3761
+ })();
3762
+ this.#skillsRefresh = run;
3763
+ return run;
3764
+ }
3765
+ /**
3766
+ * The session's MCP servers, live from the binary.
3767
+ *
3768
+ * Two sources merged, because codex splits them: `mcpServerStatus/list` says
3769
+ * what is configured and what each server exposes (including every tool's
3770
+ * full JSON Schema, which the Agent SDK does not give us), and the
3771
+ * `mcpServer/startupStatus/updated` notifications say which of them are
3772
+ * actually up.
3773
+ *
3774
+ * Answers **before the session has connected**, over a throwaway child, for
3775
+ * the same reason the skill list does: a codex session spawns nothing until
3776
+ * it has work, and a panel that said "no MCP servers configured" until the
3777
+ * first turn would be stating something false about the operator's config.
3778
+ * The request blocks until the servers are enumerated (measured: complete on
3779
+ * the very first call), so there is no half-populated answer to race.
3780
+ *
3781
+ * Resolves undefined only when there is genuinely nothing to say — the
3782
+ * session is closed, or the child could not be spoken to. The route turns
3783
+ * that into a 501.
3784
+ *
3785
+ * **Listing only.** There is no per-server reconnect or toggle on this
3786
+ * transport — hence no `reconnectMcpServer`/`setMcpServerEnabled` here, and
3787
+ * `ENGINE_CAPABILITIES.codex.mcpServerActions: false` so clients render the
3788
+ * panel read-only instead of offering buttons that cannot work.
3789
+ */
3790
+ async mcpServers() {
3791
+ if (this.#closed) return void 0;
3792
+ const live = this.#connection;
3793
+ let scratch;
3794
+ try {
3795
+ return ((await (live ?? (scratch = await this.#openScratchConnection())).request("mcpServerStatus/list", {}))?.data ?? []).map((server) => mcpServerInfo(server, this.#mcpStatus.get(server.name)));
3796
+ } catch {
3797
+ return;
3798
+ } finally {
3799
+ scratch?.close();
3800
+ }
3801
+ }
3802
+ /**
3803
+ * Announce a file the ENGINE wrote on the host, so a client can fetch it
3804
+ * without the operator having declared its directory as a host-file root.
3805
+ *
3806
+ * Deliberately narrow: only paths codex reports as *written by its own tool*
3807
+ * belong here. A path the model merely read (`imageView`) is an agent-chosen
3808
+ * claim, and those keep going through `/fs/*` and its root allowlist — see
3809
+ * the note on `file_produced` in the protocol.
3810
+ */
3811
+ #emitFileProduced(path, toolUseId) {
3812
+ if (this.#producedPaths.has(path)) return;
3813
+ this.#producedPaths.add(path);
3814
+ let bytes;
3815
+ try {
3816
+ const stat = statSync(path);
3817
+ if (stat.isFile()) bytes = stat.size;
3818
+ } catch {}
3819
+ this.#emit({
3820
+ type: "file_produced",
3821
+ fileId: producedFileId(path),
3822
+ path,
3823
+ ...producedMediaType(path) ? { mediaType: producedMediaType(path) } : {},
3824
+ ...bytes !== void 0 ? { bytes } : {},
3825
+ toolUseId
3826
+ });
3827
+ }
3828
+ /**
3540
3829
  * On resume, replay the thread's prior turns as `replay: true` events,
3541
3830
  * seq'd before any live turn — the SessionRunner backfill contract, fed
3542
3831
  * from `thread/resume`'s own `thread.turns`. When the resume response says
@@ -3751,6 +4040,21 @@ var CodexRunner = class {
3751
4040
  active.contextWindow = update.tokenUsage?.modelContextWindow ?? void 0;
3752
4041
  return;
3753
4042
  }
4043
+ case "mcpServer/startupStatus/updated": {
4044
+ const update = params;
4045
+ if (typeof update?.name !== "string") return;
4046
+ this.#mcpStatus.set(update.name, {
4047
+ status: typeof update.status === "string" ? update.status : "starting",
4048
+ ...update.error ? { error: update.error } : {},
4049
+ ...update.failureReason ? { failureReason: update.failureReason } : {}
4050
+ });
4051
+ return;
4052
+ }
4053
+ case "skills/changed": {
4054
+ const connection = this.#connection;
4055
+ if (connection) this.#refreshSkills(connection);
4056
+ return;
4057
+ }
3754
4058
  case "account/rateLimits/updated":
3755
4059
  this.#emitRateLimits(params?.rateLimits);
3756
4060
  return;
@@ -3926,6 +4230,12 @@ var CodexRunner = class {
3926
4230
  if (item.type === "mcpToolCall" && !active.toolUseEmitted.has(id)) {
3927
4231
  active.toolUseEmitted.add(id);
3928
4232
  this.#emitToolUse(id, `mcp__${item.server}__${item.tool}`, item.arguments);
4233
+ return;
4234
+ }
4235
+ if (item.type === "imageGeneration" && !active.toolUseEmitted.has(id)) {
4236
+ active.toolUseEmitted.add(id);
4237
+ this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item));
4238
+ if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
3929
4239
  }
3930
4240
  }
3931
4241
  #handleItemCompleted(item, active) {
@@ -3983,6 +4293,18 @@ var CodexRunner = class {
3983
4293
  this.#emitToolUse(id, "CodexWebSearch", { query: item.query });
3984
4294
  this.#emitToolResult(id, "", false);
3985
4295
  return;
4296
+ case "imageGeneration": {
4297
+ active.toolUseEmitted.add(id);
4298
+ this.#emitToolUse(id, CODEX_IMAGE_TOOL, imageGenerationInput(item));
4299
+ if (item.savedPath) this.#emitFileProduced(item.savedPath, id);
4300
+ const lines = [item.savedPath ? `Saved to ${item.savedPath}` : "No saved path reported", ...shortResult(item.result) ? [item.result] : []];
4301
+ this.#emitToolResult(id, lines.join("\n"), item.status === "failed");
4302
+ return;
4303
+ }
4304
+ case "imageView":
4305
+ this.#emitToolUse(id, "CodexImageView", { path: item.path });
4306
+ this.#emitToolResult(id, item.path, false);
4307
+ return;
3986
4308
  default: {
3987
4309
  const unknown = item;
3988
4310
  this.#emit({