@workflow-code/cli 0.2.4-20261011005.codex-cli-web → 0.2.4-20261011007.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
 
@@ -1144,6 +1167,626 @@ var WorkflowController = class {
1144
1167
  }
1145
1168
  };
1146
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
+
1147
1790
  // src/web/resource-cards.ts
1148
1791
  var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1149
1792
  var string = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
@@ -1250,8 +1893,8 @@ function conversationResourceCards(items, projectId, conversationId) {
1250
1893
  }
1251
1894
 
1252
1895
  // src/web/workflow-actions.ts
1253
- import { readFile, realpath } from "fs/promises";
1254
- import path from "path";
1896
+ import { readFile as readFile4, realpath as realpath3 } from "fs/promises";
1897
+ import path4 from "path";
1255
1898
  function createWorkflowActions(controller, runtime, project, revision, changed) {
1256
1899
  const states = /* @__PURE__ */ new Map();
1257
1900
  const epochs = /* @__PURE__ */ new Map();
@@ -1440,11 +2083,11 @@ function createWorkflowActions(controller, runtime, project, revision, changed)
1440
2083
  if (card.kind === "kv" || card.kind === "knowledge") {
1441
2084
  replies.push({ type: "preview", title: card.title, content: await runtime.readResource(project, run.scopeId, card.kind, card.target) });
1442
2085
  } else if (card.kind === "file") {
1443
- const root = await realpath(project.folderPath), filename = await realpath(path.resolve(root, card.target.filePath)), relative = path.relative(root, filename);
1444
- 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)) {
1445
2088
  throw new Error("File is outside this project.");
1446
2089
  }
1447
- 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") });
1448
2091
  } else {
1449
2092
  throw new Error("This resource requires an editor host.");
1450
2093
  }
@@ -1461,25 +2104,25 @@ function createWorkflowActions(controller, runtime, project, revision, changed)
1461
2104
  // src/web/runtime.ts
1462
2105
  import { spawn } from "child_process";
1463
2106
  import { existsSync, readFileSync } from "fs";
1464
- import path2 from "path";
2107
+ import path5 from "path";
1465
2108
  import { fileURLToPath } from "url";
1466
2109
  function cliRoot() {
1467
- let directory = path2.dirname(fileURLToPath(import.meta.url));
2110
+ let directory = path5.dirname(fileURLToPath(import.meta.url));
1468
2111
  for (let depth = 0; depth < 8; depth++) {
1469
2112
  try {
1470
- 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") {
1471
2114
  return directory;
1472
2115
  }
1473
2116
  } catch {
1474
2117
  }
1475
- directory = path2.dirname(directory);
2118
+ directory = path5.dirname(directory);
1476
2119
  }
1477
2120
  throw new Error("Cannot locate the CLI installation.");
1478
2121
  }
1479
2122
  function createWebRuntime(projectDir, storageDir, connection) {
1480
2123
  const root = cliRoot();
1481
- const builtWorker = path2.join(root, "dist/web/worker.mjs");
1482
- 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");
1483
2126
  const children = /* @__PURE__ */ new Set();
1484
2127
  const active = /* @__PURE__ */ new Map();
1485
2128
  let disposed = false;
@@ -1501,7 +2144,7 @@ function createWebRuntime(projectDir, storageDir, connection) {
1501
2144
  WORKFLOW_SERVER_TOKEN: "",
1502
2145
  WORKFLOW_LLM_SERVER_URL: auth?.serverUrl ?? "",
1503
2146
  WORKFLOW_LLM_SERVER_TOKEN: auth?.accessToken ?? "",
1504
- WORKFLOW_CLI_AUTH_DIR: path2.join(storageDir, "runtime-auth")
2147
+ WORKFLOW_CLI_AUTH_DIR: path5.join(storageDir, "runtime-auth")
1505
2148
  };
1506
2149
  for (const key of [
1507
2150
  "TSX_TSCONFIG_PATH",
@@ -1590,6 +2233,7 @@ function createWebRuntime(projectDir, storageDir, connection) {
1590
2233
  });
1591
2234
  return {
1592
2235
  available: true,
2236
+ kanbanBridge: (project, call) => invoke({ method: "kanban-bridge", project, call }),
1593
2237
  inspect: (paths) => invoke({ method: "inspect", paths }),
1594
2238
  resolveParams: (project, request, entrypointId) => invoke({ method: "resolve", project, request, entrypointId }),
1595
2239
  run: (request, onEvent) => invoke({ method: "run", ...request }, onEvent),
@@ -1708,8 +2352,8 @@ async function inspectWebProject(runtime, projectDir, host) {
1708
2352
 
1709
2353
  // src/web/server.ts
1710
2354
  async function startWebServer(options) {
1711
- const projectDir = await realpath2(path3.resolve(options.projectDir));
1712
- 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"));
1713
2357
  const clients = /* @__PURE__ */ new Set();
1714
2358
  let connection;
1715
2359
  let account = { status: "signed-out", serverUrl: "http://localhost:7126", revision: 0 };
@@ -1739,26 +2383,29 @@ async function startWebServer(options) {
1739
2383
  if (!options.runtime) {
1740
2384
  assertLocalCliCoreCompatible(projectDir, descriptor.runtime.coreVersion);
1741
2385
  }
1742
- if (!["conversation", "workflow"].includes(descriptor.projectType)) {
2386
+ if (!["conversation", "workflow", "kanban"].includes(descriptor.projectType)) {
1743
2387
  runtime.dispose();
1744
- throw new Error("workflow-code web currently supports Conversation and Workflow projects.");
2388
+ throw new Error("Unsupported Workflow project type.");
1745
2389
  }
1746
2390
  const project = descriptor;
1747
- const key = createHash("sha256").update(project.assistant ? `assistant:${project.id}:${project.assistant.workdir}` : projectDir).digest("hex").slice(0, 24);
1748
- 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");
1749
2393
  let saved = {};
1750
- try {
1751
- saved = JSON.parse(await readFile2(historyPath, "utf8"));
1752
- } catch (error) {
1753
- if (error.code !== "ENOENT") {
1754
- runtime.dispose();
1755
- 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
+ }
1756
2402
  }
1757
2403
  }
1758
2404
  let pendingSave = Promise.resolve();
1759
2405
  let broadcastTimer;
1760
2406
  let authRevision = 0;
1761
2407
  const isWorkflow = project.projectType === "workflow";
2408
+ const isKanban = project.projectType === "kanban";
1762
2409
  const parameterStates = /* @__PURE__ */ new Map();
1763
2410
  const parameterEpochs = /* @__PURE__ */ new Map();
1764
2411
  const publicFile = (file) => ({ ...file, previewUrl: `/api/file/${encodeURIComponent(file.id)}` });
@@ -1778,7 +2425,7 @@ async function startWebServer(options) {
1778
2425
  const state = () => publicValue(redactWorkflowData({
1779
2426
  type: "state",
1780
2427
  project,
1781
- ...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 ?? {} },
1782
2429
  account,
1783
2430
  authenticationRevision: authRevision
1784
2431
  }, [...sensitiveTokens]));
@@ -1796,27 +2443,27 @@ async function startWebServer(options) {
1796
2443
  }
1797
2444
  }, 30);
1798
2445
  };
1799
- const conversationController = isWorkflow ? void 0 : new ConversationController(project, runtime, saved, {
2446
+ const conversationController = isWorkflow || isKanban ? void 0 : new ConversationController(project, runtime, saved, {
1800
2447
  changed: broadcast,
1801
2448
  log: (message) => process.stderr.write(`${message}
1802
2449
  `),
1803
2450
  save: (sessions, activeId) => {
1804
2451
  const content = JSON.stringify({ sessions, activeId });
1805
2452
  pendingSave = pendingSave.catch(() => void 0).then(async () => {
1806
- await mkdir(path3.dirname(historyPath), { recursive: true });
2453
+ await mkdir2(path6.dirname(historyPath), { recursive: true });
1807
2454
  const temp = `${historyPath}.${process.pid}.tmp`;
1808
- await writeFile(temp, content, { mode: 384 });
1809
- await rename(temp, historyPath);
2455
+ await writeFile2(temp, content, { mode: 384 });
2456
+ await rename2(temp, historyPath);
1810
2457
  });
1811
2458
  return pendingSave;
1812
2459
  }
1813
2460
  });
1814
2461
  const controller = conversationController;
1815
- const runsPath = path3.join(storageDir, key, "runs.json");
2462
+ const runsPath = path6.join(storageDir, key, "runs.json");
1816
2463
  let workspace;
1817
2464
  if (isWorkflow) {
1818
2465
  try {
1819
- workspace = JSON.parse(await readFile2(runsPath, "utf8"));
2466
+ workspace = JSON.parse(await readFile5(runsPath, "utf8"));
1820
2467
  if (workspace?.version !== 1 || !Array.isArray(workspace.runs) || !workspace.drafts || typeof workspace.drafts !== "object") {
1821
2468
  throw new Error("Invalid Workflow history.");
1822
2469
  }
@@ -1836,15 +2483,32 @@ async function startWebServer(options) {
1836
2483
  save: (workspace2) => {
1837
2484
  const content = JSON.stringify(workspace2);
1838
2485
  pendingSave = pendingSave.catch(() => void 0).then(async () => {
1839
- await mkdir(path3.dirname(runsPath), { recursive: true });
2486
+ await mkdir2(path6.dirname(runsPath), { recursive: true });
1840
2487
  const temp = `${runsPath}.${process.pid}.tmp`;
1841
- await writeFile(temp, content, { mode: 384 });
1842
- await rename(temp, runsPath);
2488
+ await writeFile2(temp, content, { mode: 384 });
2489
+ await rename2(temp, runsPath);
1843
2490
  });
1844
2491
  return pendingSave;
1845
2492
  }
1846
2493
  }) : void 0;
1847
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
+ }
1848
2512
  if (workflowController) {
1849
2513
  await workflowController.persist();
1850
2514
  }
@@ -1887,6 +2551,7 @@ ${token}`;
1887
2551
  authRevision++;
1888
2552
  parameterStates.clear();
1889
2553
  workflowActions?.invalidate();
2554
+ kanbanController?.authenticationChanged();
1890
2555
  if (authenticationInitialized) {
1891
2556
  await stopForAuthentication();
1892
2557
  }
@@ -1912,6 +2577,7 @@ ${token}`;
1912
2577
  workflowActions?.invalidate();
1913
2578
  }
1914
2579
  connection = candidate;
2580
+ kanbanController?.authenticationChanged();
1915
2581
  const savedConnection = !hostManaged && candidate.serverUrl === config.serverUrl.replace(/\/+$/, "") && candidate.accessToken === config.apiKey;
1916
2582
  account = { ...account, status: "authenticated", user: profile, expiresAt: result.data.principal?.expiresAt ?? (savedConnection ? config.expiresAt : void 0) };
1917
2583
  } catch (error) {
@@ -1919,6 +2585,7 @@ ${token}`;
1919
2585
  return;
1920
2586
  }
1921
2587
  connection = void 0;
2588
+ kanbanController?.authenticationChanged();
1922
2589
  if (!changed) {
1923
2590
  authRevision++;
1924
2591
  parameterStates.clear();
@@ -1936,8 +2603,8 @@ ${token}`;
1936
2603
  const authPoll = setInterval(() => void refreshAuth().catch((error) => process.stderr.write(`${String(error)}
1937
2604
  `)), 5e3);
1938
2605
  authPoll.unref();
1939
- const capability = randomBytes(32).toString("hex");
1940
- const cookie = `workflow_web_${randomBytes(8).toString("hex")}`;
2606
+ const capability = randomBytes2(32).toString("hex");
2607
+ const cookie = `workflow_web_${randomBytes2(8).toString("hex")}`;
1941
2608
  let origin = "";
1942
2609
  const json = (response, status, value) => {
1943
2610
  response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
@@ -1985,6 +2652,9 @@ ${token}`;
1985
2652
  if (message.projectUri && message.projectUri !== project.uri) {
1986
2653
  throw new Error("Project changed.");
1987
2654
  }
2655
+ if (kanbanController && !["ready", "login", "verify", "logout", "usage"].includes(message.type)) {
2656
+ return kanbanController.handle(message);
2657
+ }
1988
2658
  if (workflowActions && !["ready", "login", "verify", "logout", "usage"].includes(message.type)) {
1989
2659
  return publicValue(await workflowActions.handle(message));
1990
2660
  }
@@ -2153,13 +2823,13 @@ ${token}`;
2153
2823
  if (card.kind === "kv" || card.kind === "knowledge") {
2154
2824
  replies.push({ type: "preview", title: card.title, content: await runtime.readResource(project, current.id, card.kind, card.target) });
2155
2825
  } else if (card.kind === "file") {
2156
- const root = await realpath2(project.assistant?.workdir ?? projectDir);
2157
- const filename = await realpath2(path3.resolve(root, card.target.filePath));
2158
- const relative = path3.relative(root, filename);
2159
- 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)) {
2160
2830
  throw new Error("File is outside this project.");
2161
2831
  }
2162
- 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") });
2163
2833
  } else {
2164
2834
  throw new Error("This resource requires an editor host.");
2165
2835
  }
@@ -2216,7 +2886,7 @@ ${token}`;
2216
2886
  await refreshAuth(true);
2217
2887
  break;
2218
2888
  }
2219
- 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"] });
2220
2890
  login.stderr?.on("data", (chunk) => process.stderr.write(chunk));
2221
2891
  login.once("error", (error) => {
2222
2892
  login = void 0;
@@ -2254,11 +2924,22 @@ ${token}`;
2254
2924
  response.setHeader("Referrer-Policy", "no-referrer");
2255
2925
  const host = new URL(`http://${request.headers.host ?? ""}`);
2256
2926
  const forwarded = !!options.host && ["127.0.0.1", "localhost", "[::1]"].includes(host.hostname) && !host.username && !host.password && host.pathname === "/" && !host.search && !host.hash;
2257
- 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) {
2258
2930
  json(response, 403, { message: "Invalid request origin." });
2259
2931
  return;
2260
2932
  }
2261
- 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
+ }
2262
2943
  if (url.pathname === "/api/session" && request.method === "POST") {
2263
2944
  if (!authMatches(String(request.headers.authorization ?? "").replace(/^Bearer /, ""))) {
2264
2945
  json(response, 401, { message: "Invalid session." });
@@ -2285,6 +2966,7 @@ ${token}`;
2285
2966
  return;
2286
2967
  }
2287
2968
  if (url.pathname === "/api/events" && request.method === "GET") {
2969
+ const disconnectKanban = kanbanController?.methods.connect(url.searchParams.get("clientId") ?? "");
2288
2970
  response.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
2289
2971
  response.write(`data: ${JSON.stringify(state())}
2290
2972
 
@@ -2294,6 +2976,7 @@ ${token}`;
2294
2976
  response.once("close", () => {
2295
2977
  clearInterval(heartbeat);
2296
2978
  clients.delete(response);
2979
+ disconnectKanban?.();
2297
2980
  });
2298
2981
  return;
2299
2982
  }
@@ -2303,8 +2986,9 @@ ${token}`;
2303
2986
  const operation = () => handleMessage(message).then((value2) => {
2304
2987
  messages = value2;
2305
2988
  });
2306
- const task = message.type === "resolve" || message.type === "ready" ? operation() : mutations.catch(() => void 0).then(operation);
2307
- 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) {
2308
2992
  mutations = task;
2309
2993
  }
2310
2994
  await task;
@@ -2313,7 +2997,7 @@ ${token}`;
2313
2997
  }
2314
2998
  if (url.pathname.startsWith("/api/file/") && request.method === "GET") {
2315
2999
  const file = await runtime.readFile(project, decodeURIComponent(url.pathname.slice("/api/file/".length)));
2316
- const bytes = await readFile2(file.path);
3000
+ const bytes = await readFile5(file.path);
2317
3001
  const safeType = /^(?:image\/(?:png|jpeg|gif|webp|avif)|audio\/[a-z0-9.+-]+|video\/[a-z0-9.+-]+)$/.test(file.reference.mimeType);
2318
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" });
2319
3003
  response.end(bytes);
@@ -2333,19 +3017,21 @@ ${token}`;
2333
3017
  }
2334
3018
  if (url.pathname === "/") {
2335
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:";
2336
- 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'` });
2337
- 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";
2338
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>`);
2339
3023
  return;
2340
3024
  }
2341
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" };
2342
3026
  assets["/assets/workflow-workspace.js"] = "application/javascript";
2343
3027
  assets["/assets/workflow-workspace.css"] = "text/css";
3028
+ assets["/assets/kanban-workspace.js"] = "application/javascript";
3029
+ assets["/assets/kanban-workspace.css"] = "text/css";
2344
3030
  if (!assets[url.pathname]) {
2345
3031
  json(response, 404, { message: "Not found." });
2346
3032
  return;
2347
3033
  }
2348
- 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)));
2349
3035
  response.writeHead(200, { "Content-Type": assets[url.pathname], "Cache-Control": "no-cache" });
2350
3036
  response.end(content);
2351
3037
  })().catch((error) => {
@@ -2368,6 +3054,7 @@ ${token}`;
2368
3054
  clearInterval(authPoll);
2369
3055
  conversationController?.dispose();
2370
3056
  workflowController?.dispose();
3057
+ await kanbanController?.close();
2371
3058
  runtime.dispose();
2372
3059
  throw error;
2373
3060
  }
@@ -2379,6 +3066,7 @@ ${token}`;
2379
3066
  project,
2380
3067
  controller,
2381
3068
  workflowController,
3069
+ kanbanController,
2382
3070
  setHostConnection: async (value) => {
2383
3071
  authEpoch++;
2384
3072
  authCheck?.abort();
@@ -2398,6 +3086,7 @@ ${token}`;
2398
3086
  conversationController?.dispose();
2399
3087
  workflowController?.dispose();
2400
3088
  runtime.dispose();
3089
+ await kanbanController?.close();
2401
3090
  for (const client of clients) {
2402
3091
  client.end();
2403
3092
  }
@@ -2415,7 +3104,7 @@ ${token}`;
2415
3104
  import { createInterface } from "readline";
2416
3105
 
2417
3106
  // src/web/host-config.ts
2418
- import path4 from "path";
3107
+ import path7 from "path";
2419
3108
  function parseHostConnection(value) {
2420
3109
  if (value === null || value === void 0) {
2421
3110
  return;
@@ -2439,11 +3128,11 @@ function parseHostConfiguration(value) {
2439
3128
  throw new Error("Invalid web host configuration.");
2440
3129
  }
2441
3130
  const assistant = configuration.assistant;
2442
- 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))) {
2443
3132
  throw new Error("Invalid assistant workspace.");
2444
3133
  }
2445
3134
  const assistantProjectDir = configuration.assistantProjectDir;
2446
- if (configuration.surface === "assistant" && (!assistant || typeof assistantProjectDir !== "string" || !path4.isAbsolute(assistantProjectDir))) {
3135
+ if (configuration.surface === "assistant" && (!assistant || typeof assistantProjectDir !== "string" || !path7.isAbsolute(assistantProjectDir))) {
2447
3136
  throw new Error("A dedicated assistant project and its workspace are required.");
2448
3137
  }
2449
3138
  if (configuration.surface !== "assistant" && (assistant || assistantProjectDir !== void 0)) {
@@ -2484,12 +3173,12 @@ function parseWebArgs(argv) {
2484
3173
  }
2485
3174
  options.theme = value;
2486
3175
  } else {
2487
- options.dataDir = path5.resolve(value);
3176
+ options.dataDir = path8.resolve(value);
2488
3177
  }
2489
3178
  } else if (arg.startsWith("-") || projectSet) {
2490
3179
  throw new Error(`Unknown web argument: ${arg}`);
2491
3180
  } else {
2492
- options.projectDir = path5.resolve(arg);
3181
+ options.projectDir = path8.resolve(arg);
2493
3182
  projectSet = true;
2494
3183
  }
2495
3184
  }