@workflow-code/cli 0.2.4-20261012006.codex-studio-integration → 0.2.4-20261013001.codex-studio-integration

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.
@@ -342,7 +342,33 @@ var messages_zh_cn_default = {
342
342
  "Write file": "\u5199\u5165\u6587\u4EF6",
343
343
  Yes: "\u662F",
344
344
  "Zoom in": "\u653E\u5927",
345
- "Zoom out": "\u7F29\u5C0F"
345
+ "Zoom out": "\u7F29\u5C0F",
346
+ "Open resource": "\u6253\u5F00\u8D44\u6E90",
347
+ "Show in conversation": "\u5728\u5BF9\u8BDD\u4E2D\u5C55\u793A",
348
+ "Hide preview": "\u6536\u8D77\u9884\u89C8",
349
+ "Open in new window": "\u5728\u65B0\u7A97\u53E3\u4E2D\u6253\u5F00",
350
+ "Tool call details": "\u5DE5\u5177\u8C03\u7528\u8BE6\u60C5",
351
+ "Please sign in to preview this server resource.": "\u8BF7\u767B\u5F55\u540E\u9884\u89C8\u670D\u52A1\u5668\u8D44\u6E90\u3002",
352
+ "You do not have access to this resource.": "\u4F60\u6CA1\u6709\u6B64\u8D44\u6E90\u7684\u8BBF\u95EE\u6743\u9650\u3002",
353
+ "The resource project is unavailable.": "\u8D44\u6E90\u6240\u5C5E\u9879\u76EE\u4E0D\u53EF\u7528\u3002",
354
+ "The resource project is unavailable. Import or open the target project first.": "\u8D44\u6E90\u6240\u5C5E\u9879\u76EE\u4E0D\u53EF\u7528\uFF0C\u8BF7\u5148\u5BFC\u5165\u6216\u6253\u5F00\u76EE\u6807\u9879\u76EE\u3002",
355
+ "The project does not allow this storage location.": "\u9879\u76EE\u4E0D\u5141\u8BB8\u4F7F\u7528\u6B64\u5B58\u50A8\u4F4D\u7F6E\u3002",
356
+ "This resource no longer exists.": "\u6B64\u8D44\u6E90\u5DF2\u4E0D\u5B58\u5728\u3002",
357
+ "Server source files cannot be previewed by this card.": "\u6B64\u5361\u7247\u4E0D\u652F\u6301\u9884\u89C8\u670D\u52A1\u5668\u6E90\u7801\u6587\u4EF6\u3002",
358
+ "File is outside this project.": "\u6587\u4EF6\u4E0D\u5728\u6B64\u9879\u76EE\u4E2D\u3002",
359
+ "File preview exceeds 2 MiB.": "\u6587\u4EF6\u9884\u89C8\u5927\u5C0F\u8D85\u8FC7 2 MiB\u3002",
360
+ "This project does not provide a Kanban preview.": "\u6B64\u9879\u76EE\u4E0D\u63D0\u4F9B\u770B\u677F\u9884\u89C8\u3002",
361
+ "Invalid resource preview URL.": "\u8D44\u6E90\u9884\u89C8\u5730\u5740\u65E0\u6548\u3002",
362
+ "This preview configuration no longer exists.": "\u6B64\u9884\u89C8\u914D\u7F6E\u5DF2\u4E0D\u5B58\u5728\u3002",
363
+ "Resource access changed. Open the card again.": "\u8D44\u6E90\u8BBF\u95EE\u72B6\u6001\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u5361\u7247\u3002",
364
+ "This resource preview is read-only.": "\u6B64\u8D44\u6E90\u9884\u89C8\u4E3A\u53EA\u8BFB\u3002",
365
+ "Invalid resource action.": "\u8D44\u6E90\u64CD\u4F5C\u65E0\u6548\u3002",
366
+ "Run no longer exists.": "\u6B64\u8FD0\u884C\u5DF2\u4E0D\u5B58\u5728\u3002",
367
+ "Conversation changed.": "\u4F1A\u8BDD\u5DF2\u53D8\u5316\u3002",
368
+ "Unable to load this preview. Retry or open it separately.": "\u65E0\u6CD5\u52A0\u8F7D\u6B64\u9884\u89C8\uFF0C\u8BF7\u91CD\u8BD5\u6216\u5355\u72EC\u6253\u5F00\u3002",
369
+ "Open in browser": "\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00",
370
+ "No resources": "\u6682\u65E0\u8D44\u6E90",
371
+ "Allow popups to open this resource in a new window.": "\u8BF7\u5141\u8BB8\u5F39\u51FA\u7A97\u53E3\u4EE5\u5355\u72EC\u6253\u5F00\u6B64\u8D44\u6E90\u3002"
346
372
  };
347
373
 
348
374
  // src/web/localization.ts
@@ -1706,7 +1732,6 @@ var KanbanMethodHub = class {
1706
1732
  import { readFile as readFile2, readdir, realpath as realpath2 } from "fs/promises";
1707
1733
  import { createHash } from "crypto";
1708
1734
  import path2 from "path";
1709
- var readKanbanProjectInfo = readWorkflowProjectInfo;
1710
1735
  var webProjectDataDirectory = (storageDir, projectDir) => path2.join(storageDir, createHash("sha256").update(projectDir).digest("hex").slice(0, 24));
1711
1736
  async function localKanbanRelatedProjects(project) {
1712
1737
  const info = readWorkflowProjectInfo(JSON.parse(await readFile2(path2.join(project.folderPath, "package.json"), "utf8")));
@@ -1846,7 +1871,7 @@ var KanbanController = class {
1846
1871
  async load() {
1847
1872
  const pkg = JSON.parse(await readFile3(path3.join(this.project.folderPath, "package.json"), "utf8"));
1848
1873
  const artifact = readKanbanArtifactConfig(pkg);
1849
- const projectInfo = readKanbanProjectInfo(pkg);
1874
+ const projectInfo = readWorkflowProjectInfo(pkg);
1850
1875
  let params;
1851
1876
  try {
1852
1877
  params = parseKanbanManifestText(await readFile3(path3.join(this.project.folderPath, "kanban.json"), "utf8")).params;
@@ -2145,115 +2170,7 @@ var KanbanController = class {
2145
2170
  }
2146
2171
  };
2147
2172
 
2148
- // src/web/resource-cards.ts
2149
- var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2150
- var string = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
2151
- function conversationResourceCards(items, projectId, conversationId) {
2152
- const cards = /* @__PURE__ */ new Map();
2153
- for (const [index, item] of items.entries()) {
2154
- if (!["agent-part", "opencode-part"].includes(String(item.metadata?.kind)) || item.metadata?.partType !== "tool") {
2155
- continue;
2156
- }
2157
- const content = record(item.content);
2158
- if (content?.status !== "success") {
2159
- continue;
2160
- }
2161
- let output = record(content.output);
2162
- const details = record(output?.details);
2163
- if (Array.isArray(output?.content) && output.content.length === 1) {
2164
- try {
2165
- output = record(JSON.parse(String(record(output.content[0])?.text)));
2166
- } catch {
2167
- }
2168
- }
2169
- let descriptor = record(details?.resourceCard) ?? record(record(output?.details)?.resourceCard);
2170
- const input = record(content.input) ?? {};
2171
- const tool = string(content.tool) ?? "";
2172
- if (tool === "workflow-ui_present_card") {
2173
- const presentation = output?.action ? output : input;
2174
- if (presentation.action === "remove") {
2175
- cards.delete(String(presentation.cardId));
2176
- continue;
2177
- }
2178
- if (presentation.action !== "upsert") {
2179
- continue;
2180
- }
2181
- descriptor = presentation;
2182
- }
2183
- if (!descriptor) {
2184
- const target2 = { source: "local", projectId, conversationId };
2185
- let kind2;
2186
- if (tool === "workflow-kv_kv_store" || /workflow-desktop_(?:read|list|write)_project_kv/.test(tool)) {
2187
- kind2 = "kv";
2188
- Object.assign(target2, { projectId: input.projectId ?? projectId, key: input.key, scope: input.scope ?? "project", conversationId: input.conversationId ?? conversationId });
2189
- } else if (tool === "workflow-knowledge_knowledge_documents" || /workflow-desktop_(?:query|mutate)_project_knowledge/.test(tool)) {
2190
- kind2 = "knowledge";
2191
- Object.assign(target2, { projectId: input.projectId ?? projectId, documentId: input.documentId ?? record(output?.document)?.id });
2192
- } else if (tool === "read" || tool === "workflow-desktop_open_dock_tool" && input.tool === "files") {
2193
- kind2 = "file";
2194
- target2.filePath = input.filePath ?? input.path;
2195
- } else if (tool === "workflow-desktop_open_project" && input.kind === "project") {
2196
- kind2 = "project";
2197
- target2.projectId = input.targetId;
2198
- } else if (tool === "workflow-desktop_open_dock_tool") {
2199
- if (input.tool === "browser") {
2200
- kind2 = "browser";
2201
- target2.url = input.url;
2202
- }
2203
- if (input.tool === "preview") {
2204
- kind2 = "kanban";
2205
- }
2206
- if (input.tool === "open-project" && input.openedProjectKind === "project") {
2207
- kind2 = "project";
2208
- target2.projectId = input.openedProjectId;
2209
- }
2210
- }
2211
- if (!kind2) {
2212
- continue;
2213
- }
2214
- descriptor = { kind: kind2, target: target2, title: record(output?.document)?.title };
2215
- }
2216
- const kind = descriptor.kind;
2217
- if (!["kv", "knowledge", "project", "browser", "file", "kanban"].includes(kind)) {
2218
- continue;
2219
- }
2220
- const target = Object.fromEntries(Object.entries(record(descriptor.target) ?? {}).filter((entry) => typeof entry[1] === "string" && entry[1].length < 8192));
2221
- if (target.source === "current") {
2222
- target.source = "local";
2223
- target.projectId = projectId;
2224
- }
2225
- if (target.source && !["local", "server"].includes(target.source)) {
2226
- continue;
2227
- }
2228
- if (kind === "browser") {
2229
- try {
2230
- const url = new URL(target.url);
2231
- if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
2232
- continue;
2233
- }
2234
- } catch {
2235
- continue;
2236
- }
2237
- } else {
2238
- target.source ??= "local";
2239
- target.projectId ??= projectId;
2240
- if (target.scope === "conversation") {
2241
- target.conversationId ??= conversationId;
2242
- }
2243
- if (kind === "file" && (!target.filePath || /^(?:[\\/]|[a-z]:)|(?:^|[\\/])\.\.(?:[\\/]|$)/i.test(target.filePath))) {
2244
- continue;
2245
- }
2246
- }
2247
- const id = string(descriptor.cardId) ?? `${kind}:${JSON.stringify(target)}`;
2248
- const title = (string(descriptor.title) ?? target.key ?? target.documentId ?? target.filePath ?? target.url ?? target.projectId ?? kind).slice(0, 200);
2249
- cards.set(id, { id, kind, title, target, itemId: item.id ?? String(index) });
2250
- }
2251
- return [...cards.values()];
2252
- }
2253
-
2254
2173
  // src/web/workflow-actions.ts
2255
- import { readFile as readFile4, realpath as realpath3 } from "fs/promises";
2256
- import path4 from "path";
2257
2174
  function createWorkflowActions(controller, runtime, project, revision, changed) {
2258
2175
  const states = /* @__PURE__ */ new Map();
2259
2176
  const epochs = /* @__PURE__ */ new Map();
@@ -2438,28 +2355,6 @@ function createWorkflowActions(controller, runtime, project, revision, changed)
2438
2355
  replies.push({ type: "attachments", entrypointId: id, runId: message.runId, requestId: message.requestId, paramName: message.paramName, files });
2439
2356
  break;
2440
2357
  }
2441
- case "resource": {
2442
- const run = controller.runs.find((record2) => record2.id === message.runId);
2443
- if (!run) {
2444
- throw new Error("Run no longer exists.");
2445
- }
2446
- const card = conversationResourceCards(run.items, project.id, run.scopeId).find((card2) => card2.id === message.cardId);
2447
- if (!card || card.target.projectId && card.target.projectId !== project.id || card.target.source === "server") {
2448
- throw new Error("Resource is outside this project.");
2449
- }
2450
- if (card.kind === "kv" || card.kind === "knowledge") {
2451
- replies.push({ type: "preview", title: card.title, content: await runtime.readResource(project, run.scopeId, card.kind, card.target) });
2452
- } else if (card.kind === "file") {
2453
- const root = await realpath3(project.folderPath), filename = await realpath3(path4.resolve(root, card.target.filePath)), relative = path4.relative(root, filename);
2454
- if (relative.startsWith("..") || path4.isAbsolute(relative)) {
2455
- throw new Error("File is outside this project.");
2456
- }
2457
- replies.push({ type: "preview", title: card.title, content: await readFile4(filename, "utf8") });
2458
- } else {
2459
- throw new Error("This resource requires an editor host.");
2460
- }
2461
- break;
2462
- }
2463
2358
  default:
2464
2359
  throw new Error(`Unsupported workflow action: ${message.type}`);
2465
2360
  }
@@ -2471,26 +2366,26 @@ function createWorkflowActions(controller, runtime, project, revision, changed)
2471
2366
  // src/web/runtime.ts
2472
2367
  import { spawn } from "child_process";
2473
2368
  import { existsSync, readFileSync } from "fs";
2474
- import path5 from "path";
2369
+ import path4 from "path";
2475
2370
  import { fileURLToPath } from "url";
2476
2371
  import { WORKFLOW_RUNTIME_LOCALE_ENV } from "workflow-code";
2477
2372
  function cliRoot() {
2478
- let directory = path5.dirname(fileURLToPath(import.meta.url));
2373
+ let directory = path4.dirname(fileURLToPath(import.meta.url));
2479
2374
  for (let depth = 0; depth < 8; depth++) {
2480
2375
  try {
2481
- if (JSON.parse(readFileSync(path5.join(directory, "package.json"), "utf8")).name === "@workflow-code/cli") {
2376
+ if (JSON.parse(readFileSync(path4.join(directory, "package.json"), "utf8")).name === "@workflow-code/cli") {
2482
2377
  return directory;
2483
2378
  }
2484
2379
  } catch {
2485
2380
  }
2486
- directory = path5.dirname(directory);
2381
+ directory = path4.dirname(directory);
2487
2382
  }
2488
2383
  throw new Error("Cannot locate the CLI installation.");
2489
2384
  }
2490
2385
  function createWebRuntime(projectDir, storageDir, connection, relatedProjects = () => []) {
2491
2386
  const root = cliRoot();
2492
- const builtWorker = path5.join(root, "dist/web/worker.mjs");
2493
- const worker = existsSync(builtWorker) ? builtWorker : path5.join(root, "src/web/worker.mjs");
2387
+ const builtWorker = path4.join(root, "dist/web/worker.mjs");
2388
+ const worker = existsSync(builtWorker) ? builtWorker : path4.join(root, "src/web/worker.mjs");
2494
2389
  const children = /* @__PURE__ */ new Set();
2495
2390
  const active = /* @__PURE__ */ new Map();
2496
2391
  let disposed = false;
@@ -2514,7 +2409,7 @@ function createWebRuntime(projectDir, storageDir, connection, relatedProjects =
2514
2409
  WORKFLOW_SERVER_TOKEN: "",
2515
2410
  WORKFLOW_LLM_SERVER_URL: auth?.serverUrl ?? "",
2516
2411
  WORKFLOW_LLM_SERVER_TOKEN: auth?.accessToken ?? "",
2517
- WORKFLOW_CLI_AUTH_DIR: path5.join(storageDir, "runtime-auth")
2412
+ WORKFLOW_CLI_AUTH_DIR: path4.join(storageDir, "runtime-auth")
2518
2413
  };
2519
2414
  for (const key of [
2520
2415
  "TSX_TSCONFIG_PATH",
@@ -2637,6 +2532,125 @@ function createWebRuntime(projectDir, storageDir, connection, relatedProjects =
2637
2532
  };
2638
2533
  }
2639
2534
 
2535
+ // src/web/resource-cards.ts
2536
+ var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
2537
+ var string = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
2538
+ function conversationResourceCards(items, projectId, conversationId, storageSource = "local") {
2539
+ const cards = /* @__PURE__ */ new Map();
2540
+ for (const [index, item] of items.entries()) {
2541
+ if (!["agent-part", "opencode-part"].includes(String(item.metadata?.kind)) || item.metadata?.partType !== "tool") {
2542
+ continue;
2543
+ }
2544
+ const content = record(item.content);
2545
+ if (content?.status !== "success") {
2546
+ continue;
2547
+ }
2548
+ let output = record(content.output);
2549
+ const details = record(output?.details);
2550
+ if (Array.isArray(output?.content) && output.content.length === 1) {
2551
+ try {
2552
+ output = record(JSON.parse(String(record(output.content[0])?.text)));
2553
+ } catch {
2554
+ }
2555
+ }
2556
+ let descriptor = record(details?.resourceCard) ?? record(record(output?.details)?.resourceCard);
2557
+ const input = record(content.input) ?? {};
2558
+ const tool = string(content.tool) ?? "";
2559
+ if (tool === "workflow-ui_present_card") {
2560
+ const presentation = output?.action ? output : input;
2561
+ if (presentation.action === "remove") {
2562
+ cards.delete(String(presentation.cardId));
2563
+ continue;
2564
+ }
2565
+ if (presentation.action !== "upsert") {
2566
+ continue;
2567
+ }
2568
+ descriptor = presentation;
2569
+ }
2570
+ if (!descriptor) {
2571
+ const target2 = { source: "local", projectId, conversationId };
2572
+ let kind2;
2573
+ if (tool === "workflow-kv_kv_store" || /workflow-desktop_(?:read|list|write)_project_kv/.test(tool)) {
2574
+ kind2 = "kv";
2575
+ Object.assign(target2, { projectId: input.projectId ?? projectId, key: input.key, scope: input.scope ?? "project", conversationId: input.conversationId ?? conversationId });
2576
+ } else if (tool === "workflow-knowledge_knowledge_documents" || /workflow-desktop_(?:query|mutate)_project_knowledge/.test(tool)) {
2577
+ kind2 = "knowledge";
2578
+ Object.assign(target2, { projectId: input.projectId ?? projectId, documentId: input.documentId ?? record(output?.document)?.id });
2579
+ } else if (tool === "read" || tool === "workflow-desktop_open_dock_tool" && input.tool === "files") {
2580
+ kind2 = "file";
2581
+ target2.filePath = input.filePath ?? input.path;
2582
+ } else if (tool === "workflow-desktop_open_project" && input.kind === "project") {
2583
+ kind2 = "project";
2584
+ target2.projectId = input.targetId;
2585
+ } else if (tool === "workflow-desktop_open_dock_tool") {
2586
+ if (input.tool === "browser") {
2587
+ kind2 = "browser";
2588
+ target2.url = input.url;
2589
+ }
2590
+ if (input.tool === "preview") {
2591
+ kind2 = "kanban";
2592
+ }
2593
+ if (input.tool === "open-project" && input.openedProjectKind === "project") {
2594
+ kind2 = "project";
2595
+ target2.projectId = input.openedProjectId;
2596
+ }
2597
+ }
2598
+ if (!kind2) {
2599
+ continue;
2600
+ }
2601
+ if (kind2 === "kv" || kind2 === "knowledge") {
2602
+ target2.source = input.source ?? storageSource;
2603
+ }
2604
+ descriptor = { kind: kind2, target: target2, title: record(output?.document)?.title };
2605
+ }
2606
+ const kind = descriptor.kind;
2607
+ if (!["kv", "knowledge", "project", "browser", "file", "kanban"].includes(kind)) {
2608
+ continue;
2609
+ }
2610
+ const target = Object.fromEntries(Object.entries(record(descriptor.target) ?? {}).filter((entry) => typeof entry[1] === "string" && entry[1].length < 8192));
2611
+ if (target.source === "current") {
2612
+ target.source = kind === "kv" || kind === "knowledge" ? storageSource : "local";
2613
+ target.projectId = projectId;
2614
+ }
2615
+ if (target.source && !["local", "server"].includes(target.source)) {
2616
+ continue;
2617
+ }
2618
+ if (kind === "browser") {
2619
+ try {
2620
+ const url = new URL(target.url);
2621
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
2622
+ continue;
2623
+ }
2624
+ } catch {
2625
+ continue;
2626
+ }
2627
+ } else {
2628
+ target.source ??= "local";
2629
+ target.projectId ??= projectId;
2630
+ if (target.scope === "conversation") {
2631
+ target.conversationId ??= conversationId;
2632
+ }
2633
+ if (kind === "file" && (!target.filePath || /^(?:[\\/]|[a-z]:)|(?:^|[\\/])\.\.(?:[\\/]|$)/i.test(target.filePath))) {
2634
+ continue;
2635
+ }
2636
+ }
2637
+ const id = string(descriptor.cardId) ?? `${kind}:${JSON.stringify(target)}`;
2638
+ const title = (string(descriptor.title) ?? string(record(output?.document)?.title) ?? target.key ?? target.documentId ?? target.filePath ?? target.url ?? target.projectId ?? kind).slice(0, 200);
2639
+ const kanban = record(descriptor.kanban), values = record(kanban?.values);
2640
+ cards.set(id, {
2641
+ id,
2642
+ kind,
2643
+ title,
2644
+ target,
2645
+ itemId: item.id ?? String(index),
2646
+ ...typeof descriptor.preview === "boolean" ? { preview: descriptor.preview } : {},
2647
+ ...typeof descriptor.pinned === "boolean" ? { pinned: descriptor.pinned } : {},
2648
+ ...kind === "kanban" && ["preview", "save"].includes(String(kanban?.mode)) && values && Object.keys(values).length <= 100 && Object.values(values).every((value) => typeof value === "string") ? { kanban: { mode: kanban.mode, values } } : {}
2649
+ });
2650
+ }
2651
+ return [...cards.values()];
2652
+ }
2653
+
2640
2654
  // src/web/account-auth.ts
2641
2655
  async function loginWebAccount(serverUrl, email, password) {
2642
2656
  if (!email.trim() || !password) {
@@ -2720,6 +2734,287 @@ async function inspectWebProject(runtime, projectDir, host) {
2720
2734
  };
2721
2735
  }
2722
2736
 
2737
+ // src/web/resource-preview.ts
2738
+ import { randomUUID as randomUUID3 } from "crypto";
2739
+ import { readFile as readFile4, realpath as realpath3, stat as stat3 } from "fs/promises";
2740
+ import path5 from "path";
2741
+ import { pathToFileURL as pathToFileURL2 } from "url";
2742
+ var ResourcePreviews = class {
2743
+ constructor(owner, runtime, storageDir, connection, related, resourceProjects = () => []) {
2744
+ this.owner = owner;
2745
+ this.runtime = runtime;
2746
+ this.storageDir = storageDir;
2747
+ this.connection = connection;
2748
+ this.related = related;
2749
+ this.resourceProjects = resourceProjects;
2750
+ }
2751
+ owner;
2752
+ runtime;
2753
+ storageDir;
2754
+ connection;
2755
+ related;
2756
+ resourceProjects;
2757
+ entries = /* @__PURE__ */ new Map();
2758
+ generation = 0;
2759
+ clear() {
2760
+ this.generation++;
2761
+ this.entries.clear();
2762
+ }
2763
+ get revision() {
2764
+ return this.generation;
2765
+ }
2766
+ async server(id, suffix, init = {}) {
2767
+ const connection = this.connection();
2768
+ if (!connection) {
2769
+ throw new Error(t("Please sign in to preview this server resource."));
2770
+ }
2771
+ const response = await fetch(`${connection.serverUrl}/api/workflows/${encodeURIComponent(id)}${suffix}`, {
2772
+ ...init,
2773
+ headers: { Authorization: `Bearer ${connection.accessToken}`, "Content-Type": "application/json" },
2774
+ signal: AbortSignal.timeout(3e4),
2775
+ redirect: "error"
2776
+ });
2777
+ const result = await response.json();
2778
+ if (!response.ok || result.errCode !== 0) {
2779
+ throw new Error(result.errMessage || t("You do not have access to this resource."));
2780
+ }
2781
+ return result.data;
2782
+ }
2783
+ async resolve(card) {
2784
+ if (card.kind === "browser") {
2785
+ return;
2786
+ }
2787
+ const id = card.target.projectId ?? this.owner.id;
2788
+ if (card.target.source === "server") {
2789
+ if (card.kind === "kv" || card.kind === "knowledge") {
2790
+ if (id.toLowerCase() === this.owner.id.toLowerCase()) {
2791
+ return this.owner;
2792
+ }
2793
+ return { id, uri: `workflow-server:${id}`, folderPath: "", name: id, projectType: "workflow", fields: [], entrypoints: [] };
2794
+ }
2795
+ const snapshot = await this.server(id, "/structure"), structure = snapshot.structure;
2796
+ return {
2797
+ ...structure,
2798
+ id,
2799
+ uri: `workflow-server:${id}`,
2800
+ folderPath: "",
2801
+ name: snapshot.workflowName ?? card.title,
2802
+ projectType: structure.projectType,
2803
+ fields: structure.params ?? [],
2804
+ entrypoints: structure.entrypoints ?? []
2805
+ };
2806
+ }
2807
+ if (id === this.owner.id) {
2808
+ return this.owner;
2809
+ }
2810
+ for (const directory of /* @__PURE__ */ new Set([...this.related().map((binding) => binding.projectDir), ...this.resourceProjects()])) {
2811
+ let root, pkg;
2812
+ try {
2813
+ root = await realpath3(directory);
2814
+ pkg = JSON.parse(await readFile4(path5.join(root, "package.json"), "utf8"));
2815
+ } catch {
2816
+ continue;
2817
+ }
2818
+ if (String(pkg.id).toLowerCase() !== id.toLowerCase()) {
2819
+ continue;
2820
+ }
2821
+ const inspected = await this.runtime.inspect([root]);
2822
+ const project = inspected.projects.find((project2) => project2.id.toLowerCase() === id.toLowerCase());
2823
+ if (!project) {
2824
+ throw new Error(t("The resource project is unavailable."));
2825
+ }
2826
+ return { ...project, uri: pathToFileURL2(root).href };
2827
+ }
2828
+ throw new Error(t("The resource project is unavailable. Import or open the target project first."));
2829
+ }
2830
+ async open(card) {
2831
+ const generation = this.generation;
2832
+ const project = await this.resolve(card);
2833
+ const id = randomUUID3(), data = { card, project }, entry = { data };
2834
+ const target = card.target, scopeId = target.conversationId ?? "";
2835
+ if (card.kind === "kv" || card.kind === "knowledge") {
2836
+ if (target.source === "server") {
2837
+ const query = new URLSearchParams({ scope: target.scope ?? "project" });
2838
+ if (target.conversationId) {
2839
+ query.set("conversationId", target.conversationId);
2840
+ }
2841
+ if (target.key) {
2842
+ query.set("key", target.key);
2843
+ }
2844
+ data.content = await this.server(project.id, card.kind === "knowledge" ? `/knowledge-documents${target.documentId ? `/${encodeURIComponent(target.documentId)}` : ""}` : `/kv${target.key ? "/entry" : ""}?${query}`);
2845
+ } else {
2846
+ const info = readWorkflowProjectInfo(JSON.parse(await readFile4(path5.join(project.folderPath, "package.json"), "utf8")));
2847
+ if (info.dataStorage && !isWorkflowProjectDataStorageLocationAllowed(info, "local")) {
2848
+ throw new Error(t("The project does not allow this storage location."));
2849
+ }
2850
+ data.content = await this.runtime.readResource(project, scopeId, card.kind, target);
2851
+ }
2852
+ if (card.kind === "knowledge" && (data.content === null || data.content === void 0)) {
2853
+ throw new Error(t("This resource no longer exists."));
2854
+ }
2855
+ data.format = card.kind === "knowledge" && typeof data.content.markdown === "string" ? "markdown" : "json";
2856
+ } else if (card.kind === "file") {
2857
+ if (target.source === "server") {
2858
+ throw new Error(t("Server source files cannot be previewed by this card."));
2859
+ }
2860
+ const root = await realpath3(project.assistant?.workdir ?? project.folderPath);
2861
+ const filename = await realpath3(path5.resolve(root, target.filePath));
2862
+ const relative = path5.relative(root, filename);
2863
+ if (relative === ".." || relative.startsWith(`..${path5.sep}`) || path5.isAbsolute(relative)) {
2864
+ throw new Error(t("File is outside this project."));
2865
+ }
2866
+ if (!(await stat3(filename)).isFile() || (await stat3(filename)).size > 2 * 1024 * 1024) {
2867
+ throw new Error(t("File preview exceeds 2 MiB."));
2868
+ }
2869
+ data.content = await readFile4(filename, "utf8");
2870
+ data.format = /\.mdx?$/i.test(filename) ? "markdown" : "text";
2871
+ } else if (card.kind === "browser") {
2872
+ data.format = "browser";
2873
+ data.content = target.url;
2874
+ } else if (card.kind === "kanban" || project?.projectType === "kanban" && card.preview !== false) {
2875
+ if (project?.projectType !== "kanban") {
2876
+ throw new Error(t("This project does not provide a Kanban preview."));
2877
+ }
2878
+ let params, configurations, activeId;
2879
+ if (target.source === "server") {
2880
+ const workspace = await this.server(project.id, "/kanban");
2881
+ params = workspace.manifest?.params ?? [];
2882
+ configurations = workspace.configurations;
2883
+ activeId = workspace.activeConfigurationId;
2884
+ const connection = this.connection(), url = new URL(workspace.preview.url, `${connection.serverUrl}/`);
2885
+ const expected = `/api/workflows/${encodeURIComponent(project.id)}/kanban/previews/`;
2886
+ if (url.origin !== new URL(connection.serverUrl).origin || !url.pathname.startsWith(expected) || url.search || url.hash) {
2887
+ throw new Error(t("Invalid resource preview URL."));
2888
+ }
2889
+ const segments = url.pathname.slice(expected.length).split("/");
2890
+ const assetPrefix = `${expected}${segments.slice(0, 3).join("/")}/`;
2891
+ entry.remote = { url, prefix: assetPrefix, connection };
2892
+ data.kanban = { url: `/resource-assets/${id}/${url.pathname.slice(assetPrefix.length)}`, nonce: workspace.preview.nonce, configuration: { id: "", name: "", params: {} } };
2893
+ } else {
2894
+ params = project.fields;
2895
+ try {
2896
+ params = parseKanbanManifestText(await readFile4(path5.join(project.folderPath, "kanban.json"), "utf8")).params;
2897
+ } catch (error) {
2898
+ if (error.code !== "ENOENT") {
2899
+ throw error;
2900
+ }
2901
+ }
2902
+ let workspace = {};
2903
+ try {
2904
+ workspace = JSON.parse(await readFile4(path5.join(webProjectDataDirectory(this.storageDir, project.folderPath), "kanban.json"), "utf8"));
2905
+ } catch (error) {
2906
+ if (error.code !== "ENOENT") {
2907
+ throw error;
2908
+ }
2909
+ }
2910
+ configurations = workspace.configurations ?? [];
2911
+ activeId = workspace.activeConfigurationId;
2912
+ }
2913
+ const selected = configurations.find((item) => item.id === (target.configurationId ?? activeId));
2914
+ if (target.configurationId && !selected) {
2915
+ throw new Error(t("This preview configuration no longer exists."));
2916
+ }
2917
+ const values = sanitizeKanbanValues(params, { ...selected?.values ?? createDefaultKanbanValues(params), ...card.kanban?.mode === "preview" ? card.kanban.values : {} });
2918
+ const configuration = { id: selected?.id ?? id, name: selected?.name ?? card.title, params: createInjectedKanbanParams(params, values) };
2919
+ if (!entry.remote) {
2920
+ if (!project.kanban) {
2921
+ throw new Error(t("This project does not provide a Kanban preview."));
2922
+ }
2923
+ entry.local = new KanbanPreview(project.folderPath, project.kanban, () => configuration);
2924
+ data.kanban = { url: entry.local.url, nonce: entry.local.nonce, configuration };
2925
+ } else {
2926
+ data.kanban.configuration = configuration;
2927
+ }
2928
+ data.format = "kanban";
2929
+ } else {
2930
+ data.format = "project";
2931
+ data.content = project;
2932
+ }
2933
+ if (generation !== this.generation) {
2934
+ throw new Error(t("Resource access changed. Open the card again."));
2935
+ }
2936
+ if (this.entries.size >= 64) {
2937
+ this.entries.delete(this.entries.keys().next().value);
2938
+ }
2939
+ this.entries.set(id, entry);
2940
+ return { id, data, url: `/resource/${id}?workflowLocale=${locale()}` };
2941
+ }
2942
+ get(id) {
2943
+ const entry = this.entries.get(id);
2944
+ if (!entry) {
2945
+ throw new Error(t("Resource access changed. Open the card again."));
2946
+ }
2947
+ return entry.data;
2948
+ }
2949
+ async asset(url) {
2950
+ const generation = this.generation;
2951
+ if (url.pathname.startsWith("/kanban/")) {
2952
+ for (const entry of this.entries.values()) {
2953
+ if (entry.local && url.pathname.startsWith(`/kanban/${entry.local.nonce}/`)) {
2954
+ const asset2 = await entry.local.asset(url);
2955
+ return generation === this.generation ? asset2 : void 0;
2956
+ }
2957
+ }
2958
+ return;
2959
+ }
2960
+ const match = /^\/resource-assets\/([^/]+)\/(.+)$/.exec(url.pathname);
2961
+ const remote = match && this.entries.get(match[1])?.remote;
2962
+ const connection = this.connection();
2963
+ if (!remote || remote.connection.serverUrl !== connection?.serverUrl || remote.connection.accessToken !== connection?.accessToken) {
2964
+ return;
2965
+ }
2966
+ const asset = decodeURIComponent(match[2]);
2967
+ if (asset.includes("\\") || asset.split("/").some((part) => part === ".." || !part) || !/\.[a-z\d]+$/i.test(asset)) {
2968
+ return;
2969
+ }
2970
+ const address = new URL(`${remote.prefix}${match[2]}`, remote.url.origin);
2971
+ const response = await fetch(address, { headers: { Authorization: `Bearer ${remote.connection.accessToken}` }, redirect: "error", signal: AbortSignal.timeout(3e4) });
2972
+ if (!response.ok) {
2973
+ return;
2974
+ }
2975
+ const content = Buffer.from(await response.arrayBuffer());
2976
+ if (generation !== this.generation) {
2977
+ return;
2978
+ }
2979
+ return { content, headers: {
2980
+ "Content-Type": response.headers.get("content-type") ?? "application/octet-stream",
2981
+ "Content-Security-Policy": `${response.headers.get("content-security-policy") ?? "default-src 'none'"}; frame-ancestors 'self' vscode-webview: vscode-file: http://127.0.0.1:* http://localhost:*`,
2982
+ "Cache-Control": "no-store",
2983
+ "Access-Control-Allow-Origin": "*",
2984
+ "X-Content-Type-Options": "nosniff"
2985
+ } };
2986
+ }
2987
+ async bridge(id, value) {
2988
+ const generation = this.generation;
2989
+ const data = this.get(id), project = data.project, call = parseKanbanBridgeCall(value);
2990
+ if (data.format !== "kanban" || !project) {
2991
+ throw new Error(t("This project does not provide a Kanban preview."));
2992
+ }
2993
+ if (!["runtime.getLocale", "kv.getValue", "relatedProject.kv.getValue", "knowledge.listDocuments", "knowledge.getDocument", "knowledge.searchDocuments", "knowledge.readDocumentLines", "files.readFile"].includes(call.operation)) {
2994
+ throw new Error(t("This resource preview is read-only."));
2995
+ }
2996
+ if (call.operation === "runtime.getLocale") {
2997
+ return locale();
2998
+ }
2999
+ let result;
3000
+ if (data.card.target.source === "server" || project.projectInfo?.dataStorage?.mode === "server") {
3001
+ result = await this.server(project.id, "/kanban/bridge", { method: "POST", body: JSON.stringify({ call }) });
3002
+ } else if (call.operation === "files.readFile") {
3003
+ const file = await this.runtime.readFile(project, String(call.input.fileId));
3004
+ result = { file: file.reference, base64: (await readFile4(file.path)).toString("base64") };
3005
+ } else {
3006
+ if (!this.runtime.kanbanBridge) {
3007
+ throw new Error(t("The resource project is unavailable."));
3008
+ }
3009
+ result = await this.runtime.kanbanBridge(project, call);
3010
+ }
3011
+ if (generation !== this.generation) {
3012
+ throw new Error(t("Resource access changed. Open the card again."));
3013
+ }
3014
+ return result;
3015
+ }
3016
+ };
3017
+
2723
3018
  // src/web/server.ts
2724
3019
  async function startWebServer(options) {
2725
3020
  const projectDir = await realpath4(path6.resolve(options.projectDir));
@@ -2738,6 +3033,7 @@ async function startWebServer(options) {
2738
3033
  let authCheck;
2739
3034
  const sensitiveTokens = /* @__PURE__ */ new Set();
2740
3035
  let relatedProjects = options.host?.relatedProjects ?? [];
3036
+ let resourceProjects = options.host?.resourceProjects ?? [];
2741
3037
  const runtime = options.runtime ?? createWebRuntime(projectDir, storageDir, () => connection, () => relatedProjects);
2742
3038
  let inspected;
2743
3039
  try {
@@ -2759,6 +3055,7 @@ async function startWebServer(options) {
2759
3055
  throw new Error("Unsupported Workflow project type.");
2760
3056
  }
2761
3057
  const project = descriptor;
3058
+ const resources = new ResourcePreviews(project, runtime, storageDir, () => connection, () => relatedProjects, () => resourceProjects);
2762
3059
  const key = createHash3("sha256").update(project.assistant ? `assistant:${project.id}:${project.assistant.workdir}` : projectDir).digest("hex").slice(0, 24);
2763
3060
  const historyPath = path6.join(storageDir, key, "conversations.json");
2764
3061
  let saved = {};
@@ -2798,7 +3095,8 @@ async function startWebServer(options) {
2798
3095
  project,
2799
3096
  ...isKanban ? { kanban: kanbanController.state() } : isWorkflow ? workflowActions.state() : { sessions: controller.sessions, activeId: controller.activeId, running: controller.running, parameterValues: controller.current.parameterValues ?? {} },
2800
3097
  account: { ...account, user: account.user && { userId: account.user.userId, email: account.user.email, displayName: account.user.displayName, avatarUrl: account.user.avatarUrl } },
2801
- authenticationRevision: authRevision
3098
+ authenticationRevision: authRevision,
3099
+ resourceRevision: resources.revision
2802
3100
  }, [...sensitiveTokens], false));
2803
3101
  const broadcast = () => {
2804
3102
  if (broadcastTimer) {
@@ -2918,6 +3216,7 @@ ${token}`;
2918
3216
  authIdentity = identity;
2919
3217
  const candidate = !signedOut && serverUrl && token ? { serverUrl: serverUrl.replace(/\/+$/, ""), accessToken: token } : void 0;
2920
3218
  if (changed) {
3219
+ resources.clear();
2921
3220
  connection = void 0;
2922
3221
  authRevision++;
2923
3222
  parameterStates.clear();
@@ -2959,6 +3258,7 @@ ${token}`;
2959
3258
  return;
2960
3259
  }
2961
3260
  connection = void 0;
3261
+ resources.clear();
2962
3262
  kanbanController?.authenticationChanged();
2963
3263
  if (!changed) {
2964
3264
  authRevision++;
@@ -3029,6 +3329,62 @@ ${token}`;
3029
3329
  if (message.projectUri && message.projectUri !== project.uri) {
3030
3330
  throw new Error("Project changed.");
3031
3331
  }
3332
+ if (message.type === "resource-data") {
3333
+ return [{ type: "resource-data", requestId: message.requestId, data: resources.get(String(message.id)) }];
3334
+ }
3335
+ if (message.type === "resource-refresh" || message.type === "resource-select") {
3336
+ const card = resources.get(String(message.id)).card;
3337
+ const selection = message.type === "resource-select" ? message.selection : void 0;
3338
+ const target = { ...card.target };
3339
+ if (selection && ["kv", "knowledge"].includes(card.kind)) {
3340
+ for (const key2 of card.kind === "knowledge" ? ["documentId"] : ["key", "scope", "conversationId"]) {
3341
+ if (typeof selection[key2] === "string" && selection[key2].length <= 8192) {
3342
+ target[key2] = selection[key2];
3343
+ }
3344
+ }
3345
+ }
3346
+ const preview = await resources.open({ ...card, target });
3347
+ return [{ type: "resource-data", requestId: message.requestId, id: preview.id, data: preview.data }];
3348
+ }
3349
+ if (message.type === "resource-bridge") {
3350
+ return [{ type: "resource-bridge", requestId: message.requestId, value: await resources.bridge(String(message.id), message.call) }];
3351
+ }
3352
+ if (message.type === "resource") {
3353
+ if (!["open", "inline", "window", "project", void 0].includes(message.action)) {
3354
+ throw new Error(t("Invalid resource action."));
3355
+ }
3356
+ let items, scopeId;
3357
+ if (workflowController) {
3358
+ const run = workflowController.runs.find((run2) => run2.id === message.runId);
3359
+ if (!run) {
3360
+ throw new Error(t("Run no longer exists."));
3361
+ }
3362
+ items = run.items;
3363
+ scopeId = run.scopeId;
3364
+ } else {
3365
+ const current2 = conversationController?.current;
3366
+ if (!current2 || message.sessionId && message.sessionId !== current2.id) {
3367
+ throw new Error(t("Conversation changed."));
3368
+ }
3369
+ items = current2.messages.find((answer) => answer.id === message.messageId)?.items ?? [];
3370
+ scopeId = current2.id;
3371
+ }
3372
+ const card = conversationResourceCards(items, project.id, scopeId, project.projectInfo?.dataStorage?.mode === "server" ? "server" : "local").find((card2) => card2.id === message.cardId);
3373
+ if (!card) {
3374
+ throw new Error(t("This resource no longer exists."));
3375
+ }
3376
+ const preview = await resources.open(card);
3377
+ return [{
3378
+ type: "resource-preview",
3379
+ requestId: message.requestId,
3380
+ cardId: card.id,
3381
+ action: message.action ?? "open",
3382
+ id: preview.id,
3383
+ url: preview.url,
3384
+ card,
3385
+ project: preview.data.project
3386
+ }];
3387
+ }
3032
3388
  if (kanbanController && !["ready", "login", "verify", "logout", "usage"].includes(message.type)) {
3033
3389
  return kanbanController.handle(message);
3034
3390
  }
@@ -3191,27 +3547,6 @@ ${token}`;
3191
3547
  }
3192
3548
  break;
3193
3549
  }
3194
- case "resource": {
3195
- const answer = current.messages.find((entry) => entry.id === message.messageId);
3196
- const card = conversationResourceCards(answer?.items ?? [], project.id, current.id).find((entry) => entry.id === message.cardId);
3197
- if (!card || card.target.projectId && card.target.projectId !== project.id || card.target.source === "server") {
3198
- throw new Error("Resource is outside this project.");
3199
- }
3200
- if (card.kind === "kv" || card.kind === "knowledge") {
3201
- replies.push({ type: "preview", title: card.title, content: await runtime.readResource(project, current.id, card.kind, card.target) });
3202
- } else if (card.kind === "file") {
3203
- const root = await realpath4(project.assistant?.workdir ?? projectDir);
3204
- const filename = await realpath4(path6.resolve(root, card.target.filePath));
3205
- const relative = path6.relative(root, filename);
3206
- if (relative === ".." || relative.startsWith(`..${path6.sep}`) || path6.isAbsolute(relative)) {
3207
- throw new Error("File is outside this project.");
3208
- }
3209
- replies.push({ type: "preview", title: card.title, content: await readFile5(filename, "utf8") });
3210
- } else {
3211
- throw new Error("This resource requires an editor host.");
3212
- }
3213
- break;
3214
- }
3215
3550
  case "usage": {
3216
3551
  if (!connection) {
3217
3552
  throw new Error("Workflow Server login is not configured.");
@@ -3309,13 +3644,14 @@ ${token}`;
3309
3644
  const host = new URL(`http://${request.headers.host ?? ""}`);
3310
3645
  const forwarded = !!options.host && ["127.0.0.1", "localhost", "[::1]"].includes(host.hostname) && !host.username && !host.password && host.pathname === "/" && !host.search && !host.hash;
3311
3646
  const url = new URL(request.url ?? "/", origin);
3312
- const sandboxAsset = !!kanbanController && url.pathname.startsWith("/kanban/") && ["GET", "HEAD"].includes(request.method ?? "") && request.headers.origin === "null";
3647
+ const resourceAsset = url.pathname.startsWith("/kanban/") || url.pathname.startsWith("/resource-assets/");
3648
+ const sandboxAsset = resourceAsset && ["GET", "HEAD"].includes(request.method ?? "") && request.headers.origin === "null";
3313
3649
  if (request.headers.host !== new URL(origin).host && !forwarded || request.headers.origin && request.headers.origin !== (forwarded ? host.origin : origin) && !sandboxAsset) {
3314
3650
  json(response, 403, { message: "Invalid request origin." });
3315
3651
  return;
3316
3652
  }
3317
- if (kanbanController && url.pathname.startsWith("/kanban/") && ["GET", "HEAD"].includes(request.method ?? "")) {
3318
- const asset = await kanbanController.preview.asset(url);
3653
+ if (resourceAsset && ["GET", "HEAD"].includes(request.method ?? "")) {
3654
+ const asset = await kanbanController?.preview.asset(url) ?? await resources.asset(url);
3319
3655
  if (!asset) {
3320
3656
  json(response, 404, { message: "Kanban asset not found." });
3321
3657
  return;
@@ -3349,6 +3685,10 @@ ${token}`;
3349
3685
  json(response, 200, { connected: true });
3350
3686
  return;
3351
3687
  }
3688
+ if (url.pathname === "/api/resource-state" && request.method === "GET") {
3689
+ json(response, 200, { type: "state", authenticationRevision: authRevision, resourceRevision: resources.revision });
3690
+ return;
3691
+ }
3352
3692
  if (url.pathname === "/api/events" && request.method === "GET") {
3353
3693
  const disconnectKanban = kanbanController?.methods.connect(url.searchParams.get("clientId") ?? "");
3354
3694
  response.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
@@ -3370,7 +3710,7 @@ ${token}`;
3370
3710
  const operation = () => handleMessage(message).then((value2) => {
3371
3711
  messages = value2;
3372
3712
  });
3373
- const concurrent = ["resolve", "ready", "kanban-bridge", "kanban-method-state", "kanban-method-reply", "kanban-methods"].includes(message.type);
3713
+ const concurrent = ["resolve", "ready", "resource", "resource-data", "resource-refresh", "resource-select", "resource-bridge", "kanban-bridge", "kanban-method-state", "kanban-method-reply", "kanban-methods"].includes(message.type);
3374
3714
  const task = concurrent ? operation() : mutations.catch(() => void 0).then(operation);
3375
3715
  if (!concurrent) {
3376
3716
  mutations = task;
@@ -3399,10 +3739,10 @@ ${token}`;
3399
3739
  response.end();
3400
3740
  return;
3401
3741
  }
3402
- if (url.pathname === "/") {
3742
+ if (url.pathname === "/" || /^\/resource\/[a-f\d-]{36}$/.test(url.pathname)) {
3403
3743
  const ancestors = options.host ? "'self' vscode-webview: vscode-file: https://*.vscode-cdn.net https://*.vscode-webview.net http://127.0.0.1:* http://localhost:*" : "'self' vscode-webview:";
3404
- response.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Content-Security-Policy": `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: http:; font-src 'self'; connect-src 'self'; media-src 'self' blob:; frame-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors ${ancestors}; form-action 'self'` });
3405
- const ui = isKanban ? "kanban-workspace" : isWorkflow ? "workflow-workspace" : "workflow-ui";
3744
+ response.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "Content-Security-Policy": `default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: http:; font-src 'self'; connect-src 'self'; media-src 'self' blob:; frame-src 'self' https: http:; object-src 'none'; base-uri 'none'; frame-ancestors ${ancestors}; form-action 'self'` });
3745
+ const ui = url.pathname !== "/" ? "resource-preview" : isKanban ? "kanban-workspace" : isWorkflow ? "workflow-workspace" : "workflow-ui";
3406
3746
  response.end(`<!doctype html><html lang="${locale()}" data-theme="${options.theme ?? "auto"}"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="workflow-project" content="${key}"><title>Workflow</title><link rel="stylesheet" href="/assets/codicons/codicon.css"><link rel="stylesheet" href="/assets/${ui}.css"><link rel="stylesheet" href="/assets/theme.css"></head><body data-surface="${options.host?.surface ?? "editor"}" data-account-host="${hostManaged}"><div id="app"></div><script src="/assets/${ui}.js" defer></script></body></html>`);
3407
3747
  return;
3408
3748
  }
@@ -3411,6 +3751,8 @@ ${token}`;
3411
3751
  assets["/assets/workflow-workspace.css"] = "text/css";
3412
3752
  assets["/assets/kanban-workspace.js"] = "application/javascript";
3413
3753
  assets["/assets/kanban-workspace.css"] = "text/css";
3754
+ assets["/assets/resource-preview.js"] = "application/javascript";
3755
+ assets["/assets/resource-preview.css"] = "text/css";
3414
3756
  if (!assets[url.pathname]) {
3415
3757
  json(response, 404, { message: "Not found." });
3416
3758
  return;
@@ -3453,10 +3795,19 @@ ${token}`;
3453
3795
  kanbanController,
3454
3796
  setHostRelatedProjects: (value) => {
3455
3797
  relatedProjects = value;
3798
+ resources.clear();
3799
+ },
3800
+ setHostResourceProjects: (value) => {
3801
+ resourceProjects = value;
3802
+ resources.clear();
3456
3803
  },
3457
3804
  setHostConnection: async (value) => {
3805
+ if (hostManaged && JSON.stringify(value) === JSON.stringify(hostConnection)) {
3806
+ return;
3807
+ }
3458
3808
  authEpoch++;
3459
3809
  authCheck?.abort();
3810
+ resources.clear();
3460
3811
  hostConnection = value;
3461
3812
  hostManaged = true;
3462
3813
  signedOut = !value;
@@ -3492,6 +3843,15 @@ import { createInterface } from "readline";
3492
3843
 
3493
3844
  // src/web/host-config.ts
3494
3845
  import path7 from "path";
3846
+ function parseHostResourceProjects(value) {
3847
+ if (value === void 0) {
3848
+ return [];
3849
+ }
3850
+ if (!Array.isArray(value) || value.length > 100 || value.some((directory) => typeof directory !== "string" || !path7.isAbsolute(directory))) {
3851
+ throw new Error("Invalid host resource projects.");
3852
+ }
3853
+ return [...new Set(value)];
3854
+ }
3495
3855
  function parseHostRelatedProjects(value) {
3496
3856
  if (value === void 0) {
3497
3857
  return [];
@@ -3547,7 +3907,8 @@ function parseHostConfiguration(value) {
3547
3907
  assistant,
3548
3908
  assistantProjectDir,
3549
3909
  connection: parseHostConnection(configuration.connection),
3550
- relatedProjects: parseHostRelatedProjects(configuration.relatedProjects)
3910
+ relatedProjects: parseHostRelatedProjects(configuration.relatedProjects),
3911
+ ...configuration.resourceProjects === void 0 ? {} : { resourceProjects: parseHostResourceProjects(configuration.resourceProjects) }
3551
3912
  };
3552
3913
  }
3553
3914
 
@@ -3659,6 +4020,9 @@ async function main(argv) {
3659
4020
  });
3660
4021
  } else if (message.type === "related-projects") {
3661
4022
  app.setHostRelatedProjects(parseHostRelatedProjects(message.relatedProjects));
4023
+ if (message.resourceProjects !== void 0) {
4024
+ app.setHostResourceProjects(parseHostResourceProjects(message.resourceProjects));
4025
+ }
3662
4026
  } else if (message.type === "close") {
3663
4027
  break;
3664
4028
  } else {