@workflow-code/cli 0.2.4-20261011002.codex-cli-web → 0.2.4-20261011003.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.
@@ -17,15 +17,14 @@ import "./chunk-NSPRIPOP.js";
17
17
 
18
18
  // src/web.ts
19
19
  import { spawn as spawn3 } from "child_process";
20
- import path4 from "path";
20
+ import path5 from "path";
21
21
 
22
22
  // src/web/server.ts
23
23
  import { createServer } from "http";
24
24
  import { createHash, randomBytes, timingSafeEqual } from "crypto";
25
- import { mkdir, readFile, rename, writeFile, realpath } from "fs/promises";
26
- import path2 from "path";
25
+ import { mkdir, readFile as readFile2, rename, writeFile, realpath as realpath2 } from "fs/promises";
26
+ import path3 from "path";
27
27
  import os from "os";
28
- import { pathToFileURL } from "url";
29
28
  import { spawn as spawn2 } from "child_process";
30
29
 
31
30
  // src/web/workflow-types.ts
@@ -668,28 +667,818 @@ var ConversationController = class {
668
667
  }
669
668
  };
670
669
 
670
+ // src/web/workflow-workspace.ts
671
+ function parseWorkflowArgs(text) {
672
+ const value = JSON.parse(text || "[]");
673
+ if (!Array.isArray(value) || !value.every((arg) => typeof arg === "string")) {
674
+ throw new Error("Run arguments must be a JSON string array.");
675
+ }
676
+ return value;
677
+ }
678
+ function migrateWorkflowHistory(sessions, activeId) {
679
+ const workspace = { version: 1, runs: [], drafts: {}, migratedConversations: true };
680
+ for (const session of sessions) {
681
+ let user;
682
+ for (const [index, message] of session.messages.entries()) {
683
+ if (message.role === "user") {
684
+ user = message;
685
+ continue;
686
+ }
687
+ const report = message.report;
688
+ const createdAt = message.createdAt ?? session.createdAt ?? 0;
689
+ workspace.runs.push({
690
+ id: message.id ?? `legacy-${session.id}-${index}`,
691
+ scopeId: session.id,
692
+ title: session.title,
693
+ entrypointId: message.entrypointId ?? user?.entrypointId,
694
+ createdAt,
695
+ updatedAt: session.updatedAt ?? createdAt,
696
+ status: message.status ?? report?.status ?? "success",
697
+ args: message.args ?? user?.args ?? [],
698
+ params: user?.input?.params ?? {},
699
+ policies: session.toolPermissionPolicies ?? {},
700
+ items: report?.output?.items ?? message.items ?? (message.content ? [{ content: message.content, contentType: "markdown" }] : []),
701
+ events: message.events ?? [],
702
+ nodes: report?.nodes ?? [],
703
+ logs: report?.logs ?? "",
704
+ files: [...user?.attachments ?? [], ...message.files ?? [], ...report?.files ?? []],
705
+ report,
706
+ pendingInput: message.pendingInput ?? report?.pendingUserInput,
707
+ resolvedInputs: message.resolvedInputs ?? report?.resolvedUserInputs ?? [],
708
+ error: message.error,
709
+ durationMs: message.durationMs,
710
+ tokenUsage: message.tokenUsage,
711
+ timers: message.timers,
712
+ migratedFrom: { sessionId: session.id, messageId: message.id }
713
+ });
714
+ if (session.id === activeId) {
715
+ workspace.selectedId = workspace.runs.at(-1).id;
716
+ }
717
+ }
718
+ for (const [entrypoint, values] of Object.entries(session.parameterValues ?? {})) {
719
+ workspace.drafts[entrypoint] = { scopeId: session.id, values, rawArgs: "[]", inputMode: "form", policies: session.toolPermissionPolicies ?? {} };
720
+ }
721
+ }
722
+ workspace.runs.sort((a, b) => b.createdAt - a.createdAt);
723
+ return workspace;
724
+ }
725
+ function workflowStorageReferences(value) {
726
+ const files = /* @__PURE__ */ new Set(), scopes = /* @__PURE__ */ new Set();
727
+ const visit = (item) => {
728
+ if (typeof item === "string") {
729
+ for (const id of item.match(/wf_[a-f0-9]{32}/g) ?? []) {
730
+ files.add(id);
731
+ }
732
+ if (/^[\[{]/.test(item.trim())) {
733
+ try {
734
+ visit(JSON.parse(item));
735
+ } catch {
736
+ }
737
+ }
738
+ } else if (Array.isArray(item)) {
739
+ item.forEach(visit);
740
+ } else if (item && typeof item === "object") {
741
+ const record2 = item;
742
+ if (typeof record2.scopeId === "string") {
743
+ scopes.add(record2.scopeId);
744
+ }
745
+ if (typeof record2.conversationId === "string") {
746
+ scopes.add(record2.conversationId);
747
+ }
748
+ Object.values(record2).forEach(visit);
749
+ }
750
+ };
751
+ visit(value);
752
+ return { files, scopes };
753
+ }
754
+
755
+ // src/web/workflow-controller.ts
756
+ var WorkflowController = class {
757
+ constructor(project, runtime, saved, options) {
758
+ this.project = project;
759
+ this.runtime = runtime;
760
+ this.options = options;
761
+ this.workspace = { version: 1, runs: [], drafts: {}, ...saved };
762
+ for (const record2 of this.workspace.runs) {
763
+ delete record2.submittingInput;
764
+ if (record2.status === "running" || record2.status === "waiting_for_input") {
765
+ record2.status = "interrupted";
766
+ record2.error = "\u8FD0\u884C\u5DF2\u4E2D\u65AD";
767
+ this.clearTimers(record2);
768
+ }
769
+ }
770
+ this.workspace.occupiedId = this.workspace.runs.find((record2) => record2.id === saved.occupiedId && record2.pendingInput)?.id ?? this.workspace.runs.find((record2) => !!record2.pendingInput)?.id;
771
+ if (!this.workspace.runs.some((record2) => record2.id === this.workspace.selectedId)) {
772
+ delete this.workspace.selectedId;
773
+ }
774
+ }
775
+ project;
776
+ runtime;
777
+ options;
778
+ workspace;
779
+ task;
780
+ disposed = false;
781
+ eventSave;
782
+ get runs() {
783
+ return this.workspace.runs;
784
+ }
785
+ get current() {
786
+ return this.runs.find((record2) => record2.id === this.workspace.selectedId);
787
+ }
788
+ get running() {
789
+ return !!this.workspace.occupiedId;
790
+ }
791
+ get occupied() {
792
+ return this.runs.find((record2) => record2.id === this.workspace.occupiedId);
793
+ }
794
+ draft(entrypointId) {
795
+ return this.workspace.drafts[entrypointId ?? ""] ??= { scopeId: globalThis.crypto.randomUUID(), values: {}, rawArgs: "[]", inputMode: "form", policies: {} };
796
+ }
797
+ async persist() {
798
+ await this.options.save(JSON.parse(JSON.stringify({ ...this.workspace, runs: this.runs.map(({ submittingInput: _, ...record2 }) => record2) })));
799
+ }
800
+ changed() {
801
+ if (!this.disposed) {
802
+ this.options.changed();
803
+ }
804
+ }
805
+ scheduleSave() {
806
+ if (this.eventSave) {
807
+ return;
808
+ }
809
+ this.eventSave = setTimeout(() => {
810
+ this.eventSave = void 0;
811
+ void this.persist().catch((error) => this.options.log(String(error)));
812
+ }, 100);
813
+ }
814
+ async select(id) {
815
+ if (id && !this.runs.some((record2) => record2.id === id)) {
816
+ throw new Error("Run no longer exists.");
817
+ }
818
+ this.workspace.selectedId = id;
819
+ this.changed();
820
+ await this.persist();
821
+ }
822
+ async updateDraft(entrypointId, draft) {
823
+ Object.assign(this.draft(entrypointId), draft);
824
+ this.changed();
825
+ await this.persist();
826
+ }
827
+ assertAvailable() {
828
+ if (this.disposed) {
829
+ throw new Error("Workflow workspace has closed.");
830
+ }
831
+ if (this.running || this.task) {
832
+ throw new Error("A workflow is already running or waiting for input.");
833
+ }
834
+ }
835
+ async start(input) {
836
+ this.assertAvailable();
837
+ const parameters = workflowParameters(this.project, input.entrypointId);
838
+ const args = input.inputMode === "raw" ? parseWorkflowArgs(input.rawArgs ?? "[]") : (validateWorkflowValues(parameters.fields, input.params, input.states), workflowParamArgs(parameters.fields, input.params, input.states));
839
+ const files = await this.inputFiles([input.params, args]);
840
+ this.assertAvailable();
841
+ const draft = this.draft(input.entrypointId);
842
+ const id = globalThis.crypto.randomUUID(), now = Date.now();
843
+ const record2 = {
844
+ id,
845
+ scopeId: id,
846
+ entrypointId: input.entrypointId,
847
+ title: this.project.entrypoints.find((entry) => entry.id === input.entrypointId)?.title ?? this.project.name,
848
+ createdAt: now,
849
+ updatedAt: now,
850
+ status: "running",
851
+ args: [...args],
852
+ params: { ...input.params },
853
+ policies: structuredClone(draft.policies),
854
+ items: [],
855
+ events: [],
856
+ files,
857
+ nodes: [],
858
+ logs: "",
859
+ resolvedInputs: []
860
+ };
861
+ this.runs.unshift(record2);
862
+ this.workspace.selectedId = id;
863
+ draft.values = { ...input.params };
864
+ draft.rawArgs = input.rawArgs ?? draft.rawArgs;
865
+ draft.inputMode = input.inputMode ?? "form";
866
+ this.launch(record2);
867
+ await this.persist();
868
+ return record2;
869
+ }
870
+ async rerun(previousId) {
871
+ this.assertAvailable();
872
+ const previous = this.runs.find((record3) => record3.id === previousId);
873
+ if (!previous) {
874
+ throw new Error("Run no longer exists.");
875
+ }
876
+ const files = await this.inputFiles([previous.params, previous.args]);
877
+ this.assertAvailable();
878
+ const id = globalThis.crypto.randomUUID(), now = Date.now();
879
+ const record2 = {
880
+ id,
881
+ scopeId: id,
882
+ title: previous.title,
883
+ entrypointId: previous.entrypointId,
884
+ createdAt: now,
885
+ updatedAt: now,
886
+ status: "running",
887
+ args: [...previous.args],
888
+ params: structuredClone(previous.params),
889
+ policies: structuredClone(previous.policies),
890
+ items: [],
891
+ events: [],
892
+ files,
893
+ nodes: [],
894
+ logs: "",
895
+ resolvedInputs: []
896
+ };
897
+ this.runs.unshift(record2);
898
+ this.workspace.selectedId = id;
899
+ this.launch(record2);
900
+ await this.persist();
901
+ return record2;
902
+ }
903
+ async inputFiles(value) {
904
+ return Promise.all([...workflowStorageReferences(value).files].map(async (id) => (await this.runtime.readFile(this.project, id)).reference));
905
+ }
906
+ launch(record2) {
907
+ record2.status = "running";
908
+ record2.error = void 0;
909
+ record2.updatedAt = Date.now();
910
+ this.workspace.occupiedId = record2.id;
911
+ const task = { record: record2, cancelled: false, finished: Promise.resolve() };
912
+ this.task = task;
913
+ this.changed();
914
+ task.finished = this.execute(task).catch((error) => this.options.log(`Workflow persistence: ${String(error)}`));
915
+ }
916
+ async execute(task) {
917
+ const record2 = task.record, pending = record2.pendingInput;
918
+ try {
919
+ await this.persist();
920
+ if (task.cancelled || this.disposed) {
921
+ throw new Error("Stopped.");
922
+ }
923
+ const report = await this.runtime.run({
924
+ project: this.project,
925
+ conversationId: record2.scopeId,
926
+ args: record2.args,
927
+ entrypointId: record2.entrypointId,
928
+ runId: record2.id,
929
+ toolPermissionPolicies: record2.policies,
930
+ resolvedInputs: [...record2.resolvedInputs, ...record2.submittingInput ? [record2.submittingInput] : []],
931
+ resumeAfterRequestId: record2.submittingInput?.requestId
932
+ }, (event) => {
933
+ if (task.cancelled || this.disposed || this.task !== task) {
934
+ return;
935
+ }
936
+ record2.events.push(event);
937
+ if (event.type === "node_chunk") {
938
+ applyOutputChunk(record2.items, event.chunk);
939
+ }
940
+ if (event.type === "node_started" || event.type === "node_finished") {
941
+ const index = record2.nodes.findIndex((node2) => node2.executionId === event.executionId);
942
+ const node = { ...index >= 0 ? record2.nodes[index] : {}, ...event, name: event.nodeName, status: event.status ?? "running" };
943
+ if (index >= 0) {
944
+ record2.nodes[index] = node;
945
+ } else {
946
+ record2.nodes.push(node);
947
+ }
948
+ }
949
+ if (event.type === "runtime_log" && typeof event.text === "string") {
950
+ record2.logs = (record2.logs + event.text).slice(-1e5);
951
+ }
952
+ if (event.type === "user_input_requested") {
953
+ record2.pendingInput = event;
954
+ }
955
+ if (event.type === "user_input_resolved") {
956
+ this.confirmInput(record2, event);
957
+ if (record2.pendingInput?.requestId === event.requestId) {
958
+ delete record2.pendingInput;
959
+ }
960
+ }
961
+ if (event.type === "token_usage_updated") {
962
+ record2.tokenUsage = { enabled: true, display: workflowParameters(this.project, record2.entrypointId).tokenUsage?.display !== false, run: event.usage };
963
+ }
964
+ if (event.type === "file_created") {
965
+ record2.files.push(event.file);
966
+ }
967
+ if (event.type === "timer_started" || event.type === "timer_finished") {
968
+ const timer = event.timer;
969
+ record2.timers = [...(record2.timers ?? []).filter((entry) => entry.timerId !== timer.timerId), timer];
970
+ }
971
+ record2.updatedAt = Date.now();
972
+ this.changed();
973
+ this.scheduleSave();
974
+ });
975
+ record2.report = report;
976
+ record2.durationMs = report.durationMs;
977
+ record2.status = report.status;
978
+ if (report.output) {
979
+ record2.items = report.output.items ?? [];
980
+ }
981
+ record2.nodes = report.nodes ?? record2.nodes;
982
+ record2.logs = report.logs ?? record2.logs;
983
+ record2.tokenUsage = report.tokenUsage ?? record2.tokenUsage;
984
+ record2.timers = report.timers ?? record2.timers;
985
+ record2.files = [...new Map([...record2.files, ...report.files ?? []].map((file) => [file.id, file])).values()];
986
+ record2.pendingInput = report.status === "waiting_for_input" ? report.pendingUserInput : void 0;
987
+ for (const resolved of report.resolvedUserInputs ?? []) {
988
+ this.confirmInput(record2, resolved);
989
+ }
990
+ if (record2.submittingInput && report.status === "success") {
991
+ this.confirmInput(record2, record2.submittingInput);
992
+ }
993
+ if (report.status === "failed") {
994
+ record2.error = report.result?.error?.message ?? "Workflow failed.";
995
+ }
996
+ if (record2.submittingInput && pending && report.status === "failed") {
997
+ record2.status = "waiting_for_input";
998
+ record2.pendingInput = pending;
999
+ }
1000
+ } catch (error) {
1001
+ record2.error = error instanceof Error ? error.message : String(error);
1002
+ record2.status = task.cancelled ? this.disposed ? "interrupted" : "aborted" : record2.submittingInput && pending ? "waiting_for_input" : "failed";
1003
+ if (record2.status === "waiting_for_input") {
1004
+ record2.pendingInput = pending;
1005
+ } else if (this.disposed) {
1006
+ record2.pendingInput ??= pending;
1007
+ } else {
1008
+ delete record2.pendingInput;
1009
+ }
1010
+ } finally {
1011
+ if (task.cancelled) {
1012
+ record2.status = this.disposed ? "interrupted" : "aborted";
1013
+ if (!this.disposed) {
1014
+ delete record2.pendingInput;
1015
+ }
1016
+ }
1017
+ delete record2.submittingInput;
1018
+ if (!record2.pendingInput || this.disposed) {
1019
+ delete this.workspace.occupiedId;
1020
+ }
1021
+ if (record2.status !== "running") {
1022
+ this.clearTimers(record2);
1023
+ }
1024
+ record2.updatedAt = Date.now();
1025
+ this.task = void 0;
1026
+ this.changed();
1027
+ await this.persist();
1028
+ }
1029
+ }
1030
+ clearTimers(record2) {
1031
+ record2.timers = record2.timers?.map((timer) => timer.status === "running" ? { ...timer, status: "cleared", endedAt: (/* @__PURE__ */ new Date()).toISOString(), durationMs: Math.max(0, Date.now() - Date.parse(timer.startedAt)) } : timer);
1032
+ }
1033
+ confirmInput(record2, resolved) {
1034
+ const previous = record2.resolvedInputs.find((input) => input.requestId === resolved.requestId) ?? (record2.submittingInput?.requestId === resolved.requestId ? record2.submittingInput : void 0);
1035
+ record2.resolvedInputs = [
1036
+ ...record2.resolvedInputs.filter((input) => input.requestId !== resolved.requestId),
1037
+ { ...previous, ...resolved, precedingOutputItemIds: previous?.precedingOutputItemIds ?? resolved.precedingOutputItemIds }
1038
+ ];
1039
+ if (record2.submittingInput?.requestId === resolved.requestId) {
1040
+ delete record2.submittingInput;
1041
+ }
1042
+ }
1043
+ async resume(id, requestId, values) {
1044
+ if (this.task || this.disposed) {
1045
+ throw new Error("Workflow is already running.");
1046
+ }
1047
+ const record2 = this.runs.find((record3) => record3.id === id && record3.pendingInput?.requestId === requestId);
1048
+ if (!record2?.pendingInput) {
1049
+ throw new Error("Input request is no longer available.");
1050
+ }
1051
+ if (this.workspace.occupiedId && this.workspace.occupiedId !== id) {
1052
+ throw new Error("Another workflow is waiting for input.");
1053
+ }
1054
+ const request = record2.pendingInput;
1055
+ const form = Object.fromEntries(request.params.map((field) => [field.name, workflowFormValue(field, values[field.name] ?? request.defaultValues?.[field.name])]));
1056
+ record2.submittingInput = {
1057
+ requestId,
1058
+ nodeName: request.nodeName,
1059
+ values: resolvedWorkflowValues(request.params, form),
1060
+ submittedAt: (/* @__PURE__ */ new Date()).toISOString(),
1061
+ request,
1062
+ precedingOutputItemIds: record2.items.map((item, index) => item.id ?? String(index))
1063
+ };
1064
+ this.launch(record2);
1065
+ await this.persist();
1066
+ }
1067
+ async stop(reason = "Stopped.") {
1068
+ const record2 = this.occupied;
1069
+ if (!record2) {
1070
+ return;
1071
+ }
1072
+ if (this.task) {
1073
+ this.task.cancelled = true;
1074
+ this.runtime.cancel(record2.id);
1075
+ await this.task.finished;
1076
+ }
1077
+ record2.status = "aborted";
1078
+ record2.error = reason;
1079
+ delete record2.pendingInput;
1080
+ delete this.workspace.occupiedId;
1081
+ this.clearTimers(record2);
1082
+ this.changed();
1083
+ await this.persist();
1084
+ }
1085
+ async remove(id) {
1086
+ if (this.workspace.occupiedId === id || this.task?.record.id === id) {
1087
+ throw new Error("Stop the workflow before deleting its record.");
1088
+ }
1089
+ const record2 = this.runs.find((record3) => record3.id === id);
1090
+ if (!record2) {
1091
+ throw new Error("Run no longer exists.");
1092
+ }
1093
+ const other = workflowStorageReferences([this.runs.filter((item) => item !== record2), this.workspace.drafts, this.options.legacy ?? []]);
1094
+ const own = workflowStorageReferences(record2);
1095
+ for (const id2 of other.files) {
1096
+ const file = await this.runtime.readFile(this.project, id2).catch(() => void 0);
1097
+ if (file?.reference.conversationId) {
1098
+ other.scopes.add(file.reference.conversationId);
1099
+ }
1100
+ }
1101
+ const owners = /* @__PURE__ */ new Map();
1102
+ for (const fileId of own.files) {
1103
+ if (other.files.has(fileId)) {
1104
+ continue;
1105
+ }
1106
+ const file = await this.runtime.readFile(this.project, fileId).catch(() => void 0);
1107
+ if (file?.reference.conversationId) {
1108
+ const ids = owners.get(file.reference.conversationId) ?? [];
1109
+ ids.push(fileId);
1110
+ owners.set(file.reference.conversationId, ids);
1111
+ }
1112
+ }
1113
+ for (const [scope, ids] of owners) {
1114
+ await this.runtime.deleteFiles(this.project, scope, ids);
1115
+ }
1116
+ if (!other.scopes.has(record2.scopeId)) {
1117
+ await this.runtime.deleteConversation(this.project, record2.scopeId);
1118
+ }
1119
+ this.runs.splice(this.runs.indexOf(record2), 1);
1120
+ if (this.workspace.selectedId === id) {
1121
+ delete this.workspace.selectedId;
1122
+ }
1123
+ this.changed();
1124
+ await this.persist();
1125
+ }
1126
+ async idle() {
1127
+ if (this.task) {
1128
+ await this.task.finished;
1129
+ }
1130
+ }
1131
+ dispose() {
1132
+ this.disposed = true;
1133
+ clearTimeout(this.eventSave);
1134
+ if (this.task) {
1135
+ this.task.cancelled = true;
1136
+ this.runtime.cancel(this.task.record.id);
1137
+ } else if (this.occupied) {
1138
+ this.occupied.status = "interrupted";
1139
+ this.occupied.error = "Host closed.";
1140
+ this.clearTimers(this.occupied);
1141
+ delete this.workspace.occupiedId;
1142
+ }
1143
+ }
1144
+ };
1145
+
1146
+ // src/web/resource-cards.ts
1147
+ var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1148
+ var string = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
1149
+ function conversationResourceCards(items, projectId, conversationId) {
1150
+ const cards = /* @__PURE__ */ new Map();
1151
+ for (const [index, item] of items.entries()) {
1152
+ if (!["agent-part", "opencode-part"].includes(String(item.metadata?.kind)) || item.metadata?.partType !== "tool") {
1153
+ continue;
1154
+ }
1155
+ const content = record(item.content);
1156
+ if (content?.status !== "success") {
1157
+ continue;
1158
+ }
1159
+ let output = record(content.output);
1160
+ if (Array.isArray(output?.content) && output.content.length === 1) {
1161
+ try {
1162
+ output = record(JSON.parse(String(record(output.content[0])?.text)));
1163
+ } catch {
1164
+ }
1165
+ }
1166
+ let descriptor = record(record(output?.details)?.resourceCard);
1167
+ const input = record(content.input) ?? {};
1168
+ const tool = string(content.tool) ?? "";
1169
+ if (tool === "workflow-ui_present_card") {
1170
+ const presentation = output?.action ? output : input;
1171
+ if (presentation.action === "remove") {
1172
+ cards.delete(String(presentation.cardId));
1173
+ continue;
1174
+ }
1175
+ if (presentation.action !== "upsert") {
1176
+ continue;
1177
+ }
1178
+ descriptor = presentation;
1179
+ }
1180
+ if (!descriptor) {
1181
+ const target2 = { source: "local", projectId, conversationId };
1182
+ let kind2;
1183
+ if (tool === "workflow-kv_kv_store" || /workflow-desktop_(?:read|list|write)_project_kv/.test(tool)) {
1184
+ kind2 = "kv";
1185
+ Object.assign(target2, { projectId: input.projectId ?? projectId, key: input.key, scope: input.scope ?? "project", conversationId: input.conversationId ?? conversationId });
1186
+ } else if (tool === "workflow-knowledge_knowledge_documents" || /workflow-desktop_(?:query|mutate)_project_knowledge/.test(tool)) {
1187
+ kind2 = "knowledge";
1188
+ Object.assign(target2, { projectId: input.projectId ?? projectId, documentId: input.documentId ?? record(output?.document)?.id });
1189
+ } else if (tool === "read" || tool === "workflow-desktop_open_dock_tool" && input.tool === "files") {
1190
+ kind2 = "file";
1191
+ target2.filePath = input.filePath ?? input.path;
1192
+ } else if (tool === "workflow-desktop_open_project" && input.kind === "project") {
1193
+ kind2 = "project";
1194
+ target2.projectId = input.targetId;
1195
+ } else if (tool === "workflow-desktop_open_dock_tool") {
1196
+ if (input.tool === "browser") {
1197
+ kind2 = "browser";
1198
+ target2.url = input.url;
1199
+ }
1200
+ if (input.tool === "preview") {
1201
+ kind2 = "kanban";
1202
+ }
1203
+ if (input.tool === "open-project" && input.openedProjectKind === "project") {
1204
+ kind2 = "project";
1205
+ target2.projectId = input.openedProjectId;
1206
+ }
1207
+ }
1208
+ if (!kind2) {
1209
+ continue;
1210
+ }
1211
+ descriptor = { kind: kind2, target: target2, title: record(output?.document)?.title };
1212
+ }
1213
+ const kind = descriptor.kind;
1214
+ if (!["kv", "knowledge", "project", "browser", "file", "kanban"].includes(kind)) {
1215
+ continue;
1216
+ }
1217
+ const target = Object.fromEntries(Object.entries(record(descriptor.target) ?? {}).filter((entry) => typeof entry[1] === "string" && entry[1].length < 8192));
1218
+ if (target.source === "current") {
1219
+ target.source = "local";
1220
+ target.projectId = projectId;
1221
+ }
1222
+ if (target.source && !["local", "server"].includes(target.source)) {
1223
+ continue;
1224
+ }
1225
+ if (kind === "browser") {
1226
+ try {
1227
+ const url = new URL(target.url);
1228
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
1229
+ continue;
1230
+ }
1231
+ } catch {
1232
+ continue;
1233
+ }
1234
+ } else {
1235
+ target.source ??= "local";
1236
+ target.projectId ??= projectId;
1237
+ if (target.scope === "conversation") {
1238
+ target.conversationId ??= conversationId;
1239
+ }
1240
+ if (kind === "file" && (!target.filePath || /^(?:[\\/]|[a-z]:)|(?:^|[\\/])\.\.(?:[\\/]|$)/i.test(target.filePath))) {
1241
+ continue;
1242
+ }
1243
+ }
1244
+ const id = string(descriptor.cardId) ?? `${kind}:${JSON.stringify(target)}`;
1245
+ const title = (string(descriptor.title) ?? target.key ?? target.documentId ?? target.filePath ?? target.url ?? target.projectId ?? kind).slice(0, 200);
1246
+ cards.set(id, { id, kind, title, target, itemId: item.id ?? String(index) });
1247
+ }
1248
+ return [...cards.values()];
1249
+ }
1250
+
1251
+ // src/web/workflow-actions.ts
1252
+ import { readFile, realpath } from "fs/promises";
1253
+ import path from "path";
1254
+ function createWorkflowActions(controller, runtime, project, revision, changed) {
1255
+ const states = /* @__PURE__ */ new Map();
1256
+ const epochs = /* @__PURE__ */ new Map();
1257
+ let limit = 30, query = "";
1258
+ const entry = (id) => {
1259
+ const value = typeof id === "string" && id ? id : project.defaultEntrypoint ?? project.entrypoints[0]?.id;
1260
+ if (value && !project.entrypoints.some((item) => item.id === value)) {
1261
+ throw new Error("Unknown entrypoint.");
1262
+ }
1263
+ return value;
1264
+ };
1265
+ const values = (supplied, id) => {
1266
+ if (!supplied || typeof supplied !== "object" || Array.isArray(supplied) || Object.values(supplied).some((value) => typeof value !== "string" && !(Array.isArray(value) && value.every((item) => typeof item === "string")))) {
1267
+ throw new Error("Invalid parameters.");
1268
+ }
1269
+ return Object.fromEntries(workflowParameters(project, id).fields.map((field) => [
1270
+ field.name,
1271
+ workflowParamValue(field, supplied[field.name] ?? field.defaultValue ?? (field.type === "boolean" ? "false" : ""))
1272
+ ]));
1273
+ };
1274
+ const resolve = async (message, id) => {
1275
+ const key = id ?? "", parameters = workflowParameters(project, id);
1276
+ const params = values(message.params ?? controller.draft(id).values, id);
1277
+ const mode = message.inputMode === "raw" ? "raw" : "form";
1278
+ const epoch = (epochs.get(key) ?? 0) + 1;
1279
+ epochs.set(key, epoch);
1280
+ const auth = revision();
1281
+ await controller.updateDraft(id, { values: params, rawArgs: String(message.rawArgs ?? controller.draft(id).rawArgs).slice(0, 1e5), inputMode: mode });
1282
+ const args = mode === "raw" ? parseWorkflowArgs(String(message.rawArgs ?? "[]")) : workflowParamArgs(parameters.fields, params, states.get(key));
1283
+ const trigger = message.trigger?.kind === "param" && parameters.fields.some((field) => field.name === message.trigger.paramName) ? { kind: "param", paramName: String(message.trigger.paramName) } : { kind: message.trigger?.kind === "input" ? "input" : "init" };
1284
+ const result = parameters.paramResolver?.enabled ? await runtime.resolveParams(project, { args, inputMode: mode, draftText: "", trigger }, id) : { params: {} };
1285
+ if (epoch !== epochs.get(key) || auth !== revision()) {
1286
+ throw new Error("Parameters changed. Try again.");
1287
+ }
1288
+ const next = { ...states.get(key) ?? {} };
1289
+ for (const [name, patch] of Object.entries(result.params ?? {})) {
1290
+ if (!parameters.fields.some((field) => field.name === name)) {
1291
+ continue;
1292
+ }
1293
+ next[name] = { ...next[name], ...patch, options: { ...next[name]?.options, ...patch.options } };
1294
+ if (patch.value !== void 0) {
1295
+ params[name] = patch.value === null ? workflowParamValue(parameters.fields.find((field) => field.name === name)) : patch.value;
1296
+ }
1297
+ }
1298
+ states.set(key, next);
1299
+ await controller.updateDraft(id, { values: params, rawArgs: String(message.rawArgs ?? controller.draft(id).rawArgs).slice(0, 1e5), inputMode: mode });
1300
+ return { params, states: next };
1301
+ };
1302
+ const state = () => {
1303
+ const filtered = controller.runs.filter((record2) => !query || `${record2.title} ${record2.entrypointId ?? ""} ${record2.args.join(" ")}`.toLocaleLowerCase().includes(query.toLocaleLowerCase()));
1304
+ const runs = filtered.slice(0, limit).map(({ id, title, entrypointId, status, createdAt, updatedAt, durationMs }) => ({ id, title, entrypointId, status, createdAt, updatedAt, durationMs }));
1305
+ return {
1306
+ runs,
1307
+ selectedId: controller.workspace.selectedId,
1308
+ run: controller.current,
1309
+ occupiedId: controller.workspace.occupiedId,
1310
+ occupiedRun: controller.occupied,
1311
+ running: controller.running,
1312
+ drafts: controller.workspace.drafts,
1313
+ history: { pageSize: 30, total: filtered.length, hasMore: filtered.length > limit, query },
1314
+ migratedConversations: controller.workspace.migratedConversations
1315
+ };
1316
+ };
1317
+ return {
1318
+ state,
1319
+ invalidate: () => {
1320
+ states.clear();
1321
+ for (const [key, value] of epochs) {
1322
+ epochs.set(key, value + 1);
1323
+ }
1324
+ },
1325
+ async handle(message) {
1326
+ const replies = [];
1327
+ const id = entry(message.entrypointId);
1328
+ if (message.projectUri && message.projectUri !== project.uri) {
1329
+ throw new Error("Project changed.");
1330
+ }
1331
+ switch (message.type) {
1332
+ case "ready":
1333
+ break;
1334
+ case "workflow-history": {
1335
+ const nextQuery = String(message.query ?? "").slice(0, 200);
1336
+ if (query !== nextQuery || !message.more) {
1337
+ limit = 30;
1338
+ } else {
1339
+ limit = Math.min(limit + 30, 3e4);
1340
+ }
1341
+ query = nextQuery;
1342
+ changed();
1343
+ break;
1344
+ }
1345
+ case "workflow-select":
1346
+ await controller.select(message.id ? String(message.id) : void 0);
1347
+ break;
1348
+ case "workflow-draft": {
1349
+ const draft = { values: values(message.params ?? {}, id) };
1350
+ if (message.inputMode !== void 0) {
1351
+ draft.inputMode = message.inputMode === "raw" ? "raw" : "form";
1352
+ }
1353
+ if (message.rawArgs !== void 0) {
1354
+ draft.rawArgs = String(message.rawArgs).slice(0, 1e5);
1355
+ }
1356
+ epochs.set(id ?? "", (epochs.get(id ?? "") ?? 0) + 1);
1357
+ await controller.updateDraft(id, draft);
1358
+ break;
1359
+ }
1360
+ case "resolve": {
1361
+ try {
1362
+ const result = await resolve(message, id);
1363
+ replies.push({ type: "params", projectUri: project.uri, entrypointId: id, requestId: message.requestId, params: result.states, values: result.params });
1364
+ } catch (error) {
1365
+ replies.push({ type: "params", projectUri: project.uri, entrypointId: id, requestId: message.requestId, error: error.message });
1366
+ }
1367
+ break;
1368
+ }
1369
+ case "workflow-run": {
1370
+ if (controller.running) {
1371
+ throw new Error("A workflow is already running or waiting for input.");
1372
+ }
1373
+ const resolved = await resolve(message, id);
1374
+ const record2 = await controller.start({
1375
+ entrypointId: id,
1376
+ params: resolved.params,
1377
+ states: resolved.states,
1378
+ inputMode: message.inputMode === "raw" ? "raw" : "form",
1379
+ rawArgs: String(message.rawArgs ?? "[]")
1380
+ });
1381
+ replies.push({ type: "workflow-started", id: record2.id });
1382
+ break;
1383
+ }
1384
+ case "workflow-stop":
1385
+ await controller.stop();
1386
+ break;
1387
+ case "workflow-resume":
1388
+ await controller.resume(String(message.id), String(message.requestId), message.values ?? {});
1389
+ break;
1390
+ case "workflow-rerun":
1391
+ await controller.rerun(String(message.id));
1392
+ break;
1393
+ case "workflow-delete":
1394
+ await controller.remove(String(message.id));
1395
+ break;
1396
+ case "workflow-export": {
1397
+ const record2 = controller.runs.find((record3) => record3.id === message.id);
1398
+ if (!record2) {
1399
+ throw new Error("Run no longer exists.");
1400
+ }
1401
+ replies.push({ type: "download", name: `workflow-${record2.id}.json`, content: JSON.stringify(redactWorkflowData(record2), null, 2) });
1402
+ break;
1403
+ }
1404
+ case "tool-policy": {
1405
+ const tools = workflowParameters(project, id).tools ?? [];
1406
+ const policies = Object.fromEntries(tools.map((tool) => [tool.name, {
1407
+ enabled: typeof message.policies?.[tool.name]?.enabled === "boolean" ? message.policies[tool.name].enabled : tool.enabled,
1408
+ autoApprove: typeof message.policies?.[tool.name]?.autoApprove === "boolean" ? message.policies[tool.name].autoApprove : tool.autoApprove
1409
+ }]));
1410
+ await controller.updateDraft(id, { policies });
1411
+ break;
1412
+ }
1413
+ case "upload": {
1414
+ if (!Array.isArray(message.files) || message.files.length > 32 || message.files.some((file) => typeof file?.name !== "string" || typeof file.data !== "string" || file.path !== void 0)) {
1415
+ throw new Error("Invalid upload.");
1416
+ }
1417
+ const record2 = message.runId ? controller.runs.find((record3) => record3.id === message.runId) : void 0;
1418
+ if (message.runId && (!record2?.pendingInput || record2.pendingInput.requestId !== message.requestId || controller.workspace.occupiedId !== record2.id)) {
1419
+ throw new Error("Input request changed.");
1420
+ }
1421
+ const files = await runtime.uploadFiles(project, record2?.scopeId ?? controller.draft(id).scopeId, message.files);
1422
+ if (record2) {
1423
+ record2.files.push(...files);
1424
+ await controller.persist();
1425
+ changed();
1426
+ }
1427
+ replies.push({ type: "attachments", entrypointId: id, runId: message.runId, requestId: message.requestId, paramName: message.paramName, files });
1428
+ break;
1429
+ }
1430
+ case "resource": {
1431
+ const run = controller.runs.find((record2) => record2.id === message.runId);
1432
+ if (!run) {
1433
+ throw new Error("Run no longer exists.");
1434
+ }
1435
+ const card = conversationResourceCards(run.items, project.id, run.scopeId).find((card2) => card2.id === message.cardId);
1436
+ if (!card || card.target.projectId && card.target.projectId !== project.id || card.target.source === "server") {
1437
+ throw new Error("Resource is outside this project.");
1438
+ }
1439
+ if (card.kind === "kv" || card.kind === "knowledge") {
1440
+ replies.push({ type: "preview", title: card.title, content: await runtime.readResource(project, run.scopeId, card.kind, card.target) });
1441
+ } 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)) {
1444
+ throw new Error("File is outside this project.");
1445
+ }
1446
+ replies.push({ type: "preview", title: card.title, content: await readFile(filename, "utf8") });
1447
+ } else {
1448
+ throw new Error("This resource requires an editor host.");
1449
+ }
1450
+ break;
1451
+ }
1452
+ default:
1453
+ throw new Error(`Unsupported workflow action: ${message.type}`);
1454
+ }
1455
+ return replies;
1456
+ }
1457
+ };
1458
+ }
1459
+
671
1460
  // src/web/runtime.ts
672
1461
  import { spawn } from "child_process";
673
1462
  import { existsSync, readFileSync } from "fs";
674
- import path from "path";
1463
+ import path2 from "path";
675
1464
  import { fileURLToPath } from "url";
676
1465
  function cliRoot() {
677
- let directory = path.dirname(fileURLToPath(import.meta.url));
1466
+ let directory = path2.dirname(fileURLToPath(import.meta.url));
678
1467
  for (let depth = 0; depth < 8; depth++) {
679
1468
  try {
680
- if (JSON.parse(readFileSync(path.join(directory, "package.json"), "utf8")).name === "@workflow-code/cli") {
1469
+ if (JSON.parse(readFileSync(path2.join(directory, "package.json"), "utf8")).name === "@workflow-code/cli") {
681
1470
  return directory;
682
1471
  }
683
1472
  } catch {
684
1473
  }
685
- directory = path.dirname(directory);
1474
+ directory = path2.dirname(directory);
686
1475
  }
687
1476
  throw new Error("Cannot locate the CLI installation.");
688
1477
  }
689
1478
  function createWebRuntime(projectDir, storageDir, connection) {
690
1479
  const root = cliRoot();
691
- const builtWorker = path.join(root, "dist/web/worker.mjs");
692
- const worker = existsSync(builtWorker) ? builtWorker : path.join(root, "src/web/worker.mjs");
1480
+ const builtWorker = path2.join(root, "dist/web/worker.mjs");
1481
+ const worker = existsSync(builtWorker) ? builtWorker : path2.join(root, "src/web/worker.mjs");
693
1482
  const children = /* @__PURE__ */ new Set();
694
1483
  const active = /* @__PURE__ */ new Map();
695
1484
  let disposed = false;
@@ -711,7 +1500,7 @@ function createWebRuntime(projectDir, storageDir, connection) {
711
1500
  WORKFLOW_SERVER_TOKEN: "",
712
1501
  WORKFLOW_LLM_SERVER_URL: auth?.serverUrl ?? "",
713
1502
  WORKFLOW_LLM_SERVER_TOKEN: auth?.accessToken ?? "",
714
- WORKFLOW_CLI_AUTH_DIR: path.join(storageDir, "runtime-auth")
1503
+ WORKFLOW_CLI_AUTH_DIR: path2.join(storageDir, "runtime-auth")
715
1504
  };
716
1505
  for (const key of [
717
1506
  "TSX_TSCONFIG_PATH",
@@ -742,7 +1531,8 @@ function createWebRuntime(projectDir, storageDir, connection) {
742
1531
  child.stdout.setEncoding("utf8");
743
1532
  child.stderr.setEncoding("utf8");
744
1533
  child.stderr.on("data", (chunk) => {
745
- stderr = (stderr + chunk).slice(-12e3);
1534
+ stderr = (stderr + chunk).slice(-1e5);
1535
+ onEvent?.({ type: "runtime_log", text: redactWorkflowData(chunk, [auth?.accessToken ?? ""]), runId: request.runId });
746
1536
  });
747
1537
  child.stdout.on("data", (chunk) => {
748
1538
  buffer += chunk;
@@ -795,7 +1585,7 @@ function createWebRuntime(projectDir, storageDir, connection) {
795
1585
  }
796
1586
  });
797
1587
  child.stdin.on("error", () => void 0);
798
- child.stdin.end(JSON.stringify({ ...request, storageDir }));
1588
+ child.stdin.end(JSON.stringify({ ...request, storageDir, runtimeProjectDir: projectDir }));
799
1589
  });
800
1590
  return {
801
1591
  available: true,
@@ -832,111 +1622,6 @@ function createWebRuntime(projectDir, storageDir, connection) {
832
1622
  };
833
1623
  }
834
1624
 
835
- // src/web/resource-cards.ts
836
- var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
837
- var string = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
838
- function conversationResourceCards(items, projectId, conversationId) {
839
- const cards = /* @__PURE__ */ new Map();
840
- for (const [index, item] of items.entries()) {
841
- if (!["agent-part", "opencode-part"].includes(String(item.metadata?.kind)) || item.metadata?.partType !== "tool") {
842
- continue;
843
- }
844
- const content = record(item.content);
845
- if (content?.status !== "success") {
846
- continue;
847
- }
848
- let output = record(content.output);
849
- if (Array.isArray(output?.content) && output.content.length === 1) {
850
- try {
851
- output = record(JSON.parse(String(record(output.content[0])?.text)));
852
- } catch {
853
- }
854
- }
855
- let descriptor = record(record(output?.details)?.resourceCard);
856
- const input = record(content.input) ?? {};
857
- const tool = string(content.tool) ?? "";
858
- if (tool === "workflow-ui_present_card") {
859
- const presentation = output?.action ? output : input;
860
- if (presentation.action === "remove") {
861
- cards.delete(String(presentation.cardId));
862
- continue;
863
- }
864
- if (presentation.action !== "upsert") {
865
- continue;
866
- }
867
- descriptor = presentation;
868
- }
869
- if (!descriptor) {
870
- const target2 = { source: "local", projectId, conversationId };
871
- let kind2;
872
- if (tool === "workflow-kv_kv_store" || /workflow-desktop_(?:read|list|write)_project_kv/.test(tool)) {
873
- kind2 = "kv";
874
- Object.assign(target2, { projectId: input.projectId ?? projectId, key: input.key, scope: input.scope ?? "project", conversationId: input.conversationId ?? conversationId });
875
- } else if (tool === "workflow-knowledge_knowledge_documents" || /workflow-desktop_(?:query|mutate)_project_knowledge/.test(tool)) {
876
- kind2 = "knowledge";
877
- Object.assign(target2, { projectId: input.projectId ?? projectId, documentId: input.documentId ?? record(output?.document)?.id });
878
- } else if (tool === "read" || tool === "workflow-desktop_open_dock_tool" && input.tool === "files") {
879
- kind2 = "file";
880
- target2.filePath = input.filePath ?? input.path;
881
- } else if (tool === "workflow-desktop_open_project" && input.kind === "project") {
882
- kind2 = "project";
883
- target2.projectId = input.targetId;
884
- } else if (tool === "workflow-desktop_open_dock_tool") {
885
- if (input.tool === "browser") {
886
- kind2 = "browser";
887
- target2.url = input.url;
888
- }
889
- if (input.tool === "preview") {
890
- kind2 = "kanban";
891
- }
892
- if (input.tool === "open-project" && input.openedProjectKind === "project") {
893
- kind2 = "project";
894
- target2.projectId = input.openedProjectId;
895
- }
896
- }
897
- if (!kind2) {
898
- continue;
899
- }
900
- descriptor = { kind: kind2, target: target2, title: record(output?.document)?.title };
901
- }
902
- const kind = descriptor.kind;
903
- if (!["kv", "knowledge", "project", "browser", "file", "kanban"].includes(kind)) {
904
- continue;
905
- }
906
- const target = Object.fromEntries(Object.entries(record(descriptor.target) ?? {}).filter((entry) => typeof entry[1] === "string" && entry[1].length < 8192));
907
- if (target.source === "current") {
908
- target.source = "local";
909
- target.projectId = projectId;
910
- }
911
- if (target.source && !["local", "server"].includes(target.source)) {
912
- continue;
913
- }
914
- if (kind === "browser") {
915
- try {
916
- const url = new URL(target.url);
917
- if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) {
918
- continue;
919
- }
920
- } catch {
921
- continue;
922
- }
923
- } else {
924
- target.source ??= "local";
925
- target.projectId ??= projectId;
926
- if (target.scope === "conversation") {
927
- target.conversationId ??= conversationId;
928
- }
929
- if (kind === "file" && (!target.filePath || /^(?:[\\/]|[a-z]:)|(?:^|[\\/])\.\.(?:[\\/]|$)/i.test(target.filePath))) {
930
- continue;
931
- }
932
- }
933
- const id = string(descriptor.cardId) ?? `${kind}:${JSON.stringify(target)}`;
934
- const title = (string(descriptor.title) ?? target.key ?? target.documentId ?? target.filePath ?? target.url ?? target.projectId ?? kind).slice(0, 200);
935
- cards.set(id, { id, kind, title, target, itemId: item.id ?? String(index) });
936
- }
937
- return [...cards.values()];
938
- }
939
-
940
1625
  // src/web/account-auth.ts
941
1626
  async function loginWebAccount(serverUrl, email, password) {
942
1627
  if (!email.trim() || !password) {
@@ -983,12 +1668,47 @@ async function loginWebAccount(serverUrl, email, password) {
983
1668
  }
984
1669
  }
985
1670
 
1671
+ // src/web/assistant-project.ts
1672
+ import { pathToFileURL } from "url";
1673
+ var ASSISTANT_PROTOCOL_VERSION = 1;
1674
+ var assistantTools = /* @__PURE__ */ new Set([
1675
+ "read",
1676
+ "write",
1677
+ "edit",
1678
+ "bash",
1679
+ "grep",
1680
+ "find",
1681
+ "ls",
1682
+ "skill",
1683
+ "workflow-memory_global_memory",
1684
+ "workflow-ui_present_card"
1685
+ ]);
1686
+ async function inspectWebProject(runtime, projectDir, host) {
1687
+ if (host?.assistant && !host.assistantProjectDir) {
1688
+ throw new Error("A dedicated assistant project is required.");
1689
+ }
1690
+ const inspection = await runtime.inspect([host?.assistantProjectDir ?? projectDir]);
1691
+ return {
1692
+ ...inspection,
1693
+ assistantProtocolVersion: host?.assistant ? ASSISTANT_PROTOCOL_VERSION : void 0,
1694
+ projects: inspection.projects.map((descriptor) => {
1695
+ const project = { ...descriptor, uri: pathToFileURL(descriptor.folderPath).href };
1696
+ if (host?.assistant) {
1697
+ project.assistant = host.assistant;
1698
+ project.uri = `studio-assistant:${encodeURIComponent(project.assistant.workdir)}`;
1699
+ project.requiresLogin = true;
1700
+ project.fields = project.fields.map((field) => field.name === "permissionPolicy" ? { ...field, defaultValue: "workspace" } : field);
1701
+ project.tools = project.tools?.filter((tool) => assistantTools.has(tool.name));
1702
+ }
1703
+ return project;
1704
+ })
1705
+ };
1706
+ }
1707
+
986
1708
  // src/web/server.ts
987
1709
  async function startWebServer(options) {
988
- const projectDir = await realpath(path2.resolve(options.projectDir));
989
- const key = createHash("sha256").update(options.host?.assistant ? `assistant:${options.host.assistant.workdir}` : projectDir).digest("hex").slice(0, 24);
990
- const storageDir = path2.resolve(options.dataDir ?? path2.join(os.homedir(), ".workflow-code", "web"));
991
- const historyPath = path2.join(storageDir, key, "conversations.json");
1710
+ const projectDir = await realpath2(path3.resolve(options.projectDir));
1711
+ const storageDir = path3.resolve(options.dataDir ?? path3.join(os.homedir(), ".workflow-code", "web"));
992
1712
  const clients = /* @__PURE__ */ new Set();
993
1713
  let connection;
994
1714
  let account = { status: "signed-out", serverUrl: "http://localhost:7126", revision: 0 };
@@ -999,12 +1719,13 @@ async function startWebServer(options) {
999
1719
  let hostConnection = options.host?.connection;
1000
1720
  let hostManaged = !!hostConnection;
1001
1721
  let authEpoch = 0;
1722
+ let authenticationInitialized = false;
1002
1723
  let authCheck;
1003
1724
  const sensitiveTokens = /* @__PURE__ */ new Set();
1004
1725
  const runtime = options.runtime ?? createWebRuntime(projectDir, storageDir, () => connection);
1005
1726
  let inspected;
1006
1727
  try {
1007
- inspected = await runtime.inspect([projectDir]);
1728
+ inspected = await inspectWebProject(runtime, projectDir, options.host);
1008
1729
  } catch (error) {
1009
1730
  runtime.dispose();
1010
1731
  throw error;
@@ -1021,18 +1742,12 @@ async function startWebServer(options) {
1021
1742
  runtime.dispose();
1022
1743
  throw new Error("workflow-code web currently supports Conversation and Workflow projects.");
1023
1744
  }
1024
- const project = { ...descriptor, uri: pathToFileURL(projectDir).href };
1025
- if (options.host?.assistant) {
1026
- project.assistant = options.host.assistant;
1027
- project.uri = `studio-assistant:${encodeURIComponent(project.assistant.workdir)}`;
1028
- project.requiresLogin = true;
1029
- project.fields = project.fields.map((field) => field.name === "permissionPolicy" ? { ...field, defaultValue: "workspace" } : field);
1030
- const supported = /* @__PURE__ */ new Set(["read", "write", "edit", "bash", "grep", "find", "ls", "skill", "workflow-memory_global_memory", "workflow-ui_present_card"]);
1031
- project.tools = project.tools?.filter((tool) => supported.has(tool.name));
1032
- }
1745
+ 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");
1033
1748
  let saved = {};
1034
1749
  try {
1035
- saved = JSON.parse(await readFile(historyPath, "utf8"));
1750
+ saved = JSON.parse(await readFile2(historyPath, "utf8"));
1036
1751
  } catch (error) {
1037
1752
  if (error.code !== "ENOENT") {
1038
1753
  runtime.dispose();
@@ -1042,6 +1757,7 @@ async function startWebServer(options) {
1042
1757
  let pendingSave = Promise.resolve();
1043
1758
  let broadcastTimer;
1044
1759
  let authRevision = 0;
1760
+ const isWorkflow = project.projectType === "workflow";
1045
1761
  const parameterStates = /* @__PURE__ */ new Map();
1046
1762
  const parameterEpochs = /* @__PURE__ */ new Map();
1047
1763
  const publicFile = (file) => ({ ...file, previewUrl: `/api/file/${encodeURIComponent(file.id)}` });
@@ -1058,7 +1774,13 @@ async function startWebServer(options) {
1058
1774
  }
1059
1775
  return value;
1060
1776
  };
1061
- const state = () => publicValue(redactWorkflowData({ type: "state", project, sessions: controller.sessions, activeId: controller.activeId, running: controller.running, parameterValues: controller.current.parameterValues ?? {}, account, authenticationRevision: authRevision }, [...sensitiveTokens]));
1777
+ const state = () => publicValue(redactWorkflowData({
1778
+ type: "state",
1779
+ project,
1780
+ ...isWorkflow ? workflowActions.state() : { sessions: controller.sessions, activeId: controller.activeId, running: controller.running, parameterValues: controller.current.parameterValues ?? {} },
1781
+ account,
1782
+ authenticationRevision: authRevision
1783
+ }, [...sensitiveTokens]));
1062
1784
  const broadcast = () => {
1063
1785
  if (broadcastTimer) {
1064
1786
  return;
@@ -1073,14 +1795,14 @@ async function startWebServer(options) {
1073
1795
  }
1074
1796
  }, 30);
1075
1797
  };
1076
- const controller = new ConversationController(project, runtime, saved, {
1798
+ const conversationController = isWorkflow ? void 0 : new ConversationController(project, runtime, saved, {
1077
1799
  changed: broadcast,
1078
1800
  log: (message) => process.stderr.write(`${message}
1079
1801
  `),
1080
1802
  save: (sessions, activeId) => {
1081
1803
  const content = JSON.stringify({ sessions, activeId });
1082
1804
  pendingSave = pendingSave.catch(() => void 0).then(async () => {
1083
- await mkdir(path2.dirname(historyPath), { recursive: true });
1805
+ await mkdir(path3.dirname(historyPath), { recursive: true });
1084
1806
  const temp = `${historyPath}.${process.pid}.tmp`;
1085
1807
  await writeFile(temp, content, { mode: 384 });
1086
1808
  await rename(temp, historyPath);
@@ -1088,6 +1810,51 @@ async function startWebServer(options) {
1088
1810
  return pendingSave;
1089
1811
  }
1090
1812
  });
1813
+ const controller = conversationController;
1814
+ const runsPath = path3.join(storageDir, key, "runs.json");
1815
+ let workspace;
1816
+ if (isWorkflow) {
1817
+ try {
1818
+ workspace = JSON.parse(await readFile2(runsPath, "utf8"));
1819
+ if (workspace?.version !== 1 || !Array.isArray(workspace.runs) || !workspace.drafts || typeof workspace.drafts !== "object") {
1820
+ throw new Error("Invalid Workflow history.");
1821
+ }
1822
+ } catch (error) {
1823
+ if (error.code !== "ENOENT") {
1824
+ runtime.dispose();
1825
+ throw error;
1826
+ }
1827
+ workspace = migrateWorkflowHistory(saved.sessions ?? [], saved.activeId);
1828
+ }
1829
+ }
1830
+ const workflowController = isWorkflow ? new WorkflowController(project, runtime, workspace, {
1831
+ changed: broadcast,
1832
+ legacy: saved.sessions,
1833
+ log: (message) => process.stderr.write(`${message}
1834
+ `),
1835
+ save: (workspace2) => {
1836
+ const content = JSON.stringify(workspace2);
1837
+ pendingSave = pendingSave.catch(() => void 0).then(async () => {
1838
+ await mkdir(path3.dirname(runsPath), { recursive: true });
1839
+ const temp = `${runsPath}.${process.pid}.tmp`;
1840
+ await writeFile(temp, content, { mode: 384 });
1841
+ await rename(temp, runsPath);
1842
+ });
1843
+ return pendingSave;
1844
+ }
1845
+ }) : void 0;
1846
+ const workflowActions = workflowController ? createWorkflowActions(workflowController, runtime, project, () => authRevision, broadcast) : void 0;
1847
+ if (workflowController) {
1848
+ await workflowController.persist();
1849
+ }
1850
+ const stopForAuthentication = async () => {
1851
+ for (const session of conversationController?.sessions ?? []) {
1852
+ conversationController.stop(session);
1853
+ }
1854
+ if (workflowController) {
1855
+ await workflowController.stop("Workflow login changed.");
1856
+ }
1857
+ };
1091
1858
  const refreshAuth = async (force = false) => {
1092
1859
  if ((passwordLogin || login) && !force) {
1093
1860
  return;
@@ -1118,10 +1885,12 @@ ${token}`;
1118
1885
  connection = void 0;
1119
1886
  authRevision++;
1120
1887
  parameterStates.clear();
1121
- for (const session of controller.sessions) {
1122
- controller.stop(session);
1888
+ workflowActions?.invalidate();
1889
+ if (authenticationInitialized) {
1890
+ await stopForAuthentication();
1123
1891
  }
1124
1892
  }
1893
+ authenticationInitialized = true;
1125
1894
  account = { status: candidate ? "checking" : "signed-out", serverUrl: serverUrl || "http://localhost:7126", user: candidate ? changed ? hostManaged ? void 0 : config.user : account.user : void 0, revision: account.revision + 1 };
1126
1895
  if (candidate) {
1127
1896
  let unauthorized = false;
@@ -1139,6 +1908,7 @@ ${token}`;
1139
1908
  if (!connection) {
1140
1909
  authRevision++;
1141
1910
  parameterStates.clear();
1911
+ workflowActions?.invalidate();
1142
1912
  }
1143
1913
  connection = candidate;
1144
1914
  const savedConnection = !hostManaged && candidate.serverUrl === config.serverUrl.replace(/\/+$/, "") && candidate.accessToken === config.apiKey;
@@ -1151,9 +1921,8 @@ ${token}`;
1151
1921
  if (!changed) {
1152
1922
  authRevision++;
1153
1923
  parameterStates.clear();
1154
- for (const session of controller.sessions) {
1155
- controller.stop(session);
1156
- }
1924
+ workflowActions?.invalidate();
1925
+ await stopForAuthentication();
1157
1926
  }
1158
1927
  account = { ...account, status: unauthorized ? "expired" : "error", error: String(error.message) };
1159
1928
  }
@@ -1215,11 +1984,14 @@ ${token}`;
1215
1984
  if (message.projectUri && message.projectUri !== project.uri) {
1216
1985
  throw new Error("Project changed.");
1217
1986
  }
1218
- if (message.sessionId && message.sessionId !== controller.activeId) {
1987
+ if (workflowActions && !["ready", "login", "verify", "logout", "usage"].includes(message.type)) {
1988
+ return publicValue(await workflowActions.handle(message));
1989
+ }
1990
+ if (message.sessionId && message.sessionId !== conversationController?.activeId) {
1219
1991
  throw new Error("Conversation changed.");
1220
1992
  }
1221
1993
  const replies = [];
1222
- const current = controller.current;
1994
+ const current = conversationController?.current;
1223
1995
  const entrypointId = typeof message.entrypointId === "string" && message.entrypointId ? message.entrypointId : project.defaultEntrypoint;
1224
1996
  if (entrypointId && !project.entrypoints.some((entry) => entry.id === entrypointId)) {
1225
1997
  throw new Error("Unknown entrypoint.");
@@ -1380,12 +2152,13 @@ ${token}`;
1380
2152
  if (card.kind === "kv" || card.kind === "knowledge") {
1381
2153
  replies.push({ type: "preview", title: card.title, content: await runtime.readResource(project, current.id, card.kind, card.target) });
1382
2154
  } else if (card.kind === "file") {
1383
- const filename = await realpath(path2.resolve(projectDir, card.target.filePath));
1384
- const relative = path2.relative(projectDir, filename);
1385
- if (relative.startsWith("..") || path2.isAbsolute(relative)) {
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)) {
1386
2159
  throw new Error("File is outside this project.");
1387
2160
  }
1388
- replies.push({ type: "preview", title: card.title, content: await readFile(filename, "utf8") });
2161
+ replies.push({ type: "preview", title: card.title, content: await readFile2(filename, "utf8") });
1389
2162
  } else {
1390
2163
  throw new Error("This resource requires an editor host.");
1391
2164
  }
@@ -1400,7 +2173,7 @@ ${token}`;
1400
2173
  if (!response.ok || result.errCode !== 0) {
1401
2174
  throw new Error(result.errMessage || "Cannot load usage.");
1402
2175
  }
1403
- replies.push({ type: "usage", sessionId: current.id, usage: result.data });
2176
+ replies.push({ type: "usage", sessionId: current?.id, usage: result.data });
1404
2177
  break;
1405
2178
  }
1406
2179
  case "login": {
@@ -1442,7 +2215,7 @@ ${token}`;
1442
2215
  await refreshAuth(true);
1443
2216
  break;
1444
2217
  }
1445
- login = spawn2(process.execPath, [path2.join(cliRoot(), "dist/index.js"), "workspace", "login", "--server", url.toString().replace(/\/$/, "")], { shell: false, stdio: ["ignore", "ignore", "pipe"] });
2218
+ login = spawn2(process.execPath, [path3.join(cliRoot(), "dist/index.js"), "workspace", "login", "--server", url.toString().replace(/\/$/, "")], { shell: false, stdio: ["ignore", "ignore", "pipe"] });
1446
2219
  login.stderr?.on("data", (chunk) => process.stderr.write(chunk));
1447
2220
  login.once("error", (error) => {
1448
2221
  login = void 0;
@@ -1539,7 +2312,7 @@ ${token}`;
1539
2312
  }
1540
2313
  if (url.pathname.startsWith("/api/file/") && request.method === "GET") {
1541
2314
  const file = await runtime.readFile(project, decodeURIComponent(url.pathname.slice("/api/file/".length)));
1542
- const bytes = await readFile(file.path);
2315
+ const bytes = await readFile2(file.path);
1543
2316
  const safeType = /^(?:image\/(?:png|jpeg|gif|webp|avif)|audio\/[a-z0-9.+-]+|video\/[a-z0-9.+-]+)$/.test(file.reference.mimeType);
1544
2317
  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" });
1545
2318
  response.end(bytes);
@@ -1560,15 +2333,18 @@ ${token}`;
1560
2333
  if (url.pathname === "/") {
1561
2334
  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:";
1562
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'` });
1563
- 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/workflow-ui.css"><link rel="stylesheet" href="/assets/theme.css"></head><body data-surface="${options.host?.surface ?? "editor"}"><div id="app"></div><script src="/assets/workflow-ui.js" defer></script></body></html>`);
2336
+ const ui = isWorkflow ? "workflow-workspace" : "workflow-ui";
2337
+ 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>`);
1564
2338
  return;
1565
2339
  }
1566
2340
  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
+ assets["/assets/workflow-workspace.js"] = "application/javascript";
2342
+ assets["/assets/workflow-workspace.css"] = "text/css";
1567
2343
  if (!assets[url.pathname]) {
1568
2344
  json(response, 404, { message: "Not found." });
1569
2345
  return;
1570
2346
  }
1571
- const content = await readFile(path2.join(cliRoot(), "dist/web", url.pathname.slice("/assets/".length)));
2347
+ const content = await readFile2(path3.join(cliRoot(), "dist/web", url.pathname.slice("/assets/".length)));
1572
2348
  response.writeHead(200, { "Content-Type": assets[url.pathname], "Cache-Control": "no-cache" });
1573
2349
  response.end(content);
1574
2350
  })().catch((error) => {
@@ -1589,7 +2365,8 @@ ${token}`;
1589
2365
  });
1590
2366
  } catch (error) {
1591
2367
  clearInterval(authPoll);
1592
- controller.dispose();
2368
+ conversationController?.dispose();
2369
+ workflowController?.dispose();
1593
2370
  runtime.dispose();
1594
2371
  throw error;
1595
2372
  }
@@ -1600,6 +2377,7 @@ ${token}`;
1600
2377
  url: `${origin}/#session=${capability}`,
1601
2378
  project,
1602
2379
  controller,
2380
+ workflowController,
1603
2381
  setHostConnection: async (value) => {
1604
2382
  authEpoch++;
1605
2383
  authCheck?.abort();
@@ -1616,14 +2394,17 @@ ${token}`;
1616
2394
  clearTimeout(broadcastTimer);
1617
2395
  }
1618
2396
  login?.kill();
1619
- controller.dispose();
2397
+ conversationController?.dispose();
2398
+ workflowController?.dispose();
1620
2399
  runtime.dispose();
1621
2400
  for (const client of clients) {
1622
2401
  client.end();
1623
2402
  }
1624
2403
  await new Promise((resolve) => server.close(() => resolve()));
1625
- await controller.idle();
1626
- await controller.persist();
2404
+ await conversationController?.idle();
2405
+ await conversationController?.persist();
2406
+ await workflowController?.idle();
2407
+ await workflowController?.persist();
1627
2408
  await pendingSave;
1628
2409
  })()
1629
2410
  };
@@ -1633,7 +2414,7 @@ ${token}`;
1633
2414
  import { createInterface } from "readline";
1634
2415
 
1635
2416
  // src/web/host-config.ts
1636
- import path3 from "path";
2417
+ import path4 from "path";
1637
2418
  function parseHostConnection(value) {
1638
2419
  if (value === null || value === void 0) {
1639
2420
  return;
@@ -1657,10 +2438,17 @@ function parseHostConfiguration(value) {
1657
2438
  throw new Error("Invalid web host configuration.");
1658
2439
  }
1659
2440
  const assistant = configuration.assistant;
1660
- if (assistant && (typeof assistant.workdir !== "string" || !path3.isAbsolute(assistant.workdir) || !Array.isArray(assistant.roots) || !assistant.roots.length || assistant.roots.length > 100 || assistant.roots.some((root) => typeof root.uri !== "string" || typeof root.name !== "string" || typeof root.folderPath !== "string" || !path3.isAbsolute(root.folderPath)) || !assistant.roots.some((root) => root.folderPath === assistant.workdir))) {
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))) {
1661
2442
  throw new Error("Invalid assistant workspace.");
1662
2443
  }
1663
- return { surface: configuration.surface, assistant, connection: parseHostConnection(configuration.connection) };
2444
+ const assistantProjectDir = configuration.assistantProjectDir;
2445
+ if (configuration.surface === "assistant" && (!assistant || typeof assistantProjectDir !== "string" || !path4.isAbsolute(assistantProjectDir))) {
2446
+ throw new Error("A dedicated assistant project and its workspace are required.");
2447
+ }
2448
+ if (configuration.surface !== "assistant" && (assistant || assistantProjectDir !== void 0)) {
2449
+ throw new Error("Assistant configuration requires the assistant surface.");
2450
+ }
2451
+ return { surface: configuration.surface, assistant, assistantProjectDir, connection: parseHostConnection(configuration.connection) };
1664
2452
  }
1665
2453
 
1666
2454
  // src/web.ts
@@ -1695,12 +2483,12 @@ function parseWebArgs(argv) {
1695
2483
  }
1696
2484
  options.theme = value;
1697
2485
  } else {
1698
- options.dataDir = path4.resolve(value);
2486
+ options.dataDir = path5.resolve(value);
1699
2487
  }
1700
2488
  } else if (arg.startsWith("-") || projectSet) {
1701
2489
  throw new Error(`Unknown web argument: ${arg}`);
1702
2490
  } else {
1703
- options.projectDir = path4.resolve(arg);
2491
+ options.projectDir = path5.resolve(arg);
1704
2492
  projectSet = true;
1705
2493
  }
1706
2494
  }
@@ -1712,15 +2500,6 @@ async function main(argv) {
1712
2500
  console.log(i18n.language.startsWith("zh") ? "workflow-code web [\u9879\u76EE\u76EE\u5F55]\n --port <0-65535> \u76D1\u542C\u7AEF\u53E3\uFF0C0 \u81EA\u52A8\u5206\u914D\n --no-open \u4E0D\u81EA\u52A8\u6253\u5F00\u6D4F\u89C8\u5668\n --theme <auto|light|dark>\n --data-dir <\u76EE\u5F55> \u5BF9\u8BDD\u548C\u6587\u4EF6\u5B58\u50A8\u4F4D\u7F6E\n --json \u8F93\u51FA\u7ED3\u6784\u5316\u542F\u52A8\u4FE1\u606F" : "workflow-code web [project-directory]\n --port <0-65535> Listen port; 0 selects an available port\n --no-open Do not open a browser\n --theme <auto|light|dark>\n --data-dir <path> Conversation and file storage directory\n --json Print structured startup information");
1713
2501
  return 0;
1714
2502
  }
1715
- if (options.inspect) {
1716
- const runtime = createWebRuntime(options.projectDir, options.dataDir ?? "", () => void 0);
1717
- try {
1718
- console.log(JSON.stringify(await runtime.inspect([options.projectDir])));
1719
- } finally {
1720
- runtime.dispose();
1721
- }
1722
- return 0;
1723
- }
1724
2503
  const input = options.hostStdio ? createInterface({ input: process.stdin, crlfDelay: Infinity }) : void 0;
1725
2504
  const lines = input?.[Symbol.asyncIterator]();
1726
2505
  let host;
@@ -1736,6 +2515,17 @@ async function main(argv) {
1736
2515
  input?.close();
1737
2516
  throw error;
1738
2517
  }
2518
+ if (options.inspect) {
2519
+ const runtime = createWebRuntime(options.projectDir, options.dataDir ?? "", () => void 0);
2520
+ try {
2521
+ console.log(JSON.stringify(await inspectWebProject(runtime, options.projectDir, host)));
2522
+ } finally {
2523
+ runtime.dispose();
2524
+ input?.close();
2525
+ process.stdin.pause();
2526
+ }
2527
+ return 0;
2528
+ }
1739
2529
  const app = await startWebServer({ ...options, host }).catch((error) => {
1740
2530
  input?.close();
1741
2531
  throw error;
@@ -1752,7 +2542,7 @@ async function main(argv) {
1752
2542
  };
1753
2543
  process.once("SIGINT", close);
1754
2544
  process.once("SIGTERM", close);
1755
- console.log(options.json ? JSON.stringify({ type: "ready", protocolVersion: 1, hostProtocolVersion: 1, url: app.url, project: { id: app.project.id, name: app.project.name, projectType: app.project.projectType }, runtime: app.project.runtime }) : `Workflow Web: ${app.url}`);
2545
+ console.log(options.json ? JSON.stringify({ type: "ready", protocolVersion: 1, hostProtocolVersion: 1, assistantProtocolVersion: host?.assistant ? ASSISTANT_PROTOCOL_VERSION : void 0, url: app.url, project: { id: app.project.id, name: app.project.name, projectType: app.project.projectType }, runtime: app.project.runtime }) : `Workflow Web: ${app.url}`);
1756
2546
  if (lines) {
1757
2547
  void (async () => {
1758
2548
  try {