@klarkxy/dsh-current-title 0.2.5 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,11 +27,12 @@ Titles update automatically. Under **Plugins → Auto Title** you can:
27
27
  - pick a title model (empty follows the current session model);
28
28
  - regenerate the title of the open session.
29
29
 
30
- If the model list cannot load, or some providers fail, use **Retry** beside the model picker. Retrying keeps your saved model and prompt draft.
30
+ If the model list cannot load, or some providers fail, use **Retry** beside the model picker; retrying keeps your saved model and prompt draft.
31
31
 
32
32
  ## Limits
33
33
 
34
- - Each title costs one model call; failed generations keep the existing title.
34
+ - Each title costs one model call at the model's lowest supported reasoning effort; a failed generation keeps the existing title.
35
+ - Cancellation and timeouts wait for the model call to stop; a provider that ignores cancellation can delay completion.
35
36
  - Turning the plugin off hands titles back to the built-in DSH title feature.
36
37
 
37
38
  ## License
@@ -27,11 +27,12 @@ dsh plugin --profile web add @klarkxy/dsh-current-title
27
27
  - 选择标题模型(留空跟随当前会话模型);
28
28
  - 重新生成当前会话的标题。
29
29
 
30
- 模型列表加载失败或部分提供商不可用时,可在模型选择器旁点击「重试」。重试会保留已保存的模型和提示词草稿。
30
+ 模型列表加载失败或部分提供商不可用时,可在模型选择器旁点击「重试」;重试会保留已保存的模型和提示词草稿。
31
31
 
32
32
  ## 限制
33
33
 
34
- - 每次生成标题会调用一次模型;生成失败时保留原标题。
34
+ - 每次生成标题调用一次模型,使用模型支持的最低思考强度;生成失败时保留原标题。
35
+ - 取消和超时会等待模型调用停止;提供商不响应取消时,完成时间可能延后。
35
36
  - 关闭插件后,标题交还给 DSH 自带的标题功能。
36
37
 
37
38
  ## 许可证
@@ -292,6 +292,17 @@ ${scope} {
292
292
  /* --- Layout scaffolding. --- */
293
293
  & .dsh-ui-stack { display: flex; flex-direction: column; gap: 12px; }
294
294
  & .dsh-ui-stack-lg { display: flex; flex-direction: column; gap: 20px; }
295
+ /* Plugin detail pages share one reading column. These are layout recipes;
296
+ * controls keep their own primitive styles. Form spacing follows the host's
297
+ * settings-form/fields.module.css and SettingsForm.module.css. */
298
+ & .dsh-ui-page { display: flex; flex-direction: column; gap: 16px; width: 100%; max-width: 760px; min-width: 0; }
299
+ & .dsh-ui-form { display: flex; flex-direction: column; gap: 0; min-width: 0; }
300
+ &.dsh-ui-form > .dsh-ui-field, & .dsh-ui-form > .dsh-ui-field { padding: 12px 0; }
301
+ & .dsh-ui-form-footer { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding-top: 16px; min-width: 0; }
302
+ & .dsh-ui-fields-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 20px; min-width: 0; margin: 0; padding: 0; border: 0; }
303
+ @media (max-width: 560px) {
304
+ & .dsh-ui-fields-grid { grid-template-columns: minmax(0, 1fr); }
305
+ }
295
306
  & .dsh-ui-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
296
307
  & .dsh-ui-row-wrap { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
297
308
  & .dsh-ui-spacer { flex: 1; min-width: 0; }
@@ -319,6 +330,9 @@ ${scope} {
319
330
  * official primitive, so this only owns the vertical rhythm. --- */
320
331
  & .dsh-ui-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
321
332
  & .dsh-ui-field + .dsh-ui-field { border-top: 0.5px solid var(--dsw-alias-border-l2); padding-top: 12px; }
333
+ /* Grid fields share a row, so the stacked-field divider must not win later in
334
+ * the cascade. Reset it here, after the field recipe. */
335
+ & .dsh-ui-fields-grid > .dsh-ui-field { padding: 0; border: 0; }
322
336
  & .dsh-ui-label { display: flex; align-items: center; gap: 6px; font-size: 13px; line-height: 20px; font-weight: 500; color: var(--dsw-alias-label-primary); }
323
337
  & .dsh-ui-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
324
338
  & .dsh-ui-help { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }
@@ -509,29 +523,22 @@ ${scope} {
509
523
  & *, & *::before, & *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
510
524
  }
511
525
  }
512
- `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
526
+ `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?=\s*\{)/gm, (_match, className) => `&${className}, & ${className}`);
513
527
  }
514
528
  //#endregion
515
529
  //#region ../dsh-plugin-kit/src/contracts.ts
516
- /**
517
- * Remote contribution for model-settings change notifications.
518
- * Provided by `@klarkxy/dsh-model-hub` (`ModelSettingsChangesController`);
519
- * subscribers degrade silently when that package is not installed.
520
- * The stream carries invalidations only — `ready` is never a settings snapshot.
521
- */
522
- const MODEL_SETTINGS_CHANGES_REMOTE = {
523
- package: "@klarkxy/dsh-model-hub",
524
- descriptors: [{
525
- id: "modelSettingsChanges/watch",
526
- service: "modelSettingsChangesController",
527
- namespace: "modelSettingsChanges",
528
- method: "watch",
529
- mode: "stream",
530
- invocation: { kind: "direct" },
531
- parameters: [],
532
- cancellation: { parameter: "signal" }
533
- }]
534
- };
530
+ /** `@klarkxy/dsh-git-commit` becomes `KlarkxyDshGitCommitModelSettingsChanges`. */
531
+ function modelSettingsChangesIdentity(packageName) {
532
+ const scoped = /^@([^/]+)\/(.+)$/.exec(packageName);
533
+ const parts = (scoped ? `${scoped[1]}/${scoped[2]}` : packageName).split(/[^A-Za-z0-9]+/).filter(Boolean);
534
+ if (!parts.length) throw new Error("Model settings package name has no RPC segment");
535
+ const segment = `${parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("")}ModelSettingsChanges`;
536
+ if (!/^[A-Z][A-Za-z0-9]*$/.test(segment)) throw new Error(`Model settings sync name is not an RPC segment: ${segment}`);
537
+ return {
538
+ service: segment,
539
+ namespace: segment
540
+ };
541
+ }
535
542
  //#endregion
536
543
  //#region ../dsh-plugin-kit/src/model-settings-client.ts
537
544
  /** This stream carries invalidations only; ready is never a settings snapshot. */
@@ -546,17 +553,44 @@ function parseModelSettingsChangeFrame(value) {
546
553
  };
547
554
  throw new Error("Invalid model settings change frame");
548
555
  }
549
- MODEL_SETTINGS_CHANGES_REMOTE.package, MODEL_SETTINGS_CHANGES_REMOTE.descriptors.map((descriptor) => ({
550
- ...descriptor,
551
- result: {
552
- mode: "strict",
553
- typeSymbol: "ModelSettings.ChangeFrame",
554
- create: () => ({ parse: parseModelSettingsChangeFrame })
555
- }
556
- }));
556
+ /** One owner's contribution. Each package mounts only its own endpoint. */
557
+ function modelSettingsChangesRemote(packageName, identity = modelSettingsChangesIdentity(packageName)) {
558
+ return {
559
+ package: packageName,
560
+ descriptors: [{
561
+ id: `${identity.namespace}/watch`,
562
+ service: identity.service,
563
+ namespace: identity.namespace,
564
+ method: "watch",
565
+ mode: "stream",
566
+ invocation: { kind: "direct" },
567
+ parameters: [],
568
+ cancellation: { parameter: "signal" },
569
+ result: {
570
+ mode: "strict",
571
+ typeSymbol: "ModelSettings.ChangeFrame",
572
+ create: () => ({ parse: parseModelSettingsChangeFrame })
573
+ }
574
+ }]
575
+ };
576
+ }
577
+ /** Mount from a synchronous apply. Disposal waits for an in-flight mount and
578
+ * then withdraws it, so the contribution cannot outlive the plugin. */
579
+ function mountModelSettingsChanges(ctx, packageName) {
580
+ const contribution = modelSettingsChangesRemote(packageName);
581
+ ctx.inject(["remote"], (injected) => {
582
+ const scope = injected;
583
+ const mounting = scope.remote.$mount(contribution);
584
+ mounting.catch(() => {});
585
+ scope.effect(() => async () => {
586
+ await (await mounting.catch(() => void 0))?.();
587
+ });
588
+ });
589
+ }
557
590
  /** Native RemoteStream owns carrier reconnects; no timers or browser transport.
558
591
  * A factory shares one logical stream while at least one subscriber remains. */
559
- function createModelSettingsSubscriber(ctx, namespace) {
592
+ function createModelSettingsSubscriber(ctx, packageName, identity = modelSettingsChangesIdentity(packageName), accept = (ns) => ns === packageName) {
593
+ const endpoint = identity;
560
594
  const listeners = /* @__PURE__ */ new Set();
561
595
  let injection;
562
596
  let current;
@@ -582,12 +616,18 @@ function createModelSettingsSubscriber(ctx, namespace) {
582
616
  };
583
617
  const start = () => {
584
618
  const life = ++lifecycle;
585
- injection = ctx.inject(["remote", "remote.modelSettingsChanges"], (injected) => {
619
+ injection = ctx.inject(["remote", `remote.${endpoint.namespace}`], (injected) => {
586
620
  if (life !== lifecycle || !listeners.size) return;
587
621
  const scope = injected;
622
+ const service = scope.remote[endpoint.namespace];
623
+ if (!service || !("watch" in service) || !service.watch) {
624
+ report("Model settings sync service unavailable");
625
+ return;
626
+ }
627
+ const watch = service.watch;
588
628
  const stream = scope.remote.$stream({
589
629
  name: "Model settings sync",
590
- open: (signal) => scope.remote.modelSettingsChanges.watch(signal),
630
+ open: (signal) => watch(signal),
591
631
  ended: (accepted) => /* @__PURE__ */ new Error(`Model settings sync stream ended ${accepted ? "after" : "before"} ready; refresh or reopen the page`)
592
632
  });
593
633
  const session = {
@@ -622,7 +662,7 @@ function createModelSettingsSubscriber(ctx, namespace) {
622
662
  invalidate();
623
663
  } else {
624
664
  if (!ready) throw new Error("Model settings sync change arrived before ready");
625
- if (namespace === "*" || frame.ns === namespace) invalidate();
665
+ if (accept(frame.ns)) invalidate();
626
666
  }
627
667
  }
628
668
  } catch (error) {
@@ -927,7 +967,14 @@ function useModelCatalog(load, options = {}) {
927
967
  refresh
928
968
  };
929
969
  }
930
- /** Also accepts a mapped state from a caller that owns broader async orchestration. */
970
+ /**
971
+ * Also accepts a mapped state from a caller that owns broader async orchestration.
972
+ *
973
+ * The dsh-ui-* classes carry the styling and resolve through the consuming
974
+ * plugin's own officialUiCss scope (every consumer mounts its root class, so
975
+ * e.g. `.dsh-model-hub .dsh-ui-loading` paints this); dsh-model-catalog-status
976
+ * stays as a structural hook for tests and layout, never a colour source.
977
+ */
931
978
  function ModelCatalogStatus({ state, locale }) {
932
979
  const zh = locale?.toLowerCase().startsWith("zh") === true;
933
980
  const copy = zh ? {
@@ -943,15 +990,17 @@ function ModelCatalogStatus({ state, locale }) {
943
990
  };
944
991
  if (!state.loading && !state.error && state.failures.length === 0) return null;
945
992
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
946
- className: "dsh-model-catalog-status",
993
+ className: "dsh-model-catalog-status dsh-ui-stack",
947
994
  lang: zh ? "zh" : "en",
948
995
  children: [
949
996
  state.loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
950
997
  role: "status",
998
+ className: "dsh-ui-loading",
951
999
  children: copy.loading
952
1000
  }) : null,
953
1001
  state.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
954
1002
  role: "alert",
1003
+ className: "dsh-ui-error dsh-ui-wrap",
955
1004
  children: [
956
1005
  copy.error,
957
1006
  ": ",
@@ -960,20 +1009,27 @@ function ModelCatalogStatus({ state, locale }) {
960
1009
  }) : null,
961
1010
  state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
962
1011
  role: "status",
963
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: copy.partial }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", { children: state.failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
964
- failure.name || failure.id,
965
- ": ",
966
- failure.message
967
- ] }, `${failure.id}:${index}`)) })]
1012
+ className: "dsh-ui-meta",
1013
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: copy.partial }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1014
+ className: "dsh-ui-list",
1015
+ children: state.failures.map((failure, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
1016
+ failure.name || failure.id,
1017
+ ": ",
1018
+ failure.message
1019
+ ] }, `${failure.id}:${index}`))
1020
+ })]
968
1021
  }) : null,
969
- state.error || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
970
- type: "button",
971
- size: "sm",
972
- disabled: state.loading,
973
- onClick: () => {
974
- state.refresh();
975
- },
976
- children: copy.retry
1022
+ state.error || state.failures.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1023
+ className: "dsh-ui-actions",
1024
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1025
+ type: "button",
1026
+ size: "sm",
1027
+ disabled: state.loading,
1028
+ onClick: () => {
1029
+ state.refresh();
1030
+ },
1031
+ children: copy.retry
1032
+ })
977
1033
  }) : null
978
1034
  ]
979
1035
  });
@@ -993,7 +1049,8 @@ function ModelCatalogStatus({ state, locale }) {
993
1049
  * Sticky group headings, subsequence ranking, highlight walking and focus
994
1050
  * hand-back all match the host. SYNC POINT: when the host's ModelSelect or
995
1051
  * ModelSelect.module.css changes, this copy must be re-transcribed; styles
996
- * below may only reference --dsw-* theme tokens, never literal colors.
1052
+ * below may only reference allowlisted theme tokens (the --dsw-* set plus the
1053
+ * host's --ds-* motion tokens), never literal colors.
997
1054
  *
998
1055
  * It statically imports the primitives, so only browser clients should load
999
1056
  * this entry; host code and node tests use the core entry (`./index.ts`).
@@ -1501,7 +1558,8 @@ function ModelMenu(props) {
1501
1558
  * sticky group headings (MenuGroup's own CSS), and the l1-stroke prominent
1502
1559
  * elevation at portal z-index 1100. The trigger mirrors the composer's 28px
1503
1560
  * model pill, chevron rotating open. MenuSurface paints fill and backdrop.
1504
- * Only --dsw-* theme tokens may appear here — a literal color is a bug.
1561
+ * Only allowlisted --dsw- and --ds- theme tokens may appear here — a literal
1562
+ * color is a bug.
1505
1563
  */
1506
1564
  const modelMenuCss = `
1507
1565
  .dsh-model-menu-root { min-width: 0; position: relative; display: inline-flex; }
@@ -1524,12 +1582,14 @@ const modelMenuCss = `
1524
1582
  display: flex;
1525
1583
  }
1526
1584
  .dsh-model-menu-trigger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
1527
- .dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0 2px var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
1585
+ .dsh-model-menu-trigger:focus-visible:not([data-selection-focus]) { box-shadow: 0 0 0 var(--dsw-focus-ring-width) var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary)); }
1528
1586
  .dsh-model-menu-trigger:disabled { color: var(--dsw-alias-label-dimmed); cursor: default; }
1529
1587
  .dsh-model-menu-triggerLabel { text-overflow: ellipsis; white-space: nowrap; min-width: 0; overflow: hidden; }
1530
1588
  .dsh-model-menu-triggerEffort { text-overflow: ellipsis; white-space: nowrap; min-width: 0; color: var(--dsw-alias-label-caption); flex-shrink: 1000; overflow: hidden; }
1531
- .dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform 0.12s; }
1589
+ .dsh-model-menu-chevron { color: var(--dsw-alias-label-caption); flex: none; transition: transform var(--ds-transition-duration) var(--ds-ease-in-out); }
1532
1590
  .dsh-model-menu-chevronOpen { transform: rotate(180deg); }
1591
+ /* Portal overlay: the host publishes no z-index token, so the card keeps the
1592
+ * native menu layer's 1100 verbatim. */
1533
1593
  .dsh-model-menu-menu {
1534
1594
  z-index: 1100;
1535
1595
  --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
@@ -1555,6 +1615,8 @@ const modelMenuCss = `
1555
1615
  .dsh-model-menu-searchWithQuery { padding-right: 34px; }
1556
1616
  .dsh-model-menu-searchRow .dsh-model-menu-search input { padding: 0; font-size: 12px; line-height: normal; }
1557
1617
  .dsh-model-menu-searchRow .dsh-model-menu-search input::placeholder { color: var(--dsw-alias-label-caption); }
1618
+ /* The clear button is a circle by geometry (border-radius: 50%), a shape the
1619
+ * radius token scale does not describe. */
1558
1620
  .dsh-model-menu-searchClear {
1559
1621
  width: 24px;
1560
1622
  height: 24px;
@@ -2049,7 +2111,7 @@ function TitleSettings(props) {
2049
2111
  });
2050
2112
  }
2051
2113
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2052
- className: "current-title-settings ct-root dsh-ui-panel",
2114
+ className: "current-title-settings ct-root dsh-ui-page",
2053
2115
  lang: seat.locale,
2054
2116
  "data-testid": "current-title-settings",
2055
2117
  children: [
@@ -2315,13 +2377,16 @@ function TitleSettings(props) {
2315
2377
  ]
2316
2378
  });
2317
2379
  }
2318
- const styles = `${officialUiCss("ct-root")}${modelMenuCss}
2319
- .ct-root { max-width: 760px; }
2320
- .ct-prompt { min-height: 96px; }
2380
+ const currentTitleOwnCss = `
2381
+ .ct-root .ct-prompt { min-height: 96px; }
2321
2382
  `;
2383
+ const currentTitleStyles = `${officialUiCss("ct-root")}${modelMenuCss}${currentTitleOwnCss}`;
2384
+ const styles = currentTitleStyles;
2322
2385
  function apply(ctx) {
2323
2386
  const client = ctx;
2324
- const settingsSubscriber = createModelSettingsSubscriber(ctx, "@klarkxy/dsh-current-title");
2387
+ const packageName = "@klarkxy/dsh-current-title";
2388
+ mountModelSettingsChanges(ctx, packageName);
2389
+ const settingsSubscriber = createModelSettingsSubscriber(ctx, packageName, modelSettingsChangesIdentity(packageName));
2325
2390
  const subscribeSettings = (change, error) => settingsSubscriber.subscribe(change, error);
2326
2391
  const marker = createTitleClientMarker();
2327
2392
  ctx.provide(TITLE_CLIENT_SERVICE, marker);
@@ -2361,6 +2426,8 @@ exports.TitleSettings = TitleSettings;
2361
2426
  exports.apply = apply;
2362
2427
  exports.applyMarkerFromStatus = applyMarkerFromStatus;
2363
2428
  exports.createTitleClientMarker = createTitleClientMarker;
2429
+ exports.currentTitleOwnCss = currentTitleOwnCss;
2430
+ exports.currentTitleStyles = currentTitleStyles;
2364
2431
  exports.disposeTitleClientMarker = disposeTitleClientMarker;
2365
2432
  exports.inject = inject;
2366
2433
  exports.name = name;