@workflow-code/cli 0.2.4-20261011004.codex-cli-web → 0.2.4-20261011006.codex-cli-web

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,29 +1,52 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  assertLocalCliCoreCompatible
4
- } from "./chunk-T4FXQ7AQ.js";
4
+ } from "./chunk-WN7VAGK6.js";
5
+ import {
6
+ assertKanbanMethodJson,
7
+ consumeKanbanRelatedProjectRevisionStream,
8
+ createDefaultKanbanValues,
9
+ createInjectedKanbanParams,
10
+ createKanbanBridgeError,
11
+ createKanbanContentSecurityPolicy,
12
+ encodeKanbanAssetUrlPath,
13
+ injectKanbanBridge,
14
+ isSafeKanbanPublicAssetPath,
15
+ parseKanbanBridgeCall,
16
+ parseKanbanManifestText,
17
+ parseKanbanMethodDefinitions,
18
+ parseKanbanMethodInvocation,
19
+ readKanbanArtifactConfig,
20
+ readKanbanAssetMimeType,
21
+ sanitizeKanbanValues
22
+ } from "./chunk-HLMW6LAQ.js";
5
23
  import "./chunk-J2JMC4MZ.js";
6
- import "./chunk-G7PZDTYW.js";
24
+ import "./chunk-MNY562W5.js";
7
25
  import {
8
26
  clearCliAuthConfig,
9
27
  readCliAuthConfig,
10
28
  writeCliAuthConfig
11
29
  } from "./chunk-PGM3IPWA.js";
12
- import "./chunk-BP2VDZQV.js";
13
30
  import {
31
+ isWorkflowProjectDataStorageLocationAllowed,
32
+ readWorkflowProjectInfo,
33
+ resolveWorkflowRelatedProjectAccess
34
+ } from "./chunk-P6K3NB5S.js";
35
+ import {
36
+ getCurrentLocale,
14
37
  i18n
15
38
  } from "./chunk-7WEXRL5A.js";
16
39
  import "./chunk-NSPRIPOP.js";
17
40
 
18
41
  // src/web.ts
19
42
  import { spawn as spawn3 } from "child_process";
20
- import path5 from "path";
43
+ import path8 from "path";
21
44
 
22
45
  // src/web/server.ts
23
46
  import { createServer } from "http";
24
- import { createHash, randomBytes, timingSafeEqual } from "crypto";
25
- import { mkdir, readFile as readFile2, rename, writeFile, realpath as realpath2 } from "fs/promises";
26
- import path3 from "path";
47
+ import { createHash as createHash3, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
48
+ import { mkdir as mkdir2, readFile as readFile5, rename as rename2, writeFile as writeFile2, realpath as realpath4 } from "fs/promises";
49
+ import path6 from "path";
27
50
  import os from "os";
28
51
  import { spawn as spawn2 } from "child_process";
29
52
 
@@ -991,7 +1014,8 @@ var WorkflowController = class {
991
1014
  this.confirmInput(record2, record2.submittingInput);
992
1015
  }
993
1016
  if (report.status === "failed") {
994
- record2.error = report.result?.error?.message ?? "Workflow failed.";
1017
+ const data = report.result?.data;
1018
+ record2.error = report.result?.error?.message || (data && typeof data === "object" && "errMessage" in data && typeof data.errMessage === "string" ? data.errMessage : "") || "Workflow failed.";
995
1019
  }
996
1020
  if (record2.submittingInput && pending && report.status === "failed") {
997
1021
  record2.status = "waiting_for_input";
@@ -1143,6 +1167,626 @@ var WorkflowController = class {
1143
1167
  }
1144
1168
  };
1145
1169
 
1170
+ // src/web/kanban-controller.ts
1171
+ import { randomUUID as randomUUID2, createHash as createHash2 } from "crypto";
1172
+ import { mkdir, readFile as readFile3, readdir as readdir2, rename, writeFile, stat as stat2 } from "fs/promises";
1173
+ import path3 from "path";
1174
+ import { watch } from "fs";
1175
+
1176
+ // src/web/kanban-preview.ts
1177
+ import { randomBytes } from "crypto";
1178
+ import { lstat, realpath, readFile, stat } from "fs/promises";
1179
+ import path from "path";
1180
+ function injectTheme(html, nonce) {
1181
+ const script = `<script nonce=${JSON.stringify(nonce)}>(() => {
1182
+ addEventListener('message', event => {
1183
+ const message = event.data;
1184
+ if (event.source !== parent || message?.nonce !== ${JSON.stringify(nonce)} || message.type !== 'workflow-code:kanban-theme' || !['light', 'dark'].includes(message.theme)) return;
1185
+ document.documentElement.dataset.theme = message.theme;
1186
+ document.documentElement.style.colorScheme = message.theme;
1187
+ dispatchEvent(new CustomEvent('workflow-code:kanban-theme-change', { detail: { theme: message.theme } }));
1188
+ });
1189
+ })();</script>`;
1190
+ const head = /<head(?:\s[^>]*)?>/i.exec(html);
1191
+ if (!head || head.index === void 0) {
1192
+ return `${html}${script}`;
1193
+ }
1194
+ const index = head.index + head[0].length;
1195
+ return `${html.slice(0, index)}${script}${html.slice(index)}`;
1196
+ }
1197
+ var KanbanPreview = class {
1198
+ constructor(projectRoot, artifact, configuration) {
1199
+ this.projectRoot = projectRoot;
1200
+ this.artifact = artifact;
1201
+ this.configuration = configuration;
1202
+ }
1203
+ projectRoot;
1204
+ artifact;
1205
+ configuration;
1206
+ nonce = randomBytes(24).toString("base64url");
1207
+ get url() {
1208
+ return `/kanban/${this.nonce}/${encodeKanbanAssetUrlPath(this.artifact.entry)}`;
1209
+ }
1210
+ async asset(url) {
1211
+ const prefix = `/kanban/${this.nonce}/`;
1212
+ if (!url.pathname.startsWith(prefix)) {
1213
+ return;
1214
+ }
1215
+ try {
1216
+ const asset = decodeURIComponent(url.pathname.slice(prefix.length));
1217
+ if (asset.includes("\\") || !isSafeKanbanPublicAssetPath(asset, this.artifact.entry)) {
1218
+ return;
1219
+ }
1220
+ const projectRoot = await realpath(this.projectRoot);
1221
+ let artifactRoot = projectRoot;
1222
+ if (this.artifact.artifactDir !== ".") {
1223
+ for (const segment of this.artifact.artifactDir.split("/")) {
1224
+ artifactRoot = path.join(artifactRoot, segment);
1225
+ if ((await lstat(artifactRoot)).isSymbolicLink()) {
1226
+ return;
1227
+ }
1228
+ }
1229
+ }
1230
+ let filename = artifactRoot;
1231
+ for (const segment of asset.split("/")) {
1232
+ filename = path.join(filename, segment);
1233
+ if ((await lstat(filename)).isSymbolicLink()) {
1234
+ return;
1235
+ }
1236
+ }
1237
+ const canonical = await realpath(filename), relative = path.relative(artifactRoot, canonical);
1238
+ if (!relative || relative.startsWith(`..${path.sep}`) || relative === ".." || path.isAbsolute(relative)) {
1239
+ return;
1240
+ }
1241
+ const info = await stat(canonical);
1242
+ if (!info.isFile() || info.size > 20 * 1024 * 1024) {
1243
+ return;
1244
+ }
1245
+ const headers = { "Content-Type": readKanbanAssetMimeType(asset), "Content-Security-Policy": `${createKanbanContentSecurityPolicy(this.nonce)}; form-action 'none'; frame-ancestors 'self' vscode-webview: vscode-file: https://*.vscode-cdn.net https://*.vscode-webview.net http://127.0.0.1:* http://localhost:*`, "Cache-Control": "no-store", "Access-Control-Allow-Origin": "*", "Referrer-Policy": "no-referrer", "X-Content-Type-Options": "nosniff" };
1246
+ return { headers, content: asset === this.artifact.entry ? injectKanbanBridge(injectTheme(await readFile(canonical, "utf8"), this.nonce), this.configuration(), this.nonce) : await readFile(canonical) };
1247
+ } catch {
1248
+ return;
1249
+ }
1250
+ }
1251
+ };
1252
+
1253
+ // src/web/kanban-methods.ts
1254
+ import { randomUUID } from "crypto";
1255
+ var KanbanMethodHub = class {
1256
+ constructor(configuration, emit) {
1257
+ this.configuration = configuration;
1258
+ this.emit = emit;
1259
+ }
1260
+ configuration;
1261
+ emit;
1262
+ clients = /* @__PURE__ */ new Map();
1263
+ instances = /* @__PURE__ */ new Map();
1264
+ pending = /* @__PURE__ */ new Map();
1265
+ connect(clientId) {
1266
+ if (!/^[a-zA-Z0-9-]{1,200}$/.test(clientId)) {
1267
+ throw new Error("Invalid Kanban client.");
1268
+ }
1269
+ this.disconnect(clientId);
1270
+ const connection = {};
1271
+ this.clients.set(clientId, connection);
1272
+ return () => {
1273
+ if (this.clients.get(clientId) === connection) {
1274
+ this.disconnect(clientId);
1275
+ }
1276
+ };
1277
+ }
1278
+ remove(instanceId) {
1279
+ this.instances.delete(instanceId);
1280
+ for (const [requestId, request] of this.pending) {
1281
+ if (request.instanceId !== instanceId) {
1282
+ continue;
1283
+ }
1284
+ request.finish(new Error("Kanban preview changed or closed."));
1285
+ this.emit({ type: "kanban-method-cancel", requestId, instanceId });
1286
+ }
1287
+ }
1288
+ disconnect(clientId) {
1289
+ this.clients.delete(clientId);
1290
+ for (const [instanceId, instance] of this.instances) {
1291
+ if (instance.clientId === clientId) {
1292
+ this.remove(instanceId);
1293
+ }
1294
+ }
1295
+ }
1296
+ publish(value) {
1297
+ if (!this.clients.has(value.clientId) || typeof value.instanceId !== "string" || !value.instanceId || value.instanceId.length > 200 || value.configurationId !== this.configuration().id || typeof value.ready !== "boolean" || !["fullscreen", "phone", "tablet", "card"].includes(value.presentation?.mode)) {
1298
+ throw new Error("Invalid Kanban method instance.");
1299
+ }
1300
+ const existing = this.instances.get(value.instanceId);
1301
+ if (existing && existing.clientId !== value.clientId) {
1302
+ throw new Error("Kanban method instance belongs to another client.");
1303
+ }
1304
+ this.instances.set(value.instanceId, {
1305
+ ready: value.ready,
1306
+ configurationId: value.configurationId,
1307
+ configurationName: this.configuration().name,
1308
+ presentation: value.presentation,
1309
+ methods: parseKanbanMethodDefinitions(value.methods),
1310
+ seen: Date.now(),
1311
+ clientId: value.clientId
1312
+ });
1313
+ }
1314
+ list() {
1315
+ for (const [id, instance] of this.instances) {
1316
+ if (Date.now() - instance.seen > 15e3) {
1317
+ this.remove(id);
1318
+ }
1319
+ }
1320
+ return [...this.instances].map(([instanceId, { seen: _, clientId: __, ...state }]) => ({ instanceId, ...state }));
1321
+ }
1322
+ async invoke(value) {
1323
+ const call = parseKanbanMethodInvocation(value);
1324
+ this.list();
1325
+ const instance = this.instances.get(call.instanceId);
1326
+ if (!instance?.ready || instance.configurationId !== call.configurationId || call.configurationId !== this.configuration().id) {
1327
+ throw new Error("Kanban method instance changed. List instances again.");
1328
+ }
1329
+ if (!instance.methods.some((method) => method.name === call.method)) {
1330
+ throw new Error("Kanban page did not register this method.");
1331
+ }
1332
+ if ([...this.pending.values()].filter((item) => item.instanceId === call.instanceId).length >= 4) {
1333
+ throw new Error("Too many Kanban method calls.");
1334
+ }
1335
+ const requestId = randomUUID();
1336
+ return new Promise((resolve, reject) => {
1337
+ const timer = setTimeout(() => {
1338
+ this.pending.get(requestId)?.finish(new Error("Kanban method timed out. Inspect state before retrying."));
1339
+ this.emit({ type: "kanban-method-cancel", requestId, instanceId: call.instanceId });
1340
+ }, 15e3);
1341
+ this.pending.set(requestId, { instanceId: call.instanceId, finish: (error, result) => {
1342
+ if (!this.pending.delete(requestId)) {
1343
+ return;
1344
+ }
1345
+ clearTimeout(timer);
1346
+ error ? reject(error) : resolve(result);
1347
+ } });
1348
+ this.emit({ type: "kanban-method-invoke", requestId, ...call });
1349
+ });
1350
+ }
1351
+ reply(message) {
1352
+ const request = this.pending.get(message.requestId);
1353
+ if (!request || request.instanceId !== message.instanceId || this.instances.get(message.instanceId)?.clientId !== message.clientId) {
1354
+ throw new Error("Kanban method request changed.");
1355
+ }
1356
+ if (message.ok === true) {
1357
+ assertKanbanMethodJson(message.value);
1358
+ request.finish(void 0, message.value);
1359
+ } else {
1360
+ request.finish(new Error(typeof message.error === "string" ? message.error.slice(0, 2e3) : "Kanban method failed."));
1361
+ }
1362
+ }
1363
+ clear() {
1364
+ this.instances.clear();
1365
+ for (const [requestId, request] of this.pending) {
1366
+ request.finish(new Error("Kanban preview changed or closed."));
1367
+ this.emit({ type: "kanban-method-cancel", requestId, instanceId: request.instanceId });
1368
+ }
1369
+ }
1370
+ };
1371
+
1372
+ // src/web/kanban-data.ts
1373
+ import { readFile as readFile2, readdir, realpath as realpath2 } from "fs/promises";
1374
+ import { createHash } from "crypto";
1375
+ import path2 from "path";
1376
+ var readKanbanProjectInfo = readWorkflowProjectInfo;
1377
+ var webProjectDataDirectory = (storageDir, projectDir) => path2.join(storageDir, createHash("sha256").update(projectDir).digest("hex").slice(0, 24));
1378
+ async function localKanbanRelatedProjects(project) {
1379
+ const info = readWorkflowProjectInfo(JSON.parse(await readFile2(path2.join(project.folderPath, "package.json"), "utf8")));
1380
+ const relations = info.relatedProjects;
1381
+ const projects = /* @__PURE__ */ new Map();
1382
+ if (!relations.length) {
1383
+ return { info, projects };
1384
+ }
1385
+ const parent = path2.dirname(project.folderPath);
1386
+ const directories = (await readdir(parent, { withFileTypes: true })).filter((item) => item.isDirectory() && !item.name.startsWith(".") && item.name !== "node_modules").slice(0, 500);
1387
+ await Promise.all(directories.map(async (item) => {
1388
+ try {
1389
+ const folderPath = await realpath2(path2.join(parent, item.name));
1390
+ const pkg = JSON.parse(await readFile2(path2.join(folderPath, "package.json"), "utf8"));
1391
+ const relation = relations.find((relation2) => relation2.projectId.toLowerCase() === String(pkg.id).toLowerCase());
1392
+ if (!relation) {
1393
+ return;
1394
+ }
1395
+ const projectInfo = readWorkflowProjectInfo(pkg);
1396
+ const access = resolveWorkflowRelatedProjectAccess({ consumerProjectId: project.id, consumerProjectInfo: info, targetProjectId: pkg.id, targetProjectInfo: projectInfo, alias: relation.alias });
1397
+ if (access && isWorkflowProjectDataStorageLocationAllowed(projectInfo, "local")) {
1398
+ if (projects.has(relation.alias)) {
1399
+ throw new Error(`Duplicate related project UUID: ${pkg.id}`);
1400
+ }
1401
+ projects.set(relation.alias, { folderPath, id: pkg.id, projectInfo });
1402
+ }
1403
+ } catch (error) {
1404
+ if (String(error).includes("Duplicate related project UUID")) {
1405
+ throw error;
1406
+ }
1407
+ }
1408
+ }));
1409
+ return { info, projects };
1410
+ }
1411
+
1412
+ // src/web/kanban-controller.ts
1413
+ var KanbanController = class {
1414
+ constructor(project, runtime, storageDir, hooks) {
1415
+ this.project = project;
1416
+ this.runtime = runtime;
1417
+ this.storageDir = storageDir;
1418
+ this.hooks = hooks;
1419
+ this.historyPath = path3.join(webProjectDataDirectory(storageDir, project.folderPath), "kanban.json");
1420
+ this.methods = new KanbanMethodHub(() => this.configuration(), (message) => hooks.emit(message));
1421
+ }
1422
+ project;
1423
+ runtime;
1424
+ storageDir;
1425
+ hooks;
1426
+ workspace = { version: 1, revision: 0, configurations: [] };
1427
+ params = [];
1428
+ preview;
1429
+ relatedProjectRevisions = [];
1430
+ missingRelatedProjectAliases = [];
1431
+ relatedProjectWarning = "";
1432
+ pendingSave = Promise.resolve();
1433
+ poll;
1434
+ feed;
1435
+ retry;
1436
+ disposed = false;
1437
+ generation = 0;
1438
+ polling = false;
1439
+ watcher;
1440
+ refreshTimer;
1441
+ refreshing;
1442
+ requests = /* @__PURE__ */ new Set();
1443
+ historyPath;
1444
+ methods;
1445
+ get current() {
1446
+ return this.workspace.configurations.find((item) => item.id === this.workspace.activeConfigurationId);
1447
+ }
1448
+ configuration() {
1449
+ const active = this.current;
1450
+ return active ? { id: active.id, name: active.name, params: createInjectedKanbanParams(this.params, active.values) } : { id: "", name: "", params: {} };
1451
+ }
1452
+ state() {
1453
+ return { ...this.workspace, params: this.params, configuration: this.configuration(), preview: { url: this.preview.url, nonce: this.preview.nonce }, relatedProjectRevisions: this.relatedProjectRevisions, missingRelatedProjectAliases: this.missingRelatedProjectAliases, relatedProjectWarning: this.relatedProjectWarning };
1454
+ }
1455
+ async initialize() {
1456
+ try {
1457
+ const saved = JSON.parse(await readFile3(this.historyPath, "utf8"));
1458
+ if (saved.version !== 1 || !Array.isArray(saved.configurations) || saved.configurations.some((item) => typeof item.id !== "string" || typeof item.name !== "string" || !item.values || typeof item.values !== "object")) {
1459
+ throw new Error("Invalid Kanban configuration history.");
1460
+ }
1461
+ this.workspace = { ...saved, revision: Number.isSafeInteger(saved.revision) ? saved.revision : 0 };
1462
+ } catch (error) {
1463
+ if (error.code !== "ENOENT") {
1464
+ throw error;
1465
+ }
1466
+ }
1467
+ await this.refresh();
1468
+ try {
1469
+ this.watcher = watch(this.project.folderPath, { recursive: true }, (_event, filename) => {
1470
+ const file = String(filename).replaceAll("\\", "/");
1471
+ const data = path3.relative(this.project.folderPath, webProjectDataDirectory(this.storageDir, this.project.folderPath)).replaceAll("\\", "/");
1472
+ if (!file || file.split("/").some((part) => part.startsWith(".") || part === "node_modules") || file.startsWith(`${data}/`)) {
1473
+ return;
1474
+ }
1475
+ const artifact = this.project.kanban.artifactDir;
1476
+ if (file !== "package.json" && file !== "kanban.json" && artifact !== "." && file !== artifact && !file.startsWith(`${artifact}/`)) {
1477
+ return;
1478
+ }
1479
+ clearTimeout(this.refreshTimer);
1480
+ this.refreshTimer = setTimeout(() => void this.refresh().catch((error) => this.hooks.log(String(error))), 400);
1481
+ });
1482
+ this.watcher.on("error", (error) => this.hooks.log(String(error)));
1483
+ } catch (error) {
1484
+ this.hooks.log(String(error));
1485
+ }
1486
+ this.poll = setInterval(() => {
1487
+ if (!this.polling && this.project.projectInfo?.dataStorage?.mode !== "server") {
1488
+ this.polling = true;
1489
+ void this.updateLocalRevisions().catch((error) => this.hooks.log(String(error))).finally(() => {
1490
+ this.polling = false;
1491
+ });
1492
+ }
1493
+ }, 2e3);
1494
+ this.poll.unref();
1495
+ }
1496
+ refresh() {
1497
+ clearTimeout(this.refreshTimer);
1498
+ return this.refreshing ??= this.load().finally(() => {
1499
+ this.refreshing = void 0;
1500
+ });
1501
+ }
1502
+ async load() {
1503
+ const pkg = JSON.parse(await readFile3(path3.join(this.project.folderPath, "package.json"), "utf8"));
1504
+ const artifact = readKanbanArtifactConfig(pkg);
1505
+ this.project.kanban = artifact;
1506
+ this.project.projectInfo = readKanbanProjectInfo(pkg);
1507
+ try {
1508
+ this.params = parseKanbanManifestText(await readFile3(path3.join(this.project.folderPath, "kanban.json"), "utf8")).params;
1509
+ } catch (error) {
1510
+ if (error.code !== "ENOENT") {
1511
+ throw error;
1512
+ }
1513
+ this.params = [];
1514
+ }
1515
+ this.project.fields = this.params;
1516
+ for (const item of this.workspace.configurations) {
1517
+ item.values = sanitizeKanbanValues(this.params, item.values);
1518
+ }
1519
+ if (!this.workspace.configurations.length && this.params.length) {
1520
+ this.create();
1521
+ }
1522
+ this.workspace.activeConfigurationId = this.current?.id ?? this.workspace.configurations[0]?.id;
1523
+ this.workspace.revision++;
1524
+ this.newPreview();
1525
+ await this.persist();
1526
+ this.authenticationChanged();
1527
+ await this.updateLocalRevisions();
1528
+ this.hooks.changed();
1529
+ }
1530
+ newPreview() {
1531
+ this.generation++;
1532
+ this.methods.clear();
1533
+ this.preview = new KanbanPreview(this.project.folderPath, this.project.kanban, () => this.configuration());
1534
+ }
1535
+ name() {
1536
+ let index = 1;
1537
+ while (this.workspace.configurations.some((item) => item.name === `\u914D\u7F6E ${index}`)) {
1538
+ index++;
1539
+ }
1540
+ return `\u914D\u7F6E ${index}`;
1541
+ }
1542
+ create(values = createDefaultKanbanValues(this.params), name = this.name()) {
1543
+ const date = (/* @__PURE__ */ new Date()).toISOString(), item = { id: randomUUID2(), name, values: { ...values }, createdAt: date, updatedAt: date };
1544
+ this.workspace.configurations.push(item);
1545
+ this.workspace.activeConfigurationId = item.id;
1546
+ return item;
1547
+ }
1548
+ async persist() {
1549
+ const content = JSON.stringify(this.workspace);
1550
+ this.pendingSave = this.pendingSave.catch(() => void 0).then(async () => {
1551
+ await mkdir(path3.dirname(this.historyPath), { recursive: true });
1552
+ const temp = `${this.historyPath}.${process.pid}.tmp`;
1553
+ await writeFile(temp, content, { mode: 384 });
1554
+ await rename(temp, this.historyPath);
1555
+ });
1556
+ return this.pendingSave;
1557
+ }
1558
+ async handle(message) {
1559
+ if (message.type === "kanban-method-state") {
1560
+ if (message.nonce !== this.preview.nonce) {
1561
+ throw new Error("Kanban preview changed.");
1562
+ }
1563
+ this.methods.publish(message);
1564
+ return [];
1565
+ }
1566
+ if (message.type === "kanban-method-reply") {
1567
+ this.methods.reply(message);
1568
+ return [];
1569
+ }
1570
+ if (message.type === "kanban-methods") {
1571
+ return [{ type: "kanban-methods-result", requestId: message.requestId, value: message.action === "list" ? this.methods.list() : message.action === "invoke" ? await this.methods.invoke(message.call) : (() => {
1572
+ throw new Error("Invalid Kanban methods action.");
1573
+ })() }];
1574
+ }
1575
+ if (message.type === "kanban-bridge") {
1576
+ try {
1577
+ if (message.nonce !== this.preview.nonce) {
1578
+ throw Object.assign(new Error("Kanban preview changed."), { code: "KANBAN_BRIDGE_SESSION_INVALID" });
1579
+ }
1580
+ const value = await this.bridge(parseKanbanBridgeCall(message.call));
1581
+ return [{ type: "kanban-bridge-result", requestId: message.requestId, ok: true, value }];
1582
+ } catch (error) {
1583
+ return [{ type: "kanban-bridge-result", requestId: message.requestId, ok: false, error: createKanbanBridgeError(error) }];
1584
+ }
1585
+ }
1586
+ if (message.type === "kanban-refresh") {
1587
+ await this.refresh();
1588
+ return [{ type: "kanban-updated", requestId: message.requestId, kanban: this.state() }];
1589
+ }
1590
+ if (message.type !== "kanban-configuration") {
1591
+ throw new Error(`Unsupported Kanban action: ${message.type}`);
1592
+ }
1593
+ if (message.revision !== this.workspace.revision) {
1594
+ throw new Error("Kanban configuration changed. Try again.");
1595
+ }
1596
+ const action = String(message.action), item = this.workspace.configurations.find((item2) => item2.id === message.id);
1597
+ if (!["create", "duplicate", "rename", "save", "select", "delete"].includes(action)) {
1598
+ throw new Error("Unknown Kanban configuration action.");
1599
+ }
1600
+ if (action !== "create" && !item) {
1601
+ throw new Error("Kanban configuration no longer exists.");
1602
+ }
1603
+ switch (action) {
1604
+ case "create":
1605
+ if (!this.params.length) {
1606
+ throw new Error("The project does not define parameters.");
1607
+ }
1608
+ this.create();
1609
+ this.newPreview();
1610
+ break;
1611
+ case "duplicate":
1612
+ this.create(sanitizeKanbanValues(this.params, item.values), `${item.name} (copy)`);
1613
+ this.newPreview();
1614
+ break;
1615
+ case "rename": {
1616
+ if (typeof message.name !== "string" || !message.name.trim() || message.name.length > 200) {
1617
+ throw new Error("Invalid configuration name.");
1618
+ }
1619
+ item.name = message.name.trim();
1620
+ item.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1621
+ break;
1622
+ }
1623
+ case "save": {
1624
+ if (!message.values || typeof message.values !== "object" || Array.isArray(message.values) || Object.values(message.values).some((value) => typeof value !== "string")) {
1625
+ throw new Error("Invalid Kanban parameters.");
1626
+ }
1627
+ item.values = sanitizeKanbanValues(this.params, message.values);
1628
+ item.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1629
+ break;
1630
+ }
1631
+ case "select":
1632
+ this.workspace.activeConfigurationId = item.id;
1633
+ this.newPreview();
1634
+ break;
1635
+ case "delete": {
1636
+ if (this.workspace.configurations.length <= 1) {
1637
+ throw new Error("The last configuration cannot be deleted.");
1638
+ }
1639
+ this.workspace.configurations = this.workspace.configurations.filter((value) => value !== item);
1640
+ if (item.id === this.workspace.activeConfigurationId) {
1641
+ this.workspace.activeConfigurationId = this.workspace.configurations[0].id;
1642
+ this.newPreview();
1643
+ }
1644
+ break;
1645
+ }
1646
+ }
1647
+ this.workspace.revision++;
1648
+ await this.persist();
1649
+ this.hooks.changed();
1650
+ return [{ type: "kanban-updated", requestId: message.requestId, kanban: this.state() }];
1651
+ }
1652
+ async bridge(call) {
1653
+ if (this.disposed) {
1654
+ throw new Error("Kanban has closed.");
1655
+ }
1656
+ if (call.operation === "runtime.getLocale") {
1657
+ return getCurrentLocale();
1658
+ }
1659
+ const generation = this.generation;
1660
+ let result;
1661
+ if (this.project.projectInfo?.dataStorage?.mode === "server" || call.operation.startsWith("files.")) {
1662
+ const connection = this.hooks.connection();
1663
+ if (!connection) {
1664
+ throw Object.assign(new Error("Workflow Server login is required."), { code: call.operation.startsWith("files.") ? "KANBAN_FILE_UNAVAILABLE" : "KANBAN_BRIDGE_UNAVAILABLE" });
1665
+ }
1666
+ const abort = new AbortController();
1667
+ this.requests.add(abort);
1668
+ try {
1669
+ const response = await fetch(`${connection.serverUrl}/api/workflows/${encodeURIComponent(this.project.id)}/kanban/bridge`, { method: "POST", headers: { Authorization: `Bearer ${connection.accessToken}`, "Content-Type": "application/json" }, body: JSON.stringify({ call }), redirect: "error", signal: AbortSignal.any([abort.signal, AbortSignal.timeout(call.operation.startsWith("files.") ? 12e4 : 15e3)]) });
1670
+ const value = await response.json();
1671
+ if (!response.ok || value.errCode !== 0) {
1672
+ throw Object.assign(new Error("Kanban Server operation failed."), { code: call.operation.startsWith("files.") ? "KANBAN_FILE_UNAVAILABLE" : "KANBAN_DATA_OPERATION_FAILED" });
1673
+ }
1674
+ result = value.data ?? null;
1675
+ } finally {
1676
+ this.requests.delete(abort);
1677
+ }
1678
+ } else {
1679
+ if (!this.runtime.kanbanBridge) {
1680
+ throw new Error("Project Core does not support Kanban data.");
1681
+ }
1682
+ result = await this.runtime.kanbanBridge(this.project, call);
1683
+ }
1684
+ if (generation !== this.generation || this.disposed) {
1685
+ throw Object.assign(new Error("Kanban preview changed."), { code: "KANBAN_BRIDGE_SESSION_INVALID" });
1686
+ }
1687
+ return result;
1688
+ }
1689
+ authenticationChanged() {
1690
+ this.generation++;
1691
+ this.methods.clear();
1692
+ for (const request of this.requests) {
1693
+ request.abort();
1694
+ }
1695
+ this.feed?.abort();
1696
+ clearTimeout(this.retry);
1697
+ this.relatedProjectRevisions = [];
1698
+ this.relatedProjectWarning = "";
1699
+ this.connectServerRevisions();
1700
+ }
1701
+ connectServerRevisions() {
1702
+ if (this.project.projectInfo?.dataStorage?.mode !== "server" || this.disposed) {
1703
+ return;
1704
+ }
1705
+ this.missingRelatedProjectAliases = [];
1706
+ const connection = this.hooks.connection();
1707
+ if (!connection) {
1708
+ this.relatedProjectWarning = "\u8BF7\u767B\u5F55 Workflow Server \u4EE5\u8FDE\u63A5\u770B\u677F\u6570\u636E\u3002";
1709
+ return;
1710
+ }
1711
+ const abort = this.feed = new AbortController();
1712
+ void (async () => {
1713
+ try {
1714
+ const response = await fetch(`${connection.serverUrl}/api/workflows/${encodeURIComponent(this.project.id)}/kanban/related-project-revisions`, { headers: { Authorization: `Bearer ${connection.accessToken}` }, signal: abort.signal, redirect: "error" });
1715
+ if (!response.ok || !response.body) {
1716
+ throw new Error("Kanban related project updates are unavailable.");
1717
+ }
1718
+ const apply = (revisions) => {
1719
+ if (!abort.signal.aborted) {
1720
+ this.relatedProjectRevisions = revisions;
1721
+ this.relatedProjectWarning = "";
1722
+ this.hooks.changed();
1723
+ }
1724
+ };
1725
+ await consumeKanbanRelatedProjectRevisionStream(response.body, { onSnapshot: (value) => apply(value.revisions), onRevision: (value) => apply([...this.relatedProjectRevisions.filter((item) => item.alias !== value.alias), value]), onUnavailable: () => {
1726
+ throw new Error("Related projects are unavailable.");
1727
+ } }, abort.signal);
1728
+ } catch {
1729
+ if (!abort.signal.aborted) {
1730
+ this.relatedProjectWarning = "\u5173\u8054\u9879\u76EE\u66F4\u65B0\u8FDE\u63A5\u5DF2\u4E2D\u65AD\uFF0C\u6B63\u5728\u91CD\u8FDE\u3002";
1731
+ this.hooks.changed();
1732
+ }
1733
+ }
1734
+ if (!abort.signal.aborted && !this.disposed) {
1735
+ this.retry = setTimeout(() => this.connectServerRevisions(), 5e3);
1736
+ this.retry.unref();
1737
+ }
1738
+ })();
1739
+ }
1740
+ async updateLocalRevisions() {
1741
+ if (this.project.projectInfo?.dataStorage?.mode === "server" || this.disposed) {
1742
+ return;
1743
+ }
1744
+ const generation = this.generation, { info, projects } = await localKanbanRelatedProjects(this.project);
1745
+ const missing = info.relatedProjects.filter((item) => !projects.has(item.alias)).map((item) => item.alias);
1746
+ const revisions = await Promise.all([...projects].sort(([left], [right]) => left.localeCompare(right)).map(async ([alias, target]) => {
1747
+ const hash = createHash2("sha256"), root = path3.join(webProjectDataDirectory(this.storageDir, target.folderPath), "kv");
1748
+ const visit = async (directory) => {
1749
+ for (const item of (await readdir2(directory, { withFileTypes: true }).catch(() => [])).sort((a, b) => a.name.localeCompare(b.name))) {
1750
+ if (item.name.startsWith(".") || item.name.endsWith(".lock") || item.name.endsWith(".tmp")) {
1751
+ continue;
1752
+ }
1753
+ const file = path3.join(directory, item.name);
1754
+ if (item.isDirectory()) {
1755
+ await visit(file);
1756
+ } else if (item.isFile()) {
1757
+ const info2 = await stat2(file).catch(() => void 0);
1758
+ hash.update(`${file}:${info2?.size}:${info2?.mtimeMs}`);
1759
+ }
1760
+ }
1761
+ };
1762
+ await visit(root);
1763
+ return { alias, revision: hash.digest("hex") };
1764
+ }));
1765
+ if (generation !== this.generation || this.disposed) {
1766
+ return;
1767
+ }
1768
+ if (JSON.stringify([missing, revisions]) !== JSON.stringify([this.missingRelatedProjectAliases, this.relatedProjectRevisions])) {
1769
+ this.missingRelatedProjectAliases = missing;
1770
+ this.relatedProjectRevisions = revisions;
1771
+ this.hooks.changed();
1772
+ }
1773
+ }
1774
+ async close() {
1775
+ this.disposed = true;
1776
+ this.watcher?.close();
1777
+ clearTimeout(this.refreshTimer);
1778
+ this.methods.clear();
1779
+ clearInterval(this.poll);
1780
+ clearTimeout(this.retry);
1781
+ this.feed?.abort();
1782
+ for (const request of this.requests) {
1783
+ request.abort();
1784
+ }
1785
+ await this.refreshing?.catch(() => void 0);
1786
+ await this.pendingSave;
1787
+ }
1788
+ };
1789
+
1146
1790
  // src/web/resource-cards.ts
1147
1791
  var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1148
1792
  var string = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
@@ -1249,8 +1893,8 @@ function conversationResourceCards(items, projectId, conversationId) {
1249
1893
  }
1250
1894
 
1251
1895
  // src/web/workflow-actions.ts
1252
- import { readFile, realpath } from "fs/promises";
1253
- import path from "path";
1896
+ import { readFile as readFile4, realpath as realpath3 } from "fs/promises";
1897
+ import path4 from "path";
1254
1898
  function createWorkflowActions(controller, runtime, project, revision, changed) {
1255
1899
  const states = /* @__PURE__ */ new Map();
1256
1900
  const epochs = /* @__PURE__ */ new Map();
@@ -1439,11 +2083,11 @@ function createWorkflowActions(controller, runtime, project, revision, changed)
1439
2083
  if (card.kind === "kv" || card.kind === "knowledge") {
1440
2084
  replies.push({ type: "preview", title: card.title, content: await runtime.readResource(project, run.scopeId, card.kind, card.target) });
1441
2085
  } else if (card.kind === "file") {
1442
- const root = await realpath(project.folderPath), filename = await realpath(path.resolve(root, card.target.filePath)), relative = path.relative(root, filename);
1443
- if (relative.startsWith("..") || path.isAbsolute(relative)) {
2086
+ const root = await realpath3(project.folderPath), filename = await realpath3(path4.resolve(root, card.target.filePath)), relative = path4.relative(root, filename);
2087
+ if (relative.startsWith("..") || path4.isAbsolute(relative)) {
1444
2088
  throw new Error("File is outside this project.");
1445
2089
  }
1446
- replies.push({ type: "preview", title: card.title, content: await readFile(filename, "utf8") });
2090
+ replies.push({ type: "preview", title: card.title, content: await readFile4(filename, "utf8") });
1447
2091
  } else {
1448
2092
  throw new Error("This resource requires an editor host.");
1449
2093
  }
@@ -1460,25 +2104,25 @@ function createWorkflowActions(controller, runtime, project, revision, changed)
1460
2104
  // src/web/runtime.ts
1461
2105
  import { spawn } from "child_process";
1462
2106
  import { existsSync, readFileSync } from "fs";
1463
- import path2 from "path";
2107
+ import path5 from "path";
1464
2108
  import { fileURLToPath } from "url";
1465
2109
  function cliRoot() {
1466
- let directory = path2.dirname(fileURLToPath(import.meta.url));
2110
+ let directory = path5.dirname(fileURLToPath(import.meta.url));
1467
2111
  for (let depth = 0; depth < 8; depth++) {
1468
2112
  try {
1469
- if (JSON.parse(readFileSync(path2.join(directory, "package.json"), "utf8")).name === "@workflow-code/cli") {
2113
+ if (JSON.parse(readFileSync(path5.join(directory, "package.json"), "utf8")).name === "@workflow-code/cli") {
1470
2114
  return directory;
1471
2115
  }
1472
2116
  } catch {
1473
2117
  }
1474
- directory = path2.dirname(directory);
2118
+ directory = path5.dirname(directory);
1475
2119
  }
1476
2120
  throw new Error("Cannot locate the CLI installation.");
1477
2121
  }
1478
2122
  function createWebRuntime(projectDir, storageDir, connection) {
1479
2123
  const root = cliRoot();
1480
- const builtWorker = path2.join(root, "dist/web/worker.mjs");
1481
- const worker = existsSync(builtWorker) ? builtWorker : path2.join(root, "src/web/worker.mjs");
2124
+ const builtWorker = path5.join(root, "dist/web/worker.mjs");
2125
+ const worker = existsSync(builtWorker) ? builtWorker : path5.join(root, "src/web/worker.mjs");
1482
2126
  const children = /* @__PURE__ */ new Set();
1483
2127
  const active = /* @__PURE__ */ new Map();
1484
2128
  let disposed = false;
@@ -1500,7 +2144,7 @@ function createWebRuntime(projectDir, storageDir, connection) {
1500
2144
  WORKFLOW_SERVER_TOKEN: "",
1501
2145
  WORKFLOW_LLM_SERVER_URL: auth?.serverUrl ?? "",
1502
2146
  WORKFLOW_LLM_SERVER_TOKEN: auth?.accessToken ?? "",
1503
- WORKFLOW_CLI_AUTH_DIR: path2.join(storageDir, "runtime-auth")
2147
+ WORKFLOW_CLI_AUTH_DIR: path5.join(storageDir, "runtime-auth")
1504
2148
  };
1505
2149
  for (const key of [
1506
2150
  "TSX_TSCONFIG_PATH",
@@ -1589,6 +2233,7 @@ function createWebRuntime(projectDir, storageDir, connection) {
1589
2233
  });
1590
2234
  return {
1591
2235
  available: true,
2236
+ kanbanBridge: (project, call) => invoke({ method: "kanban-bridge", project, call }),
1592
2237
  inspect: (paths) => invoke({ method: "inspect", paths }),
1593
2238
  resolveParams: (project, request, entrypointId) => invoke({ method: "resolve", project, request, entrypointId }),
1594
2239
  run: (request, onEvent) => invoke({ method: "run", ...request }, onEvent),
@@ -1707,8 +2352,8 @@ async function inspectWebProject(runtime, projectDir, host) {
1707
2352
 
1708
2353
  // src/web/server.ts
1709
2354
  async function startWebServer(options) {
1710
- const projectDir = await realpath2(path3.resolve(options.projectDir));
1711
- const storageDir = path3.resolve(options.dataDir ?? path3.join(os.homedir(), ".workflow-code", "web"));
2355
+ const projectDir = await realpath4(path6.resolve(options.projectDir));
2356
+ const storageDir = path6.resolve(options.dataDir ?? path6.join(os.homedir(), ".workflow-code", "web"));
1712
2357
  const clients = /* @__PURE__ */ new Set();
1713
2358
  let connection;
1714
2359
  let account = { status: "signed-out", serverUrl: "http://localhost:7126", revision: 0 };
@@ -1738,26 +2383,29 @@ async function startWebServer(options) {
1738
2383
  if (!options.runtime) {
1739
2384
  assertLocalCliCoreCompatible(projectDir, descriptor.runtime.coreVersion);
1740
2385
  }
1741
- if (!["conversation", "workflow"].includes(descriptor.projectType)) {
2386
+ if (!["conversation", "workflow", "kanban"].includes(descriptor.projectType)) {
1742
2387
  runtime.dispose();
1743
- throw new Error("workflow-code web currently supports Conversation and Workflow projects.");
2388
+ throw new Error("Unsupported Workflow project type.");
1744
2389
  }
1745
2390
  const project = descriptor;
1746
- const key = createHash("sha256").update(project.assistant ? `assistant:${project.id}:${project.assistant.workdir}` : projectDir).digest("hex").slice(0, 24);
1747
- const historyPath = path3.join(storageDir, key, "conversations.json");
2391
+ const key = createHash3("sha256").update(project.assistant ? `assistant:${project.id}:${project.assistant.workdir}` : projectDir).digest("hex").slice(0, 24);
2392
+ const historyPath = path6.join(storageDir, key, "conversations.json");
1748
2393
  let saved = {};
1749
- try {
1750
- saved = JSON.parse(await readFile2(historyPath, "utf8"));
1751
- } catch (error) {
1752
- if (error.code !== "ENOENT") {
1753
- runtime.dispose();
1754
- throw new Error(`Cannot read conversation history: ${String(error)}`);
2394
+ if (project.projectType !== "kanban") {
2395
+ try {
2396
+ saved = JSON.parse(await readFile5(historyPath, "utf8"));
2397
+ } catch (error) {
2398
+ if (error.code !== "ENOENT") {
2399
+ runtime.dispose();
2400
+ throw new Error(`Cannot read conversation history: ${String(error)}`);
2401
+ }
1755
2402
  }
1756
2403
  }
1757
2404
  let pendingSave = Promise.resolve();
1758
2405
  let broadcastTimer;
1759
2406
  let authRevision = 0;
1760
2407
  const isWorkflow = project.projectType === "workflow";
2408
+ const isKanban = project.projectType === "kanban";
1761
2409
  const parameterStates = /* @__PURE__ */ new Map();
1762
2410
  const parameterEpochs = /* @__PURE__ */ new Map();
1763
2411
  const publicFile = (file) => ({ ...file, previewUrl: `/api/file/${encodeURIComponent(file.id)}` });
@@ -1777,7 +2425,7 @@ async function startWebServer(options) {
1777
2425
  const state = () => publicValue(redactWorkflowData({
1778
2426
  type: "state",
1779
2427
  project,
1780
- ...isWorkflow ? workflowActions.state() : { sessions: controller.sessions, activeId: controller.activeId, running: controller.running, parameterValues: controller.current.parameterValues ?? {} },
2428
+ ...isKanban ? { kanban: kanbanController.state() } : isWorkflow ? workflowActions.state() : { sessions: controller.sessions, activeId: controller.activeId, running: controller.running, parameterValues: controller.current.parameterValues ?? {} },
1781
2429
  account,
1782
2430
  authenticationRevision: authRevision
1783
2431
  }, [...sensitiveTokens]));
@@ -1795,27 +2443,27 @@ async function startWebServer(options) {
1795
2443
  }
1796
2444
  }, 30);
1797
2445
  };
1798
- const conversationController = isWorkflow ? void 0 : new ConversationController(project, runtime, saved, {
2446
+ const conversationController = isWorkflow || isKanban ? void 0 : new ConversationController(project, runtime, saved, {
1799
2447
  changed: broadcast,
1800
2448
  log: (message) => process.stderr.write(`${message}
1801
2449
  `),
1802
2450
  save: (sessions, activeId) => {
1803
2451
  const content = JSON.stringify({ sessions, activeId });
1804
2452
  pendingSave = pendingSave.catch(() => void 0).then(async () => {
1805
- await mkdir(path3.dirname(historyPath), { recursive: true });
2453
+ await mkdir2(path6.dirname(historyPath), { recursive: true });
1806
2454
  const temp = `${historyPath}.${process.pid}.tmp`;
1807
- await writeFile(temp, content, { mode: 384 });
1808
- await rename(temp, historyPath);
2455
+ await writeFile2(temp, content, { mode: 384 });
2456
+ await rename2(temp, historyPath);
1809
2457
  });
1810
2458
  return pendingSave;
1811
2459
  }
1812
2460
  });
1813
2461
  const controller = conversationController;
1814
- const runsPath = path3.join(storageDir, key, "runs.json");
2462
+ const runsPath = path6.join(storageDir, key, "runs.json");
1815
2463
  let workspace;
1816
2464
  if (isWorkflow) {
1817
2465
  try {
1818
- workspace = JSON.parse(await readFile2(runsPath, "utf8"));
2466
+ workspace = JSON.parse(await readFile5(runsPath, "utf8"));
1819
2467
  if (workspace?.version !== 1 || !Array.isArray(workspace.runs) || !workspace.drafts || typeof workspace.drafts !== "object") {
1820
2468
  throw new Error("Invalid Workflow history.");
1821
2469
  }
@@ -1835,15 +2483,32 @@ async function startWebServer(options) {
1835
2483
  save: (workspace2) => {
1836
2484
  const content = JSON.stringify(workspace2);
1837
2485
  pendingSave = pendingSave.catch(() => void 0).then(async () => {
1838
- await mkdir(path3.dirname(runsPath), { recursive: true });
2486
+ await mkdir2(path6.dirname(runsPath), { recursive: true });
1839
2487
  const temp = `${runsPath}.${process.pid}.tmp`;
1840
- await writeFile(temp, content, { mode: 384 });
1841
- await rename(temp, runsPath);
2488
+ await writeFile2(temp, content, { mode: 384 });
2489
+ await rename2(temp, runsPath);
1842
2490
  });
1843
2491
  return pendingSave;
1844
2492
  }
1845
2493
  }) : void 0;
1846
2494
  const workflowActions = workflowController ? createWorkflowActions(workflowController, runtime, project, () => authRevision, broadcast) : void 0;
2495
+ const kanbanController = isKanban ? new KanbanController(project, runtime, storageDir, { changed: broadcast, emit: (message) => {
2496
+ for (const client of clients) {
2497
+ client.write(`data: ${JSON.stringify(message)}
2498
+
2499
+ `);
2500
+ }
2501
+ }, connection: () => connection, log: (message) => process.stderr.write(`${message}
2502
+ `) }) : void 0;
2503
+ if (kanbanController) {
2504
+ try {
2505
+ await kanbanController.initialize();
2506
+ } catch (error) {
2507
+ await kanbanController.close();
2508
+ runtime.dispose();
2509
+ throw error;
2510
+ }
2511
+ }
1847
2512
  if (workflowController) {
1848
2513
  await workflowController.persist();
1849
2514
  }
@@ -1886,6 +2551,7 @@ ${token}`;
1886
2551
  authRevision++;
1887
2552
  parameterStates.clear();
1888
2553
  workflowActions?.invalidate();
2554
+ kanbanController?.authenticationChanged();
1889
2555
  if (authenticationInitialized) {
1890
2556
  await stopForAuthentication();
1891
2557
  }
@@ -1911,6 +2577,7 @@ ${token}`;
1911
2577
  workflowActions?.invalidate();
1912
2578
  }
1913
2579
  connection = candidate;
2580
+ kanbanController?.authenticationChanged();
1914
2581
  const savedConnection = !hostManaged && candidate.serverUrl === config.serverUrl.replace(/\/+$/, "") && candidate.accessToken === config.apiKey;
1915
2582
  account = { ...account, status: "authenticated", user: profile, expiresAt: result.data.principal?.expiresAt ?? (savedConnection ? config.expiresAt : void 0) };
1916
2583
  } catch (error) {
@@ -1918,6 +2585,7 @@ ${token}`;
1918
2585
  return;
1919
2586
  }
1920
2587
  connection = void 0;
2588
+ kanbanController?.authenticationChanged();
1921
2589
  if (!changed) {
1922
2590
  authRevision++;
1923
2591
  parameterStates.clear();
@@ -1935,8 +2603,8 @@ ${token}`;
1935
2603
  const authPoll = setInterval(() => void refreshAuth().catch((error) => process.stderr.write(`${String(error)}
1936
2604
  `)), 5e3);
1937
2605
  authPoll.unref();
1938
- const capability = randomBytes(32).toString("hex");
1939
- const cookie = `workflow_web_${randomBytes(8).toString("hex")}`;
2606
+ const capability = randomBytes2(32).toString("hex");
2607
+ const cookie = `workflow_web_${randomBytes2(8).toString("hex")}`;
1940
2608
  let origin = "";
1941
2609
  const json = (response, status, value) => {
1942
2610
  response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
@@ -1984,6 +2652,9 @@ ${token}`;
1984
2652
  if (message.projectUri && message.projectUri !== project.uri) {
1985
2653
  throw new Error("Project changed.");
1986
2654
  }
2655
+ if (kanbanController && !["ready", "login", "verify", "logout", "usage"].includes(message.type)) {
2656
+ return kanbanController.handle(message);
2657
+ }
1987
2658
  if (workflowActions && !["ready", "login", "verify", "logout", "usage"].includes(message.type)) {
1988
2659
  return publicValue(await workflowActions.handle(message));
1989
2660
  }
@@ -2152,13 +2823,13 @@ ${token}`;
2152
2823
  if (card.kind === "kv" || card.kind === "knowledge") {
2153
2824
  replies.push({ type: "preview", title: card.title, content: await runtime.readResource(project, current.id, card.kind, card.target) });
2154
2825
  } else if (card.kind === "file") {
2155
- const root = await realpath2(project.assistant?.workdir ?? projectDir);
2156
- const filename = await realpath2(path3.resolve(root, card.target.filePath));
2157
- const relative = path3.relative(root, filename);
2158
- if (relative === ".." || relative.startsWith(`..${path3.sep}`) || path3.isAbsolute(relative)) {
2826
+ const root = await realpath4(project.assistant?.workdir ?? projectDir);
2827
+ const filename = await realpath4(path6.resolve(root, card.target.filePath));
2828
+ const relative = path6.relative(root, filename);
2829
+ if (relative === ".." || relative.startsWith(`..${path6.sep}`) || path6.isAbsolute(relative)) {
2159
2830
  throw new Error("File is outside this project.");
2160
2831
  }
2161
- replies.push({ type: "preview", title: card.title, content: await readFile2(filename, "utf8") });
2832
+ replies.push({ type: "preview", title: card.title, content: await readFile5(filename, "utf8") });
2162
2833
  } else {
2163
2834
  throw new Error("This resource requires an editor host.");
2164
2835
  }
@@ -2215,7 +2886,7 @@ ${token}`;
2215
2886
  await refreshAuth(true);
2216
2887
  break;
2217
2888
  }
2218
- login = spawn2(process.execPath, [path3.join(cliRoot(), "dist/index.js"), "workspace", "login", "--server", url.toString().replace(/\/$/, "")], { shell: false, stdio: ["ignore", "ignore", "pipe"] });
2889
+ login = spawn2(process.execPath, [path6.join(cliRoot(), "dist/index.js"), "workspace", "login", "--server", url.toString().replace(/\/$/, "")], { shell: false, stdio: ["ignore", "ignore", "pipe"] });
2219
2890
  login.stderr?.on("data", (chunk) => process.stderr.write(chunk));
2220
2891
  login.once("error", (error) => {
2221
2892
  login = void 0;
@@ -2253,11 +2924,22 @@ ${token}`;
2253
2924
  response.setHeader("Referrer-Policy", "no-referrer");
2254
2925
  const host = new URL(`http://${request.headers.host ?? ""}`);
2255
2926
  const forwarded = !!options.host && ["127.0.0.1", "localhost", "[::1]"].includes(host.hostname) && !host.username && !host.password && host.pathname === "/" && !host.search && !host.hash;
2256
- if (request.headers.host !== new URL(origin).host && !forwarded || request.headers.origin && request.headers.origin !== (forwarded ? host.origin : origin)) {
2927
+ const url = new URL(request.url ?? "/", origin);
2928
+ const sandboxAsset = !!kanbanController && url.pathname.startsWith("/kanban/") && ["GET", "HEAD"].includes(request.method ?? "") && request.headers.origin === "null";
2929
+ if (request.headers.host !== new URL(origin).host && !forwarded || request.headers.origin && request.headers.origin !== (forwarded ? host.origin : origin) && !sandboxAsset) {
2257
2930
  json(response, 403, { message: "Invalid request origin." });
2258
2931
  return;
2259
2932
  }
2260
- const url = new URL(request.url ?? "/", origin);
2933
+ if (kanbanController && url.pathname.startsWith("/kanban/") && ["GET", "HEAD"].includes(request.method ?? "")) {
2934
+ const asset = await kanbanController.preview.asset(url);
2935
+ if (!asset) {
2936
+ json(response, 404, { message: "Kanban asset not found." });
2937
+ return;
2938
+ }
2939
+ response.writeHead(200, asset.headers);
2940
+ response.end(request.method === "HEAD" ? void 0 : asset.content);
2941
+ return;
2942
+ }
2261
2943
  if (url.pathname === "/api/session" && request.method === "POST") {
2262
2944
  if (!authMatches(String(request.headers.authorization ?? "").replace(/^Bearer /, ""))) {
2263
2945
  json(response, 401, { message: "Invalid session." });
@@ -2284,6 +2966,7 @@ ${token}`;
2284
2966
  return;
2285
2967
  }
2286
2968
  if (url.pathname === "/api/events" && request.method === "GET") {
2969
+ const disconnectKanban = kanbanController?.methods.connect(url.searchParams.get("clientId") ?? "");
2287
2970
  response.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
2288
2971
  response.write(`data: ${JSON.stringify(state())}
2289
2972
 
@@ -2293,6 +2976,7 @@ ${token}`;
2293
2976
  response.once("close", () => {
2294
2977
  clearInterval(heartbeat);
2295
2978
  clients.delete(response);
2979
+ disconnectKanban?.();
2296
2980
  });
2297
2981
  return;
2298
2982
  }
@@ -2302,8 +2986,9 @@ ${token}`;
2302
2986
  const operation = () => handleMessage(message).then((value2) => {
2303
2987
  messages = value2;
2304
2988
  });
2305
- const task = message.type === "resolve" || message.type === "ready" ? operation() : mutations.catch(() => void 0).then(operation);
2306
- if (message.type !== "resolve" && message.type !== "ready") {
2989
+ const concurrent = ["resolve", "ready", "kanban-bridge", "kanban-method-state", "kanban-method-reply", "kanban-methods"].includes(message.type);
2990
+ const task = concurrent ? operation() : mutations.catch(() => void 0).then(operation);
2991
+ if (!concurrent) {
2307
2992
  mutations = task;
2308
2993
  }
2309
2994
  await task;
@@ -2312,7 +2997,7 @@ ${token}`;
2312
2997
  }
2313
2998
  if (url.pathname.startsWith("/api/file/") && request.method === "GET") {
2314
2999
  const file = await runtime.readFile(project, decodeURIComponent(url.pathname.slice("/api/file/".length)));
2315
- const bytes = await readFile2(file.path);
3000
+ const bytes = await readFile5(file.path);
2316
3001
  const safeType = /^(?:image\/(?:png|jpeg|gif|webp|avif)|audio\/[a-z0-9.+-]+|video\/[a-z0-9.+-]+)$/.test(file.reference.mimeType);
2317
3002
  response.writeHead(200, { "Content-Type": safeType ? file.reference.mimeType : "application/octet-stream", "Content-Disposition": `${safeType ? "inline" : "attachment"}; filename*=UTF-8''${encodeURIComponent(file.reference.name)}`, "Cache-Control": "no-store" });
2318
3003
  response.end(bytes);
@@ -2332,19 +3017,21 @@ ${token}`;
2332
3017
  }
2333
3018
  if (url.pathname === "/") {
2334
3019
  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:";
2335
- 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:; object-src 'none'; base-uri 'none'; frame-ancestors ${ancestors}; form-action 'self'` });
2336
- const ui = isWorkflow ? "workflow-workspace" : "workflow-ui";
3020
+ 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'` });
3021
+ const ui = isKanban ? "kanban-workspace" : isWorkflow ? "workflow-workspace" : "workflow-ui";
2337
3022
  response.end(`<!doctype html><html lang="zh-CN" 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"}"><div id="app"></div><script src="/assets/${ui}.js" defer></script></body></html>`);
2338
3023
  return;
2339
3024
  }
2340
3025
  const assets = { "/assets/workflow-ui.js": "application/javascript", "/assets/workflow-ui.css": "text/css", "/assets/theme.css": "text/css", "/assets/codicons/codicon.css": "text/css", "/assets/codicons/codicon.ttf": "font/ttf" };
2341
3026
  assets["/assets/workflow-workspace.js"] = "application/javascript";
2342
3027
  assets["/assets/workflow-workspace.css"] = "text/css";
3028
+ assets["/assets/kanban-workspace.js"] = "application/javascript";
3029
+ assets["/assets/kanban-workspace.css"] = "text/css";
2343
3030
  if (!assets[url.pathname]) {
2344
3031
  json(response, 404, { message: "Not found." });
2345
3032
  return;
2346
3033
  }
2347
- const content = await readFile2(path3.join(cliRoot(), "dist/web", url.pathname.slice("/assets/".length)));
3034
+ const content = await readFile5(path6.join(cliRoot(), "dist/web", url.pathname.slice("/assets/".length)));
2348
3035
  response.writeHead(200, { "Content-Type": assets[url.pathname], "Cache-Control": "no-cache" });
2349
3036
  response.end(content);
2350
3037
  })().catch((error) => {
@@ -2367,6 +3054,7 @@ ${token}`;
2367
3054
  clearInterval(authPoll);
2368
3055
  conversationController?.dispose();
2369
3056
  workflowController?.dispose();
3057
+ await kanbanController?.close();
2370
3058
  runtime.dispose();
2371
3059
  throw error;
2372
3060
  }
@@ -2378,6 +3066,7 @@ ${token}`;
2378
3066
  project,
2379
3067
  controller,
2380
3068
  workflowController,
3069
+ kanbanController,
2381
3070
  setHostConnection: async (value) => {
2382
3071
  authEpoch++;
2383
3072
  authCheck?.abort();
@@ -2397,6 +3086,7 @@ ${token}`;
2397
3086
  conversationController?.dispose();
2398
3087
  workflowController?.dispose();
2399
3088
  runtime.dispose();
3089
+ await kanbanController?.close();
2400
3090
  for (const client of clients) {
2401
3091
  client.end();
2402
3092
  }
@@ -2414,7 +3104,7 @@ ${token}`;
2414
3104
  import { createInterface } from "readline";
2415
3105
 
2416
3106
  // src/web/host-config.ts
2417
- import path4 from "path";
3107
+ import path7 from "path";
2418
3108
  function parseHostConnection(value) {
2419
3109
  if (value === null || value === void 0) {
2420
3110
  return;
@@ -2438,11 +3128,11 @@ function parseHostConfiguration(value) {
2438
3128
  throw new Error("Invalid web host configuration.");
2439
3129
  }
2440
3130
  const assistant = configuration.assistant;
2441
- if (assistant && (typeof assistant.workdir !== "string" || !path4.isAbsolute(assistant.workdir) || !Array.isArray(assistant.roots) || !assistant.roots.length || assistant.roots.length > 100 || assistant.standalone !== void 0 && typeof assistant.standalone !== "boolean" || assistant.roots.some((root) => typeof root.uri !== "string" || typeof root.name !== "string" || typeof root.folderPath !== "string" || !path4.isAbsolute(root.folderPath)) || !assistant.roots.some((root) => root.folderPath === assistant.workdir))) {
3131
+ if (assistant && (typeof assistant.workdir !== "string" || !path7.isAbsolute(assistant.workdir) || !Array.isArray(assistant.roots) || !assistant.roots.length || assistant.roots.length > 100 || assistant.standalone !== void 0 && typeof assistant.standalone !== "boolean" || assistant.roots.some((root) => typeof root.uri !== "string" || typeof root.name !== "string" || typeof root.folderPath !== "string" || !path7.isAbsolute(root.folderPath)) || !assistant.roots.some((root) => root.folderPath === assistant.workdir))) {
2442
3132
  throw new Error("Invalid assistant workspace.");
2443
3133
  }
2444
3134
  const assistantProjectDir = configuration.assistantProjectDir;
2445
- if (configuration.surface === "assistant" && (!assistant || typeof assistantProjectDir !== "string" || !path4.isAbsolute(assistantProjectDir))) {
3135
+ if (configuration.surface === "assistant" && (!assistant || typeof assistantProjectDir !== "string" || !path7.isAbsolute(assistantProjectDir))) {
2446
3136
  throw new Error("A dedicated assistant project and its workspace are required.");
2447
3137
  }
2448
3138
  if (configuration.surface !== "assistant" && (assistant || assistantProjectDir !== void 0)) {
@@ -2483,12 +3173,12 @@ function parseWebArgs(argv) {
2483
3173
  }
2484
3174
  options.theme = value;
2485
3175
  } else {
2486
- options.dataDir = path5.resolve(value);
3176
+ options.dataDir = path8.resolve(value);
2487
3177
  }
2488
3178
  } else if (arg.startsWith("-") || projectSet) {
2489
3179
  throw new Error(`Unknown web argument: ${arg}`);
2490
3180
  } else {
2491
- options.projectDir = path5.resolve(arg);
3181
+ options.projectDir = path8.resolve(arg);
2492
3182
  projectSet = true;
2493
3183
  }
2494
3184
  }