@oh-story/dsh 0.1.0 → 0.1.2

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
@@ -24,10 +24,21 @@ __export(index_exports, {
24
24
  name: () => name
25
25
  });
26
26
  module.exports = __toCommonJS(index_exports);
27
- var import_react = require("react"), import_react_dom = require("react-dom");
27
+ var import_client = require("@deepseek-ai/dsh-client-runtime/client"), 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
+ function streamingAssistant(timeline) {
32
+ for (let turnNumber of timeline.turnOrder.toReversed()) {
33
+ let turn = timeline.turns.get(turnNumber);
34
+ if (turn !== void 0)
35
+ for (let step of turn.steps.toReversed()) {
36
+ let assistant = step.data.get("assistant-step");
37
+ if (assistant?.status === "running") return assistant;
38
+ }
39
+ }
40
+ return null;
41
+ }
31
42
  function decodeEscape(character) {
32
43
  switch (character) {
33
44
  case '"':
@@ -80,7 +91,16 @@ function completedString(raw, key) {
80
91
  let value = jsonStringPrefix(raw, key);
81
92
  return value?.complete === !0 ? value.value : void 0;
82
93
  }
83
- function mutation(name2, callId, argsRaw, stage) {
94
+ function parsedArgs(raw) {
95
+ try {
96
+ let value = JSON.parse(raw);
97
+ return typeof value == "object" && value !== null && !Array.isArray(value) ? value : void 0;
98
+ } catch {
99
+ return;
100
+ }
101
+ }
102
+ function mutationFromArgs(name2, callId, argsRaw, stage) {
103
+ let complete = parsedArgs(argsRaw);
84
104
  if (name2 === "write")
85
105
  return {
86
106
  callId,
@@ -90,7 +110,8 @@ function mutation(name2, callId, argsRaw, stage) {
90
110
  path: jsonStringPrefix(argsRaw, "file_path")?.value,
91
111
  operation: "replace-file",
92
112
  oldText: void 0,
93
- newText: jsonStringPrefix(argsRaw, "content")?.value
113
+ newText: jsonStringPrefix(argsRaw, "content")?.value,
114
+ replaceAll: !1
94
115
  };
95
116
  if (name2 === "edit")
96
117
  return {
@@ -101,7 +122,8 @@ function mutation(name2, callId, argsRaw, stage) {
101
122
  path: jsonStringPrefix(argsRaw, "file_path")?.value,
102
123
  operation: "replace-text",
103
124
  oldText: completedString(argsRaw, "old_string"),
104
- newText: jsonStringPrefix(argsRaw, "new_string")?.value
125
+ newText: jsonStringPrefix(argsRaw, "new_string")?.value,
126
+ replaceAll: complete?.replace_all === !0
105
127
  };
106
128
  if (name2 !== "str_replace_editor") return;
107
129
  let command = completedString(argsRaw, "command");
@@ -115,7 +137,8 @@ function mutation(name2, callId, argsRaw, stage) {
115
137
  path,
116
138
  operation: "replace-file",
117
139
  oldText: void 0,
118
- newText: jsonStringPrefix(argsRaw, "file_text")?.value
140
+ newText: jsonStringPrefix(argsRaw, "file_text")?.value,
141
+ replaceAll: !1
119
142
  } : command === "str_replace" ? {
120
143
  callId,
121
144
  name: name2,
@@ -124,7 +147,8 @@ function mutation(name2, callId, argsRaw, stage) {
124
147
  path,
125
148
  operation: "replace-text",
126
149
  oldText: completedString(argsRaw, "old_str"),
127
- newText: jsonStringPrefix(argsRaw, "new_str")?.value
150
+ newText: jsonStringPrefix(argsRaw, "new_str")?.value ?? (complete !== void 0 ? "" : void 0),
151
+ replaceAll: complete?.replace_all === !0
128
152
  } : command === "insert" ? {
129
153
  callId,
130
154
  name: name2,
@@ -133,27 +157,71 @@ function mutation(name2, callId, argsRaw, stage) {
133
157
  path,
134
158
  operation: "insert-text",
135
159
  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;
160
+ newText: jsonStringPrefix(argsRaw, "new_str")?.value,
161
+ replaceAll: !1
162
+ } : { callId, name: name2, argsRaw, stage, path, operation: void 0, oldText: void 0, newText: void 0, replaceAll: !1 };
163
+ }
164
+ function mutationsFromRunning(call) {
165
+ let direct = mutationFromArgs(call.name, call.callId, call.argsRaw, "running");
166
+ if (direct === void 0) return [];
167
+ let view = call.callView;
168
+ return view?.card !== "diff" || view.diffs.length === 0 ? [direct] : view.diffs.map((diff, index) => ({
169
+ ...direct,
170
+ callId: view.diffs.length === 1 ? call.callId : `${call.callId}:${String(index)}`,
171
+ path: diff.path,
172
+ operation: diff.oldText === null ? "replace-file" : "replace-text",
173
+ oldText: diff.oldText ?? void 0,
174
+ newText: diff.newText
175
+ }));
176
+ }
177
+ function visitRunning(blocks, visit) {
178
+ for (let block of blocks)
179
+ "kind" in block || visit(block), visitRunning(block.subCalls, visit);
180
+ }
181
+ function fileMutations(runningCalls, partial = null) {
182
+ let values = [];
183
+ visitRunning(runningCalls, (call) => {
184
+ values.push(...mutationsFromRunning(call));
185
+ });
186
+ for (let block of partial?.blocks ?? []) {
187
+ if (block.kind !== "tool-call") continue;
188
+ let value = mutationFromArgs(block.name, block.callId, block.argsRaw, "streaming");
189
+ value !== void 0 && !values.some((candidate) => candidate.callId === value.callId) && values.push(value);
145
190
  }
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;
191
+ return values;
192
+ }
193
+ function mutatingCallIds(runningCalls) {
194
+ let ids = /* @__PURE__ */ new Set();
195
+ return visitRunning(runningCalls, (call) => {
196
+ MUTATING_CALLS.has(call.name) && ids.add(call.callId);
197
+ }), ids;
198
+ }
199
+ function settledMutationSignals(block) {
200
+ let nested = block.subCalls.flatMap(settledMutationSignals);
201
+ if (!("kind" in block) || block.isError) return nested;
202
+ let diffs = block.resultView?.card === "diff" ? block.resultView.diffs : block.callView?.card === "diff" ? block.callView.diffs : void 0;
203
+ if (diffs !== void 0) return [
204
+ ...diffs.map((diff, index) => `${block.callId}:${String(index)}\0${diff.path}`),
205
+ ...nested
206
+ ];
207
+ if (block.call === null) return nested;
208
+ let mutation = mutationFromArgs(block.call.name, block.callId, block.call.argsRaw, "running");
209
+ return mutation?.path === void 0 ? nested : [`${block.callId}\0${mutation.path}`, ...nested];
210
+ }
211
+ function latestSettledMutation(chat) {
212
+ for (let key of chat.order.toReversed()) {
213
+ let node = chat.nodes.get(key);
214
+ if (node?.kind !== "tool-call") continue;
215
+ let root = node.data.root, signal = root === void 0 ? void 0 : settledMutationSignals(root).at(-1);
216
+ if (signal !== void 0) return signal;
151
217
  }
152
218
  }
153
219
  function creativeRelativePath(path, cwd) {
154
220
  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 === ""))
221
+ let normalized = path.replaceAll("\\", "/"), root = cwd?.replaceAll("\\", "/").replace(/\/$/u, ""), insideRoot = root !== void 0 && normalized.startsWith(`${root}/`);
222
+ if ((normalized.startsWith("/") || /^[a-z]:\//iu.test(normalized) || normalized.startsWith("file:")) && !insideRoot) return;
223
+ let relative = insideRoot ? normalized.slice(root.length + 1) : normalized.replace(/^\.\//u, ""), [directory2] = relative.split("/", 1);
224
+ 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
225
  return relative;
158
226
  }
159
227
  function workbenchModeForPath(path) {
@@ -165,7 +233,8 @@ function workbenchModeForPath(path) {
165
233
  function previewMutation(activity, base) {
166
234
  if (activity.operation === "replace-file") return activity.newText;
167
235
  if (activity.operation === "replace-text") {
168
- if (activity.oldText === void 0 || activity.newText === void 0) return;
236
+ if (activity.oldText === void 0 || activity.newText === void 0 || activity.oldText === "") return;
237
+ if (activity.replaceAll) return base.includes(activity.oldText) ? base.split(activity.oldText).join(activity.newText) : void 0;
169
238
  let at = base.indexOf(activity.oldText);
170
239
  return at < 0 ? void 0 : `${base.slice(0, at)}${activity.newText}${base.slice(at + activity.oldText.length)}`;
171
240
  }
@@ -457,16 +526,16 @@ function MarkdownPreview({ content, label }) {
457
526
 
458
527
  // inline-css:/home/runner/work/oh-story-dsh/oh-story-dsh/packages/dsh-plugin/src/client/plugin.css
459
528
  var plugin_default = `.oh-story-bridge-marker { display: none; }
460
- .oh-story-split-surface { display: contents; }
529
+ .oh-story-split-surface { display: contents; font-family: var(--dsw-font-family); }
461
530
 
462
531
  /* DSH documents every [data-slot] wrapper as a stable addressable styling
463
532
  seam. The official Session body and composer stay mounted; this only turns
464
533
  their scroll host into a three-column writing surface. */
465
534
  [data-conversation-scroll]:has(> [data-slot="conversation.session"] > .oh-story-split-surface) {
466
535
  --os-border: var(--dsw-alias-border-l2);
467
- --os-muted: var(--dsw-alias-label-tertiary);
536
+ --os-muted: var(--dsw-alias-label-secondary);
468
537
  display: grid;
469
- grid-template-columns: clamp(108px, 16%, 190px) minmax(220px, 1fr) minmax(260px, 38%);
538
+ grid-template-columns: clamp(184px, 16%, 200px) minmax(240px, 1fr) clamp(408px, 40%, 520px);
470
539
  grid-template-rows: minmax(0, 1fr);
471
540
  min-height: 0;
472
541
  overflow-x: hidden;
@@ -534,7 +603,10 @@ var plugin_default = `.oh-story-bridge-marker { display: none; }
534
603
  gap: 12px;
535
604
  }
536
605
 
537
- .oh-story-brand { min-height: 28px; padding: 0 5px 10px 7px; font-size: 14px; font-weight: 500; }
606
+ .oh-story-brand { min-height: 28px; gap: 8px; padding: 0 5px 8px 7px; font-size: 14px; font-weight: 500; white-space: nowrap; }
607
+ .oh-story-brand-cluster { display: flex; min-width: 0; align-items: center; gap: 6px; }
608
+ .oh-story-brand-cluster strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: inherit; }
609
+ .oh-story-kind { flex: none; padding: 1px 6px; border-radius: 5px; color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-button-ghost-active-fill); font-size: 11px; line-height: 16px; font-weight: 500; }
538
610
 
539
611
  .oh-story-brand button,
540
612
  .oh-story-save,
@@ -560,39 +632,26 @@ var plugin_default = `.oh-story-bridge-marker { display: none; }
560
632
  .oh-story-role button:focus-visible { outline: 2px solid var(--dsw-alias-state-business-primary); outline-offset: -2px; }
561
633
  .oh-story-save:disabled { opacity: .48; cursor: default; }
562
634
 
563
- .oh-story-mode-tabs {
564
- display: grid;
565
- grid-template-columns: 1fr 1fr;
566
- margin: 0 4px 10px;
567
- border-bottom: 1px solid var(--os-border);
568
- }
635
+ .oh-story-mode-tabs { display: flex; margin: 0 4px 8px; padding: 2px; border: 1px solid var(--os-border); border-radius: 7px; background: var(--dsw-alias-bg-layer-1); }
569
636
 
570
637
  .oh-story-mode-tabs button,
571
638
  .oh-story-editor-tabs button {
572
639
  position: relative;
573
- min-height: 30px;
640
+ min-height: 24px;
641
+ flex: 1;
574
642
  border: 0;
575
643
  color: var(--os-muted);
576
644
  background: transparent;
645
+ border-radius: 5px;
577
646
  cursor: pointer;
578
647
  font: inherit;
579
- font-size: 12px;
648
+ font-size: 11px;
580
649
  }
581
650
 
582
651
  .oh-story-mode-tabs button:hover,
583
652
  .oh-story-editor-tabs button:hover { color: var(--dsw-alias-label-primary); }
584
653
  .oh-story-mode-tabs button[aria-selected="true"],
585
- .oh-story-editor-tabs button[aria-selected="true"] { color: var(--dsw-alias-label-primary); font-weight: 500; }
586
- .oh-story-mode-tabs button[aria-selected="true"]::after {
587
- position: absolute;
588
- right: 14px;
589
- bottom: -1px;
590
- left: 14px;
591
- height: 2px;
592
- border-radius: 2px 2px 0 0;
593
- background: var(--dsw-alias-state-business-primary);
594
- content: "";
595
- }
654
+ .oh-story-editor-tabs button[aria-selected="true"] { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-base); box-shadow: 0 1px 2px rgb(0 0 0 / 8%); font-weight: 500; }
596
655
 
597
656
  .oh-story-file-group,
598
657
  .oh-story-file-folder { margin: 0; }
@@ -610,6 +669,7 @@ var plugin_default = `.oh-story-bridge-marker { display: none; }
610
669
  font-size: 11px;
611
670
  font-weight: 600;
612
671
  }
672
+ .oh-story-file-folder > summary { padding-left: min(calc(7px + var(--oh-story-indent, 0px)), 49px); }
613
673
  .oh-story-file-group > summary { margin-top: 3px; }
614
674
  .oh-story-file-group > summary::-webkit-details-marker,
615
675
  .oh-story-file-folder > summary::-webkit-details-marker { display: none; }
@@ -620,13 +680,13 @@ var plugin_default = `.oh-story-bridge-marker { display: none; }
620
680
  .oh-story-file-group > summary:hover,
621
681
  .oh-story-file-folder > summary:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
622
682
  .oh-story-file-group > summary span,
623
- .oh-story-file-folder > summary span { margin-left: auto; color: var(--dsw-alias-label-caption); font-weight: 400; }
683
+ .oh-story-file-folder > summary span { margin-left: auto; color: var(--dsw-alias-label-secondary); font-weight: 400; }
624
684
 
625
685
  .oh-story-tree nav button {
626
686
  display: block;
627
687
  width: 100%;
628
688
  overflow: hidden;
629
- padding: 6px 8px;
689
+ padding: 6px 8px 6px min(calc(14px + var(--oh-story-indent, 0px)), 56px);
630
690
  border: 0;
631
691
  border-radius: 6px;
632
692
  color: inherit;
@@ -649,13 +709,17 @@ var plugin_default = `.oh-story-bridge-marker { display: none; }
649
709
  border-bottom: 1px solid var(--os-border);
650
710
  }
651
711
 
652
- .oh-story-editor header span {
712
+ .oh-story-editor-path {
653
713
  overflow: hidden;
714
+ min-width: 0;
715
+ flex: 1;
654
716
  text-overflow: ellipsis;
655
717
  white-space: nowrap;
656
718
  font-size: 12px;
657
719
  color: var(--os-muted);
658
720
  }
721
+ .oh-story-editor-path > span { overflow: hidden; display: block; text-overflow: ellipsis; }
722
+ .oh-story-editor-path > strong { display: none; overflow: hidden; text-overflow: ellipsis; font: inherit; }
659
723
 
660
724
  .oh-story-editor-actions { display: flex; flex: none; align-items: center; gap: 8px; }
661
725
  .oh-story-editor-tabs {
@@ -666,7 +730,6 @@ var plugin_default = `.oh-story-bridge-marker { display: none; }
666
730
  background: var(--dsw-alias-bg-layer-1);
667
731
  }
668
732
  .oh-story-editor-tabs button { min-height: 24px; padding: 0 9px; border-radius: 5px; font-size: 11px; }
669
- .oh-story-editor-tabs button[aria-selected="true"] { background: var(--dsw-alias-bg-base); box-shadow: 0 1px 2px rgb(0 0 0 / 8%); }
670
733
  .oh-story-save { min-height: 28px; }
671
734
 
672
735
  .oh-story-editor textarea {
@@ -677,7 +740,7 @@ var plugin_default = `.oh-story-bridge-marker { display: none; }
677
740
  resize: none;
678
741
  border: 0;
679
742
  outline: 0;
680
- padding: clamp(20px, 4vw, 48px) clamp(22px, 6vw, 84px) 48px;
743
+ padding: clamp(20px, 6%, 48px) clamp(22px, 10%, 84px) 48px;
681
744
  color: var(--dsw-alias-label-primary);
682
745
  background: transparent;
683
746
  font: 16px/1.9 ui-serif, "Songti SC", "STSong", Georgia, serif;
@@ -693,7 +756,7 @@ var plugin_default = `.oh-story-bridge-marker { display: none; }
693
756
  min-height: 0;
694
757
  flex: 1;
695
758
  overflow: auto;
696
- padding: clamp(24px, 5vw, 56px) clamp(24px, 7vw, 92px) 64px;
759
+ padding: clamp(24px, 8%, 56px) clamp(24px, 10%, 92px) 64px;
697
760
  color: var(--dsw-alias-label-primary);
698
761
  font: 16px/1.85 ui-serif, "Songti SC", "STSong", Georgia, serif;
699
762
  overflow-wrap: anywhere;
@@ -705,7 +768,7 @@ var plugin_default = `.oh-story-bridge-marker { display: none; }
705
768
  .oh-story-markdown h3,
706
769
  .oh-story-markdown h4,
707
770
  .oh-story-markdown h5,
708
- .oh-story-markdown h6 { margin: 1.45em 0 .6em; font-family: ui-sans-serif, system-ui, "PingFang SC", sans-serif; line-height: 1.35; letter-spacing: -.01em; }
771
+ .oh-story-markdown h6 { margin: 1.45em 0 .6em; font-family: var(--dsw-font-family); line-height: 1.35; letter-spacing: -.01em; }
709
772
  .oh-story-markdown h1 { font-size: 1.75em; }
710
773
  .oh-story-markdown h2 { padding-bottom: .35em; border-bottom: 1px solid var(--os-border); font-size: 1.35em; }
711
774
  .oh-story-markdown h3 { font-size: 1.15em; }
@@ -750,7 +813,7 @@ var plugin_default = `.oh-story-bridge-marker { display: none; }
750
813
  .oh-story-jsonl-records { overflow: auto; padding: 14px 18px 36px; }
751
814
  .oh-story-jsonl details,
752
815
  .oh-story-jsonl-error { overflow: hidden; margin-bottom: 10px; border: 1px solid var(--os-border); border-radius: 10px; background: var(--dsw-alias-bg-layer-1); }
753
- .oh-story-jsonl summary { display: flex; min-height: 42px; align-items: center; gap: 9px; padding: 0 12px; cursor: pointer; list-style: none; font: 12px/1.4 ui-sans-serif, system-ui, "PingFang SC", sans-serif; }
816
+ .oh-story-jsonl summary { display: flex; min-height: 42px; align-items: center; gap: 9px; padding: 0 12px; cursor: pointer; list-style: none; font: 12px/1.4 var(--dsw-font-family); }
754
817
  .oh-story-jsonl summary::-webkit-details-marker { display: none; }
755
818
  .oh-story-jsonl summary::before { content: "\u203A"; color: var(--os-muted); font-size: 16px; transition: transform .12s ease; }
756
819
  .oh-story-jsonl details[open] summary::before { transform: rotate(90deg); }
@@ -765,7 +828,8 @@ var plugin_default = `.oh-story-bridge-marker { display: none; }
765
828
  .oh-story-jsonl-error pre { margin: 3px -12px -12px; }
766
829
 
767
830
  .oh-story-stream,
768
- .oh-story-conflict {
831
+ .oh-story-conflict,
832
+ .oh-story-warning {
769
833
  flex: none;
770
834
  padding: 7px 12px;
771
835
  border-bottom: 1px solid var(--os-border);
@@ -776,6 +840,18 @@ var plugin_default = `.oh-story-bridge-marker { display: none; }
776
840
  .oh-story-stream { color: var(--dsw-alias-state-business-primary); background: var(--dsw-alias-button-ghost-active-fill); }
777
841
  .oh-story-stream[data-stage="streaming"] { animation: oh-story-pulse 1.2s ease-in-out infinite alternate; }
778
842
  .oh-story-conflict { color: var(--dsw-alias-state-warn-label); background: var(--dsw-specific-tip); }
843
+ .oh-story-conflict > div { display: flex; gap: 6px; margin-top: 6px; }
844
+ .oh-story-conflict button,
845
+ .oh-story-empty button {
846
+ border: 1px solid var(--os-border);
847
+ border-radius: 6px;
848
+ color: inherit;
849
+ background: var(--dsw-alias-bg-base);
850
+ padding: 3px 8px;
851
+ cursor: pointer;
852
+ font: inherit;
853
+ }
854
+ .oh-story-warning { color: var(--dsw-alias-state-warn-label); background: var(--dsw-specific-tip); }
779
855
 
780
856
  @keyframes oh-story-pulse { to { opacity: .62; } }
781
857
  @media (prefers-reduced-motion: reduce) { .oh-story-stream[data-stage="streaming"] { animation: none; } }
@@ -793,19 +869,104 @@ var plugin_default = `.oh-story-bridge-marker { display: none; }
793
869
  .oh-story-role pre { max-height: 260px; overflow: auto; margin: 4px 0 4px 4px; padding: 12px 16px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; color: var(--dsw-alias-label-secondary); background: var(--dsw-alias-markdown-code-block); white-space: pre-wrap; font: var(--dsw-font-markdown-code-block-small); }
794
870
  .oh-story-role > button { align-self: flex-start; margin: 4px 0 2px 4px; font-size: 11px; }
795
871
 
796
- @media (max-width: 980px) {
797
- [data-conversation-scroll]:has(> [data-slot="conversation.session"] > .oh-story-split-surface) {
798
- grid-template-columns: 104px minmax(210px, 1fr) minmax(252px, 40%);
799
- }
800
- .oh-story-editor textarea { padding-right: 28px; padding-left: 28px; }
872
+ [data-conversation-scroll][data-oh-story-layout="medium"]:has(> [data-slot="conversation.session"] > .oh-story-split-surface) {
873
+ grid-template-columns: clamp(104px, 18%, 184px) minmax(200px, 1fr) clamp(300px, 45%, 408px);
874
+ }
875
+
876
+ [data-conversation-scroll][data-oh-story-layout="compact"]:has(> [data-slot="conversation.session"] > .oh-story-split-surface) {
877
+ grid-template-columns: clamp(80px, 16%, 104px) minmax(0, 1fr) clamp(228px, 48%, 300px);
878
+ }
879
+
880
+ [data-oh-story-layout="medium"] .oh-story-file-folder > summary { padding-left: min(calc(7px + var(--oh-story-indent, 0px)), 35px); }
881
+ [data-oh-story-layout="medium"] .oh-story-tree nav button { padding-left: min(calc(14px + var(--oh-story-indent, 0px)), 42px); }
882
+ [data-oh-story-layout="medium"] .oh-story-editor textarea { padding-right: 28px; padding-left: 28px; }
883
+
884
+ [data-oh-story-layout="compact"] .oh-story-tree { padding-inline: 4px; }
885
+ [data-oh-story-layout="compact"] .oh-story-brand { gap: 4px; padding-inline: 2px; font-size: 11px; }
886
+ [data-oh-story-layout="compact"] .oh-story-brand-cluster { gap: 4px; }
887
+ [data-oh-story-layout="compact"] .oh-story-brand-cluster strong > span { display: none; }
888
+ [data-oh-story-layout="compact"] .oh-story-kind { padding-inline: 4px; font-size: 11px; }
889
+ [data-oh-story-layout="compact"] .oh-story-brand button { width: 24px; height: 24px; }
890
+ [data-oh-story-layout="compact"] .oh-story-mode-tabs { margin-inline: 0; }
891
+ [data-oh-story-layout="compact"] .oh-story-mode-tabs button { padding-inline: 3px; font-size: 11px; }
892
+ [data-oh-story-layout="compact"] .oh-story-file-folder > summary { padding-left: min(calc(5px + var(--oh-story-indent, 0px)), 19px); }
893
+ [data-oh-story-layout="compact"] .oh-story-tree nav button { padding-right: 4px; padding-left: min(calc(7px + var(--oh-story-indent, 0px)), 21px); }
894
+ [data-oh-story-layout="compact"] .oh-story-editor header {
895
+ display: grid;
896
+ min-height: 68px;
897
+ grid-template-columns: minmax(0, 1fr);
898
+ grid-template-rows: 24px 36px;
899
+ align-content: center;
900
+ gap: 0;
901
+ padding: 4px 3px;
801
902
  }
903
+ [data-oh-story-layout="compact"] .oh-story-editor-path > span { display: none; }
904
+ [data-oh-story-layout="compact"] .oh-story-editor-path > strong { display: block; font-size: 11px; line-height: 24px; }
905
+ [data-oh-story-layout="compact"] .oh-story-editor-actions { width: 100%; min-width: 0; justify-content: flex-end; gap: 4px; }
906
+ [data-oh-story-layout="compact"] .oh-story-editor-tabs { min-width: 0; flex: 1; }
907
+ [data-oh-story-layout="compact"] .oh-story-editor-tabs button { min-width: 0; padding-inline: 2px; white-space: nowrap; }
908
+ [data-oh-story-layout="compact"] .oh-story-save { flex: none; padding-inline: 4px; white-space: nowrap; font-size: 11px; }
909
+ [data-oh-story-layout="compact"] .oh-story-editor textarea { padding: 16px 12px 40px; }
910
+ [data-oh-story-layout="compact"] .oh-story-markdown { padding: 18px 12px 48px; }
911
+ [data-oh-story-layout="compact"] .oh-story-jsonl-summary { padding-inline: 10px; }
912
+ [data-oh-story-layout="compact"] .oh-story-jsonl-records { padding: 8px 7px 28px; }
913
+ [data-oh-story-layout="compact"] .oh-story-jsonl summary { gap: 6px; padding-inline: 7px; }
914
+ [data-oh-story-layout="compact"] .oh-story-jsonl summary > span { display: none; }
915
+ [data-oh-story-layout="compact"] .oh-story-jsonl summary > strong { min-width: 32px; }
916
+ [data-oh-story-layout="compact"] .oh-story-jsonl summary > code,
917
+ [data-oh-story-layout="compact"] .oh-story-jsonl summary > em { display: none; }
802
918
  `;
803
919
 
804
920
  // src/client/index.tsx
805
- var import_jsx_runtime3 = require("react/jsx-runtime"), name = "oh-story", inject = ["slots"], GROUP_ORDER = {
921
+ var import_jsx_runtime3 = require("react/jsx-runtime"), name = "oh-story", inject = ["slots"];
922
+ function applyUpdate(current, update) {
923
+ return typeof update == "function" ? update(current) : update;
924
+ }
925
+ function createWorkbenchStore() {
926
+ return (0, import_client.defineStore)({
927
+ init: () => ({
928
+ buffers: {},
929
+ editorMode: "preview",
930
+ expanded: {},
931
+ selected: void 0,
932
+ workbench: "story"
933
+ }),
934
+ actions: {
935
+ setBuffers: (draft, update) => {
936
+ draft.buffers = applyUpdate(draft.buffers, update);
937
+ },
938
+ setEditorMode: (draft, update) => {
939
+ draft.editorMode = applyUpdate(draft.editorMode, update);
940
+ },
941
+ setExpanded: (draft, update) => {
942
+ draft.expanded = applyUpdate(draft.expanded, update);
943
+ },
944
+ setSelected: (draft, update) => {
945
+ draft.selected = applyUpdate(draft.selected, update);
946
+ },
947
+ setWorkbench: (draft, update) => {
948
+ draft.workbench = applyUpdate(draft.workbench, update);
949
+ }
950
+ }
951
+ });
952
+ }
953
+ var WorkspaceRequestError = class extends Error {
954
+ constructor(status, message) {
955
+ super(message);
956
+ this.status = status;
957
+ }
958
+ status;
959
+ }, GROUP_ORDER = {
806
960
  story: ["\u6B63\u6587", "\u5927\u7EB2", "\u8BBE\u5B9A", "\u8FFD\u8E2A", "\u5BF9\u6807", "\u53C2\u8003\u8D44\u6599"],
807
961
  drama: ["\u9879\u76EE", "\u8F93\u5165", "\u9879\u76EE\u5F00\u53D1", "\u8BBE\u5B9A\u96C6", "\u5267\u96C6", "\u5BA1\u67E5", "\u521B\u4F5C\u8005\u51B3\u7B56", "\u4EA4\u4ED8"]
808
- };
962
+ }, WORKBENCH_MODES = ["story", "drama"], EDITOR_MODES = ["preview", "source"];
963
+ function handleTabKey(event, values, current, select) {
964
+ let index;
965
+ if (event.key === "Home" ? index = 0 : event.key === "End" ? index = values.length - 1 : event.key === "ArrowRight" ? index = (values.indexOf(current) + 1) % values.length : event.key === "ArrowLeft" && (index = (values.indexOf(current) - 1 + values.length) % values.length), index === void 0) return;
966
+ event.preventDefault();
967
+ let value = values[index];
968
+ value !== void 0 && (select(value), event.currentTarget.parentElement?.querySelectorAll("[role='tab']")[index]?.focus());
969
+ }
809
970
  function groupForPath(path) {
810
971
  return path === "short-drama.json" ? "\u9879\u76EE" : path.split("/", 1)[0] ?? "\u5176\u4ED6";
811
972
  }
@@ -817,14 +978,13 @@ function preferredFile(files, mode) {
817
978
  }
818
979
  return matching[0]?.path;
819
980
  }
820
- var EMPTY_BLOCKS = [], EMPTY_CALLS = [];
821
981
  function endpoint(path, sessionId, file) {
822
982
  let url = new URL(`/oh-story/${path}`, globalThis.location.origin);
823
983
  return url.searchParams.set("sessionId", sessionId), file !== void 0 && url.searchParams.set("path", file), url.toString();
824
984
  }
825
985
  async function json(response) {
826
986
  let value = await response.json();
827
- if (!response.ok) throw new Error(value.error ?? `HTTP ${String(response.status)}`);
987
+ if (!response.ok) throw new WorkspaceRequestError(response.status, value.error ?? `HTTP ${String(response.status)}`);
828
988
  return value;
829
989
  }
830
990
  function FileTreeNodes({
@@ -841,7 +1001,9 @@ function FileTreeNodes({
841
1001
  "button",
842
1002
  {
843
1003
  type: "button",
844
- style: { paddingLeft: `${String(14 + depth * 14)}px` },
1004
+ style: { "--oh-story-indent": `${String(depth * 14)}px` },
1005
+ title: node.path,
1006
+ "aria-label": node.path,
845
1007
  "data-file-path": node.path,
846
1008
  "data-agent-target": node.path === activityPath || void 0,
847
1009
  "aria-current": node.path === selected ? "page" : void 0,
@@ -852,11 +1014,11 @@ function FileTreeNodes({
852
1014
  },
853
1015
  node.path
854
1016
  );
855
- let open = expanded[node.path] ?? selected?.startsWith(`${node.path}/`) === !0;
1017
+ let open = selected?.startsWith(`${node.path}/`) === !0 || expanded[node.path] === !0;
856
1018
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("details", { className: "oh-story-file-folder", open, onToggle: (event) => {
857
1019
  onToggle(node.path, event.currentTarget.open);
858
1020
  }, children: [
859
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("summary", { style: { paddingLeft: `${String(7 + depth * 14)}px` }, children: [
1021
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("summary", { style: { "--oh-story-indent": `${String(depth * 14)}px` }, title: node.path, children: [
860
1022
  node.name,
861
1023
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: node.fileCount })
862
1024
  ] }),
@@ -876,89 +1038,182 @@ function FileTreeNodes({
876
1038
  }) });
877
1039
  }
878
1040
  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);
1041
+ 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)(() => {
1042
+ setLoading(!0), setVersion((value) => value + 1);
881
1043
  }, []);
882
1044
  return (0, import_react.useEffect)(() => {
883
1045
  let controller = new AbortController();
884
1046
  return setError(void 0), fetch(endpoint("workspace", sessionId), { signal: controller.signal }).then((response) => json(response)).then(setWorkspace).catch((reason) => {
885
1047
  controller.signal.aborted || setError(reason instanceof Error ? reason.message : String(reason));
1048
+ }).finally(() => {
1049
+ controller.signal.aborted || setLoading(!1);
886
1050
  }), () => {
887
1051
  controller.abort();
888
1052
  };
889
- }, [sessionId, version]), { workspace, error, reload };
1053
+ }, [sessionId, version]), { workspace, error, loading, reload };
890
1054
  }
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");
1055
+ function CreativeWorkbench({
1056
+ sessionId,
1057
+ runningCalls,
1058
+ partial,
1059
+ settledMutation,
1060
+ useStore,
1061
+ actions
1062
+ }) {
1063
+ let { workspace, error, loading: workspaceLoading, reload } = useWorkspace(sessionId), activities = (0, import_react.useMemo)(
1064
+ () => fileMutations(runningCalls, partial),
1065
+ [partial, runningCalls]
1066
+ ), normalizedActivities = (0, import_react.useMemo)(() => activities.flatMap((activity2) => {
1067
+ let path = creativeRelativePath(activity2.path, workspace?.cwd);
1068
+ return path === void 0 ? [] : [{ activity: activity2, path }];
1069
+ }), [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, workbench = useStore((memory) => memory.workbench), setWorkbench = actions.setWorkbench, initializedWorkbench = (0, import_react.useRef)(!1), selected = useStore((memory) => memory.selected), setSelected = actions.setSelected, buffers = useStore((memory) => memory.buffers), setBuffers = actions.setBuffers, buffersRef = (0, import_react.useRef)({}), expanded = useStore((memory) => memory.expanded), setExpanded = actions.setExpanded, 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()), previousSettledMutation = (0, import_react.useRef)(settledMutation), 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 = useStore((memory) => memory.editorMode), setEditorMode = actions.setEditorMode, modeSelection = (0, import_react.useRef)(selected), textareaRef = (0, import_react.useRef)(null), editorPositions = (0, import_react.useRef)(/* @__PURE__ */ new Map()), editorReady = buffer !== void 0 && buffer.missing !== !0, availableModes = (0, import_react.useMemo)(() => {
1070
+ let value = /* @__PURE__ */ new Set();
1071
+ for (let file of workspace?.files ?? []) {
1072
+ let mode = workbenchModeForPath(file.path);
1073
+ mode !== void 0 && value.add(mode);
1074
+ }
1075
+ return WORKBENCH_MODES.filter((mode) => value.has(mode));
1076
+ }, [workspace?.files]), showModeTabs = workspace !== void 0 && availableModes.length !== 1, workspaceKind = availableModes.length === 1 ? availableModes[0] : void 0;
893
1077
  (0, import_react.useEffect)(() => {
894
1078
  buffersRef.current = buffers;
895
- }, [buffers]), (0, import_react.useEffect)(() => {
896
- if (workspace === void 0 || initializedWorkbench.current) return;
897
- let hasStory = workspace.files.some((file) => workbenchModeForPath(file.path) === "story");
898
- workspace.shortDrama !== null && !hasStory && setWorkbench("drama"), initializedWorkbench.current = !0;
899
- }, [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;
1079
+ }, [buffers]);
1080
+ let rememberEditorPosition = (0, import_react.useCallback)(() => {
1081
+ let element = textareaRef.current;
1082
+ element === null || selected === void 0 || element.getAttribute("aria-label") !== selected || editorPositions.current.set(selected, {
1083
+ scrollTop: element.scrollTop,
1084
+ selectionStart: element.selectionStart,
1085
+ selectionEnd: element.selectionEnd
1086
+ });
1087
+ }, [selected]);
1088
+ (0, import_react.useLayoutEffect)(() => {
1089
+ if (editorMode !== "source" || selected === void 0 || !editorReady) return;
1090
+ let element = textareaRef.current, position = editorPositions.current.get(selected);
1091
+ if (element === null || position === void 0) return;
1092
+ let end = Math.min(position.selectionEnd, element.value.length);
1093
+ element.setSelectionRange(Math.min(position.selectionStart, end), end), element.scrollTop = position.scrollTop;
1094
+ }, [editorMode, editorReady, selected]), (0, import_react.useEffect)(() => {
1095
+ let warn = (event) => {
1096
+ Object.values(buffersRef.current).some((value) => value.source === "human" && value.content !== value.saved) && event.preventDefault();
1097
+ };
1098
+ return globalThis.addEventListener("beforeunload", warn), () => {
1099
+ globalThis.removeEventListener("beforeunload", warn);
1100
+ };
1101
+ }, []);
1102
+ let expandPath = (0, import_react.useCallback)((path) => {
1103
+ let segments = path.split("/"), ancestors = [groupForPath(path)];
1104
+ for (let index = 1; index < segments.length - 1; index += 1) ancestors.push(segments.slice(0, index + 1).join("/"));
1105
+ setExpanded((current) => {
1106
+ let next = { ...current };
1107
+ for (let ancestor of ancestors) next[ancestor] = !0;
1108
+ return next;
1109
+ });
1110
+ }, []), revealPath = (0, import_react.useCallback)((path) => {
1111
+ rememberEditorPosition(), setWorkbench(workbenchModeForPath(path) ?? "story"), setSelected(path), expandPath(path);
1112
+ }, [expandPath, rememberEditorPosition]), followAgentPath = (0, import_react.useCallback)((path) => {
1113
+ expandPath(path);
1114
+ let current = selected === void 0 ? void 0 : buffersRef.current[selected];
1115
+ path !== selected && current?.source === "human" && current.content !== current.saved && surfaceRef.current?.ownerDocument.activeElement === textareaRef.current || revealPath(path);
1116
+ }, [expandPath, revealPath, selected]);
1117
+ (0, import_react.useEffect)(() => {
1118
+ workspace === void 0 || initializedWorkbench.current || (availableModes.length === 1 && setWorkbench(availableModes[0] ?? "story"), initializedWorkbench.current = !0);
1119
+ }, [availableModes, workspace]), (0, import_react.useEffect)(() => {
1120
+ activityPath !== void 0 && activityPath === selected && setEditorMode("source");
1121
+ }, [activityPath, selected]), (0, import_react.useEffect)(() => {
1122
+ modeSelection.current !== selected && (modeSelection.current = selected, setEditorMode(activityPath === selected ? "source" : previewable ? "preview" : "source"));
1123
+ }, [activityPath, previewable, selected]), (0, import_react.useEffect)(() => {
1124
+ workspaceLoading || activityPath === void 0 && (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)));
1125
+ }, [activityPath, buffers, selected, workbench, workspace, workspaceLoading]), (0, import_react.useEffect)(() => {
1126
+ if (workspace === void 0 || workspaceLoading) return;
1127
+ let paths = new Set(workspace.files.map((file) => file.path));
1128
+ setBuffers((current) => {
1129
+ let changed = !1, next = { ...current };
1130
+ for (let [path, value] of Object.entries(current))
1131
+ 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));
1132
+ return changed ? next : current;
1133
+ });
1134
+ }, [activityPaths, workspace, workspaceLoading]), (0, import_react.useEffect)(() => {
1135
+ if (selected === void 0 || activityPaths.has(selected) || !(workspace?.files.some((file) => file.path === selected) ?? !1)) return;
913
1136
  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`);
918
- return;
919
- }
920
- setBuffers((current) => ({
921
- ...current,
922
- [file.path]: { content: file.content, saved: file.content, source: "disk", version: file.version }
923
- }));
1137
+ return setBuffers((current) => {
1138
+ let existing = current[selected];
1139
+ return existing === void 0 ? current : { ...current, [selected]: { ...existing, error: void 0 } };
1140
+ }), fetch(endpoint("file", sessionId, selected), { signal: controller.signal }).then((response) => json(response)).then((file) => {
1141
+ setBuffers((current) => {
1142
+ let existing = current[file.path];
1143
+ return existing?.source === "human" && existing.content !== existing.saved ? existing.version === file.version ? { ...current, [file.path]: { ...existing, missing: !1, error: void 0 } } : {
1144
+ ...current,
1145
+ [file.path]: {
1146
+ ...existing,
1147
+ missing: !1,
1148
+ error: void 0,
1149
+ conflict: {
1150
+ message: `${file.path} \u5DF2\u5728\u78C1\u76D8\u4E0A\u66F4\u65B0\uFF1B\u4F60\u7684\u672C\u5730\u8349\u7A3F\u6CA1\u6709\u88AB\u8986\u76D6\u3002`,
1151
+ theirs: file.content,
1152
+ theirsVersion: file.version
1153
+ }
1154
+ }
1155
+ } : {
1156
+ ...current,
1157
+ [file.path]: { content: file.content, saved: file.content, source: "disk", version: file.version }
1158
+ };
1159
+ });
924
1160
  }).catch((reason) => {
925
- controller.signal.aborted || setFileError(reason instanceof Error ? reason.message : String(reason));
1161
+ controller.signal.aborted || setBuffers((current) => {
1162
+ let existing = current[selected];
1163
+ return existing === void 0 ? current : {
1164
+ ...current,
1165
+ [selected]: { ...existing, error: reason instanceof Error ? reason.message : String(reason) }
1166
+ };
1167
+ });
926
1168
  }), () => {
927
1169
  controller.abort();
928
1170
  };
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 ?? ""
1171
+ }, [activityPaths, selected, sessionId, workspace?.files]), (0, import_react.useEffect)(() => {
1172
+ if (normalizedActivities.length !== 0) {
1173
+ for (let { path } of normalizedActivities) expandPath(path);
1174
+ activityPath !== void 0 && followAgentPath(activityPath), setBuffers((current) => {
1175
+ let next = current;
1176
+ for (let { activity: currentActivity, path } of normalizedActivities) {
1177
+ let existing = next[path];
1178
+ if (existing?.source === "human" && existing.content !== existing.saved) {
1179
+ next = {
1180
+ ...next,
1181
+ [path]: {
1182
+ ...existing,
1183
+ 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` }
1184
+ }
1185
+ };
1186
+ continue;
1187
+ }
1188
+ let basis = activityBases.current.get(currentActivity.callId);
1189
+ (basis === void 0 || basis.path !== path) && (basis = { path, base: existing?.content ?? "" }, activityBases.current.set(currentActivity.callId, basis));
1190
+ let preview = previewMutation(currentActivity, basis.base);
1191
+ preview === void 0 || existing?.source === "agent" && existing.content === preview || (next = {
1192
+ ...next,
1193
+ [path]: {
1194
+ content: preview,
1195
+ saved: existing?.saved ?? "",
1196
+ source: "agent",
1197
+ version: existing?.version ?? ""
1198
+ }
1199
+ });
947
1200
  }
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;
1201
+ return next;
1202
+ });
954
1203
  }
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)(() => {
1204
+ }, [activityPath, expandPath, followAgentPath, normalizedActivities]), (0, import_react.useEffect)(() => {
1205
+ let signals = new Set(mutatingCallIds(runningCalls));
1206
+ for (let { activity: currentActivity } of normalizedActivities) signals.add(currentActivity.callId.split(":", 1)[0] ?? currentActivity.callId);
1207
+ let settled = [...previousSignals.current].some((callId) => !signals.has(callId));
1208
+ for (let callId of activityBases.current.keys())
1209
+ signals.has(callId.split(":", 1)[0] ?? callId) || activityBases.current.delete(callId);
1210
+ previousSignals.current = signals, settled && reload();
1211
+ }, [normalizedActivities, reload, runningCalls]), (0, import_react.useEffect)(() => {
1212
+ if (settledMutation === void 0 || settledMutation === previousSettledMutation.current) return;
1213
+ previousSettledMutation.current = settledMutation;
1214
+ let path = creativeRelativePath(settledMutation.slice(settledMutation.indexOf("\0") + 1), workspace?.cwd);
1215
+ path !== void 0 && followAgentPath(path), reload();
1216
+ }, [followAgentPath, reload, settledMutation, workspace?.cwd]), (0, import_react.useEffect)(() => {
962
1217
  if (selected !== void 0) {
963
1218
  for (let button of navRef.current?.querySelectorAll("button[data-file-path]") ?? [])
964
1219
  if (button.dataset.filePath === selected) {
@@ -967,7 +1222,7 @@ function CreativeWorkbench({ sessionId, useSession }) {
967
1222
  }
968
1223
  }
969
1224
  }, [selected]), (0, import_react.useEffect)(() => {
970
- if (activity !== void 0 || workspace === void 0) return;
1225
+ if (normalizedActivities.length > 0 || workspace === void 0) return;
971
1226
  let sessionSurface = surfaceRef.current?.parentElement;
972
1227
  if (sessionSurface == null) return;
973
1228
  let knownPaths = new Set(workspace.files.map((file) => file.path)), followOfficialFileLink = (event) => {
@@ -977,9 +1232,9 @@ function CreativeWorkbench({ sessionId, useSession }) {
977
1232
  if (control === null || control.closest(".oh-story-split-surface") !== null) return;
978
1233
  let candidates = [control.title, control.getAttribute("aria-label"), control.textContent];
979
1234
  for (let candidate of candidates) {
980
- let path = creativeRelativePath(candidate?.trim().replace(/^Open\s+/u, ""), workspace.cwd);
1235
+ let path = creativeRelativePath(candidate?.trim().replace(/^(?:Open|打开)\s+/u, ""), workspace.cwd);
981
1236
  if (!(path === void 0 || !knownPaths.has(path))) {
982
- event.preventDefault(), event.stopPropagation(), setConflict(void 0), setWorkbench(workbenchModeForPath(path) ?? "story"), setSelected(path);
1237
+ event.preventDefault(), event.stopPropagation(), revealPath(path);
983
1238
  break;
984
1239
  }
985
1240
  }
@@ -987,39 +1242,88 @@ function CreativeWorkbench({ sessionId, useSession }) {
987
1242
  return sessionSurface.addEventListener("click", followOfficialFileLink, !0), () => {
988
1243
  sessionSurface.removeEventListener("click", followOfficialFileLink, !0);
989
1244
  };
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
1245
+ }, [normalizedActivities.length, revealPath, workspace]);
1246
+ let savePath = (0, import_react.useCallback)(async (path) => {
1247
+ if (saveLocks.current.has(path)) return;
1248
+ let submitted = buffersRef.current[path];
1249
+ if (!(submitted === void 0 || submitted.missing === !0 || submitted.content === submitted.saved)) {
1250
+ saveLocks.current.add(path), setBuffers((current) => {
1251
+ let existing = current[path];
1252
+ return existing === void 0 ? current : { ...current, [path]: { ...existing, saving: !0, error: void 0 } };
1253
+ });
1254
+ try {
1255
+ let file = await json(await fetch(endpoint("file", sessionId, path), {
1256
+ method: "PUT",
1257
+ headers: { "content-type": "application/json" },
1258
+ body: JSON.stringify({ content: submitted.content, baseVersion: submitted.version })
1259
+ }));
1260
+ setBuffers((current) => {
1261
+ let latest = current[path];
1262
+ if (latest === void 0) return current;
1263
+ let unchanged = latest.content === submitted.content;
1264
+ return {
1265
+ ...current,
1266
+ [path]: {
1267
+ content: unchanged ? file.content : latest.content,
1268
+ saved: file.content,
1269
+ source: unchanged ? "disk" : "human",
1270
+ version: file.version,
1271
+ saving: !1
1272
+ }
1273
+ };
1274
+ }), reload();
1275
+ } catch (reason) {
1276
+ if (reason instanceof WorkspaceRequestError && reason.status === 412)
1277
+ try {
1278
+ let theirs = await json(await fetch(endpoint("file", sessionId, path)));
1279
+ setBuffers((current) => {
1280
+ let latest = current[path];
1281
+ return latest === void 0 ? current : {
1282
+ ...current,
1283
+ [path]: {
1284
+ ...latest,
1285
+ saving: !1,
1286
+ conflict: {
1287
+ message: `${path} \u5DF2\u5728\u78C1\u76D8\u4E0A\u66F4\u65B0\uFF1B\u8BF7\u9009\u62E9\u4FDD\u7559\u54EA\u4E00\u7248\u3002`,
1288
+ theirs: theirs.content,
1289
+ theirsVersion: theirs.version
1290
+ }
1291
+ }
1292
+ };
1293
+ });
1294
+ } catch (refreshError) {
1295
+ setBuffers((current) => {
1296
+ let existing = current[path];
1297
+ return existing === void 0 ? current : {
1298
+ ...current,
1299
+ [path]: { ...existing, saving: !1, error: refreshError instanceof Error ? refreshError.message : String(refreshError) }
1300
+ };
1301
+ });
1012
1302
  }
1013
- };
1014
- }), setConflict(void 0), reload();
1015
- } catch (reason) {
1016
- setFileError(reason instanceof Error ? reason.message : String(reason));
1017
- } finally {
1018
- setSaving(!1);
1303
+ else
1304
+ setBuffers((current) => {
1305
+ let existing = current[path];
1306
+ return existing === void 0 ? current : {
1307
+ ...current,
1308
+ [path]: { ...existing, saving: !1, error: reason instanceof Error ? reason.message : String(reason) }
1309
+ };
1310
+ });
1311
+ } finally {
1312
+ saveLocks.current.delete(path);
1313
+ }
1019
1314
  }
1020
- }, [buffer, reload, selected, sessionId]), groups = (0, import_react.useMemo)(() => {
1315
+ }, [reload, sessionId]);
1316
+ (0, import_react.useEffect)(() => {
1317
+ let saveShortcut = (event) => {
1318
+ !(event.metaKey || event.ctrlKey) || event.key.toLocaleLowerCase() !== "s" || (event.preventDefault(), selected !== void 0 && savePath(selected));
1319
+ };
1320
+ return globalThis.addEventListener("keydown", saveShortcut), () => {
1321
+ globalThis.removeEventListener("keydown", saveShortcut);
1322
+ };
1323
+ }, [savePath, selected]);
1324
+ let groups = (0, import_react.useMemo)(() => {
1021
1325
  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"));
1326
+ 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
1327
  for (let file of all) {
1024
1328
  let directory2 = groupForPath(file.path), files = value.get(directory2) ?? [];
1025
1329
  files.push(file), value.set(directory2, files);
@@ -1030,30 +1334,58 @@ function CreativeWorkbench({ sessionId, useSession }) {
1030
1334
  return (leftIndex < 0 ? order.length : leftIndex) - (rightIndex < 0 ? order.length : rightIndex) || left.localeCompare(right, "zh-Hans-CN");
1031
1335
  });
1032
1336
  }, [activityPath, workbench, workspace]), selectWorkbench = (next) => {
1033
- setWorkbench(next), setConflict(void 0);
1337
+ setWorkbench(next);
1034
1338
  let target = workspace === void 0 ? void 0 : preferredFile(workspace.files, next);
1035
- setSelected(target);
1036
- }, selectedGroup = selected === void 0 ? void 0 : groupForPath(selected), toggleGroup = (key, open) => {
1339
+ target === void 0 ? setSelected(void 0) : revealPath(target);
1340
+ }, selectEditorMode = (next) => {
1341
+ next === "preview" && rememberEditorPosition(), setEditorMode(next);
1342
+ }, selectedLabel = selected ?? `\u5728\u5F53\u524D DSH workspace \u4E2D\u9009\u62E9${workbench === "story" ? "\u5C0F\u8BF4" : "\u77ED\u5267"}\u6587\u4EF6`, selectedBasename = selected?.split("/").at(-1) ?? selectedLabel, selectedGroup = selected === void 0 ? void 0 : groupForPath(selected), toggleGroup = (key, open) => {
1037
1343
  setExpanded((current) => ({ ...current, [key]: open }));
1344
+ }, resolveConflict = (keepLocal) => {
1345
+ if (selected === void 0 || conflict?.theirs === void 0 || conflict.theirsVersion === void 0) return;
1346
+ let theirs = conflict.theirs, theirsVersion = conflict.theirsVersion;
1347
+ setBuffers((current) => {
1348
+ let existing = current[selected];
1349
+ return existing === void 0 ? current : {
1350
+ ...current,
1351
+ [selected]: keepLocal ? { ...existing, saved: theirs, version: theirsVersion, source: "human", conflict: void 0 } : { content: theirs, saved: theirs, source: "disk", version: theirsVersion }
1352
+ };
1353
+ });
1038
1354
  };
1039
1355
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { ref: surfaceRef, className: "oh-story-split-surface", children: [
1040
1356
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("style", { children: plugin_default }),
1041
1357
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("aside", { className: "oh-story-tree", children: [
1042
1358
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "oh-story-brand", children: [
1043
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "\u2726 Oh Story" }),
1044
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: reload, title: "\u5237\u65B0", children: "\u21BB" })
1045
- ] }),
1046
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "oh-story-mode-tabs", role: "tablist", "aria-label": "\u521B\u4F5C\u5DE5\u4F5C\u53F0", children: [
1047
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", role: "tab", "aria-selected": workbench === "story", onClick: () => {
1048
- selectWorkbench("story");
1049
- }, children: "\u5C0F\u8BF4" }),
1050
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", role: "tab", "aria-selected": workbench === "drama", onClick: () => {
1051
- selectWorkbench("drama");
1052
- }, children: "\u77ED\u5267" })
1359
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "oh-story-brand-cluster", children: [
1360
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("strong", { children: [
1361
+ "\u2726 ",
1362
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: "Oh Story" })
1363
+ ] }),
1364
+ workspaceKind !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "oh-story-kind", children: workspaceKind === "story" ? "\u5C0F\u8BF4" : "\u77ED\u5267" })
1365
+ ] }),
1366
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: reload, title: "\u5237\u65B0", "aria-label": "\u5237\u65B0\u9879\u76EE\u6587\u4EF6", children: "\u21BB" })
1053
1367
  ] }),
1368
+ showModeTabs && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "oh-story-mode-tabs", role: "tablist", "aria-label": "\u521B\u4F5C\u5DE5\u4F5C\u53F0", children: WORKBENCH_MODES.map((mode) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1369
+ "button",
1370
+ {
1371
+ type: "button",
1372
+ role: "tab",
1373
+ tabIndex: workbench === mode ? 0 : -1,
1374
+ "aria-selected": workbench === mode,
1375
+ onKeyDown: (event) => {
1376
+ handleTabKey(event, WORKBENCH_MODES, workbench, selectWorkbench);
1377
+ },
1378
+ onClick: () => {
1379
+ selectWorkbench(mode);
1380
+ },
1381
+ children: mode === "story" ? "\u5C0F\u8BF4" : "\u77ED\u5267"
1382
+ },
1383
+ mode
1384
+ )) }),
1054
1385
  error !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "oh-story-error", children: error }),
1386
+ workspace?.metadataErrors.map((message) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "oh-story-warning", children: message }, message)),
1055
1387
  /* @__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;
1388
+ let groupOpen = selectedGroup === directory2 || expanded[directory2] === !0;
1057
1389
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("details", { className: "oh-story-file-group", open: groupOpen, onToggle: (event) => {
1058
1390
  toggleGroup(directory2, event.currentTarget.open);
1059
1391
  }, children: [
@@ -1070,7 +1402,7 @@ function CreativeWorkbench({ sessionId, useSession }) {
1070
1402
  selected,
1071
1403
  activityPath,
1072
1404
  onToggle: toggleGroup,
1073
- onSelect: setSelected
1405
+ onSelect: revealPath
1074
1406
  }
1075
1407
  )
1076
1408
  ] }, directory2);
@@ -1078,26 +1410,48 @@ function CreativeWorkbench({ sessionId, useSession }) {
1078
1410
  ] }),
1079
1411
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("main", { className: "oh-story-editor", children: [
1080
1412
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("header", { children: [
1081
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { title: selected, children: selected ?? `\u5728\u5F53\u524D DSH workspace \u4E2D\u9009\u62E9${workbench === "story" ? "\u5C0F\u8BF4" : "\u77ED\u5267"}\u6587\u4EF6` }),
1413
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("span", { className: "oh-story-editor-path", title: selected, children: [
1414
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: selectedLabel }),
1415
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("strong", { children: selectedBasename })
1416
+ ] }),
1082
1417
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "oh-story-editor-actions", children: [
1083
- previewable && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "oh-story-editor-tabs", role: "tablist", "aria-label": markdown ? "Markdown \u67E5\u770B\u65B9\u5F0F" : "JSONL \u67E5\u770B\u65B9\u5F0F", children: [
1084
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", role: "tab", "aria-selected": editorMode === "preview", onClick: () => {
1085
- setEditorMode("preview");
1086
- }, children: "\u9884\u89C8" }),
1087
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", role: "tab", "aria-selected": editorMode === "source", onClick: () => {
1088
- setEditorMode("source");
1089
- }, children: "\u6E90\u7801" })
1090
- ] }),
1091
- (dirty || saving) && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "oh-story-save", type: "button", disabled: saving, onClick: () => {
1092
- save();
1418
+ previewable && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "oh-story-editor-tabs", role: "tablist", "aria-label": markdown ? "Markdown \u67E5\u770B\u65B9\u5F0F" : "JSONL \u67E5\u770B\u65B9\u5F0F", children: EDITOR_MODES.map((mode) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1419
+ "button",
1420
+ {
1421
+ type: "button",
1422
+ role: "tab",
1423
+ tabIndex: editorMode === mode ? 0 : -1,
1424
+ "aria-selected": editorMode === mode,
1425
+ onKeyDown: (event) => {
1426
+ handleTabKey(event, EDITOR_MODES, editorMode, selectEditorMode);
1427
+ },
1428
+ onClick: () => {
1429
+ selectEditorMode(mode);
1430
+ },
1431
+ children: mode === "preview" ? "\u9884\u89C8" : "\u6E90\u7801"
1432
+ },
1433
+ mode
1434
+ )) }),
1435
+ (dirty || saving) && selected !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "oh-story-save", type: "button", disabled: saving || buffer?.missing === !0, onClick: () => {
1436
+ savePath(selected);
1093
1437
  }, children: saving ? "\u4FDD\u5B58\u4E2D\u2026" : "\u4FDD\u5B58" })
1094
1438
  ] })
1095
1439
  ] }),
1096
- activityPath === selected && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "oh-story-stream", "data-stage": activity?.stage, children: [
1440
+ 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
1441
  "\u25CF ",
1098
- activity?.stage === "running" ? "Agent \u6B63\u5728\u5E94\u7528\u4FEE\u6539" : "Agent \u6B63\u5728\u751F\u6210\u6587\u4EF6\u5185\u5BB9"
1442
+ activity.stage === "running" ? "Agent \u6B63\u5728\u5E94\u7528\u4FEE\u6539" : "Agent \u6B63\u5728\u751F\u6210\u6587\u4EF6\u5185\u5BB9"
1443
+ ] }),
1444
+ conflict !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "oh-story-conflict", role: "alert", children: [
1445
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: conflict.message }),
1446
+ conflict.theirs !== void 0 && conflict.theirsVersion !== void 0 && selected !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
1447
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => {
1448
+ resolveConflict(!1);
1449
+ }, children: "\u8F7D\u5165\u78C1\u76D8\u7248\u672C" }),
1450
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => {
1451
+ resolveConflict(!0);
1452
+ }, children: "\u4FDD\u7559\u672C\u5730\u8349\u7A3F" })
1453
+ ] })
1099
1454
  ] }),
1100
- conflict !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "oh-story-conflict", children: conflict }),
1101
1455
  fileError !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "oh-story-error", children: fileError }),
1102
1456
  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
1457
  "\u5F53\u524D workspace \u8FD8\u6CA1\u6709\u5C0F\u8BF4\u6587\u4EF6\u3002\u53EF\u5728\u53F3\u4FA7 Chat \u4E2D\u8FD0\u884C ",
@@ -1111,11 +1465,23 @@ function CreativeWorkbench({ sessionId, useSession }) {
1111
1465
  "\u6B63\u5728\u52A0\u8F7D ",
1112
1466
  selected,
1113
1467
  "\u2026"
1468
+ ] }) : buffer.missing === !0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "oh-story-empty", children: [
1469
+ "\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",
1470
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "button", onClick: () => {
1471
+ setBuffers((current) => {
1472
+ let next = { ...current };
1473
+ return delete next[selected], next;
1474
+ }), setSelected(workspace === void 0 ? void 0 : preferredFile(workspace.files, workbench));
1475
+ }, children: "\u653E\u5F03\u672C\u5730\u8349\u7A3F" })
1114
1476
  ] }) : 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
1477
  "textarea",
1116
1478
  {
1479
+ ref: textareaRef,
1117
1480
  value: buffer.content,
1118
1481
  "data-format": structured ? "structured" : "prose",
1482
+ onBlur: rememberEditorPosition,
1483
+ onScroll: rememberEditorPosition,
1484
+ onSelect: rememberEditorPosition,
1119
1485
  onChange: (event) => {
1120
1486
  let content = event.target.value;
1121
1487
  setBuffers((current) => ({
@@ -1124,13 +1490,12 @@ function CreativeWorkbench({ sessionId, useSession }) {
1124
1490
  content,
1125
1491
  saved: current[selected]?.saved ?? "",
1126
1492
  source: "human",
1127
- version: current[selected]?.version ?? ""
1493
+ version: current[selected]?.version ?? "",
1494
+ conflict: current[selected]?.conflict,
1495
+ saving: current[selected]?.saving
1128
1496
  }
1129
1497
  }));
1130
1498
  },
1131
- onKeyDown: (event) => {
1132
- (event.metaKey || event.ctrlKey) && event.key.toLocaleLowerCase() === "s" && (event.preventDefault(), save());
1133
- },
1134
1499
  spellCheck: !structured,
1135
1500
  "aria-label": selected
1136
1501
  }
@@ -1138,27 +1503,51 @@ function CreativeWorkbench({ sessionId, useSession }) {
1138
1503
  ] })
1139
1504
  ] });
1140
1505
  }
1141
- function CreativeSplitBridge(props) {
1142
- let marker = (0, import_react.useRef)(null), [target, setTarget] = (0, import_react.useState)();
1506
+ function CreativeSplitBridge({ sessionId, useSession, useStore, actions }) {
1507
+ let marker = (0, import_react.useRef)(null), [target, setTarget] = (0, import_react.useState)(), runningCalls = useSession((snapshot) => snapshot.runningCalls), partial = useSession((snapshot) => streamingAssistant(snapshot.chat.timeline)), settledMutation = useSession((snapshot) => latestSettledMutation(snapshot.chat));
1143
1508
  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)(() => {
1509
+ let document = marker.current?.ownerDocument;
1510
+ if (document === void 0) return;
1511
+ let locate = () => {
1512
+ let anchor = document.querySelector("[data-conversation-scroll] > [data-slot='conversation.session']");
1513
+ setTarget((current) => current === anchor ? current : anchor ?? void 0);
1514
+ };
1515
+ locate();
1516
+ let observer = new MutationObserver(locate);
1517
+ return observer.observe(document.body, { childList: !0, subtree: !0 }), () => {
1518
+ observer.disconnect();
1519
+ };
1520
+ }, [sessionId]), (0, import_react.useLayoutEffect)(() => {
1147
1521
  let scroller = target?.parentElement;
1148
1522
  if (scroller == null) return;
1149
- let publishHeight = () => {
1523
+ let publishLayout = () => {
1150
1524
  scroller.style.setProperty("--oh-story-scroll-height", `${String(scroller.clientHeight)}px`);
1525
+ let layout = scroller.clientWidth < 620 ? "compact" : scroller.clientWidth < 900 ? "medium" : "wide";
1526
+ scroller.dataset.ohStoryLayout !== layout && (scroller.dataset.ohStoryLayout = layout);
1151
1527
  };
1152
- publishHeight();
1153
- let observer = new ResizeObserver(publishHeight);
1528
+ publishLayout();
1529
+ let observer = new ResizeObserver(publishLayout);
1154
1530
  return observer.observe(scroller), () => {
1155
- observer.disconnect(), scroller.style.removeProperty("--oh-story-scroll-height");
1531
+ observer.disconnect(), scroller.style.removeProperty("--oh-story-scroll-height"), delete scroller.dataset.ohStoryLayout;
1156
1532
  };
1157
1533
  }, [target]), /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1158
1534
  /* @__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)
1535
+ target === void 0 ? null : (0, import_react_dom.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1536
+ CreativeWorkbench,
1537
+ {
1538
+ sessionId,
1539
+ runningCalls,
1540
+ partial,
1541
+ settledMutation,
1542
+ useStore,
1543
+ actions
1544
+ }
1545
+ ), target)
1160
1546
  ] });
1161
1547
  }
1548
+ function WorkbenchSeat({ SessionProvider, renderSlot }) {
1549
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(SessionProvider, { children: () => renderSlot("oh-story.workspace", {}) });
1550
+ }
1162
1551
  function argsOf(block) {
1163
1552
  let raw = ("kind" in block ? block.call?.argsRaw : block.argsRaw) ?? "{}";
1164
1553
  try {
@@ -1187,11 +1576,18 @@ function RoleToolView({ block, inspect }) {
1187
1576
  ] });
1188
1577
  }
1189
1578
  function apply(context) {
1190
- context.slots.inject("conversation.session.header.actions", () => context.slots.register({
1191
- name: "conversation.session.header.actions",
1192
- id: "oh-story-workspace",
1193
- order: -100
1194
- }, CreativeSplitBridge)), context.slots.inject("tool.call.toolview", () => context.slots.register({
1579
+ context.slots.inject("shell.overlay", () => {
1580
+ let disposeSeat = context.slots.register({
1581
+ name: "shell.overlay",
1582
+ id: "oh-story-workspace",
1583
+ order: -100,
1584
+ children: { "oh-story.workspace": { kind: "single", scope: "session" } }
1585
+ }, WorkbenchSeat), disposeWorkbench = context.slots.register({
1586
+ name: "oh-story.workspace",
1587
+ store: createWorkbenchStore
1588
+ }, CreativeSplitBridge);
1589
+ return [disposeSeat, disposeWorkbench];
1590
+ }), context.slots.inject("tool.call.toolview", () => context.slots.register({
1195
1591
  name: "tool.call.toolview",
1196
1592
  key: "oh_story_role"
1197
1593
  }, RoleToolView));