@oh-story/dsh 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -27,7 +27,7 @@ module.exports = __toCommonJS(index_exports);
27
27
  var import_react = require("react"), import_react_dom = require("react-dom");
28
28
 
29
29
  // src/client/file-activity.ts
30
- var STORY_DIRECTORIES = /* @__PURE__ */ new Set(["\u6B63\u6587", "\u5927\u7EB2", "\u8BBE\u5B9A", "\u8FFD\u8E2A", "\u5BF9\u6807", "\u53C2\u8003\u8D44\u6599"]), DRAMA_DIRECTORIES = /* @__PURE__ */ new Set(["\u8F93\u5165", "\u9879\u76EE\u5F00\u53D1", "\u8BBE\u5B9A\u96C6", "\u5267\u96C6", "\u4EA4\u4ED8", "\u521B\u4F5C\u8005\u51B3\u7B56", "\u5BA1\u67E5"]), EDITABLE_EXTENSION = /\.(?:md|txt|json|jsonl)$/iu;
30
+ var STORY_DIRECTORIES = /* @__PURE__ */ new Set(["\u6B63\u6587", "\u5927\u7EB2", "\u8BBE\u5B9A", "\u8FFD\u8E2A", "\u5BF9\u6807", "\u53C2\u8003\u8D44\u6599"]), DRAMA_DIRECTORIES = /* @__PURE__ */ new Set(["\u8F93\u5165", "\u9879\u76EE\u5F00\u53D1", "\u8BBE\u5B9A\u96C6", "\u5267\u96C6", "\u4EA4\u4ED8", "\u521B\u4F5C\u8005\u51B3\u7B56", "\u5BA1\u67E5"]), EDITABLE_EXTENSION = /\.(?:md|txt|json|jsonl)$/iu, MUTATING_CALLS = /* @__PURE__ */ new Set(["write", "edit", "str_replace_editor", "bash", "run_code", "oh_story_role"]);
31
31
  function decodeEscape(character) {
32
32
  switch (character) {
33
33
  case '"':
@@ -80,7 +80,16 @@ function completedString(raw, key) {
80
80
  let value = jsonStringPrefix(raw, key);
81
81
  return value?.complete === !0 ? value.value : void 0;
82
82
  }
83
- function mutation(name2, callId, argsRaw, stage) {
83
+ function parsedArgs(raw) {
84
+ try {
85
+ let value = JSON.parse(raw);
86
+ return typeof value == "object" && value !== null && !Array.isArray(value) ? value : void 0;
87
+ } catch {
88
+ return;
89
+ }
90
+ }
91
+ function mutationFromArgs(name2, callId, argsRaw, stage) {
92
+ let complete = parsedArgs(argsRaw);
84
93
  if (name2 === "write")
85
94
  return {
86
95
  callId,
@@ -90,7 +99,8 @@ function mutation(name2, callId, argsRaw, stage) {
90
99
  path: jsonStringPrefix(argsRaw, "file_path")?.value,
91
100
  operation: "replace-file",
92
101
  oldText: void 0,
93
- newText: jsonStringPrefix(argsRaw, "content")?.value
102
+ newText: jsonStringPrefix(argsRaw, "content")?.value,
103
+ replaceAll: !1
94
104
  };
95
105
  if (name2 === "edit")
96
106
  return {
@@ -101,7 +111,8 @@ function mutation(name2, callId, argsRaw, stage) {
101
111
  path: jsonStringPrefix(argsRaw, "file_path")?.value,
102
112
  operation: "replace-text",
103
113
  oldText: completedString(argsRaw, "old_string"),
104
- newText: jsonStringPrefix(argsRaw, "new_string")?.value
114
+ newText: jsonStringPrefix(argsRaw, "new_string")?.value,
115
+ replaceAll: complete?.replace_all === !0
105
116
  };
106
117
  if (name2 !== "str_replace_editor") return;
107
118
  let command = completedString(argsRaw, "command");
@@ -115,7 +126,8 @@ function mutation(name2, callId, argsRaw, stage) {
115
126
  path,
116
127
  operation: "replace-file",
117
128
  oldText: void 0,
118
- newText: jsonStringPrefix(argsRaw, "file_text")?.value
129
+ newText: jsonStringPrefix(argsRaw, "file_text")?.value,
130
+ replaceAll: !1
119
131
  } : command === "str_replace" ? {
120
132
  callId,
121
133
  name: name2,
@@ -124,7 +136,8 @@ function mutation(name2, callId, argsRaw, stage) {
124
136
  path,
125
137
  operation: "replace-text",
126
138
  oldText: completedString(argsRaw, "old_str"),
127
- newText: jsonStringPrefix(argsRaw, "new_str")?.value
139
+ newText: jsonStringPrefix(argsRaw, "new_str")?.value ?? (complete !== void 0 ? "" : void 0),
140
+ replaceAll: complete?.replace_all === !0
128
141
  } : command === "insert" ? {
129
142
  callId,
130
143
  name: name2,
@@ -133,27 +146,50 @@ function mutation(name2, callId, argsRaw, stage) {
133
146
  path,
134
147
  operation: "insert-text",
135
148
  oldText: void 0,
136
- newText: jsonStringPrefix(argsRaw, "new_str")?.value
137
- } : { callId, name: name2, argsRaw, stage, path, operation: void 0, oldText: void 0, newText: void 0 };
138
- }
139
- function activeFileMutation(partialBlocks, runningCalls) {
140
- for (let index = runningCalls.length - 1; index >= 0; index -= 1) {
141
- let call = runningCalls[index];
142
- if (call === void 0) continue;
143
- let value = mutation(call.name, call.callId, call.argsRaw, "running");
144
- if (value !== void 0) return value;
145
- }
146
- for (let index = partialBlocks.length - 1; index >= 0; index -= 1) {
147
- let block = partialBlocks[index];
148
- if (block?.kind !== "tool-call") continue;
149
- let value = mutation(block.name, block.callId, block.argsRaw, "streaming");
150
- if (value !== void 0) return value;
149
+ newText: jsonStringPrefix(argsRaw, "new_str")?.value,
150
+ replaceAll: !1
151
+ } : { callId, name: name2, argsRaw, stage, path, operation: void 0, oldText: void 0, newText: void 0, replaceAll: !1 };
152
+ }
153
+ function mutationsFromRunning(call) {
154
+ let direct = mutationFromArgs(call.name, call.callId, call.argsRaw, "running");
155
+ if (direct === void 0) return [];
156
+ let view = call.callView;
157
+ return view?.card !== "diff" || view.diffs.length === 0 ? [direct] : view.diffs.map((diff, index) => ({
158
+ ...direct,
159
+ callId: view.diffs.length === 1 ? call.callId : `${call.callId}:${String(index)}`,
160
+ path: diff.path,
161
+ operation: diff.oldText === null ? "replace-file" : "replace-text",
162
+ oldText: diff.oldText ?? void 0,
163
+ newText: diff.newText
164
+ }));
165
+ }
166
+ function visitRunning(blocks, visit) {
167
+ for (let block of blocks)
168
+ "kind" in block || visit(block), visitRunning(block.subCalls, visit);
169
+ }
170
+ function fileMutations(runningCalls, projectedCalls = []) {
171
+ let values = [];
172
+ visitRunning(runningCalls, (call) => {
173
+ values.push(...mutationsFromRunning(call));
174
+ });
175
+ for (let call of projectedCalls) {
176
+ let value = mutationFromArgs(call.name, call.callId, call.argsRaw, "streaming");
177
+ value !== void 0 && !values.some((candidate) => candidate.callId === value.callId) && values.push(value);
151
178
  }
179
+ return values;
180
+ }
181
+ function mutatingCallIds(runningCalls) {
182
+ let ids = /* @__PURE__ */ new Set();
183
+ return visitRunning(runningCalls, (call) => {
184
+ MUTATING_CALLS.has(call.name) && ids.add(call.callId);
185
+ }), ids;
152
186
  }
153
187
  function creativeRelativePath(path, cwd) {
154
188
  if (path === void 0 || path === "") return;
155
- let normalized = path.replaceAll("\\", "/"), root = cwd?.replaceAll("\\", "/").replace(/\/$/u, ""), relative = root !== void 0 && normalized.startsWith(`${root}/`) ? normalized.slice(root.length + 1) : normalized.replace(/^\.\//u, "").replace(/^\//u, ""), [directory2] = relative.split("/", 1);
156
- if (!(!(directory2 !== void 0 && (STORY_DIRECTORIES.has(directory2) || DRAMA_DIRECTORIES.has(directory2))) && relative !== "short-drama.json" || !EDITABLE_EXTENSION.test(relative)) && !relative.split("/").some((part) => part === ".." || part === ""))
189
+ let normalized = path.replaceAll("\\", "/"), root = cwd?.replaceAll("\\", "/").replace(/\/$/u, ""), insideRoot = root !== void 0 && normalized.startsWith(`${root}/`);
190
+ if ((normalized.startsWith("/") || /^[a-z]:\//iu.test(normalized) || normalized.startsWith("file:")) && !insideRoot) return;
191
+ let relative = insideRoot ? normalized.slice(root.length + 1) : normalized.replace(/^\.\//u, ""), [directory2] = relative.split("/", 1);
192
+ if (!(!(directory2 !== void 0 && (STORY_DIRECTORIES.has(directory2) || DRAMA_DIRECTORIES.has(directory2))) && relative !== "short-drama.json" || !EDITABLE_EXTENSION.test(relative)) && !relative.split("/").some((part) => part === ".." || part === "." || part === ""))
157
193
  return relative;
158
194
  }
159
195
  function workbenchModeForPath(path) {
@@ -165,7 +201,8 @@ function workbenchModeForPath(path) {
165
201
  function previewMutation(activity, base) {
166
202
  if (activity.operation === "replace-file") return activity.newText;
167
203
  if (activity.operation === "replace-text") {
168
- if (activity.oldText === void 0 || activity.newText === void 0) return;
204
+ if (activity.oldText === void 0 || activity.newText === void 0 || activity.oldText === "") return;
205
+ if (activity.replaceAll) return base.includes(activity.oldText) ? base.split(activity.oldText).join(activity.newText) : void 0;
169
206
  let at = base.indexOf(activity.oldText);
170
207
  return at < 0 ? void 0 : `${base.slice(0, at)}${activity.newText}${base.slice(at + activity.oldText.length)}`;
171
208
  }
@@ -466,7 +503,7 @@ var plugin_default = `.oh-story-bridge-marker { display: none; }
466
503
  --os-border: var(--dsw-alias-border-l2);
467
504
  --os-muted: var(--dsw-alias-label-tertiary);
468
505
  display: grid;
469
- grid-template-columns: clamp(108px, 16%, 190px) minmax(220px, 1fr) minmax(260px, 38%);
506
+ grid-template-columns: clamp(96px, 16%, 190px) minmax(180px, 1fr) minmax(252px, 38%);
470
507
  grid-template-rows: minmax(0, 1fr);
471
508
  min-height: 0;
472
509
  overflow-x: hidden;
@@ -765,7 +802,8 @@ var plugin_default = `.oh-story-bridge-marker { display: none; }
765
802
  .oh-story-jsonl-error pre { margin: 3px -12px -12px; }
766
803
 
767
804
  .oh-story-stream,
768
- .oh-story-conflict {
805
+ .oh-story-conflict,
806
+ .oh-story-warning {
769
807
  flex: none;
770
808
  padding: 7px 12px;
771
809
  border-bottom: 1px solid var(--os-border);
@@ -776,6 +814,18 @@ var plugin_default = `.oh-story-bridge-marker { display: none; }
776
814
  .oh-story-stream { color: var(--dsw-alias-state-business-primary); background: var(--dsw-alias-button-ghost-active-fill); }
777
815
  .oh-story-stream[data-stage="streaming"] { animation: oh-story-pulse 1.2s ease-in-out infinite alternate; }
778
816
  .oh-story-conflict { color: var(--dsw-alias-state-warn-label); background: var(--dsw-specific-tip); }
817
+ .oh-story-conflict > div { display: flex; gap: 6px; margin-top: 6px; }
818
+ .oh-story-conflict button,
819
+ .oh-story-empty button {
820
+ border: 1px solid var(--os-border);
821
+ border-radius: 6px;
822
+ color: inherit;
823
+ background: var(--dsw-alias-bg-base);
824
+ padding: 3px 8px;
825
+ cursor: pointer;
826
+ font: inherit;
827
+ }
828
+ .oh-story-warning { color: var(--dsw-alias-state-warn-label); background: var(--dsw-specific-tip); }
779
829
 
780
830
  @keyframes oh-story-pulse { to { opacity: .62; } }
781
831
  @media (prefers-reduced-motion: reduce) { .oh-story-stream[data-stage="streaming"] { animation: none; } }
@@ -799,10 +849,40 @@ var plugin_default = `.oh-story-bridge-marker { display: none; }
799
849
  }
800
850
  .oh-story-editor textarea { padding-right: 28px; padding-left: 28px; }
801
851
  }
852
+
853
+ @media (max-width: 700px) {
854
+ [data-conversation-scroll]:has(> [data-slot="conversation.session"] > .oh-story-split-surface) {
855
+ grid-template-columns: 72px minmax(0, 1fr) 236px;
856
+ }
857
+ .oh-story-tree { padding-inline: 5px; }
858
+ .oh-story-brand { padding-inline: 3px; font-size: 11px; }
859
+ .oh-story-brand button { width: 24px; }
860
+ .oh-story-mode-tabs { margin-inline: 0; }
861
+ .oh-story-mode-tabs button { font-size: 10px; }
862
+ .oh-story-editor header { padding-inline: 7px; }
863
+ .oh-story-editor header > span { display: none; }
864
+ .oh-story-editor-tabs button { padding-inline: 6px; }
865
+ .oh-story-jsonl summary > code,
866
+ .oh-story-jsonl summary > em { display: none; }
867
+ }
802
868
  `;
803
869
 
870
+ // src/file-activity-projection-types.ts
871
+ var FILE_ACTIVITY_PROJECTION_KEY = "ohStoryFileActivity";
872
+
804
873
  // src/client/index.tsx
805
- var import_jsx_runtime3 = require("react/jsx-runtime"), name = "oh-story", inject = ["slots"], GROUP_ORDER = {
874
+ var import_jsx_runtime3 = require("react/jsx-runtime"), name = "oh-story", inject = ["sessions", "slots"], EMPTY_FILE_ACTIVITY_STORE = {
875
+ subscribe: () => () => {
876
+ },
877
+ getSnapshot: () => {
878
+ }
879
+ }, WorkspaceRequestError = class extends Error {
880
+ constructor(status, message) {
881
+ super(message);
882
+ this.status = status;
883
+ }
884
+ status;
885
+ }, workbenchMemory = /* @__PURE__ */ new Map(), GROUP_ORDER = {
806
886
  story: ["\u6B63\u6587", "\u5927\u7EB2", "\u8BBE\u5B9A", "\u8FFD\u8E2A", "\u5BF9\u6807", "\u53C2\u8003\u8D44\u6599"],
807
887
  drama: ["\u9879\u76EE", "\u8F93\u5165", "\u9879\u76EE\u5F00\u53D1", "\u8BBE\u5B9A\u96C6", "\u5267\u96C6", "\u5BA1\u67E5", "\u521B\u4F5C\u8005\u51B3\u7B56", "\u4EA4\u4ED8"]
808
888
  };
@@ -817,14 +897,14 @@ function preferredFile(files, mode) {
817
897
  }
818
898
  return matching[0]?.path;
819
899
  }
820
- var EMPTY_BLOCKS = [], EMPTY_CALLS = [];
900
+ var EMPTY_CALLS = [];
821
901
  function endpoint(path, sessionId, file) {
822
902
  let url = new URL(`/oh-story/${path}`, globalThis.location.origin);
823
903
  return url.searchParams.set("sessionId", sessionId), file !== void 0 && url.searchParams.set("path", file), url.toString();
824
904
  }
825
905
  async function json(response) {
826
906
  let value = await response.json();
827
- if (!response.ok) throw new Error(value.error ?? `HTTP ${String(response.status)}`);
907
+ if (!response.ok) throw new WorkspaceRequestError(response.status, value.error ?? `HTTP ${String(response.status)}`);
828
908
  return value;
829
909
  }
830
910
  function FileTreeNodes({
@@ -852,7 +932,7 @@ function FileTreeNodes({
852
932
  },
853
933
  node.path
854
934
  );
855
- let open = expanded[node.path] ?? selected?.startsWith(`${node.path}/`) === !0;
935
+ let open = selected?.startsWith(`${node.path}/`) === !0 || expanded[node.path] === !0;
856
936
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("details", { className: "oh-story-file-folder", open, onToggle: (event) => {
857
937
  onToggle(node.path, event.currentTarget.open);
858
938
  }, children: [
@@ -876,89 +956,157 @@ function FileTreeNodes({
876
956
  }) });
877
957
  }
878
958
  function useWorkspace(sessionId) {
879
- let [version, setVersion] = (0, import_react.useState)(0), [workspace, setWorkspace] = (0, import_react.useState)(), [error, setError] = (0, import_react.useState)(), reload = (0, import_react.useCallback)(() => {
880
- setVersion((value) => value + 1);
959
+ let [version, setVersion] = (0, import_react.useState)(0), [workspace, setWorkspace] = (0, import_react.useState)(), [error, setError] = (0, import_react.useState)(), [loading, setLoading] = (0, import_react.useState)(!0), reload = (0, import_react.useCallback)(() => {
960
+ setLoading(!0), setVersion((value) => value + 1);
881
961
  }, []);
882
962
  return (0, import_react.useEffect)(() => {
883
963
  let controller = new AbortController();
884
964
  return setError(void 0), fetch(endpoint("workspace", sessionId), { signal: controller.signal }).then((response) => json(response)).then(setWorkspace).catch((reason) => {
885
965
  controller.signal.aborted || setError(reason instanceof Error ? reason.message : String(reason));
966
+ }).finally(() => {
967
+ controller.signal.aborted || setLoading(!1);
886
968
  }), () => {
887
969
  controller.abort();
888
970
  };
889
- }, [sessionId, version]), { workspace, error, reload };
971
+ }, [sessionId, version]), { workspace, error, loading, reload };
890
972
  }
891
- function CreativeWorkbench({ sessionId, useSession }) {
892
- let { workspace, error, reload } = useWorkspace(sessionId), partialBlocks = useSession((value) => value.partial?.blocks ?? EMPTY_BLOCKS), runningCalls = useSession((value) => value.runningCalls ?? EMPTY_CALLS), activity = (0, import_react.useMemo)(() => activeFileMutation(partialBlocks, runningCalls), [partialBlocks, runningCalls]), activityPath = creativeRelativePath(activity?.path, workspace?.cwd), activityMode = workbenchModeForPath(activityPath), [workbench, setWorkbench] = (0, import_react.useState)("story"), initializedWorkbench = (0, import_react.useRef)(!1), [selected, setSelected] = (0, import_react.useState)(), [buffers, setBuffers] = (0, import_react.useState)({}), buffersRef = (0, import_react.useRef)({}), [refreshVersion, setRefreshVersion] = (0, import_react.useState)(0), [fileError, setFileError] = (0, import_react.useState)(), [conflict, setConflict] = (0, import_react.useState)(), [saving, setSaving] = (0, import_react.useState)(!1), [expanded, setExpanded] = (0, import_react.useState)({}), surfaceRef = (0, import_react.useRef)(null), navRef = (0, import_react.useRef)(null), lastActivity = (0, import_react.useRef)(), settleTimer = (0, import_react.useRef)(void 0), buffer = selected === void 0 ? void 0 : buffers[selected], dirty = buffer?.source === "human" && buffer.content !== buffer.saved, selectedLower = selected?.toLocaleLowerCase(), markdown = selectedLower?.endsWith(".md") === !0, jsonl = selectedLower?.endsWith(".jsonl") === !0, structured = jsonl || selectedLower?.endsWith(".json") === !0, previewable = markdown || jsonl, [editorMode, setEditorMode] = (0, import_react.useState)("preview");
973
+ function CreativeWorkbench({
974
+ sessionId,
975
+ runningCalls,
976
+ projectedCalls
977
+ }) {
978
+ let { workspace, error, loading: workspaceLoading, reload } = useWorkspace(sessionId), activities = (0, import_react.useMemo)(
979
+ () => fileMutations(runningCalls, projectedCalls),
980
+ [projectedCalls, runningCalls]
981
+ ), normalizedActivities = (0, import_react.useMemo)(() => activities.flatMap((activity2) => {
982
+ let path = creativeRelativePath(activity2.path, workspace?.cwd);
983
+ return path === void 0 ? [] : [{ activity: activity2, path }];
984
+ }), [activities, workspace?.cwd]), primaryActivity = normalizedActivities.at(-1), activityPaths = (0, import_react.useMemo)(() => new Set(normalizedActivities.map((value) => value.path)), [normalizedActivities]), activity = primaryActivity?.activity, activityPath = primaryActivity?.path, remembered = (0, import_react.useMemo)(() => workbenchMemory.get(sessionId), [sessionId]), [workbench, setWorkbench] = (0, import_react.useState)(remembered?.workbench ?? "story"), initializedWorkbench = (0, import_react.useRef)(!1), [selected, setSelected] = (0, import_react.useState)(remembered?.selected), [buffers, setBuffers] = (0, import_react.useState)(remembered?.buffers ?? {}), buffersRef = (0, import_react.useRef)({}), [expanded, setExpanded] = (0, import_react.useState)(remembered?.expanded ?? {}), surfaceRef = (0, import_react.useRef)(null), navRef = (0, import_react.useRef)(null), activityBases = (0, import_react.useRef)(/* @__PURE__ */ new Map()), previousSignals = (0, import_react.useRef)(/* @__PURE__ */ new Set()), saveLocks = (0, import_react.useRef)(/* @__PURE__ */ new Set()), buffer = selected === void 0 ? void 0 : buffers[selected], dirty = buffer?.source === "human" && buffer.content !== buffer.saved, saving = buffer?.saving === !0, fileError = buffer?.error, conflict = buffer?.conflict, selectedLower = selected?.toLocaleLowerCase(), markdown = selectedLower?.endsWith(".md") === !0, jsonl = selectedLower?.endsWith(".jsonl") === !0, structured = jsonl || selectedLower?.endsWith(".json") === !0, previewable = markdown || jsonl, [editorMode, setEditorMode] = (0, import_react.useState)(remembered?.editorMode ?? "preview"), modeSelection = (0, import_react.useRef)(selected);
893
985
  (0, import_react.useEffect)(() => {
894
986
  buffersRef.current = buffers;
895
987
  }, [buffers]), (0, import_react.useEffect)(() => {
988
+ workbenchMemory.set(sessionId, { buffers, editorMode, expanded, selected, workbench });
989
+ }, [buffers, editorMode, expanded, selected, sessionId, workbench]), (0, import_react.useEffect)(() => {
990
+ let warn = (event) => {
991
+ Object.values(buffersRef.current).some((value) => value.source === "human" && value.content !== value.saved) && event.preventDefault();
992
+ };
993
+ return globalThis.addEventListener("beforeunload", warn), () => {
994
+ globalThis.removeEventListener("beforeunload", warn);
995
+ };
996
+ }, []);
997
+ let revealPath = (0, import_react.useCallback)((path) => {
998
+ setWorkbench(workbenchModeForPath(path) ?? "story"), setSelected(path);
999
+ let segments = path.split("/"), ancestors = [groupForPath(path)];
1000
+ for (let index = 1; index < segments.length - 1; index += 1) ancestors.push(segments.slice(0, index + 1).join("/"));
1001
+ setExpanded((current) => {
1002
+ let next = { ...current };
1003
+ for (let ancestor of ancestors) next[ancestor] = !0;
1004
+ return next;
1005
+ });
1006
+ }, []);
1007
+ (0, import_react.useEffect)(() => {
896
1008
  if (workspace === void 0 || initializedWorkbench.current) return;
897
1009
  let hasStory = workspace.files.some((file) => workbenchModeForPath(file.path) === "story");
898
1010
  workspace.shortDrama !== null && !hasStory && setWorkbench("drama"), initializedWorkbench.current = !0;
899
1011
  }, [workspace]), (0, import_react.useEffect)(() => {
900
- activityMode !== void 0 && setWorkbench(activityMode);
901
- }, [activityMode]), (0, import_react.useEffect)(() => {
902
- setEditorMode(previewable ? "preview" : "source");
903
- }, [previewable, selected]), (0, import_react.useEffect)(() => {
904
- if (activityPath !== void 0) {
905
- setSelected(activityPath);
906
- return;
907
- }
908
- selected !== void 0 && ((workspace?.files.some((file) => file.path === selected) ?? !1) || buffers[selected] !== void 0) && workbenchModeForPath(selected) === workbench || setSelected(workspace === void 0 ? void 0 : preferredFile(workspace.files, workbench));
909
- }, [activityPath, buffers, selected, workbench, workspace]), (0, import_react.useEffect)(() => {
910
- if (selected === void 0) return;
911
- let exists = workspace?.files.some((file) => file.path === selected) ?? !1;
912
- if (activityPath === selected && !exists) return;
913
- let controller = new AbortController();
914
- return setFileError(void 0), fetch(endpoint("file", sessionId, selected), { signal: controller.signal }).then((response) => json(response)).then((file) => {
915
- let existing = buffersRef.current[file.path];
916
- if (existing?.source === "human" && existing.content !== existing.saved) {
917
- setConflict(`${file.path} \u5DF2\u88AB Agent \u66F4\u65B0\uFF1B\u4F60\u7684\u672A\u4FDD\u5B58\u7248\u672C\u4ECD\u4FDD\u7559\u3002`);
1012
+ activityPath !== void 0 && setEditorMode("source");
1013
+ }, [activityPath]), (0, import_react.useEffect)(() => {
1014
+ modeSelection.current !== selected && (modeSelection.current = selected, setEditorMode(activityPath === selected ? "source" : previewable ? "preview" : "source"));
1015
+ }, [activityPath, previewable, selected]), (0, import_react.useEffect)(() => {
1016
+ if (!workspaceLoading) {
1017
+ if (activityPath !== void 0) {
1018
+ revealPath(activityPath);
918
1019
  return;
919
1020
  }
920
- setBuffers((current) => ({
921
- ...current,
922
- [file.path]: { content: file.content, saved: file.content, source: "disk", version: file.version }
923
- }));
1021
+ selected !== void 0 && ((workspace?.files.some((file) => file.path === selected) ?? !1) || buffers[selected] !== void 0) && workbenchModeForPath(selected) === workbench || setSelected(workspace === void 0 ? void 0 : preferredFile(workspace.files, workbench));
1022
+ }
1023
+ }, [activityPath, buffers, revealPath, selected, workbench, workspace, workspaceLoading]), (0, import_react.useEffect)(() => {
1024
+ if (workspace === void 0 || workspaceLoading) return;
1025
+ let paths = new Set(workspace.files.map((file) => file.path));
1026
+ setBuffers((current) => {
1027
+ let changed = !1, next = { ...current };
1028
+ for (let [path, value] of Object.entries(current))
1029
+ paths.has(path) || activityPaths.has(path) || (value.source === "human" && value.content !== value.saved ? value.missing !== !0 && (next[path] = { ...value, missing: !0, error: "\u6587\u4EF6\u5DF2\u4ECE workspace \u79FB\u9664\u3002\u672C\u5730\u8349\u7A3F\u4ECD\u4FDD\u7559\uFF0C\u53EF\u590D\u5236\u540E\u653E\u5F03\u8349\u7A3F\u3002" }, changed = !0) : (delete next[path], changed = !0));
1030
+ return changed ? next : current;
1031
+ });
1032
+ }, [activityPaths, workspace, workspaceLoading]), (0, import_react.useEffect)(() => {
1033
+ if (selected === void 0 || activityPaths.has(selected) || !(workspace?.files.some((file) => file.path === selected) ?? !1)) return;
1034
+ let controller = new AbortController();
1035
+ return setBuffers((current) => {
1036
+ let existing = current[selected];
1037
+ return existing === void 0 ? current : { ...current, [selected]: { ...existing, error: void 0 } };
1038
+ }), fetch(endpoint("file", sessionId, selected), { signal: controller.signal }).then((response) => json(response)).then((file) => {
1039
+ setBuffers((current) => {
1040
+ let existing = current[file.path];
1041
+ return existing?.source === "human" && existing.content !== existing.saved ? existing.version === file.version ? { ...current, [file.path]: { ...existing, missing: !1, error: void 0 } } : {
1042
+ ...current,
1043
+ [file.path]: {
1044
+ ...existing,
1045
+ missing: !1,
1046
+ error: void 0,
1047
+ conflict: {
1048
+ message: `${file.path} \u5DF2\u5728\u78C1\u76D8\u4E0A\u66F4\u65B0\uFF1B\u4F60\u7684\u672C\u5730\u8349\u7A3F\u6CA1\u6709\u88AB\u8986\u76D6\u3002`,
1049
+ theirs: file.content,
1050
+ theirsVersion: file.version
1051
+ }
1052
+ }
1053
+ } : {
1054
+ ...current,
1055
+ [file.path]: { content: file.content, saved: file.content, source: "disk", version: file.version }
1056
+ };
1057
+ });
924
1058
  }).catch((reason) => {
925
- controller.signal.aborted || setFileError(reason instanceof Error ? reason.message : String(reason));
1059
+ controller.signal.aborted || setBuffers((current) => {
1060
+ let existing = current[selected];
1061
+ return existing === void 0 ? current : {
1062
+ ...current,
1063
+ [selected]: { ...existing, error: reason instanceof Error ? reason.message : String(reason) }
1064
+ };
1065
+ });
926
1066
  }), () => {
927
1067
  controller.abort();
928
1068
  };
929
- }, [activityPath, refreshVersion, selected, sessionId, workspace?.files]), (0, import_react.useEffect)(() => {
930
- if (activity === void 0 || activityPath === void 0) return;
931
- setSelected(activityPath);
932
- let existing = buffers[activityPath];
933
- if (existing?.source === "human" && existing.content !== existing.saved) {
934
- setConflict(`${activityPath} \u6B63\u7531 Agent \u4FEE\u6539\uFF1B\u4F60\u7684\u672A\u4FDD\u5B58\u7248\u672C\u5DF2\u9501\u5B9A\uFF0C\u4E0D\u4F1A\u88AB\u8986\u76D6\u3002`);
935
- return;
936
- }
937
- let next = previewMutation(activity, existing?.saved ?? "");
938
- next !== void 0 && (setConflict(void 0), setBuffers((current) => {
939
- let currentBuffer = current[activityPath];
940
- return currentBuffer?.source === "agent" && currentBuffer.content === next ? current : {
941
- ...current,
942
- [activityPath]: {
943
- content: next,
944
- saved: currentBuffer?.saved ?? "",
945
- source: "agent",
946
- version: currentBuffer?.version ?? ""
1069
+ }, [activityPaths, selected, sessionId, workspace?.files]), (0, import_react.useEffect)(() => {
1070
+ if (normalizedActivities.length !== 0) {
1071
+ for (let { path } of normalizedActivities) revealPath(path);
1072
+ setBuffers((current) => {
1073
+ let next = current;
1074
+ for (let { activity: currentActivity, path } of normalizedActivities) {
1075
+ let existing = next[path];
1076
+ if (existing?.source === "human" && existing.content !== existing.saved) {
1077
+ next = {
1078
+ ...next,
1079
+ [path]: {
1080
+ ...existing,
1081
+ conflict: { message: `${path} \u6B63\u7531 Agent \u4FEE\u6539\uFF1B\u4F60\u7684\u672C\u5730\u8349\u7A3F\u5DF2\u9501\u5B9A\uFF0C\u4E0D\u4F1A\u88AB\u8986\u76D6\u3002` }
1082
+ }
1083
+ };
1084
+ continue;
1085
+ }
1086
+ let basis = activityBases.current.get(currentActivity.callId);
1087
+ (basis === void 0 || basis.path !== path) && (basis = { path, base: existing?.content ?? "" }, activityBases.current.set(currentActivity.callId, basis));
1088
+ let preview = previewMutation(currentActivity, basis.base);
1089
+ preview === void 0 || existing?.source === "agent" && existing.content === preview || (next = {
1090
+ ...next,
1091
+ [path]: {
1092
+ content: preview,
1093
+ saved: existing?.saved ?? "",
1094
+ source: "agent",
1095
+ version: existing?.version ?? ""
1096
+ }
1097
+ });
947
1098
  }
948
- };
949
- }));
950
- }, [activity, activityPath, buffers]), (0, import_react.useEffect)(() => {
951
- if (activity !== void 0) {
952
- settleTimer.current !== void 0 && clearTimeout(settleTimer.current), lastActivity.current = activity;
953
- return;
1099
+ return next;
1100
+ });
954
1101
  }
955
- if (lastActivity.current !== void 0)
956
- return settleTimer.current = setTimeout(() => {
957
- lastActivity.current = void 0, reload(), setRefreshVersion((value) => value + 1);
958
- }, 180), () => {
959
- settleTimer.current !== void 0 && clearTimeout(settleTimer.current);
960
- };
961
- }, [activity, reload]), (0, import_react.useEffect)(() => {
1102
+ }, [normalizedActivities, revealPath]), (0, import_react.useEffect)(() => {
1103
+ let signals = new Set(mutatingCallIds(runningCalls));
1104
+ for (let { activity: currentActivity } of normalizedActivities) signals.add(currentActivity.callId.split(":", 1)[0] ?? currentActivity.callId);
1105
+ let settled = [...previousSignals.current].some((callId) => !signals.has(callId));
1106
+ for (let callId of activityBases.current.keys())
1107
+ signals.has(callId.split(":", 1)[0] ?? callId) || activityBases.current.delete(callId);
1108
+ previousSignals.current = signals, settled && reload();
1109
+ }, [normalizedActivities, reload, runningCalls]), (0, import_react.useEffect)(() => {
962
1110
  if (selected !== void 0) {
963
1111
  for (let button of navRef.current?.querySelectorAll("button[data-file-path]") ?? [])
964
1112
  if (button.dataset.filePath === selected) {
@@ -967,7 +1115,7 @@ function CreativeWorkbench({ sessionId, useSession }) {
967
1115
  }
968
1116
  }
969
1117
  }, [selected]), (0, import_react.useEffect)(() => {
970
- if (activity !== void 0 || workspace === void 0) return;
1118
+ if (normalizedActivities.length > 0 || workspace === void 0) return;
971
1119
  let sessionSurface = surfaceRef.current?.parentElement;
972
1120
  if (sessionSurface == null) return;
973
1121
  let knownPaths = new Set(workspace.files.map((file) => file.path)), followOfficialFileLink = (event) => {
@@ -977,9 +1125,9 @@ function CreativeWorkbench({ sessionId, useSession }) {
977
1125
  if (control === null || control.closest(".oh-story-split-surface") !== null) return;
978
1126
  let candidates = [control.title, control.getAttribute("aria-label"), control.textContent];
979
1127
  for (let candidate of candidates) {
980
- let path = creativeRelativePath(candidate?.trim().replace(/^Open\s+/u, ""), workspace.cwd);
1128
+ let path = creativeRelativePath(candidate?.trim().replace(/^(?:Open|打开)\s+/u, ""), workspace.cwd);
981
1129
  if (!(path === void 0 || !knownPaths.has(path))) {
982
- event.preventDefault(), event.stopPropagation(), setConflict(void 0), setWorkbench(workbenchModeForPath(path) ?? "story"), setSelected(path);
1130
+ event.preventDefault(), event.stopPropagation(), revealPath(path);
983
1131
  break;
984
1132
  }
985
1133
  }
@@ -987,39 +1135,88 @@ function CreativeWorkbench({ sessionId, useSession }) {
987
1135
  return sessionSurface.addEventListener("click", followOfficialFileLink, !0), () => {
988
1136
  sessionSurface.removeEventListener("click", followOfficialFileLink, !0);
989
1137
  };
990
- }, [activity, workspace]);
991
- let save = (0, import_react.useCallback)(async () => {
992
- if (selected === void 0 || buffer === void 0) return;
993
- let submitted = buffer;
994
- setSaving(!0), setFileError(void 0);
995
- try {
996
- let file = await json(await fetch(endpoint("file", sessionId, selected), {
997
- method: "PUT",
998
- headers: { "content-type": "application/json" },
999
- body: JSON.stringify({ content: submitted.content, baseVersion: submitted.version })
1000
- }));
1001
- setBuffers((current) => {
1002
- let latest = current[selected];
1003
- if (latest === void 0) return current;
1004
- let unchanged = latest.content === submitted.content;
1005
- return {
1006
- ...current,
1007
- [selected]: {
1008
- content: unchanged ? file.content : latest.content,
1009
- saved: file.content,
1010
- source: unchanged ? "disk" : "human",
1011
- version: file.version
1138
+ }, [normalizedActivities.length, revealPath, workspace]);
1139
+ let savePath = (0, import_react.useCallback)(async (path) => {
1140
+ if (saveLocks.current.has(path)) return;
1141
+ let submitted = buffersRef.current[path];
1142
+ if (!(submitted === void 0 || submitted.missing === !0 || submitted.content === submitted.saved)) {
1143
+ saveLocks.current.add(path), setBuffers((current) => {
1144
+ let existing = current[path];
1145
+ return existing === void 0 ? current : { ...current, [path]: { ...existing, saving: !0, error: void 0 } };
1146
+ });
1147
+ try {
1148
+ let file = await json(await fetch(endpoint("file", sessionId, path), {
1149
+ method: "PUT",
1150
+ headers: { "content-type": "application/json" },
1151
+ body: JSON.stringify({ content: submitted.content, baseVersion: submitted.version })
1152
+ }));
1153
+ setBuffers((current) => {
1154
+ let latest = current[path];
1155
+ if (latest === void 0) return current;
1156
+ let unchanged = latest.content === submitted.content;
1157
+ return {
1158
+ ...current,
1159
+ [path]: {
1160
+ content: unchanged ? file.content : latest.content,
1161
+ saved: file.content,
1162
+ source: unchanged ? "disk" : "human",
1163
+ version: file.version,
1164
+ saving: !1
1165
+ }
1166
+ };
1167
+ }), reload();
1168
+ } catch (reason) {
1169
+ if (reason instanceof WorkspaceRequestError && reason.status === 412)
1170
+ try {
1171
+ let theirs = await json(await fetch(endpoint("file", sessionId, path)));
1172
+ setBuffers((current) => {
1173
+ let latest = current[path];
1174
+ return latest === void 0 ? current : {
1175
+ ...current,
1176
+ [path]: {
1177
+ ...latest,
1178
+ saving: !1,
1179
+ conflict: {
1180
+ message: `${path} \u5DF2\u5728\u78C1\u76D8\u4E0A\u66F4\u65B0\uFF1B\u8BF7\u9009\u62E9\u4FDD\u7559\u54EA\u4E00\u7248\u3002`,
1181
+ theirs: theirs.content,
1182
+ theirsVersion: theirs.version
1183
+ }
1184
+ }
1185
+ };
1186
+ });
1187
+ } catch (refreshError) {
1188
+ setBuffers((current) => {
1189
+ let existing = current[path];
1190
+ return existing === void 0 ? current : {
1191
+ ...current,
1192
+ [path]: { ...existing, saving: !1, error: refreshError instanceof Error ? refreshError.message : String(refreshError) }
1193
+ };
1194
+ });
1012
1195
  }
1013
- };
1014
- }), setConflict(void 0), reload();
1015
- } catch (reason) {
1016
- setFileError(reason instanceof Error ? reason.message : String(reason));
1017
- } finally {
1018
- setSaving(!1);
1196
+ else
1197
+ setBuffers((current) => {
1198
+ let existing = current[path];
1199
+ return existing === void 0 ? current : {
1200
+ ...current,
1201
+ [path]: { ...existing, saving: !1, error: reason instanceof Error ? reason.message : String(reason) }
1202
+ };
1203
+ });
1204
+ } finally {
1205
+ saveLocks.current.delete(path);
1206
+ }
1019
1207
  }
1020
- }, [buffer, reload, selected, sessionId]), groups = (0, import_react.useMemo)(() => {
1208
+ }, [reload, sessionId]);
1209
+ (0, import_react.useEffect)(() => {
1210
+ let saveShortcut = (event) => {
1211
+ !(event.metaKey || event.ctrlKey) || event.key.toLocaleLowerCase() !== "s" || (event.preventDefault(), selected !== void 0 && savePath(selected));
1212
+ };
1213
+ return globalThis.addEventListener("keydown", saveShortcut), () => {
1214
+ globalThis.removeEventListener("keydown", saveShortcut);
1215
+ };
1216
+ }, [savePath, selected]);
1217
+ let groups = (0, import_react.useMemo)(() => {
1021
1218
  let value = /* @__PURE__ */ new Map(), all = [...workspace?.files ?? []].filter((file) => workbenchModeForPath(file.path) === workbench);
1022
- activityPath !== void 0 && !all.some((file) => file.path === activityPath) && all.push({ path: activityPath, bytes: 0 }), all.sort((left, right) => left.path.localeCompare(right.path, "zh-Hans-CN"));
1219
+ activityPath !== void 0 && !all.some((file) => file.path === activityPath) && all.push({ path: activityPath, bytes: 0, version: "" }), all.sort((left, right) => left.path.localeCompare(right.path, "zh-Hans-CN"));
1023
1220
  for (let file of all) {
1024
1221
  let directory2 = groupForPath(file.path), files = value.get(directory2) ?? [];
1025
1222
  files.push(file), value.set(directory2, files);
@@ -1030,11 +1227,21 @@ function CreativeWorkbench({ sessionId, useSession }) {
1030
1227
  return (leftIndex < 0 ? order.length : leftIndex) - (rightIndex < 0 ? order.length : rightIndex) || left.localeCompare(right, "zh-Hans-CN");
1031
1228
  });
1032
1229
  }, [activityPath, workbench, workspace]), selectWorkbench = (next) => {
1033
- setWorkbench(next), setConflict(void 0);
1230
+ setWorkbench(next);
1034
1231
  let target = workspace === void 0 ? void 0 : preferredFile(workspace.files, next);
1035
- setSelected(target);
1232
+ target === void 0 ? setSelected(void 0) : revealPath(target);
1036
1233
  }, selectedGroup = selected === void 0 ? void 0 : groupForPath(selected), toggleGroup = (key, open) => {
1037
1234
  setExpanded((current) => ({ ...current, [key]: open }));
1235
+ }, resolveConflict = (keepLocal) => {
1236
+ if (selected === void 0 || conflict?.theirs === void 0 || conflict.theirsVersion === void 0) return;
1237
+ let theirs = conflict.theirs, theirsVersion = conflict.theirsVersion;
1238
+ setBuffers((current) => {
1239
+ let existing = current[selected];
1240
+ return existing === void 0 ? current : {
1241
+ ...current,
1242
+ [selected]: keepLocal ? { ...existing, saved: theirs, version: theirsVersion, source: "human", conflict: void 0 } : { content: theirs, saved: theirs, source: "disk", version: theirsVersion }
1243
+ };
1244
+ });
1038
1245
  };
1039
1246
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { ref: surfaceRef, className: "oh-story-split-surface", children: [
1040
1247
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: plugin_default }),
@@ -1052,8 +1259,9 @@ function CreativeWorkbench({ sessionId, useSession }) {
1052
1259
  }, children: "\u77ED\u5267" })
1053
1260
  ] }),
1054
1261
  error !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "oh-story-error", children: error }),
1262
+ workspace?.metadataErrors.map((message) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "oh-story-warning", children: message }, message)),
1055
1263
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("nav", { ref: navRef, "aria-label": workbench === "story" ? "\u5C0F\u8BF4\u9879\u76EE\u6587\u4EF6" : "\u77ED\u5267\u9879\u76EE\u6587\u4EF6", children: groups.map(([directory2, files]) => {
1056
- let groupOpen = expanded[directory2] ?? selectedGroup === directory2;
1264
+ let groupOpen = selectedGroup === directory2 || expanded[directory2] === !0;
1057
1265
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("details", { className: "oh-story-file-group", open: groupOpen, onToggle: (event) => {
1058
1266
  toggleGroup(directory2, event.currentTarget.open);
1059
1267
  }, children: [
@@ -1070,7 +1278,7 @@ function CreativeWorkbench({ sessionId, useSession }) {
1070
1278
  selected,
1071
1279
  activityPath,
1072
1280
  onToggle: toggleGroup,
1073
- onSelect: setSelected
1281
+ onSelect: revealPath
1074
1282
  }
1075
1283
  )
1076
1284
  ] }, directory2);
@@ -1088,16 +1296,26 @@ function CreativeWorkbench({ sessionId, useSession }) {
1088
1296
  setEditorMode("source");
1089
1297
  }, children: "\u6E90\u7801" })
1090
1298
  ] }),
1091
- (dirty || saving) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "oh-story-save", type: "button", disabled: saving, onClick: () => {
1092
- save();
1299
+ (dirty || saving) && selected !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "oh-story-save", type: "button", disabled: saving || buffer?.missing === !0, onClick: () => {
1300
+ savePath(selected);
1093
1301
  }, children: saving ? "\u4FDD\u5B58\u4E2D\u2026" : "\u4FDD\u5B58" })
1094
1302
  ] })
1095
1303
  ] }),
1096
- activityPath === selected && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "oh-story-stream", "data-stage": activity?.stage, children: [
1304
+ activity !== void 0 && activityPath !== void 0 && activityPath === selected && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "oh-story-stream", "data-stage": activity.stage, role: "status", "aria-live": "polite", children: [
1097
1305
  "\u25CF ",
1098
- activity?.stage === "running" ? "Agent \u6B63\u5728\u5E94\u7528\u4FEE\u6539" : "Agent \u6B63\u5728\u751F\u6210\u6587\u4EF6\u5185\u5BB9"
1306
+ activity.stage === "running" ? "Agent \u6B63\u5728\u5E94\u7528\u4FEE\u6539" : "Agent \u6B63\u5728\u751F\u6210\u6587\u4EF6\u5185\u5BB9"
1307
+ ] }),
1308
+ conflict !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "oh-story-conflict", role: "alert", children: [
1309
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: conflict.message }),
1310
+ conflict.theirs !== void 0 && conflict.theirsVersion !== void 0 && selected !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
1311
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => {
1312
+ resolveConflict(!1);
1313
+ }, children: "\u8F7D\u5165\u78C1\u76D8\u7248\u672C" }),
1314
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => {
1315
+ resolveConflict(!0);
1316
+ }, children: "\u4FDD\u7559\u672C\u5730\u8349\u7A3F" })
1317
+ ] })
1099
1318
  ] }),
1100
- conflict !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "oh-story-conflict", children: conflict }),
1101
1319
  fileError !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "oh-story-error", children: fileError }),
1102
1320
  selected === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "oh-story-empty", children: workbench === "story" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1103
1321
  "\u5F53\u524D workspace \u8FD8\u6CA1\u6709\u5C0F\u8BF4\u6587\u4EF6\u3002\u53EF\u5728\u53F3\u4FA7 Chat \u4E2D\u8FD0\u884C ",
@@ -1111,6 +1329,14 @@ function CreativeWorkbench({ sessionId, useSession }) {
1111
1329
  "\u6B63\u5728\u52A0\u8F7D ",
1112
1330
  selected,
1113
1331
  "\u2026"
1332
+ ] }) : buffer.missing === !0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "oh-story-empty", children: [
1333
+ "\u6587\u4EF6\u5DF2\u4ECE workspace \u79FB\u9664\uFF0C\u672C\u5730\u8349\u7A3F\u4ECD\u4FDD\u7559\u3002\u8BF7\u5148\u590D\u5236\u9700\u8981\u7684\u5185\u5BB9\uFF0C\u518D\u653E\u5F03\u8349\u7A3F\u3002",
1334
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => {
1335
+ setBuffers((current) => {
1336
+ let next = { ...current };
1337
+ return delete next[selected], next;
1338
+ }), setSelected(workspace === void 0 ? void 0 : preferredFile(workspace.files, workbench));
1339
+ }, children: "\u653E\u5F03\u672C\u5730\u8349\u7A3F" })
1114
1340
  ] }) : previewable && editorMode === "preview" ? markdown ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MarkdownPreview, { content: buffer.content, label: selected }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(JsonlPreview, { content: buffer.content, label: selected }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1115
1341
  "textarea",
1116
1342
  {
@@ -1124,13 +1350,12 @@ function CreativeWorkbench({ sessionId, useSession }) {
1124
1350
  content,
1125
1351
  saved: current[selected]?.saved ?? "",
1126
1352
  source: "human",
1127
- version: current[selected]?.version ?? ""
1353
+ version: current[selected]?.version ?? "",
1354
+ conflict: current[selected]?.conflict,
1355
+ saving: current[selected]?.saving
1128
1356
  }
1129
1357
  }));
1130
1358
  },
1131
- onKeyDown: (event) => {
1132
- (event.metaKey || event.ctrlKey) && event.key.toLocaleLowerCase() === "s" && (event.preventDefault(), save());
1133
- },
1134
1359
  spellCheck: !structured,
1135
1360
  "aria-label": selected
1136
1361
  }
@@ -1138,12 +1363,25 @@ function CreativeWorkbench({ sessionId, useSession }) {
1138
1363
  ] })
1139
1364
  ] });
1140
1365
  }
1141
- function CreativeSplitBridge(props) {
1142
- let marker = (0, import_react.useRef)(null), [target, setTarget] = (0, import_react.useState)();
1366
+ function CreativeSplitBridge({ context }) {
1367
+ let marker = (0, import_react.useRef)(null), [target, setTarget] = (0, import_react.useState)(), provideFace = context.sessions.currentProvideInfo, subscribeProvide = (0, import_react.useCallback)((listener) => provideFace.subscribe(listener), [provideFace]), getProvide = (0, import_react.useCallback)(() => provideFace.getSnapshot(), [provideFace]), provide = (0, import_react.useSyncExternalStore)(subscribeProvide, getProvide, getProvide), sessionId = provide.sessionId, source = provide.hooks.session, subscribe = (0, import_react.useCallback)((listener) => source?.subscribe(listener) ?? (() => {
1368
+ }), [source]), getSnapshot = (0, import_react.useCallback)(() => source?.getSnapshot(), [source]), snapshot = (0, import_react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot), projectionSource = (0, import_react.useMemo)(
1369
+ () => provide.projections?.faceOf(FILE_ACTIVITY_PROJECTION_KEY) ?? EMPTY_FILE_ACTIVITY_STORE,
1370
+ [provide.projections]
1371
+ ), subscribeProjection = (0, import_react.useCallback)((listener) => projectionSource.subscribe(listener), [projectionSource]), getProjection = (0, import_react.useCallback)(() => projectionSource.getSnapshot(), [projectionSource]), projection = (0, import_react.useSyncExternalStore)(subscribeProjection, getProjection, getProjection);
1143
1372
  return (0, import_react.useLayoutEffect)(() => {
1144
- let anchor = marker.current?.closest("[data-phase]")?.querySelector("[data-conversation-scroll] > [data-slot='conversation.session']");
1145
- setTarget(anchor ?? void 0);
1146
- }, [props.sessionId]), (0, import_react.useLayoutEffect)(() => {
1373
+ let document = marker.current?.ownerDocument;
1374
+ if (document === void 0) return;
1375
+ let locate = () => {
1376
+ let anchor = document.querySelector("[data-conversation-scroll] > [data-slot='conversation.session']");
1377
+ setTarget((current) => current === anchor ? current : anchor ?? void 0);
1378
+ };
1379
+ locate();
1380
+ let observer = new MutationObserver(locate);
1381
+ return observer.observe(document.body, { childList: !0, subtree: !0 }), () => {
1382
+ observer.disconnect();
1383
+ };
1384
+ }, [sessionId]), (0, import_react.useLayoutEffect)(() => {
1147
1385
  let scroller = target?.parentElement;
1148
1386
  if (scroller == null) return;
1149
1387
  let publishHeight = () => {
@@ -1156,7 +1394,15 @@ function CreativeSplitBridge(props) {
1156
1394
  };
1157
1395
  }, [target]), /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1158
1396
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { ref: marker, className: "oh-story-bridge-marker", "aria-hidden": !0 }),
1159
- target === void 0 ? null : (0, import_react_dom.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CreativeWorkbench, { ...props }, props.sessionId), target)
1397
+ target === void 0 || sessionId === void 0 ? null : (0, import_react_dom.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1398
+ CreativeWorkbench,
1399
+ {
1400
+ sessionId,
1401
+ runningCalls: snapshot?.runningCalls ?? EMPTY_CALLS,
1402
+ projectedCalls: projection?.calls ?? []
1403
+ },
1404
+ sessionId
1405
+ ), target)
1160
1406
  ] });
1161
1407
  }
1162
1408
  function argsOf(block) {
@@ -1187,11 +1433,11 @@ function RoleToolView({ block, inspect }) {
1187
1433
  ] });
1188
1434
  }
1189
1435
  function apply(context) {
1190
- context.slots.inject("conversation.session.header.actions", () => context.slots.register({
1191
- name: "conversation.session.header.actions",
1436
+ context.slots.inject("shell.overlay", () => context.slots.register({
1437
+ name: "shell.overlay",
1192
1438
  id: "oh-story-workspace",
1193
1439
  order: -100
1194
- }, CreativeSplitBridge)), context.slots.inject("tool.call.toolview", () => context.slots.register({
1440
+ }, () => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CreativeSplitBridge, { context }))), context.slots.inject("tool.call.toolview", () => context.slots.register({
1195
1441
  name: "tool.call.toolview",
1196
1442
  key: "oh_story_role"
1197
1443
  }, RoleToolView));