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 +22 -0
- package/README.md +6 -5
- package/client.js +63 -12
- package/package.json +1 -1
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:
|
|
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-
|
|
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
|
|
104
|
-
CLI 用户可运行 `dsh plugin --profile web add dsh-composer-predictions
|
|
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
|
-
演示、完整配置与限制见本页;[
|
|
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
|
-
|
|
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
|
-
/**
|
|
166
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
+
"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",
|