dsh-vibegap 0.1.0

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 ADDED
@@ -0,0 +1,55 @@
1
+ # dsh-vibegap
2
+
3
+ VibeGap 的 DeepSeek Harness 原生插件。Agent 持续运行 18 秒后,dsh web
4
+ 右下角会出现拼写单词卡;会话完成或等待确认时,完成当前单词后自动收起或继续。
5
+ 插件可独立运行,不要求安装 VibeGap Python 桌面端。
6
+ 如果本机 VibeGap daemon 正在运行,插件会自动改用桌面端的当前词书和进度游标;
7
+ 连接失败或运行中断线时,会回退到浏览器内的独立进度。
8
+
9
+ ## 安装
10
+
11
+ 本地开发安装:
12
+
13
+ ```bash
14
+ dsh plugin --profile web add link:<仓库绝对路径>/vibegap/adapters/dsh/plugin
15
+ dsh web
16
+ ```
17
+
18
+ 卸载:
19
+
20
+ ```bash
21
+ dsh plugin --profile web remove dsh-vibegap
22
+ ```
23
+
24
+ 首次弹出单词卡时,点击“下载词库”获取 CET6 词库。词库、乱序种子、游标和
25
+ 自动发音偏好通过 dsh 官方 snapshot store 保存在当前浏览器中。无痕模式或浏览器
26
+ 拒绝持久化时,卡片仍可使用,但刷新后进度可能丢失。
27
+
28
+ 本机 daemon 可用时,单词和进度改由 `http://127.0.0.1:8765/panel/*` 提供,
29
+ 仅允许来自 `localhost` 或 `127.0.0.1` 的 HTTP(S) 浏览器 Origin。
30
+
31
+ ## 使用
32
+
33
+ - 点击卡片后直接拼写;正确字母会逐个点亮。
34
+ - 输入错误会清空当前拼写并计数。
35
+ - `Tab` 显示或隐藏答案;看过答案后,本词按 fail 完成并前进。
36
+ - `Esc` 或右上角 `×` 隐藏卡片;同一批运行中的会话不会再次弹出。
37
+ - “发音”可手动播放美式发音,“自动发音”可随时开关。
38
+
39
+ ## 开发
40
+
41
+ 浏览器端是手写的 `lib/client.js` ModuleLoader bundle,不需要 webpack、tsc
42
+ 或其他构建步骤。修改 bundle 后刷新页面;修改 `package.json` 或
43
+ `cordis.patch.yml` 后重启 `dsh web`。
44
+
45
+ 服务端 `lib/index.js` 会在本机 VibeGap daemon 存在时继续上报 agent 生命周期;
46
+ 连接失败会静默降级,不影响 dsh。
47
+
48
+ ## 词库来源与许可
49
+
50
+ 默认 CET6 数据在用户点击后从
51
+ [qwerty-learner](https://github.com/RealKai42/qwerty-learner) 下载。该项目使用
52
+ GPL-3.0 许可证;词库不会打包进本 MIT 插件。请同时遵守上游项目及词库数据来源的
53
+ 许可条款。
54
+
55
+ 插件代码使用 MIT 许可证。
@@ -0,0 +1,5 @@
1
+ # dsh-vibegap bundle patch: one host-side roster row.
2
+ # Format verified against dsh-web-attention-badge (insert list of {id, name}).
3
+ - insert:
4
+ - id: vibegap-bridge
5
+ name: 'dsh-vibegap'
package/lib/client.js ADDED
@@ -0,0 +1,499 @@
1
+ /** dsh-vibegap browser half: a self-contained spelling card for dsh web. */
2
+ window.__ModuleLoader__.load({
3
+ id: "dsh-vibegap",
4
+ factory: (require) => {
5
+ var module = { exports: {} };
6
+ var exports = module.exports;
7
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
8
+ var React = require("react");
9
+ var primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
+ var runtime = require("@deepseek-ai/dsh-client-runtime/client");
11
+
12
+ var POPUP_DELAY_MS = 18000;
13
+ var SUMMARY_LINGER_MS = 2000;
14
+ var WORD_COMMIT_DELAY_MS = 240;
15
+ var SHAKE_MS = 300;
16
+ var DOWNLOAD_TIMEOUT_MS = 30000;
17
+ var AUDIO_TIMEOUT_MS = 6000;
18
+ var TRANSLATION_MAX_CHARS = 100;
19
+ var DAEMON_URL = "http://127.0.0.1:8765/panel";
20
+ var DAEMON_TIMEOUT_MS = 1000;
21
+ var DICT_URL = "https://raw.githubusercontent.com/RealKai42/qwerty-learner/master/public/dicts/CET6_T.json";
22
+ var STYLE_ID = "vg-card-styles";
23
+ var DONE_NOTICE = "会话已完成 · 拼完当前词后收起";
24
+ var WAIT_NOTICE = "会话等待确认 · 拼完当前词后收起";
25
+ var inject = ["slots"];
26
+ var h = React.createElement;
27
+ var Button = primitives.Button;
28
+ var initialSeed = (Date.now() ^ 0x6d2b79f5) >>> 0;
29
+ var progressStore = runtime.createSnapshotStore(
30
+ { mode: "shuffle", seed: initialSeed, cursor: 0, words: null },
31
+ { persist: { name: "vibegap.progress" } },
32
+ );
33
+ var prefsStore = runtime.createSnapshotStore(
34
+ { autoPronounce: true },
35
+ { persist: { name: "vibegap.prefs" } },
36
+ );
37
+
38
+ var css = [
39
+ ".vg-card{position:fixed;right:24px;bottom:24px;width:min(390px,calc(100vw - 32px));box-sizing:border-box;padding:16px;border:1px solid var(--dsw-alias-border-l,#d8d8d8);border-radius:14px;background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#202020);box-shadow:0 12px 34px rgba(0,0,0,.18);font-family:inherit;pointer-events:auto;outline:none;z-index:1}",
40
+ ".vg-card:focus{border-color:var(--dsw-alias-border-focus,#5794ff);box-shadow:0 12px 34px rgba(0,0,0,.18),0 0 0 2px rgba(87,148,255,.2)}",
41
+ ".vg-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}",
42
+ ".vg-title{font-size:13px;font-weight:650;letter-spacing:.02em}",
43
+ ".vg-close{min-width:28px!important;padding:0!important;font-size:18px!important}",
44
+ ".vg-banner{margin:-2px 0 14px;padding:8px 10px;border-radius:8px;background:var(--dsw-alias-interactive-bg-hover,#f2f3f5);font-size:12px;line-height:1.4}",
45
+ ".vg-word{display:flex;flex-wrap:wrap;justify-content:center;gap:4px;min-height:48px;margin:16px 0 12px}",
46
+ ".vg-char{display:inline-flex;align-items:center;justify-content:center;width:24px;height:38px;border-bottom:2px solid var(--dsw-alias-border-l,#bbb);font:600 24px/1 ui-monospace,SFMono-Regular,Consolas,monospace}",
47
+ ".vg-char-ok{color:var(--dsw-alias-state-success-primary,#238636);border-color:currentColor}",
48
+ ".vg-char-reveal{color:var(--dsw-alias-label-secondary,#777)}",
49
+ ".vg-char-cursor{animation:vg-blink 1s steps(1) infinite}",
50
+ ".vg-trans{min-height:42px;text-align:center;color:var(--dsw-alias-label-secondary,#666);font-size:14px;line-height:1.5}",
51
+ ".vg-meta{display:flex;align-items:center;justify-content:center;gap:8px;min-height:28px;margin-top:8px;color:var(--dsw-alias-label-tertiary,#888);font-size:12px}",
52
+ ".vg-actions{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin-top:12px}",
53
+ ".vg-hint{margin-top:12px;text-align:center;color:var(--dsw-alias-label-tertiary,#888);font-size:11px}",
54
+ ".vg-empty{padding:18px 4px 8px;text-align:center;color:var(--dsw-alias-label-secondary,#666);font-size:13px;line-height:1.6}",
55
+ ".vg-error{margin-top:10px;color:var(--dsw-alias-state-danger-primary,#c93c37);font-size:12px}",
56
+ ".vg-shake{animation:vg-shake .28s ease-in-out}",
57
+ "@keyframes vg-blink{50%{border-color:transparent}}",
58
+ "@keyframes vg-shake{25%{transform:translateX(-5px)}75%{transform:translateX(5px)}}",
59
+ "@media(max-width:520px){.vg-card{right:16px;bottom:16px}.vg-char{width:20px;font-size:21px}}",
60
+ ].join("");
61
+
62
+ function apply(ctx) {
63
+ ctx.slots.inject("shell.overlay", function () {
64
+ return ctx.slots.register(
65
+ { name: "shell.overlay", id: "vibegap", order: 90000 },
66
+ VibegapCard,
67
+ );
68
+ });
69
+ }
70
+
71
+ function identity(value) { return value; }
72
+
73
+ function useSnapshot(store) {
74
+ var state = React.useState(store.getSnapshot());
75
+ React.useEffect(function () {
76
+ return store.subscribe(function () { state[1](store.getSnapshot()); });
77
+ }, [store]);
78
+ return state[0];
79
+ }
80
+ function useCell(value) {
81
+ return React.useState({ current: value })[0];
82
+ }
83
+
84
+ function mulberry32(seed) {
85
+ return function random() {
86
+ var value = seed += 0x6d2b79f5;
87
+ value = Math.imul(value ^ value >>> 15, value | 1);
88
+ value ^= value + Math.imul(value ^ value >>> 7, value | 61);
89
+ return ((value ^ value >>> 14) >>> 0) / 4294967296;
90
+ };
91
+ }
92
+
93
+ function shuffled(words, seed) {
94
+ var result = words.slice();
95
+ var random = mulberry32(seed >>> 0);
96
+ for (var i = result.length - 1; i > 0; i -= 1) {
97
+ var j = Math.floor(random() * (i + 1));
98
+ var swap = result[i]; result[i] = result[j]; result[j] = swap;
99
+ }
100
+ return result;
101
+ }
102
+
103
+ function validWord(entry) {
104
+ return entry && typeof entry.name === "string" && entry.name.length > 0 &&
105
+ Array.isArray(entry.trans);
106
+ }
107
+
108
+ function normalizeWords(payload) {
109
+ if (!Array.isArray(payload)) throw new Error("invalid wordbook");
110
+ var words = payload.filter(validWord).map(function (entry) {
111
+ return {
112
+ name: entry.name,
113
+ trans: entry.trans.filter(function (item) { return typeof item === "string"; }),
114
+ usphone: typeof entry.usphone === "string" ? entry.usphone : "",
115
+ };
116
+ });
117
+ if (words.length === 0) throw new Error("empty wordbook");
118
+ return words;
119
+ }
120
+
121
+ function rootRows(state) {
122
+ var rows = {};
123
+ state.ids.forEach(function (id) {
124
+ var row = state.byId[id];
125
+ if (row && row.origin !== "subagent") rows[id] = row;
126
+ });
127
+ return rows;
128
+ }
129
+
130
+ function transitionNotice(previous, current) {
131
+ var ids = Object.keys(current);
132
+ for (var i = 0; i < ids.length; i += 1) {
133
+ var row = current[ids[i]];
134
+ var before = previous[ids[i]];
135
+ if (!before) continue;
136
+ if (!before.pendingInteraction && row.pendingInteraction) return WAIT_NOTICE;
137
+ if (before.running && row.completed === true) return DONE_NOTICE;
138
+ }
139
+ return null;
140
+ }
141
+
142
+ function noticeStillActive(notice, rows) {
143
+ return Object.keys(rows).some(function (id) {
144
+ var row = rows[id];
145
+ return notice === WAIT_NOTICE ? !!row.pendingInteraction : row.completed === true;
146
+ });
147
+ }
148
+
149
+ function safePronounce(word) {
150
+ try {
151
+ var signal = AbortSignal.timeout(AUDIO_TIMEOUT_MS);
152
+ var audio = new Audio("https://dict.youdao.com/dictvoice?audio=" +
153
+ encodeURIComponent(word) + "&type=2");
154
+ var stop = function () { try { audio.pause(); audio.removeAttribute("src"); } catch (_) {} };
155
+ signal.addEventListener("abort", stop, { once: true });
156
+ var played = audio.play();
157
+ if (played && played.catch) played.catch(function () {});
158
+ } catch (_) {}
159
+ }
160
+
161
+ async function downloadWords(setStatus) {
162
+ setStatus({ busy: true, error: "" });
163
+ try {
164
+ var response = await fetch(DICT_URL, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
165
+ if (!response.ok) throw new Error("download failed");
166
+ var words = normalizeWords(await response.json());
167
+ progressStore.set({ mode: "shuffle", seed: initialSeed, cursor: 0, words: words });
168
+ setStatus({ busy: false, error: "" });
169
+ } catch (_) {
170
+ setStatus({ busy: false, error: "下载失败,请检查网络后重试" });
171
+ }
172
+ }
173
+
174
+ async function panelRequest(path, options) {
175
+ var init = options || {};
176
+ init.signal = AbortSignal.timeout(DAEMON_TIMEOUT_MS);
177
+ var response = await fetch(DAEMON_URL + path, init);
178
+ if (!response.ok) throw new Error("panel request failed");
179
+ var payload = await response.json();
180
+ if (payload.error) throw new Error(payload.error);
181
+ return payload;
182
+ }
183
+ function useDaemonBackend() {
184
+ var backend = React.useState({ mode: "probing", word: null });
185
+ React.useEffect(function () {
186
+ var active = true;
187
+ (async function () {
188
+ try {
189
+ var state = await panelRequest("/state");
190
+ if (!state.ready) throw new Error("daemon has no wordbook");
191
+ var word = await panelRequest("/next-word");
192
+ if (active) backend[1]({ mode: "daemon", word: word });
193
+ } catch (_) { if (active) backend[1]({ mode: "local", word: null }); }
194
+ })();
195
+ return function () { active = false; };
196
+ }, []);
197
+ return backend;
198
+ }
199
+
200
+ function advanceProgress(total) {
201
+ progressStore.update(function (draft) {
202
+ if (draft.cursor + 1 < total) { draft.cursor += 1; return; }
203
+ draft.cursor = 0;
204
+ draft.seed = (draft.seed + 0x9e3779b9) >>> 0;
205
+ });
206
+ }
207
+
208
+ function LetterGrid(props) {
209
+ var chars = props.word.split("");
210
+ return h("div", { className: "vg-word" + (props.shaking ? " vg-shake" : "") },
211
+ chars.map(function (char, index) {
212
+ var shown = index < props.typed || props.revealed;
213
+ var classes = "vg-char";
214
+ if (index < props.typed) classes += " vg-char-ok";
215
+ else if (props.revealed) classes += " vg-char-reveal";
216
+ if (props.focused && index === props.typed && !props.revealed) classes += " vg-char-cursor";
217
+ return h("span", { className: classes, key: index }, shown ? char : "\u00a0");
218
+ }),
219
+ );
220
+ }
221
+
222
+ function EmptyCard(props) {
223
+ return h("div", { className: "vg-empty" },
224
+ h("div", null, "首次使用需要下载词库(CET6,约 0.5MB)"),
225
+ h("div", { className: "vg-actions" },
226
+ h(Button, {
227
+ variant: "primary", size: "sm", disabled: props.status.busy,
228
+ onClick: function () { downloadWords(props.setStatus); },
229
+ }, props.status.busy ? "正在下载" : "下载词库"),
230
+ ),
231
+ props.status.error ? h("div", { className: "vg-error", role: "alert" }, props.status.error) : null,
232
+ );
233
+ }
234
+
235
+ function useLifecycle(sessionState, visible, setVisible, showBanner, onRestart) {
236
+ var ref = useCell({ running: false, rows: {}, suppressed: false, arm: null, pending: null });
237
+ var runningRef = useCell(false);
238
+ var visibleRef = useCell(visible);
239
+ React.useEffect(function () { visibleRef.current = visible; }, [visible]);
240
+ React.useEffect(function () {
241
+ var anyRunning = sessionState.ids.some(function (id) {
242
+ var row = sessionState.byId[id]; return row && row.running;
243
+ });
244
+ var life = ref.current;
245
+ runningRef.current = anyRunning;
246
+ if (!life.running && anyRunning) {
247
+ life.suppressed = false; life.pending = null;
248
+ onRestart();
249
+ if (!visibleRef.current) life.arm = setTimeout(function () {
250
+ if (!runningRef.current || life.suppressed) return;
251
+ setVisible(true);
252
+ if (life.pending) { showBanner(life.pending); life.pending = null; }
253
+ }, POPUP_DELAY_MS);
254
+ }
255
+ if (life.running && !anyRunning && life.arm) {
256
+ clearTimeout(life.arm); life.arm = null; life.pending = null;
257
+ }
258
+ var rows = rootRows(sessionState);
259
+ if (life.pending && !noticeStillActive(life.pending, rows)) life.pending = null;
260
+ var notice = transitionNotice(life.rows, rows);
261
+ if (notice && visibleRef.current && !life.suppressed) showBanner(notice);
262
+ else if (notice && anyRunning && !life.suppressed) life.pending = notice;
263
+ life.running = anyRunning;
264
+ life.rows = rows;
265
+ }, [sessionState]);
266
+ React.useEffect(function () { return function () { if (ref.current.arm) clearTimeout(ref.current.arm); }; }, []);
267
+ return { state: ref, running: runningRef };
268
+ }
269
+
270
+ function useTyping(config) {
271
+ React.useEffect(function () {
272
+ if (!config.visible || !config.focused || !config.word || config.busy) return;
273
+ function onKey(event) {
274
+ if (document.activeElement !== config.card.current) return;
275
+ if (event.key === "Escape") { event.preventDefault(); config.hide(); return; }
276
+ if (event.key === "Tab") {
277
+ event.preventDefault(); config.setPeeked(true); config.setTyped(0);
278
+ config.setRevealed(function (value) { return !value; }); return;
279
+ }
280
+ if (event.key.length !== 1 || !/[a-zA-Z'\- ]/.test(event.key)) return;
281
+ event.preventDefault();
282
+ if (config.revealed) config.setRevealed(false);
283
+ if (event.key.toLowerCase() === config.word[config.typed].toLowerCase()) {
284
+ var next = config.typed + 1; config.setTyped(next);
285
+ if (next === config.word.length) config.complete();
286
+ return;
287
+ }
288
+ config.setTypos(function (value) { return value + 1; });
289
+ config.setTyped(0); config.shake();
290
+ }
291
+ document.addEventListener("keydown", onKey);
292
+ return function () { document.removeEventListener("keydown", onKey); };
293
+ }, [config]);
294
+ }
295
+
296
+ function CardBody(props) {
297
+ if (!props.word) return h(EmptyCard, { status: props.download, setStatus: props.setDownload });
298
+ var translation = props.word.trans.join(";");
299
+ return h(React.Fragment, null,
300
+ h(LetterGrid, {
301
+ word: props.word.name, typed: props.typed, revealed: props.revealed,
302
+ focused: props.focused, shaking: props.shaking,
303
+ }),
304
+ h("div", { className: "vg-trans", title: translation }, translation.slice(0, TRANSLATION_MAX_CHARS)),
305
+ h("div", { className: "vg-meta" },
306
+ props.word.usphone ? "/" + props.word.usphone + "/" : null,
307
+ props.typos ? "错误 " + props.typos : "尚无错误",
308
+ props.outcome ? h("span", { role: "status" }, props.outcome === "fail" ? "已记为需复习" : "拼写正确") : null,
309
+ ),
310
+ h("div", { className: "vg-actions" },
311
+ h(Button, { size: "sm", variant: "outline", onClick: props.pronounce }, "发音"),
312
+ h(Button, { size: "sm", variant: "ghost", onClick: props.toggleAuto },
313
+ "自动发音:" + (props.autoPronounce ? "开" : "关")),
314
+ ),
315
+ h("div", { className: "vg-hint" }, "点击卡片后拼写 · Tab 查看答案 · Esc 隐藏"),
316
+ );
317
+ }
318
+
319
+ function useCardStates() {
320
+ return {
321
+ visible: React.useState(false), banner: React.useState(null), focused: React.useState(false),
322
+ typed: React.useState(0), typos: React.useState(0), revealed: React.useState(false),
323
+ peeked: React.useState(false), shaking: React.useState(false), busy: React.useState(false),
324
+ outcome: React.useState(""), holdWord: React.useState(null),
325
+ download: React.useState({ busy: false, error: "" }),
326
+ };
327
+ }
328
+
329
+ function useWordSelection(progress) {
330
+ var words = progress && Array.isArray(progress.words) && progress.words.every(validWord) ? progress.words : null;
331
+ var seed = progress && Number(progress.seed) || 0;
332
+ var cache = useCell({ words: null, seed: null, ordered: [] });
333
+ if (cache.current.words !== words || cache.current.seed !== seed) {
334
+ cache.current = { words: words, seed: seed, ordered: words ? shuffled(words, seed) : [] };
335
+ }
336
+ var ordered = cache.current.ordered;
337
+ var cursorValue = progress && Number(progress.cursor) || 0;
338
+ var cursor = ordered.length ? Math.min(Math.max(cursorValue, 0), ordered.length - 1) : 0;
339
+ return { ordered: ordered, word: ordered[cursor] || null };
340
+ }
341
+ function activeSelection(local, backend) {
342
+ var remote = backend[0];
343
+ if (remote.mode === "daemon" && remote.word) {
344
+ return { ordered: [remote.word], word: remote.word, isDaemon: true };
345
+ }
346
+ return { ordered: local.ordered, word: local.word, isDaemon: false };
347
+ }
348
+
349
+ function useLifecycleBridge(sessionState, states, refs) {
350
+ var showBanner = function (message) {
351
+ refs.banner.current = message; states.banner[1](message);
352
+ };
353
+ var onRestart = function () {
354
+ if (!refs.hideTimer.current) return;
355
+ clearTimeout(refs.hideTimer.current); refs.hideTimer.current = null;
356
+ showBanner(null); resetInput(states);
357
+ };
358
+ var tracker = useLifecycle(sessionState, states.visible[0], states.visible[1], showBanner, onRestart);
359
+ return { tracker: tracker, showBanner: showBanner };
360
+ }
361
+ function resetInput(states) {
362
+ states.typed[1](0); states.typos[1](0); states.revealed[1](false);
363
+ states.peeked[1](false); states.busy[1](false); states.shaking[1](false);
364
+ states.outcome[1](""); states.holdWord[1](null);
365
+ }
366
+
367
+ function hideCard(model, suppress) {
368
+ if (model.refs.hideTimer.current) clearTimeout(model.refs.hideTimer.current);
369
+ model.refs.hideTimer.current = null;
370
+ var life = model.lifecycle.tracker.state.current;
371
+ if (life.arm) clearTimeout(life.arm);
372
+ life.arm = null;
373
+ life.pending = null;
374
+ if (suppress) life.suppressed = true;
375
+ model.refs.banner.current = null; model.states.banner[1](null);
376
+ model.states.visible[1](false); model.states.focused[1](false);
377
+ resetInput(model.states);
378
+ }
379
+
380
+ function completeWord(model) {
381
+ model.states.busy[1](true);
382
+ model.states.outcome[1](model.states.peeked[0] ? "fail" : "pass");
383
+ setTimeout(async function () {
384
+ var shouldHide = !!model.refs.banner.current && !model.lifecycle.tracker.running.current;
385
+ if (shouldHide) model.states.holdWord[1](model.selection.word);
386
+ await advanceSelection(model);
387
+ if (!model.refs.banner.current) { resetInput(model.states); return; }
388
+ if (model.lifecycle.tracker.running.current) {
389
+ model.lifecycle.showBanner(null); resetInput(model.states); return;
390
+ }
391
+ model.refs.hideTimer.current = setTimeout(function () {
392
+ hideCard(model, false);
393
+ }, SUMMARY_LINGER_MS);
394
+ }, WORD_COMMIT_DELAY_MS);
395
+ }
396
+ async function advanceSelection(model) {
397
+ if (!model.selection.isDaemon) {
398
+ advanceProgress(model.selection.ordered.length); return;
399
+ }
400
+ try {
401
+ await panelRequest("/commit", {
402
+ method: "POST", headers: { "Content-Type": "application/json" },
403
+ body: JSON.stringify({
404
+ result: model.states.peeked[0] ? "fail" : "pass",
405
+ typo_count: model.states.typos[0],
406
+ }),
407
+ });
408
+ var word = await panelRequest("/next-word");
409
+ model.backend[1]({ mode: "daemon", word: word });
410
+ } catch (_) { model.backend[1]({ mode: "local", word: null }); }
411
+ }
412
+
413
+ function shakeCard(states) {
414
+ states.shaking[1](true);
415
+ setTimeout(function () { states.shaking[1](false); }, SHAKE_MS);
416
+ }
417
+
418
+ function useCardEffects(model) {
419
+ var word = displayedWord(model);
420
+ React.useEffect(function () { resetInput(model.states); }, [word && word.name]);
421
+ React.useEffect(function () {
422
+ if (model.states.visible[0] && word && model.autoPronounce) safePronounce(word.name);
423
+ }, [model.states.visible[0], word && word.name, model.autoPronounce]);
424
+ React.useEffect(function () {
425
+ return function () { if (model.refs.hideTimer.current) clearTimeout(model.refs.hideTimer.current); };
426
+ }, []);
427
+ }
428
+
429
+ function useCardTyping(model) {
430
+ var states = model.states;
431
+ var word = displayedWord(model);
432
+ useTyping({
433
+ visible: states.visible[0], focused: states.focused[0],
434
+ word: word && word.name,
435
+ busy: states.busy[0], typed: states.typed[0], revealed: states.revealed[0],
436
+ card: model.refs.card, setTyped: states.typed[1], setTypos: states.typos[1],
437
+ setRevealed: states.revealed[1], setPeeked: states.peeked[1],
438
+ hide: function () { hideCard(model, true); },
439
+ complete: function () { completeWord(model); },
440
+ shake: function () { shakeCard(states); },
441
+ });
442
+ }
443
+
444
+ function renderCard(model) {
445
+ var states = model.states;
446
+ return h(React.Fragment, null, h("style", { id: STYLE_ID }, css),
447
+ h("section", {
448
+ className: "vg-card", tabIndex: 0, ref: model.refs.card, "aria-label": "VibeGap 单词卡",
449
+ onFocus: function (event) { if (event.target === event.currentTarget) states.focused[1](true); },
450
+ onBlur: function () { states.focused[1](false); },
451
+ onMouseDown: function (event) {
452
+ if (!event.target.closest("button,a,input,textarea,select")) model.refs.card.current.focus();
453
+ },
454
+ },
455
+ h("div", { className: "vg-head" }, h("div", { className: "vg-title" }, "VibeGap"),
456
+ h(Button, { className: "vg-close", size: "sm", variant: "ghost", title: "隐藏",
457
+ onClick: function () { hideCard(model, true); } }, "×")),
458
+ states.banner[0] ? h("div", { className: "vg-banner", role: "status" }, states.banner[0]) : null,
459
+ h(CardBody, cardBodyProps(model))),
460
+ );
461
+ }
462
+
463
+ function cardBodyProps(model) {
464
+ var states = model.states;
465
+ var word = displayedWord(model);
466
+ return {
467
+ word: word, typed: states.typed[0], typos: states.typos[0], revealed: states.revealed[0],
468
+ focused: states.focused[0], shaking: states.shaking[0], download: states.download[0],
469
+ outcome: states.outcome[0], setDownload: states.download[1], autoPronounce: model.autoPronounce,
470
+ pronounce: function () { if (word) safePronounce(word.name); },
471
+ toggleAuto: function () { prefsStore.set({ autoPronounce: !model.autoPronounce }); },
472
+ };
473
+ }
474
+ function displayedWord(model) {
475
+ return model.states.holdWord[0] || model.selection.word;
476
+ }
477
+ function VibegapCard(props) {
478
+ var sessionState = props.useSessions(identity);
479
+ var progress = useSnapshot(progressStore);
480
+ var prefs = useSnapshot(prefsStore);
481
+ var states = useCardStates();
482
+ var refs = { card: useCell(null), hideTimer: useCell(null), banner: useCell(null) };
483
+ var lifecycle = useLifecycleBridge(sessionState, states, refs);
484
+ var backend = useDaemonBackend();
485
+ var selection = activeSelection(useWordSelection(progress), backend);
486
+ var model = {
487
+ states: states, refs: refs, lifecycle: lifecycle, selection: selection, backend: backend,
488
+ autoPronounce: !prefs || prefs.autoPronounce !== false,
489
+ };
490
+ useCardEffects(model);
491
+ useCardTyping(model);
492
+ return states.visible[0] ? renderCard(model) : h("style", { id: STYLE_ID }, css);
493
+ }
494
+
495
+ exports.apply = apply;
496
+ exports.inject = inject;
497
+ return module.exports;
498
+ },
499
+ });
package/lib/index.js ADDED
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ // dsh-vibegap: report agent lifecycle to the local VibeGap daemon.
3
+ // Event contract source-verified against deepseek-harness (2026-08-26):
4
+ // packages/core/agent/src/runtime-types.ts
5
+ // 'agent/status'(payload: { agent: Agent; status: AgentStatus }): void
6
+ // AgentStatus = 'idle' | 'running' (emitted on every transition)
7
+ // Fire-and-forget, 1s timeout, catch-all -- must never break the harness.
8
+ // Plain CJS on purpose: no build step, installable via `link:` directly.
9
+
10
+ const PORT = 8765;
11
+
12
+ exports.name = "dsh-vibegap";
13
+
14
+ function post(event, sessionId, cwd) {
15
+ try {
16
+ const controller = new AbortController();
17
+ const timer = setTimeout(() => controller.abort(), 1000);
18
+ fetch(`http://127.0.0.1:${PORT}/hook/dsh/${event}?src=vibegap`, {
19
+ method: "POST",
20
+ body: JSON.stringify({
21
+ session_id: String(sessionId || "dsh-session"),
22
+ cwd: String(cwd || ""),
23
+ }),
24
+ signal: controller.signal,
25
+ })
26
+ .catch(() => {})
27
+ .finally(() => clearTimeout(timer));
28
+ } catch {
29
+ /* never break the harness */
30
+ }
31
+ }
32
+
33
+ exports.apply = function apply(ctx) {
34
+ ctx.on(
35
+ "agent/status",
36
+ function onStatus(payload) {
37
+ const agent = payload && payload.agent;
38
+ const sid = (agent && agent.session && agent.session.id) || "dsh-session";
39
+ // TODO(装机确认): cwd 字段名 -- AgentOptions 里按 cwd/workspace 双名兼容取
40
+ const opts = (agent && agent.options) || {};
41
+ const cwd = opts.cwd || opts.workspace || "";
42
+ post(payload.status === "running" ? "running" : "done", sid, cwd);
43
+ },
44
+ { global: true },
45
+ );
46
+ };
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "dsh-vibegap",
3
+ "version": "0.1.0",
4
+ "description": "Self-contained vocabulary flashcards for DeepSeek Harness web sessions",
5
+ "main": "lib/index.js",
6
+ "exports": {
7
+ ".": "./lib/index.js",
8
+ "./client": "./lib/client.js",
9
+ "./package.json": "./package.json"
10
+ },
11
+ "keywords": ["dsh-plugin", "vibegap", "vocabulary", "productivity"],
12
+ "files": ["lib", "cordis.patch.yml", "README.md"],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/ktao732084-arch/vibegap.git",
16
+ "directory": "vibegap/adapters/dsh/plugin"
17
+ },
18
+ "homepage": "https://github.com/ktao732084-arch/vibegap/tree/main/vibegap/adapters/dsh/plugin#readme",
19
+ "bugs": "https://github.com/ktao732084-arch/vibegap/issues",
20
+ "license": "MIT",
21
+ "dsh": {
22
+ "bundle": { "patch": "./cordis.patch.yml" },
23
+ "client": {
24
+ "platform": "web",
25
+ "inject": ["@deepseek-ai/dsh-client-runtime"]
26
+ }
27
+ }
28
+ }