dsh-composer-predictions 1.2.3 → 1.2.5

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/CHANGELOG.md CHANGED
@@ -6,6 +6,28 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.2.5] - 2026-10-25
10
+
11
+ ### Changed
12
+
13
+ - The **Settings → General** row draws its own switch instead of loading
14
+ `@deepseek-ai/dsh-client-ui-primitives`. A harness-side change to that package
15
+ could previously fail the whole plugin at load; the markup, theme tokens and
16
+ behaviour are unchanged.
17
+ - The README comparison table names the Settings switch.
18
+
19
+
20
+ ## [1.2.4] - 2026-10-25
21
+
22
+ ### Fixed
23
+
24
+ - The ghost is anchored to the composer card that owns the overlay, found
25
+ through the overlay's own DOM ancestry. It previously measured the first
26
+ `[data-composer-card]` in the document, so with more than one composer
27
+ mounted (another panel, a transition, a second window) the suggestion could
28
+ paint over another conversation and Tab would consult the wrong card.
29
+ - Switching Sessions also clears the measured box, not just the suggestion.
30
+
9
31
  ## [1.2.3] - 2026-10-25
10
32
 
11
33
  ### Changed
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  AI autocomplete and ghost-text suggestions for the composer in **DeepSeek
8
8
  Harness desktop and the Web GUI**.
9
9
 
10
- **Current release: 1.2.2** · [Install or update](#install) · [中文说明](#中文说明)
10
+ **Current release: [latest on npm](https://www.npmjs.com/package/dsh-composer-predictions)** · [Install or update](#install) · [中文说明](#中文说明)
11
11
 
12
12
  - **Next message** — after the agent replies, the empty composer shows a
13
13
  streamed suggestion for your likely next message. **Tab** accepts it as an
@@ -40,9 +40,10 @@ for you.
40
40
  | Streamed rendering | ✅ SSE (`/v2`) | not documented | ✅ | ✅ |
41
41
  | Recent tool calls and outcomes in the prompt | ✅ | — | — | — |
42
42
  | Cache | ✅ per turn + per draft | ✅ | ✅ | not documented |
43
+ | Switch off in Settings, no uninstall | ✅ Settings → General | ✅ Settings section | not documented | not documented |
43
44
 
44
45
  "—" means the feature is not mentioned in that package's README as of
45
- 2026-10-24, not that it is impossible. If all you want is an after-turn
46
+ 2026-10-25, not that it is impossible. If all you want is an after-turn
46
47
  suggestion, `dsh-composer-prediction` is a perfectly good, more established
47
48
  choice — this plugin is for people who also want the caret continuation and
48
49
  keyboard-only candidate switching.
@@ -100,12 +101,12 @@ https://raw.githubusercontent.com/Stijnus/dsh-composer-predictions/main/market-i
100
101
  `Tab` 采纳已完成的候选为可编辑草稿,不会自动发送;`Alt+↓/↑` 切换最多三个候选,
101
102
  `Esc` 关闭。设置 → 通用 → 输入预测可开关,设置保存在当前设备的浏览器中。
102
103
 
103
- 安装或更新:**插件 → 安装 → `dsh-composer-predictions@1.2.2`**,然后重启 Harness。
104
- CLI 用户可运行 `dsh plugin --profile web add dsh-composer-predictions@1.2.2`,
104
+ 安装或更新:**插件 → 安装 → `dsh-composer-predictions`**,然后重启 Harness。
105
+ CLI 用户可运行 `dsh plugin --profile web add dsh-composer-predictions`,
105
106
  把 `web` 换成自己的 profile。预测会调用会话模型并消耗 token,虽然不会出现在会话日志和 token 统计中。
106
107
 
107
108
  也可以把上方 [Install](#install) 中的市场索引添加到「插件 → 发现」。
108
- 演示、完整配置与限制见本页;[1.2.2 发布说明](https://github.com/Stijnus/dsh-composer-predictions/releases/tag/v1.2.2)。
109
+ 演示、完整配置与限制见本页;[最新发布说明](https://github.com/Stijnus/dsh-composer-predictions/releases/latest)。
109
110
 
110
111
  ## How it works
111
112
 
package/client.js CHANGED
@@ -20,7 +20,38 @@ window.__ModuleLoader__.load({
20
20
  factory(require) {
21
21
  const React = require('react')
22
22
  const h = React.createElement
23
- const { Switch } = require('@deepseek-ai/dsh-client-ui-primitives')
23
+
24
+ /**
25
+ * Local switch button. The host's own switch is a harness Client package,
26
+ * which a bundle must not load: it can change without notice, and a throw
27
+ * would take the whole plugin down. This reproduces its markup, CSS and
28
+ * behaviour with plugin-owned classes and theme tokens only.
29
+ */
30
+ const SWITCH_CSS = [
31
+ '.cp-switch{box-sizing:border-box;position:relative;flex:0 0 auto;width:36px;height:20px;padding:2px;border:0;border-radius:999px;background:var(--dsw-alias-border-l3);cursor:pointer}',
32
+ '.cp-switch[aria-checked="true"]{background:var(--dsw-alias-brand-primary)}',
33
+ '.cp-switch:disabled{cursor:default;opacity:.5}',
34
+ '.cp-switch:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px}',
35
+ '.cp-switch__thumb{display:block;width:16px;height:16px;border-radius:50%;background:var(--dsw-alias-label-primary-foreground);transition:transform .12s}',
36
+ '.cp-switch[aria-checked="false"] .cp-switch__thumb{background:var(--dsw-alias-switch-thumb)}',
37
+ '.cp-switch[aria-checked="true"] .cp-switch__thumb{transform:translate(16px)}',
38
+ ].join('')
39
+
40
+ function Switch({ checked, label, onChange, disabled }) {
41
+ return h(
42
+ 'button',
43
+ {
44
+ type: 'button',
45
+ role: 'switch',
46
+ 'aria-checked': checked === true ? 'true' : 'false',
47
+ 'aria-label': label,
48
+ className: 'cp-switch',
49
+ disabled: disabled === true,
50
+ onClick: () => onChange(checked !== true),
51
+ },
52
+ h('span', { className: 'cp-switch__thumb', 'aria-hidden': 'true' }),
53
+ )
54
+ }
24
55
 
25
56
  const NS = 'composer-predictions'
26
57
  const ENDPOINT = '/composer-prediction/v2'
@@ -92,6 +123,7 @@ window.__ModuleLoader__.load({
92
123
  function PredictionsSetting({ t }) {
93
124
  const checked = useEnabled()
94
125
  return h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 24, padding: '16px 0', borderBottom: '0.5px solid var(--dsw-alias-border-l2)' } },
126
+ h('style', null, SWITCH_CSS),
95
127
  h('div', null,
96
128
  h('div', { style: { fontSize: 14, lineHeight: '20px' } }, t('enabled')),
97
129
  h('div', { style: { fontSize: 12, lineHeight: '18px', marginTop: 4, color: 'var(--dsw-alias-label-secondary)' } }, t('description')),
@@ -162,10 +194,18 @@ window.__ModuleLoader__.load({
162
194
  opacity: 0.8,
163
195
  }
164
196
 
165
- /** Locate the resident composer card and its editable root. */
166
- function composerElements() {
197
+ /**
198
+ * Locate the composer card this overlay actually belongs to.
199
+ *
200
+ * The seat is rendered inside the resident composer card, so the anchor node
201
+ * is an ancestor-scoped key: a global `querySelector` would pick whichever
202
+ * card happens to be first in the document and paint that conversation's
203
+ * ghost over another one.
204
+ */
205
+ function composerElementsNear(anchor) {
167
206
  if (typeof document === 'undefined') return null
168
- const card = document.querySelector('[data-composer-card]')
207
+ if (anchor === null || anchor === undefined || !anchor.isConnected) return null
208
+ const card = anchor.closest('[data-composer-card]')
169
209
  if (card === null) return null
170
210
  const editor = card.querySelector('[contenteditable="true"]') || card.querySelector('[role="textbox"]')
171
211
  return editor === null ? null : { card, editor }
@@ -364,6 +404,8 @@ window.__ModuleLoader__.load({
364
404
  const [box, setBox] = React.useState(null)
365
405
  const [compositionEnd, setCompositionEnd] = React.useState(0)
366
406
 
407
+ const anchorRef = React.useRef(null)
408
+ const elements = () => composerElementsNear(anchorRef.current)
367
409
  const abortRef = React.useRef(null)
368
410
  const composingRef = React.useRef(false)
369
411
  const dismissedRef = React.useRef(null)
@@ -484,6 +526,7 @@ window.__ModuleLoader__.load({
484
526
  dismissedRef.current = null
485
527
  abortRef.current?.abort()
486
528
  setGhost(NO_GHOST)
529
+ setBox(null)
487
530
  return () => abortRef.current?.abort()
488
531
  }, [sessionId])
489
532
 
@@ -524,7 +567,7 @@ window.__ModuleLoader__.load({
524
567
  if (dismissedRef.current !== null && dismissedRef.current.draft === draft) return undefined
525
568
  const timer = setTimeout(() => {
526
569
  if (composingRef.current) return
527
- const found = composerElements()
570
+ const found = elements()
528
571
  if (found === null) return
529
572
  if (caretAtEnd(found.editor, draft) === null) return
530
573
  void request('continue', draft.slice(-MAX_DRAFT_CHARS))
@@ -564,7 +607,7 @@ window.__ModuleLoader__.load({
564
607
  React.useLayoutEffect(() => {
565
608
  if (ghost.mode !== 'next') return undefined
566
609
  const measure = () => {
567
- const found = composerElements()
610
+ const found = elements()
568
611
  if (found === null) {
569
612
  setBox((previous) => (previous === null ? previous : null))
570
613
  return
@@ -590,7 +633,7 @@ window.__ModuleLoader__.load({
590
633
  setBox((previous) => (sameBox(previous, next) ? previous : next))
591
634
  }
592
635
  measure()
593
- const found = composerElements()
636
+ const found = elements()
594
637
  let observer
595
638
  if (found !== null && typeof ResizeObserver === 'function') {
596
639
  observer = new ResizeObserver(measure)
@@ -610,7 +653,7 @@ window.__ModuleLoader__.load({
610
653
  let frame = 0
611
654
  const measure = () => {
612
655
  frame = 0
613
- const found = composerElements()
656
+ const found = elements()
614
657
  if (found === null) {
615
658
  setBox((previous) => (previous === null ? previous : null))
616
659
  return
@@ -661,7 +704,7 @@ window.__ModuleLoader__.load({
661
704
  measure()
662
705
  document.addEventListener('selectionchange', schedule)
663
706
  document.addEventListener('scroll', schedule, true)
664
- const found = composerElements()
707
+ const found = elements()
665
708
  window.addEventListener('resize', schedule)
666
709
  let observer
667
710
  if (found !== null && typeof ResizeObserver === 'function') {
@@ -694,7 +737,7 @@ window.__ModuleLoader__.load({
694
737
  }
695
738
  // Accepting leaves an editable draft: hand the keyboard back to the editor.
696
739
  try {
697
- const found = composerElements()
740
+ const found = elements()
698
741
  if (found !== null && !found.card.contains(document.activeElement)) found.editor.focus()
699
742
  } catch {
700
743
  // Focus is a courtesy, never a failure.
@@ -717,7 +760,7 @@ window.__ModuleLoader__.load({
717
760
  if (event.defaultPrevented) return
718
761
  // An IME candidate window owns the keyboard while composing.
719
762
  if (event.isComposing === true || event.keyCode === 229) return
720
- const found = composerElements()
763
+ const found = elements()
721
764
  if (found === null) return
722
765
  const target = typeof Node !== 'undefined' && event.target instanceof Node ? event.target : null
723
766
  const inEditor = found.editor.contains(document.activeElement)
@@ -755,7 +798,14 @@ window.__ModuleLoader__.load({
755
798
  return () => document.removeEventListener('keydown', onKeyDown, true)
756
799
  }, [visible, draft, ghost.items.length, ghost.active, ghost.mode])
757
800
 
758
- if (!visible || box === null) return null
801
+ const anchor = h('span', {
802
+ ref: anchorRef,
803
+ 'data-composer-prediction-anchor': '',
804
+ 'aria-hidden': 'true',
805
+ style: { display: 'none' },
806
+ })
807
+
808
+ if (!visible || box === null) return anchor
759
809
 
760
810
  const continuation = ghost.mode === 'continue'
761
811
  const style = continuation ? CONTINUE_GHOST_STYLE : NEXT_GHOST_STYLE
@@ -775,6 +825,7 @@ window.__ModuleLoader__.load({
775
825
  showBadge ? h('span', { style: BADGE_STYLE, title: t('more') }, `${ghost.index + 1}/${ghost.items.length}`) : null,
776
826
  showKeycap ? h('kbd', { style: KEYCAP_STYLE, title: t('accept') }, 'Tab') : null,
777
827
  ),
828
+ anchor,
778
829
  )
779
830
  }
780
831
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-composer-predictions",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "description": "Tab autocomplete for the DeepSeek Harness desktop and web composer: next-message ghost text, inline draft completion and keyboard candidate cycling",
5
5
  "type": "module",
6
6
  "license": "MIT",