dsh-novel-generator 0.1.1 → 0.1.3

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 +39 -22
  2. package/index.js +4 -3
  3. package/package.json +3 -2
package/client.js CHANGED
@@ -185,6 +185,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
185
185
  .dsn-input:focus,.dsn-select:focus,.dsn-textarea:focus{border-color:var(--dsw-alias-brand-primary)}
186
186
  .dsn-input.dsn-readonly{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);cursor:default}
187
187
  .dsn-input.dsn-readonly:focus{border-color:var(--dsw-alias-border-l1)}
188
+ .dsn-input.dsn-input-invalid{border-color:var(--dsw-alias-state-warn-primary)}
188
189
  .dsn-path{display:flex;gap:6px}
189
190
  .dsn-list{display:flex;flex-direction:column;gap:8px}
190
191
  .dsn-row{display:flex;align-items:center;gap:12px;padding:12px 14px;border-radius:12px;
@@ -425,7 +426,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
425
426
  }, busy ? t('保存中…', "Saving…") : t('保存', "Save")))));
426
427
  }
427
428
 
428
- function ProjectsPage({ boot, workspace, onOpen, onReload, onEditModel }) {
429
+ function ProjectsPage({ boot, resolveWorkspace, onOpen, onReload, onEditModel }) {
429
430
  const [title, setTitle] = useState('');
430
431
  const [cwd, setCwd] = useState('');
431
432
  const [preset, setPreset] = useState('');
@@ -436,9 +437,10 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
436
437
  const [busy, setBusy] = useState(false);
437
438
  const [error, setError] = useState('');
438
439
 
439
- // The title becomes a folder name inside the working directory.
440
- const effectiveTitle = title.trim() === '' ? t('未命名作品', "Untitled work") : title;
441
- const titleIssue = titleProblem(effectiveTitle);
440
+ // The title becomes a folder name inside the working directory, and it is
441
+ // required: an empty name is reported when the user tries to create.
442
+ const trimmedTitle = title.trim();
443
+ const titleIssue = trimmedTitle === '' ? '' : titleProblem(trimmedTitle);
442
444
 
443
445
  const groups = (boot.models && boot.models.groups) || [];
444
446
  const models = useMemo(() => {
@@ -451,20 +453,31 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
451
453
  }, [models]);
452
454
 
453
455
  const pickDirectory = useCallback(async () => {
454
- if (!workspace || typeof workspace.pickDirectory !== 'function') {
455
- setError(t('当前界面无法调用目录选择器,这个部署暂时不能创建作品', "The directory picker is unavailable here, so this deployment cannot create works."));
456
+ // Resolved at click time: the workspace service may activate after this
457
+ // plugin, so an instance captured in `apply` can come back undefined.
458
+ const ui = typeof resolveWorkspace === 'function' ? resolveWorkspace() : null;
459
+ if (!ui || typeof ui.pickDirectory !== 'function') {
460
+ setError(t('目录选择器还没就绪,请稍后再点一次「选择」', 'The directory picker is not ready yet — try "Select" again in a moment.'));
456
461
  return;
457
462
  }
458
463
  try {
459
- const picked = await workspace.pickDirectory();
464
+ const picked = await ui.pickDirectory();
460
465
  if (picked) setCwd(picked);
461
466
  } catch (pickError) {
462
467
  setError(messageOf(pickError));
463
468
  }
464
- }, [workspace]);
469
+ }, [resolveWorkspace]);
465
470
 
466
471
  const create = useCallback(async () => {
467
472
  setError('');
473
+ if (trimmedTitle === '') {
474
+ setError(t('作品名称不能为空', "Title cannot be empty."));
475
+ return;
476
+ }
477
+ if (titleIssue) {
478
+ setError(titleIssue);
479
+ return;
480
+ }
468
481
  if (!cwd.trim()) {
469
482
  setError(t('请先点「选择」挑一个工作目录', "Pick a working directory with \"Select\" first."));
470
483
  return;
@@ -472,7 +485,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
472
485
  setBusy(true);
473
486
  try {
474
487
  const result = await api('/project/create', {
475
- title: title.trim() || t('未命名作品', "Untitled work"),
488
+ title: trimmedTitle,
476
489
  cwd: cwd.trim(),
477
490
  agentPreset: preset || undefined,
478
491
  model: provider && model ? { provider, model } : null,
@@ -485,7 +498,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
485
498
  } finally {
486
499
  setBusy(false);
487
500
  }
488
- }, [title, cwd, preset, provider, model, onOpen, onReload]);
501
+ }, [trimmedTitle, titleIssue, cwd, preset, provider, model, onOpen, onReload]);
489
502
 
490
503
  const remove = useCallback(async (project) => {
491
504
  if (!window.confirm(t('确定删除作品「{title}」?这会同时删除工作目录下的作品文件夹(正文、梗概、世界快照)。作品的会话本身保留。', 'Delete the work “{title}”? This also deletes its folder in the working directory (chapters, outlines, world snapshots). The session itself stays.', { title: project.title }))) return;
@@ -502,13 +515,12 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
502
515
  h('label', { className: 'dsn-field' },
503
516
  h('span', { className: 'dsn-label' }, t('作品名称', "Title")),
504
517
  h('input', {
505
- className: 'dsn-input',
518
+ className: `dsn-input${titleIssue ? ' dsn-input-invalid' : ''}`,
506
519
  value: title,
507
520
  placeholder: t('例如:长夜将尽', "e.g. The Long Night Ends"),
521
+ title: titleIssue || t('会成为工作目录下的文件夹名', "Becomes a folder name inside the working directory."),
508
522
  onChange: (event) => setTitle(event.target.value),
509
- }),
510
- h('span', { className: `dsn-hint${titleIssue ? ' warn' : ''}` },
511
- titleIssue || t('会成为工作目录下的文件夹名', "Becomes a folder name inside the working directory."))),
523
+ })),
512
524
  h('label', { className: 'dsn-field' },
513
525
  h('span', { className: 'dsn-label' }, t('工作目录', "Working directory")),
514
526
  h('div', { className: 'dsn-path' },
@@ -552,8 +564,8 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
552
564
  h('button', {
553
565
  className: 'dsn-btn primary',
554
566
  type: 'button',
555
- disabled: busy || Boolean(titleIssue),
556
- title: titleIssue || '',
567
+ // Enabled even when the form is incomplete: the click itself says why.
568
+ disabled: busy,
557
569
  onClick: create,
558
570
  }, busy ? t('创建中…', "Creating…") : t('创建作品并开会话', "Create work and open its session")))),
559
571
 
@@ -1654,7 +1666,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
1654
1666
 
1655
1667
  /* ------------------------------------------------------------ studio app */
1656
1668
 
1657
- function StudioApp({ workspace }) {
1669
+ function StudioApp({ resolveWorkspace }) {
1658
1670
  // Subscribes every screen below to Settings → General → Language.
1659
1671
  useLocaleId();
1660
1672
  const [boot, setBoot] = useState(null);
@@ -1742,7 +1754,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
1742
1754
  ? h('div', { className: 'dsn-scroll' },
1743
1755
  h(ProjectsPage, {
1744
1756
  boot,
1745
- workspace,
1757
+ resolveWorkspace,
1746
1758
  onOpen: openProject,
1747
1759
  onReload: loadBoot,
1748
1760
  onEditModel: setModelTarget,
@@ -1772,12 +1784,12 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
1772
1784
  : null);
1773
1785
  }
1774
1786
 
1775
- function StudioOverlay({ workspace }) {
1787
+ function StudioOverlay({ resolveWorkspace }) {
1776
1788
  const open = useOverlayOpen();
1777
1789
  if (!open) return null;
1778
1790
  // No backdrop-click close: leaving with unsaved chapter text goes through
1779
1791
  // the 关闭 button, which asks first.
1780
- return h('div', { className: 'dsn-overlay' }, h(StudioApp, { workspace }));
1792
+ return h('div', { className: 'dsn-overlay' }, h(StudioApp, { resolveWorkspace }));
1781
1793
  }
1782
1794
 
1783
1795
  /* ---------------------------------------------------------------- apply */
@@ -1785,7 +1797,12 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
1785
1797
  return {
1786
1798
  inject: ['slots'],
1787
1799
  apply(ctx) {
1788
- const uiWorkspace = ctx.get('uiWorkspace');
1800
+ /**
1801
+ * Read at use time, not at activation: this plugin may mount before the
1802
+ * workspace UI that owns `uiWorkspace`, and a captured instance would
1803
+ * then be undefined for the life of the page.
1804
+ */
1805
+ const resolveWorkspace = () => ctx.get('uiWorkspace') || null;
1789
1806
  // Settings → General → Language drives every string that goes through
1790
1807
  // the translator below; a host without the service stays Chinese.
1791
1808
  localeService = ctx.get('locale') || null;
@@ -1798,7 +1815,7 @@ body[data-ds-dark-theme] .dsn-shell{color-scheme:dark}
1798
1815
  name: 'shell.overlay',
1799
1816
  id: 'dsh-novel-generator-studio',
1800
1817
  order: 40,
1801
- }, () => h(StudioOverlay, { workspace: uiWorkspace })));
1818
+ }, () => h(StudioOverlay, { resolveWorkspace })));
1802
1819
  },
1803
1820
  };
1804
1821
  },
package/index.js CHANGED
@@ -982,7 +982,7 @@ async function bootstrap(ctx, root) {
982
982
  }
983
983
  }
984
984
  return {
985
- revision: '2026-10-20.14',
985
+ revision: '2026-10-20.15',
986
986
  projects,
987
987
  models: {
988
988
  default: modelCatalog.default || null,
@@ -1005,8 +1005,9 @@ async function bootstrap(ctx, root) {
1005
1005
  async function createProject(ctx, root, body) {
1006
1006
  const controller = ctx.get('sessionController');
1007
1007
  if (controller === undefined) throw httpError(503, '当前部署缺少 sessionController 服务,无法创建会话');
1008
- const title = asText(body.title) || '未命名作品';
1009
- // The title becomes a folder name inside the chosen directory.
1008
+ // The title is required and becomes a folder name inside the chosen
1009
+ // directory, so an empty or illegal one is refused rather than defaulted.
1010
+ const title = asText(body.title);
1010
1011
  const titleIssue = titleProblem(title);
1011
1012
  if (titleIssue !== null) throw httpError(400, titleIssue);
1012
1013
  const cwd = asText(body.cwd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-novel-generator",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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",
@@ -51,7 +51,8 @@
51
51
  "platform": "web",
52
52
  "immediately": true,
53
53
  "inject": [
54
- "@deepseek-ai/dsh-client-locale"
54
+ "@deepseek-ai/dsh-client-locale",
55
+ "@deepseek-ai/dsh-client-ui-workspace"
55
56
  ]
56
57
  }
57
58
  }