dsh-novel-generator 0.1.16 → 0.1.17

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.
Files changed (3) hide show
  1. package/client.js +66 -11
  2. package/index.js +4 -1
  3. package/package.json +1 -1
package/client.js CHANGED
@@ -1041,7 +1041,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
1041
1041
 
1042
1042
  /* ---------------------------------------------------------- outline page */
1043
1043
 
1044
- function OutlinePage({ detail, chapter, isFirstChapter, onBack, onSaved, notify, onRegisterGuard, requestConfirm }) {
1044
+ function OutlinePage({ detail, chapter, isFirstChapter, onBack, onSaved, notify, onRegisterGuard, requestConfirm, onBusyChange }) {
1045
1045
  const buildForm = useCallback(() => ({
1046
1046
  title: chapter ? chapter.title || '' : '',
1047
1047
  durationDays: chapter && chapter.outline ? chapter.outline.durationDays : (detail.timeline.unitDays || 30),
@@ -1182,6 +1182,22 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
1182
1182
  });
1183
1183
  }, [requestConfirm, startJob]);
1184
1184
 
1185
+ // Report the running job upward so the studio can lock itself and warn
1186
+ // before the panel is closed mid-generation.
1187
+ useEffect(() => {
1188
+ if (typeof onBusyChange !== 'function') return;
1189
+ const running = job && job.state === 'running';
1190
+ if (!running) {
1191
+ onBusyChange(null);
1192
+ return undefined;
1193
+ }
1194
+ onBusyChange({
1195
+ label: job.kind === 'compose' ? t('正在自动推演…', 'Deducing…') : t('正在生成章节故事…', 'Generating the chapter…'),
1196
+ message: job.message || '',
1197
+ });
1198
+ return () => onBusyChange(null);
1199
+ }, [job ? job.jobId : null, job ? job.state : null, job ? job.message : null, onBusyChange]);
1200
+
1185
1201
  useEffect(() => {
1186
1202
  if (!job) return;
1187
1203
  if (job.kind === 'generate' && job.state === 'done') {
@@ -1312,7 +1328,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
1312
1328
  * The only place a chapter is created: word bounds and the story length are
1313
1329
  * required, and one button can deduce the synopsis and write the chapter.
1314
1330
  */
1315
- function NewChapterDialog({ detail, startDay, onCancel, onCreated, notify }) {
1331
+ function NewChapterDialog({ detail, startDay, onCancel, onCreated, notify, onBusyChange }) {
1316
1332
  const [form, setForm] = useState({
1317
1333
  title: '',
1318
1334
  minWords: 1200,
@@ -1337,6 +1353,20 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
1337
1353
 
1338
1354
  const composing = Boolean(job && job.state === 'running' && job.kind === 'compose');
1339
1355
 
1356
+ // A deducing dialog locks the studio behind it and warns before closing.
1357
+ useEffect(() => {
1358
+ if (typeof onBusyChange !== 'function') return undefined;
1359
+ if (!composing) {
1360
+ onBusyChange(null);
1361
+ return undefined;
1362
+ }
1363
+ onBusyChange({
1364
+ label: t('正在自动推演新章节…', 'Deducing a new chapter…'),
1365
+ message: (job && job.message) || '',
1366
+ });
1367
+ return () => onBusyChange(null);
1368
+ }, [composing, job ? job.message : null, onBusyChange]);
1369
+
1340
1370
  const create = useCallback(async () => {
1341
1371
  setBusy(true);
1342
1372
  setError('');
@@ -1500,13 +1530,25 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
1500
1530
 
1501
1531
  /* ------------------------------------------------------------- workspace */
1502
1532
 
1503
- function ProjectWorkspace({ detail, onReload, notify, onRegisterLeaveGuard }) {
1533
+ function ProjectWorkspace({ detail, onReload, notify, onRegisterLeaveGuard, onBusyChange }) {
1504
1534
  const [view, setView] = useState('world');
1505
1535
  const [chapterId, setChapterId] = useState(null);
1506
1536
  // A running snapshot job: { label, message }. Non-null locks the studio,
1507
1537
  // because the job keeps rewriting this work's snapshots.
1508
1538
  const [snapshotJob, setSnapshotJob] = useState(null);
1509
- const snapshotBusy = snapshotJob !== null;
1539
+ // The same idea for the chapter jobs (自动推演 / 生成章节故事), reported up
1540
+ // by the pages that own them.
1541
+ const [childBusy, setChildBusy] = useState(null);
1542
+ const busy = snapshotJob || childBusy;
1543
+ const snapshotBusy = Boolean(snapshotJob);
1544
+
1545
+ // Tell the shell whether a long operation is in flight, so closing the
1546
+ // panel can ask first.
1547
+ useEffect(() => {
1548
+ if (typeof onBusyChange !== 'function') return undefined;
1549
+ onBusyChange(busy ? { label: busy.label, message: busy.message } : null);
1550
+ return () => onBusyChange(null);
1551
+ }, [busy ? busy.label : null, busy ? busy.message : null, onBusyChange]);
1510
1552
  const [dialogOpen, setDialogOpen] = useState(false);
1511
1553
  const [text, setText] = useState('');
1512
1554
  const [dirty, setDirty] = useState(false);
@@ -1706,14 +1748,14 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
1706
1748
  const trackIndex = view === 'world' ? 0 : view === 'story' ? 1 : 2;
1707
1749
 
1708
1750
  return h('div', { className: 'dsn-stage' },
1709
- // A running snapshot job owns the work: nothing else may be edited
1710
- // while it rewrites snapshots underneath.
1711
- snapshotBusy
1751
+ // Any running job owns the work: nothing else may be edited while it
1752
+ // rewrites the novel underneath.
1753
+ busy
1712
1754
  ? h('div', { className: 'dsn-lock' },
1713
1755
  h('div', { className: 'dsn-lock-box' },
1714
- h('div', { className: 'dsn-lock-title' }, snapshotJob.label),
1715
- h('div', { className: 'dsn-lock-text' }, snapshotJob.message),
1716
- h('div', { className: 'dsn-lock-hint' }, t('快照生成期间界面不可操作,完成后会自动刷新', 'The studio is locked while snapshots are generated; it refreshes when done.'))))
1756
+ h('div', { className: 'dsn-lock-title' }, busy.label),
1757
+ h('div', { className: 'dsn-lock-text' }, busy.message),
1758
+ h('div', { className: 'dsn-lock-hint' }, t('生成期间界面不可操作,完成后会自动刷新', 'The studio is locked while this runs; it refreshes when done.'))))
1717
1759
  : null,
1718
1760
  h('div', { className: 'dsn-track', style: { transform: `translateX(-${trackIndex * 100}%)` } },
1719
1761
  h('section', { className: 'dsn-page' },
@@ -1756,6 +1798,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
1756
1798
  notify,
1757
1799
  onRegisterGuard: registerOutlineGuard,
1758
1800
  requestConfirm: setConfirmRequest,
1801
+ onBusyChange: setChildBusy,
1759
1802
  isFirstChapter: Boolean(chapter && chapters.length > 0 && chapter.chapterId === chapters[0].chapterId),
1760
1803
  }))),
1761
1804
  confirmRequest
@@ -1804,6 +1847,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
1804
1847
  onCancel: () => setDialogOpen(false),
1805
1848
  onCreated: created,
1806
1849
  notify,
1850
+ onBusyChange: setChildBusy,
1807
1851
  })
1808
1852
  : null);
1809
1853
  }
@@ -1818,6 +1862,8 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
1818
1862
  const [projectId, setProjectId] = useState(null);
1819
1863
  const [detail, setDetail] = useState(null);
1820
1864
  const [toast, setToast] = useToast();
1865
+ // Set while a work runs a model job, so leaving the panel can warn first.
1866
+ const [busyNow, setBusyNow] = useState(null);
1821
1867
  const [modelTarget, setModelTarget] = useState(null);
1822
1868
  const leaveGuard = useRef(null);
1823
1869
  const registerLeaveGuard = useCallback((guard) => {
@@ -1825,13 +1871,21 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
1825
1871
  }, []);
1826
1872
  /** Every way out of the studio asks the workspace first. */
1827
1873
  const requestLeave = useCallback((proceed) => {
1874
+ // A running generation outranks the editor guard: say it is still going
1875
+ // before anything closes.
1876
+ if (busyNow) {
1877
+ const label = busyNow.label || t('正在生成', 'Generation is running');
1878
+ if (!window.confirm(t('{label},现在离开吗?生成会在后台继续,回来时刷新即可看到结果。', '{label} — leave now? It keeps running in the background; reopen the panel later to see the result.', { label }))) return;
1879
+ proceed();
1880
+ return;
1881
+ }
1828
1882
  const guard = leaveGuard.current;
1829
1883
  if (typeof guard === 'function') {
1830
1884
  guard(proceed);
1831
1885
  return;
1832
1886
  }
1833
1887
  proceed();
1834
- }, []);
1888
+ }, [busyNow]);
1835
1889
  const close = useCallback(() => requestLeave(() => setOverlayOpen(false)), [requestLeave]);
1836
1890
 
1837
1891
  const loadBoot = useCallback(async () => {
@@ -1911,6 +1965,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
1911
1965
  onReload: reloadDetail,
1912
1966
  notify,
1913
1967
  onRegisterLeaveGuard: registerLeaveGuard,
1968
+ onBusyChange: setBusyNow,
1914
1969
  })),
1915
1970
  modelTarget
1916
1971
  ? h(ModelDialog, {
package/index.js CHANGED
@@ -891,6 +891,9 @@ async function rebindSession(ctx, root, project) {
891
891
  draft.sessionId = sessionId;
892
892
  });
893
893
  try {
894
+ // The memo skips projects already grouped; a rebind starts a new session
895
+ // that has never been grouped, so the entry has to go.
896
+ ensuredWorkspaces.delete(project.projectId);
894
897
  await ensureWorkspace(ctx, root, project);
895
898
  } catch {
896
899
  // Grouping is cosmetic; the session works either way.
@@ -1361,7 +1364,7 @@ async function bootstrap(ctx, root) {
1361
1364
  }
1362
1365
  }
1363
1366
  return {
1364
- revision: '2026-10-20.44',
1367
+ revision: '2026-10-20.45',
1365
1368
  projects,
1366
1369
  models: {
1367
1370
  default: modelCatalog.default || null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-novel-generator",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "type": "module",
5
5
  "description": "Novel studio for DeepSeek Harness: a draggable story timeline, world snapshots, chapter outlines, and chapters written by the work's own agent.",
6
6
  "license": "MIT",