@klarkxy/dsh-current-title 0.1.4 → 0.1.6

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
@@ -167,24 +167,42 @@ window.__ModuleLoader__.load({
167
167
  var exports = module.exports;
168
168
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
169
169
  let react = require("react");
170
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
170
171
  let react_jsx_runtime = require("react/jsx-runtime");
171
- //#region ../dsh-ai-services/src/client-utils.ts
172
+ //#region ../dsh-plugin-kit/src/client-utils.ts
172
173
  /** The Editor retains its manuscript session separately; native DSH exposes the main-view binding. */
173
174
  function selectedSessionId(client) {
174
- return client.uiWorkspace.current ? client.uiWorkspace.current.getSnapshot()?.sessionId ?? "" : client.uiSession.adapter.current.getSnapshot().key ?? "";
175
+ const workspace = client.uiWorkspace?.current;
176
+ return workspace ? workspace.getSnapshot()?.sessionId ?? "" : client.uiSession.adapter.current.getSnapshot().key ?? "";
177
+ }
178
+ /**
179
+ * Owner props are data the host composed, never a Cordis context: reading an
180
+ * undeclared property off one throws instead of returning undefined, which
181
+ * would crash the slot entry and abdicate its cell for good. Tolerate a
182
+ * throwing read so a mistaken prop degrades to "not supplied".
183
+ */
184
+ function read(source, key) {
185
+ if (source === null || typeof source !== "object") return void 0;
186
+ try {
187
+ return source[key];
188
+ } catch {
189
+ return;
190
+ }
191
+ }
192
+ function readString(source, key) {
193
+ const value = read(source, key);
194
+ return typeof value === "string" ? value : void 0;
175
195
  }
176
196
  /** Owner props win; native settings only supplies close, so resolve its selected session. */
177
197
  function useNativeSeat(client, props) {
178
- const current = (0, react.useSyncExternalStore)((0, react.useCallback)((fn) => (client.uiWorkspace.current ?? client.uiSession.adapter.current).subscribe(fn), [client]), (0, react.useCallback)(() => selectedSessionId(client), [client]), () => "");
198
+ const current = (0, react.useSyncExternalStore)((0, react.useCallback)((fn) => (client.uiWorkspace?.current ?? client.uiSession.adapter.current).subscribe(fn), [client]), (0, react.useCallback)(() => selectedSessionId(client), [client]), () => "");
179
199
  const language = (0, react.useSyncExternalStore)((0, react.useCallback)((fn) => client.locale.subscribe(fn), [client]), (0, react.useCallback)(() => client.locale.getSnapshot().active, [client]), () => "en");
180
- const row = props && typeof props === "object" ? props : {};
181
- const owner = row.owner && typeof row.owner === "object" ? row.owner : {};
182
- const sessionId = typeof row.sessionId === "string" ? row.sessionId : typeof owner.sessionId === "string" ? owner.sessionId : current;
183
- const requestedLocale = row.locale ?? owner.locale ?? language;
200
+ const sessionId = readString(props, "sessionId") ?? readString(read(props, "owner"), "sessionId") ?? current;
201
+ const requestedLocale = read(props, "locale") ?? read(read(props, "owner"), "locale") ?? language;
184
202
  return {
185
203
  sessionId,
186
204
  locale: String(requestedLocale).startsWith("zh") ? "zh" : "en",
187
- hidden: row.hidden === true || owner.hidden === true
205
+ hidden: read(props, "hidden") === true || read(read(props, "owner"), "hidden") === true
188
206
  };
189
207
  }
190
208
  /** Refresh on native log boundaries/reconnect/focus. Read-only settling is bounded to an observed job. */
@@ -240,6 +258,378 @@ function useFeatureRefresh(client, sessionId, refresh, working = false, enabled
240
258
  enabled
241
259
  ]);
242
260
  }
261
+ Object.values({
262
+ /** Surfaces, from the page up to the topmost nested card. */
263
+ surface: [
264
+ "--dsw-alias-bg-base",
265
+ "--dsw-alias-bg-layer-1",
266
+ "--dsw-alias-bg-layer-2",
267
+ "--dsw-alias-bg-layer-3",
268
+ "--dsw-alias-bg-layer-4",
269
+ "--dsw-alias-bg-module-platform",
270
+ "--dsw-alias-bg-overlay"
271
+ ],
272
+ /** Hairlines. l1 is near-invisible on a menu surface, so menus use l2+. */
273
+ border: [
274
+ "--dsw-alias-border-l1",
275
+ "--dsw-alias-border-l2",
276
+ "--dsw-alias-border-l3",
277
+ "--dsw-alias-border-l4"
278
+ ],
279
+ /** Text. `caption` is menu shortcut text, `dimmed` is placeholder text. */
280
+ label: [
281
+ "--dsw-alias-label-primary",
282
+ "--dsw-alias-label-secondary",
283
+ "--dsw-alias-label-tertiary",
284
+ "--dsw-alias-label-caption",
285
+ "--dsw-alias-label-dimmed",
286
+ "--dsw-alias-label-error",
287
+ "--dsw-alias-label-primary-foreground"
288
+ ],
289
+ /** Interactive fills. The `-danger` hover belongs to destructive rows. */
290
+ interactive: [
291
+ "--dsw-alias-interactive-bg-hover",
292
+ "--dsw-alias-interactive-bg-active",
293
+ "--dsw-alias-interactive-bg-hover-danger"
294
+ ],
295
+ /** Accent and status. `business` is the focus ring's default colour. */
296
+ accent: [
297
+ "--dsw-alias-brand-primary",
298
+ "--dsw-alias-button-primary-fill",
299
+ "--dsw-alias-button-primary-hover",
300
+ "--dsw-alias-button-tool-bar-fill",
301
+ "--dsw-alias-button-tool-bar-hover",
302
+ "--dsw-alias-button-ghost-active-fill",
303
+ "--dsw-alias-button-ghost-active-border"
304
+ ],
305
+ state: [
306
+ "--dsw-alias-state-business-primary",
307
+ "--dsw-alias-state-success-primary",
308
+ "--dsw-alias-state-warn-primary",
309
+ "--dsw-alias-state-error-primary",
310
+ "--dsw-alias-state-idle-primary"
311
+ ],
312
+ /** Corner scale: sm 8, md 12, lg the large card, panel the modal. */
313
+ radius: [
314
+ "--dsw-radius-sm",
315
+ "--dsw-radius-md",
316
+ "--dsw-radius-lg",
317
+ "--dsw-radius-panel"
318
+ ],
319
+ /** An elevated surface draws its edge in the shadow, so it keeps border: 0. */
320
+ elevation: [
321
+ "--dsw-elevation-soft",
322
+ "--dsw-elevation-stroke",
323
+ "--dsw-elevation-stroke-color",
324
+ "--dsw-elevation-panel",
325
+ "--dsw-elevation-prominent"
326
+ ],
327
+ /** Floats, masks and scrollbars. */
328
+ overlay: [
329
+ "--dsw-menu-surface-fill",
330
+ "--dsw-menu-backdrop-filter",
331
+ "--dsw-mask-blur",
332
+ "--dsw-alias-bg-mask-1",
333
+ "--dsw-alias-menu-icon",
334
+ "--dsw-alias-scrollbar-bg-l2",
335
+ "--dsw-alias-scrollbar-hover-l2",
336
+ "--dsw-shadow-lv3"
337
+ ],
338
+ /** Motion, focus and type axis, published by the host rather than by a component. */
339
+ system: [
340
+ "--dsw-focus-ring-width",
341
+ "--dsw-focus-ring-color",
342
+ "--dsw-font-markdown-code-font-family",
343
+ "--ds-transition-duration",
344
+ "--ds-ease-in-out",
345
+ "--ds-font-family-code",
346
+ "--dsh-scrollbar-thumb",
347
+ "--dsh-scrollbar-thumb-hover",
348
+ "--dsh-content-font-size-secondary",
349
+ "--dsh-content-font-delta",
350
+ "--dsh-frame-top-clearance"
351
+ ]
352
+ }).flat();
353
+ /**
354
+ * The host's focus ring shorthand, verbatim. Writing this per control is how
355
+ * rings drift apart; a stylesheet that draws its own focus style should use
356
+ * {@link officialFocus} instead. The colour fallback is the host's own default.
357
+ */
358
+ const OFFICIAL_FOCUS_RING = "var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary))";
359
+ /**
360
+ * A complete `outline` declaration at the official geometry. `offset` defaults
361
+ * to the 2px the native controls use; the host's inline field rules sit at 1px
362
+ * and a segmented tab pulls the ring inside itself at -2px.
363
+ */
364
+ function officialFocus(offset = "2px") {
365
+ return `outline: ${OFFICIAL_FOCUS_RING}; outline-offset: ${offset}`;
366
+ }
367
+ /**
368
+ * An elevated surface. The host draws a raised surface's edge inside its
369
+ * shadow rather than as a border, so a card sets `border: 0` and takes the edge
370
+ * from the elevation — which is also why its border stays off the alias-border
371
+ * set that a flat, non-elevated rule would use.
372
+ */
373
+ function officialElevation(level) {
374
+ return `border: 0; box-shadow: var(--dsw-elevation-${level})`;
375
+ }
376
+ /**
377
+ * The shared stylesheet for a plugin UI, scoped under that plugin's root class.
378
+ *
379
+ * Scoping is what keeps this safe to always-bundle into every client: the rules
380
+ * can only match inside the plugin's own subtree, so two plugins can both mount
381
+ * `dsh-ui-card` without either styling the other's markup.
382
+ *
383
+ * Each recipe matches the root element *and* its descendants, because the
384
+ * commonest arrangement is one element that is both — a surface that carries
385
+ * the root class and `dsh-ui-card`, or a portaled dialog that is the root and
386
+ * the `dsh-ui-surface`. A descendant-only scope would silently leave those
387
+ * unstyled, with no error anywhere.
388
+ *
389
+ * A caveat worth knowing: `Modal` and `Menu` portal to `document.body`, so their
390
+ * content leaves the plugin's subtree entirely. A portaled surface therefore
391
+ * needs its own root class on an element inside the portal.
392
+ *
393
+ * Only patterns that primitives do not provide live here. If a control appears
394
+ * below that `Button`/`Input`/`Switch`/`Tag`/`Menu` already covers, that is a
395
+ * bug in this file: use the primitive and delete the recipe.
396
+ */
397
+ function officialUiCss(roots) {
398
+ const scope = (typeof roots === "string" ? [roots] : roots).map((root) => `.${root}`).join(", ");
399
+ return `
400
+ /* Official DSH Web UI contract. Generated by @klarkxy/dsh-plugin-kit/official-ui.
401
+ * Do not hand-edit the copy inside a plugin: change this module so every plugin
402
+ * moves together. Scope: ${scope} */
403
+ /* The scope rule below stays open for the whole sheet: every rule after it
404
+ * nests with the parent selector, so one stylesheet can cover several unrelated
405
+ * roots without leaking into a neighbouring plugin. Its closing brace is last. */
406
+ ${scope} {
407
+ color: var(--dsw-alias-label-primary);
408
+ font-family: inherit;
409
+ font-size: 14px;
410
+ line-height: 22px;
411
+ min-width: 0;
412
+
413
+ & *, & *::before, & *::after { box-sizing: border-box; }
414
+ & h1, & h2, & h3, & h4, & p, & figure { margin: 0; }
415
+ /* Keyboard focus only: pointer input keeps the native resting appearance. The
416
+ * ring needs no radius of its own — the outline already follows the control's
417
+ * own border-radius, and overriding it here would flatten every control. */
418
+ & :focus-visible { ${officialFocus()}; }
419
+ & [hidden] { display: none !important; }
420
+
421
+ /* --- Type tiers. The same 14/22 and 13/20 steps the primitives use. --- */
422
+ & .dsh-ui-title { font-size: 16px; line-height: 24px; font-weight: 500; color: var(--dsw-alias-label-primary); }
423
+ & .dsh-ui-heading { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
424
+ & .dsh-ui-compact { font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-primary); }
425
+ & .dsh-ui-meta { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }
426
+ & .dsh-ui-hint { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
427
+ & .dsh-ui-error { font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-error-primary); }
428
+ & .dsh-ui-warn { color: var(--dsw-alias-state-warn-primary); }
429
+ & .dsh-ui-success { color: var(--dsw-alias-state-success-primary); }
430
+ & .dsh-ui-muted { color: var(--dsw-alias-label-tertiary); }
431
+ & .dsh-ui-truncate { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
432
+ & .dsh-ui-wrap { min-width: 0; overflow-wrap: anywhere; }
433
+
434
+ /* --- Layout scaffolding. --- */
435
+ & .dsh-ui-stack { display: flex; flex-direction: column; gap: 12px; }
436
+ & .dsh-ui-stack-lg { display: flex; flex-direction: column; gap: 20px; }
437
+ & .dsh-ui-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
438
+ & .dsh-ui-row-wrap { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
439
+ & .dsh-ui-spacer { flex: 1; min-width: 0; }
440
+ /* A 0.5px l2 rule is the host's section divider; reserve the same rhythm. */
441
+ & .dsh-ui-divider { height: 0.5px; background: var(--dsw-alias-border-l2); }
442
+ & .dsh-ui-section { display: flex; flex-direction: column; gap: 12px; padding-top: 16px; border-top: 0.5px solid var(--dsw-alias-border-l2); }
443
+
444
+ /* --- Card: a raised surface whose edge lives in the shadow. --- */
445
+ & .dsh-ui-card {
446
+ ${officialElevation("stroke")}
447
+ display: flex;
448
+ flex-direction: column;
449
+ gap: 12px;
450
+ padding: 16px;
451
+ border-radius: var(--dsw-radius-lg);
452
+ background: var(--dsw-alias-bg-layer-1);
453
+ color: var(--dsw-alias-label-primary);
454
+ min-width: 0;
455
+ }
456
+ & .dsh-ui-card--flat { border: 0.5px solid var(--dsw-alias-border-l3); box-shadow: none; }
457
+ & .dsh-ui-card--nested { background: var(--dsw-alias-bg-layer-2); }
458
+ & .dsh-ui-card--plain { background: none; }
459
+
460
+ /* --- Field: label, control, help, error. The control itself is an
461
+ * official primitive, so this only owns the vertical rhythm. --- */
462
+ & .dsh-ui-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
463
+ & .dsh-ui-field + .dsh-ui-field { border-top: 0.5px solid var(--dsw-alias-border-l2); padding-top: 12px; }
464
+ & .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); }
465
+ & .dsh-ui-label-row { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
466
+ & .dsh-ui-help { font-size: 12px; line-height: 1.6; color: var(--dsw-alias-label-secondary); }
467
+ & .dsh-ui-help p { margin: 0; }
468
+ & .dsh-ui-help p + p { margin-top: 8px; }
469
+ & .dsh-ui-required { color: var(--dsw-alias-state-error-primary); }
470
+ & .dsh-ui-control { width: 100%; min-width: 0; }
471
+ /* An invalid control is marked on the control, not only in the message. */
472
+ & .dsh-ui-control[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
473
+ & .dsh-ui-readonly { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
474
+ & .dsh-ui-readonly > dt { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
475
+ & .dsh-ui-readonly > dd { margin: 0; font-size: 13px; line-height: 20px; overflow-wrap: anywhere; }
476
+
477
+ /* --- Buttons row. A no-op container: it exists so actions line up on the
478
+ * trailing edge the way every native panel does. --- */
479
+ & .dsh-ui-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-top: 4px; }
480
+ & .dsh-ui-actions-end { justify-content: flex-end; }
481
+
482
+ /* --- Banners. A banner is a left-rail notice, the shape used for warnings,
483
+ * deprecations and blocking instructions. --- */
484
+ & .dsh-ui-banner {
485
+ display: flex;
486
+ flex-direction: column;
487
+ gap: 8px;
488
+ padding: 10px 14px;
489
+ border: 0.5px solid var(--dsw-alias-border-l3);
490
+ border-left: 3px solid var(--dsw-alias-state-warn-primary);
491
+ border-radius: var(--dsw-radius-md);
492
+ background: var(--dsw-alias-bg-layer-1);
493
+ font-size: 13px;
494
+ line-height: 20px;
495
+ color: var(--dsw-alias-label-secondary);
496
+ }
497
+ & .dsh-ui-banner-title { font-weight: 500; color: var(--dsw-alias-state-warn-primary); }
498
+ & .dsh-ui-banner--info { border-left-color: var(--dsw-alias-state-business-primary); }
499
+ & .dsh-ui-banner--danger { border-left-color: var(--dsw-alias-state-error-primary); }
500
+ & .dsh-ui-banner--success { border-left-color: var(--dsw-alias-state-success-primary); }
501
+ & .dsh-ui-banner p { margin: 0; }
502
+
503
+ /* --- Inline notice: a transient confirmation with no chrome of its own. --- */
504
+ & .dsh-ui-notice { display: flex; align-items: center; gap: 6px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-success-primary); }
505
+ & .dsh-ui-notice--error { color: var(--dsw-alias-state-error-primary); }
506
+
507
+ /* --- Empty and loading states. Both sit in a dashed well so an empty panel
508
+ * reads as "nothing here yet" rather than as a broken layout. --- */
509
+ & .dsh-ui-empty {
510
+ display: flex;
511
+ flex-direction: column;
512
+ align-items: flex-start;
513
+ gap: 6px;
514
+ padding: 20px 16px;
515
+ border: 0.5px dashed var(--dsw-alias-border-l3);
516
+ border-radius: var(--dsw-radius-md);
517
+ font-size: 13px;
518
+ line-height: 20px;
519
+ color: var(--dsw-alias-label-secondary);
520
+ }
521
+ & .dsh-ui-loading { display: flex; align-items: center; gap: 8px; padding: 20px 0; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }
522
+
523
+ /* --- Selectable list. A row is a button-sized target with a hover fill and
524
+ * a selected fill; the name carries the weight, the description does not. --- */
525
+ & .dsh-ui-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
526
+ & .dsh-ui-list-scroll { max-height: min(66vh, 760px); overflow-y: auto; overflow-x: hidden; }
527
+ & .dsh-ui-list-row { border-radius: var(--dsw-radius-md); }
528
+ & .dsh-ui-list-row:hover { background: var(--dsw-alias-interactive-bg-hover); }
529
+ & .dsh-ui-list-row[aria-selected='true'], & .dsh-ui-list-row[data-selected] { background: var(--dsw-alias-interactive-bg-active); }
530
+ & .dsh-ui-list-item {
531
+ display: flex;
532
+ flex-direction: column;
533
+ align-items: flex-start;
534
+ gap: 4px;
535
+ width: 100%;
536
+ min-width: 0;
537
+ padding: 10px 12px;
538
+ border: 0;
539
+ border-radius: var(--dsw-radius-md);
540
+ background: none;
541
+ font: inherit;
542
+ color: inherit;
543
+ text-align: start;
544
+ cursor: pointer;
545
+ }
546
+ & .dsh-ui-list-item:disabled { cursor: default; }
547
+ & .dsh-ui-list-item:disabled:hover { background: none; }
548
+ & .dsh-ui-list-name { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); overflow-wrap: anywhere; }
549
+ & .dsh-ui-list-desc { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-tertiary); }
550
+
551
+ /* --- Code and structured output. Wraps rather than scrolls, because a
552
+ * feature panel is read rather than scanned. --- */
553
+ & .dsh-ui-code {
554
+ padding: 10px 12px;
555
+ border: 0.5px solid var(--dsw-alias-border-l3);
556
+ border-radius: var(--dsw-radius-md);
557
+ background: var(--dsw-alias-bg-layer-1);
558
+ font-family: var(--ds-font-family-code, ui-monospace, 'Cascadia Mono', Consolas, monospace);
559
+ font-size: 12px;
560
+ line-height: 20px;
561
+ color: var(--dsw-alias-label-secondary);
562
+ overflow-wrap: anywhere;
563
+ white-space: pre-wrap;
564
+ }
565
+ & .dsh-ui-scroll { max-height: 60vh; overflow: auto; overscroll-behavior: contain; }
566
+
567
+ /* --- A toggle row: label on the left, switch on the trailing edge. This is
568
+ * layout only; the switch itself is the official primitive. --- */
569
+ & .dsh-ui-toggle-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-width: 0; padding: 12px 0; }
570
+ & .dsh-ui-toggle-row + .dsh-ui-toggle-row { border-top: 0.5px solid var(--dsw-alias-border-l2); }
571
+ & .dsh-ui-toggle-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
572
+ & .dsh-ui-toggle-label { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
573
+
574
+ /* --- Floating surface: a panel anchored to a trigger. This is the material
575
+ * a menu card is made of — the translucent fill plus its backdrop blur, with
576
+ * the edge riding inside the prominent elevation. A plugin that hand-rolls this
577
+ * is how tokens like --dsw-specific-menu get invented; use this instead. --- */
578
+ & .dsh-ui-surface {
579
+ --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
580
+ --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
581
+ --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
582
+ position: relative;
583
+ display: flex;
584
+ flex-direction: column;
585
+ border: 0;
586
+ border-radius: var(--dsw-radius-lg);
587
+ background: var(--dsw-menu-surface-fill);
588
+ backdrop-filter: var(--dsw-menu-backdrop-filter);
589
+ box-shadow: var(--dsw-elevation-prominent);
590
+ color: var(--dsw-alias-label-primary);
591
+ font-size: 13px;
592
+ line-height: 20px;
593
+ min-width: 0;
594
+ }
595
+ & .dsh-ui-surface-body { display: flex; flex-direction: column; gap: 8px; padding: 12px 14px 14px; overflow-y: auto; }
596
+ & .dsh-ui-surface-head { display: flex; align-items: center; gap: 8px; min-width: 0; }
597
+ & .dsh-ui-surface-title { font-size: 14px; line-height: 22px; font-weight: 500; color: var(--dsw-alias-label-primary); }
598
+
599
+ /* --- Native select. The primitives ship no select, so a plugin that offers a
600
+ * closed list of choices keeps the platform control and matches the official
601
+ * settings field geometry exactly (34px, 0.5px l4, radius-md, layer-3). A
602
+ * SegmentedControl is the primitive to reach for when the choices are few
603
+ * enough to show at once. --- */
604
+ & .dsh-ui-select {
605
+ box-sizing: border-box;
606
+ width: 100%;
607
+ min-width: 0;
608
+ height: 34px;
609
+ padding: 0 12px;
610
+ border: 0.5px solid var(--dsw-alias-border-l4);
611
+ border-radius: var(--dsw-radius-md);
612
+ background: var(--dsw-alias-bg-layer-3);
613
+ font: inherit;
614
+ font-size: 13px;
615
+ line-height: 1.5;
616
+ color: var(--dsw-alias-label-primary);
617
+ cursor: pointer;
618
+ }
619
+ & .dsh-ui-select:focus-visible { outline: none; border-color: var(--dsw-alias-state-business-primary); }
620
+ & .dsh-ui-select:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
621
+ & .dsh-ui-select[aria-invalid='true'] { border-color: var(--dsw-alias-state-error-primary); }
622
+
623
+ /* Long panels scroll inside themselves instead of growing the host page. */
624
+ & .dsh-ui-panel { display: flex; flex-direction: column; gap: 16px; min-width: 0; }
625
+ & .dsh-ui-panel-wide { max-width: 100%; }
626
+
627
+ @media (prefers-reduced-motion: reduce) {
628
+ & *, & *::before, & *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
629
+ }
630
+ }
631
+ `.replace(/^& (\.dsh-ui-[a-z0-9-]+)(?![+>~])/gm, (_match, className) => `&${className}, & ${className}`);
632
+ }
243
633
  //#endregion
244
634
  //#region src/contracts.ts
245
635
  const RPC_CHANNEL = "/dsh-current-title";
@@ -248,26 +638,58 @@ const TITLE_CLIENT_SERVICE = "dshCurrentTitleClient";
248
638
  //#region src/client-work.ts
249
639
  const titleCopy = {
250
640
  zh: {
251
- label: "当前标题",
641
+ label: "自动标题",
252
642
  pinned: "已手动命名",
253
643
  generating: "正在生成标题…",
254
644
  regenerate: "重新生成",
255
645
  locale: "类型标签语言",
256
646
  auto: "自动",
647
+ prompt: "标题提示词",
648
+ promptHint: "留空用内置说明,仍返回类型和摘要;离开输入框即保存。",
649
+ loading: "正在读取标题设置…",
650
+ model: "标题模型",
651
+ modelDefault: "跟随会话模型",
652
+ effort: "思考强度",
653
+ effortDefault: "默认",
654
+ cadence: "更新节奏",
655
+ cadenceAll: "跟随当前任务(每条消息)",
656
+ cadenceFirst: "仅首条消息",
257
657
  saved: "已保存。",
258
- failed: "无法读取标题设置。"
658
+ failed: "无法读取标题设置。",
659
+ inactive: "标题服务未接管,新消息暂时不会自动更新。"
259
660
  },
260
661
  en: {
261
- label: "Current title",
662
+ label: "Auto Title",
262
663
  pinned: "Renamed manually",
263
664
  generating: "Generating title…",
264
665
  regenerate: "Regenerate",
265
666
  locale: "Type-label language",
266
667
  auto: "Auto",
668
+ prompt: "Title prompt",
669
+ promptHint: "Empty uses the built-in instruction; a type and summary are still returned. Saves on blur.",
670
+ loading: "Loading title settings…",
671
+ model: "Title model",
672
+ modelDefault: "Use the session model",
673
+ effort: "Reasoning effort",
674
+ effortDefault: "Default",
675
+ cadence: "Update cadence",
676
+ cadenceAll: "Track the current task (every message)",
677
+ cadenceFirst: "First message only",
267
678
  saved: "Saved.",
268
- failed: "Could not read title settings."
679
+ failed: "Could not read title settings.",
680
+ inactive: "Title generation is not active, so new messages will not rename this session."
269
681
  }
270
682
  };
683
+ /** `undefined` draft = not edited; an empty string is a real edit (use built-in instruction). */
684
+ function promptFieldValue(draft, saved) {
685
+ return draft ?? saved;
686
+ }
687
+ /** The prompt to persist on blur, or `undefined` when nothing changed. */
688
+ function promptToSave(draft, saved) {
689
+ if (draft === void 0) return void 0;
690
+ const next = draft.trim();
691
+ return next === saved ? void 0 : next;
692
+ }
271
693
  function shouldSkipTitleRefresh(input) {
272
694
  return input.busy;
273
695
  }
@@ -348,7 +770,9 @@ async function runTitleSettingsSave(input) {
348
770
  input.onBusy(true);
349
771
  try {
350
772
  const settings = await invoke(input.call, "settings", {
351
- locale: input.locale,
773
+ ...input.prompt !== void 0 ? { prompt: input.prompt } : {},
774
+ ...input.model !== void 0 ? { model: input.model } : {},
775
+ ...input.cadence !== void 0 ? { cadence: input.cadence } : {},
352
776
  expectedRevision: input.expectedRevision
353
777
  });
354
778
  if (!input.isCurrent()) return;
@@ -380,19 +804,65 @@ const name = "dsh-current-title-client";
380
804
  const inject = [
381
805
  "slots",
382
806
  "connection",
807
+ "remote",
808
+ "remote.session",
383
809
  "sessions",
384
810
  "locale",
385
811
  "uiWorkspace",
386
812
  "uiSession"
387
813
  ];
814
+ function catalogChoices(value, selected) {
815
+ const envelope = value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
816
+ const root = (envelope?.ok === true && envelope.value && typeof envelope.value === "object" && !Array.isArray(envelope.value) ? envelope.value : void 0) ?? envelope ?? {};
817
+ const groups = Array.isArray(root.groups) ? root.groups : [];
818
+ const choices = [];
819
+ for (const group of groups) {
820
+ if (!group || typeof group !== "object") continue;
821
+ const row = group;
822
+ const provider = typeof row.id === "string" ? row.id : "";
823
+ const providerName = typeof row.name === "string" && row.name ? row.name : provider;
824
+ if (!provider || !Array.isArray(row.models)) continue;
825
+ for (const model of row.models) {
826
+ if (!model || typeof model !== "object") continue;
827
+ const item = model;
828
+ const id = typeof item.id === "string" ? item.id : "";
829
+ if (!id) continue;
830
+ const name = typeof item.name === "string" && item.name ? item.name : id;
831
+ const reasoning = item.reasoning && typeof item.reasoning === "object" ? item.reasoning : void 0;
832
+ const efforts = Array.isArray(reasoning?.efforts) ? reasoning.efforts.flatMap((effort) => {
833
+ if (!effort || typeof effort !== "object") return [];
834
+ const entry = effort;
835
+ return typeof entry.id === "string" && entry.id ? [{
836
+ id: entry.id,
837
+ name: typeof entry.name === "string" && entry.name ? entry.name : entry.id
838
+ }] : [];
839
+ }) : [];
840
+ choices.push({
841
+ provider,
842
+ model: id,
843
+ label: `${providerName} / ${name}`,
844
+ efforts
845
+ });
846
+ }
847
+ }
848
+ if (selected.provider && selected.model && !choices.some((item) => item.provider === selected.provider && item.model === selected.model)) choices.push({
849
+ provider: selected.provider,
850
+ model: selected.model,
851
+ label: `${selected.provider} / ${selected.model}`,
852
+ efforts: []
853
+ });
854
+ return choices;
855
+ }
388
856
  function TitleSettings(props) {
389
- const seat = useNativeSeat(props.client, props.owner);
857
+ const seat = useNativeSeat(props.client);
390
858
  const sessionId = seat.sessionId;
391
859
  const text = titleCopy[seat.locale];
392
860
  const [status, setStatus] = (0, react.useState)();
393
861
  const [busy, setBusy] = (0, react.useState)(false);
394
862
  const [note, setNote] = (0, react.useState)("");
395
863
  const [error, setError] = (0, react.useState)("");
864
+ const [prompt, setPrompt] = (0, react.useState)(void 0);
865
+ const [choices, setChoices] = (0, react.useState)([]);
396
866
  const workRef = (0, react.useRef)(void 0);
397
867
  if (!workRef.current) workRef.current = createTitleClientWork();
398
868
  const work = workRef.current;
@@ -405,6 +875,26 @@ function TitleSettings(props) {
405
875
  (0, react.useEffect)(() => () => {
406
876
  work.dispose();
407
877
  }, [work]);
878
+ (0, react.useEffect)(() => {
879
+ const catalog = props.client.remote?.session?.modelCatalog;
880
+ if (!catalog) return void 0;
881
+ let live = true;
882
+ catalog().then((value) => {
883
+ if (live) setChoices(catalogChoices(value, status?.settings.model ?? {
884
+ provider: "",
885
+ model: ""
886
+ }));
887
+ }).catch(() => {
888
+ if (live) setChoices([]);
889
+ });
890
+ return () => {
891
+ live = false;
892
+ };
893
+ }, [
894
+ props.client,
895
+ status?.settings.model.provider,
896
+ status?.settings.model.model
897
+ ]);
408
898
  (0, react.useEffect)(() => {
409
899
  const generation = work.beginLoad();
410
900
  const isCurrent = () => work.isLoad(generation);
@@ -456,113 +946,279 @@ function TitleSettings(props) {
456
946
  work
457
947
  ]);
458
948
  useFeatureRefresh(props.client, sessionId, refreshStatus, status?.session?.generating === true && !busy, Boolean(sessionId));
459
- const locale = status?.settings.locale ?? "auto";
460
- const modes = [
461
- {
462
- id: "auto",
463
- label: text.auto
464
- },
465
- {
466
- id: "zh",
467
- label: "中文"
468
- },
469
- {
470
- id: "en",
471
- label: "English"
472
- }
473
- ];
949
+ const savedPrompt = status?.settings.prompt ?? "";
950
+ const savedModel = status?.settings.model ?? {
951
+ provider: "",
952
+ model: ""
953
+ };
954
+ const modelKey = savedModel.provider && savedModel.model ? `${savedModel.provider}\u001f${savedModel.model}` : "";
474
955
  const title = status?.session?.title;
475
956
  const pinned = status?.session?.pinned === true;
476
957
  const weOwn = status?.support.weOwn === true;
477
958
  const canRegenerate = Boolean(sessionId) && weOwn && !busy;
959
+ const selectedChoice = choices.find((item) => item.provider === savedModel.provider && item.model === savedModel.model);
960
+ const efforts = selectedChoice ? [...selectedChoice.efforts] : [];
961
+ if (selectedChoice && savedModel.reasoningEffort && !efforts.some((item) => item.id === savedModel.reasoningEffort)) efforts.push({
962
+ id: savedModel.reasoningEffort,
963
+ name: savedModel.reasoningEffort
964
+ });
965
+ function saveSettings(patch) {
966
+ if (!status) return;
967
+ const token = work.beginRequest();
968
+ setError("");
969
+ setNote("");
970
+ runTitleSettingsSave({
971
+ call,
972
+ ...patch,
973
+ expectedRevision: status.settings.revision,
974
+ isCurrent: () => work.isRequest(token),
975
+ savedMessage: text.saved,
976
+ failedMessage: text.failed,
977
+ onBusy: setBusy,
978
+ onSettings: (settings) => setStatus((current) => current ? {
979
+ ...current,
980
+ settings
981
+ } : current),
982
+ onNote: setNote,
983
+ onError: setError
984
+ });
985
+ }
478
986
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
479
- className: "current-title-settings",
987
+ className: "current-title-settings ct-root dsh-ui-panel",
480
988
  "data-testid": "current-title-settings",
481
989
  children: [
482
990
  !status && !error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
483
991
  role: "status",
484
- children: seat.locale === "en" ? "Loading title settings…" : "正在读取标题设置…"
992
+ className: "dsh-ui-hint",
993
+ children: text.loading
485
994
  }) : null,
486
995
  title ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
487
996
  translate: "no",
997
+ className: "dsh-ui-title dsh-ui-wrap",
488
998
  children: title
489
999
  }) : null,
490
1000
  pinned ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
491
- className: "current-title-meta",
1001
+ className: "dsh-ui-meta",
492
1002
  children: text.pinned
493
1003
  }) : null,
494
1004
  status?.session?.generating ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
495
1005
  role: "status",
1006
+ className: "dsh-ui-hint",
496
1007
  children: text.generating
497
1008
  }) : null,
1009
+ status && !weOwn ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1010
+ role: "status",
1011
+ className: "dsh-ui-hint",
1012
+ children: [text.inactive, status.support.limitation ? ` ${status.support.limitation}` : ""]
1013
+ }) : null,
498
1014
  error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
499
1015
  role: "alert",
1016
+ className: "dsh-ui-error dsh-ui-wrap",
500
1017
  children: error
501
1018
  }) : null,
502
1019
  note ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
503
1020
  role: "status",
1021
+ className: "dsh-ui-notice",
504
1022
  children: note
505
1023
  }) : null,
506
- sessionId ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
507
- type: "button",
508
- disabled: !canRegenerate,
509
- "aria-label": text.regenerate,
510
- onClick: () => {
511
- if (!sessionId) return;
512
- const token = work.beginRequest();
513
- setError("");
514
- setNote("");
515
- runTitleRegenerateFlow({
516
- call,
517
- sessionId,
518
- isCurrent: () => work.isRequest(token),
519
- failedMessage: text.failed,
520
- onBusy: setBusy,
521
- onStatus: setStatus,
522
- onError: setError,
523
- onMarker: commitMarker
524
- });
525
- },
526
- children: text.regenerate
527
- }) : null,
528
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("fieldset", {
529
- disabled: busy || !status,
530
- "aria-label": text.locale,
531
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("legend", { children: text.locale }), modes.map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
532
- type: "radio",
533
- name: "current-title-locale",
534
- value: mode.id,
535
- checked: locale === mode.id,
536
- onChange: () => {
537
- if (!status) return;
1024
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1025
+ className: "dsh-ui-actions",
1026
+ children: sessionId ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1027
+ variant: "outline",
1028
+ size: "md",
1029
+ disabled: !canRegenerate,
1030
+ onClick: () => {
1031
+ if (!sessionId) return;
538
1032
  const token = work.beginRequest();
539
1033
  setError("");
540
1034
  setNote("");
541
- runTitleSettingsSave({
1035
+ runTitleRegenerateFlow({
542
1036
  call,
543
- locale: mode.id,
544
- expectedRevision: status.settings.revision,
1037
+ sessionId,
545
1038
  isCurrent: () => work.isRequest(token),
546
- savedMessage: text.saved,
547
1039
  failedMessage: text.failed,
548
1040
  onBusy: setBusy,
549
- onSettings: (settings) => setStatus({
550
- ...status,
551
- settings
552
- }),
553
- onNote: setNote,
554
- onError: setError
1041
+ onStatus: setStatus,
1042
+ onError: setError,
1043
+ onMarker: commitMarker
555
1044
  });
556
- }
557
- }), mode.label] }, mode.id))]
1045
+ },
1046
+ children: text.regenerate
1047
+ }) : null
1048
+ }),
1049
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1050
+ className: "dsh-ui-field",
1051
+ children: [
1052
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1053
+ className: "dsh-ui-label",
1054
+ children: text.prompt
1055
+ }),
1056
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1057
+ className: "ct-prompt",
1058
+ value: status ? promptFieldValue(prompt, savedPrompt) : "",
1059
+ maxLength: 4e3,
1060
+ rows: 5,
1061
+ disabled: busy || !status,
1062
+ onChange: (event) => {
1063
+ setPrompt(event.target.value);
1064
+ setNote("");
1065
+ },
1066
+ onBlur: () => {
1067
+ if (!status) return;
1068
+ const next = promptToSave(prompt, savedPrompt);
1069
+ if (next === void 0) {
1070
+ setPrompt(void 0);
1071
+ return;
1072
+ }
1073
+ const token = work.beginRequest();
1074
+ setError("");
1075
+ runTitleSettingsSave({
1076
+ call,
1077
+ prompt: next,
1078
+ expectedRevision: status.settings.revision,
1079
+ isCurrent: () => work.isRequest(token),
1080
+ savedMessage: text.saved,
1081
+ failedMessage: text.failed,
1082
+ onBusy: setBusy,
1083
+ onSettings: (settings) => {
1084
+ setStatus((current) => current ? {
1085
+ ...current,
1086
+ settings
1087
+ } : current);
1088
+ setPrompt(void 0);
1089
+ },
1090
+ onNote: setNote,
1091
+ onError: setError
1092
+ });
1093
+ }
1094
+ }),
1095
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1096
+ className: "dsh-ui-help",
1097
+ children: text.promptHint
1098
+ })
1099
+ ]
1100
+ }),
1101
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1102
+ className: "dsh-ui-field",
1103
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1104
+ className: "dsh-ui-label",
1105
+ children: text.model
1106
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1107
+ className: "dsh-ui-select",
1108
+ value: modelKey,
1109
+ disabled: busy || !status,
1110
+ onChange: (event) => {
1111
+ const value = event.target.value;
1112
+ const choice = choices.find((item) => `${item.provider}\u001f${item.model}` === value);
1113
+ saveSettings({ model: choice ? {
1114
+ provider: choice.provider,
1115
+ model: choice.model
1116
+ } : {
1117
+ provider: "",
1118
+ model: ""
1119
+ } });
1120
+ },
1121
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1122
+ value: "",
1123
+ children: text.modelDefault
1124
+ }), choices.map((choice) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1125
+ value: `${choice.provider}\u001f${choice.model}`,
1126
+ children: choice.label
1127
+ }, `${choice.provider}\u001f${choice.model}`))]
1128
+ })]
1129
+ }),
1130
+ selectedChoice && efforts.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1131
+ className: "dsh-ui-field",
1132
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1133
+ className: "dsh-ui-label",
1134
+ children: text.effort
1135
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1136
+ className: "dsh-ui-select",
1137
+ value: savedModel.reasoningEffort ?? "",
1138
+ disabled: busy || !status,
1139
+ onChange: (event) => {
1140
+ const effort = event.target.value;
1141
+ const model = {
1142
+ provider: savedModel.provider,
1143
+ model: savedModel.model
1144
+ };
1145
+ saveSettings({ model: effort ? {
1146
+ ...model,
1147
+ reasoningEffort: effort
1148
+ } : model });
1149
+ },
1150
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1151
+ value: "",
1152
+ children: text.effortDefault
1153
+ }), efforts.map((effort) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1154
+ value: effort.id,
1155
+ children: effort.name
1156
+ }, effort.id))]
1157
+ })]
1158
+ }) : null,
1159
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1160
+ className: "dsh-ui-field",
1161
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1162
+ className: "dsh-ui-label",
1163
+ children: text.cadence
1164
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1165
+ className: "dsh-ui-select",
1166
+ value: status?.settings.cadence ?? "all-prompts",
1167
+ disabled: busy || !status,
1168
+ onChange: (event) => {
1169
+ saveSettings({ cadence: event.target.value === "first-prompt" ? "first-prompt" : "all-prompts" });
1170
+ },
1171
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1172
+ value: "all-prompts",
1173
+ children: text.cadenceAll
1174
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1175
+ value: "first-prompt",
1176
+ children: text.cadenceFirst
1177
+ })]
1178
+ })]
558
1179
  })
559
1180
  ]
560
1181
  });
561
1182
  }
1183
+ const styles = `${officialUiCss("ct-root")}
1184
+ .ct-root { max-width: 760px; }
1185
+ .ct-prompt {
1186
+ box-sizing: border-box;
1187
+ width: 100%;
1188
+ min-width: 0;
1189
+ min-height: 96px;
1190
+ resize: vertical;
1191
+ padding: 8px 12px;
1192
+ border: 0.5px solid var(--dsw-alias-border-l4);
1193
+ border-radius: var(--dsw-radius-md);
1194
+ background: var(--dsw-alias-bg-layer-3);
1195
+ font: inherit;
1196
+ font-size: 13px;
1197
+ line-height: 1.5;
1198
+ color: var(--dsw-alias-label-primary);
1199
+ }
1200
+ .ct-prompt:disabled { color: var(--dsw-alias-label-tertiary); cursor: default; }
1201
+ .ct-prompt::placeholder { color: var(--dsw-alias-label-dimmed); }
1202
+ `;
562
1203
  function apply(ctx) {
563
1204
  const client = ctx;
564
1205
  const marker = createTitleClientMarker();
565
1206
  ctx.provide(TITLE_CLIENT_SERVICE, marker);
1207
+ ctx.effect(() => {
1208
+ if (typeof document === "undefined") return () => {};
1209
+ const style = document.createElement("style");
1210
+ style.setAttribute("data-plugin", name);
1211
+ style.textContent = styles;
1212
+ document.head.appendChild(style);
1213
+ return () => style.remove();
1214
+ }, "current-title.styles");
1215
+ ctx.effect(() => client.slots.inject("plugins.bundle.config", () => client.slots.register({
1216
+ name: "plugins.bundle.config",
1217
+ key: "@klarkxy/dsh-current-title"
1218
+ }, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TitleSettings, {
1219
+ client,
1220
+ marker
1221
+ }))), "current-title.settings");
566
1222
  ctx.effect(() => {
567
1223
  let live = true;
568
1224
  client.connection.rpc.call(RPC_CHANNEL, "status", {}).then((result) => {