dsh-vibegap 0.1.2 → 0.1.3
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 +12 -2
- package/lib/client.js +158 -35
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -4,8 +4,9 @@ VibeGap 的 DeepSeek Harness 原生插件。Agent 持续运行 18 秒后,dsh w
|
|
|
4
4
|
右下角会出现拼写单词卡;会话完成或等待确认时,完成当前单词后自动收起或继续。
|
|
5
5
|
插件可独立运行,不要求安装 VibeGap Python 桌面端。
|
|
6
6
|
如果本机 VibeGap Core 正在运行,插件会自动改用桌面端的当前词书和进度游标;
|
|
7
|
-
Core 在页面打开后才由另一个 Agent
|
|
8
|
-
|
|
7
|
+
Core 在页面打开后才由另一个 Agent 拉起也能自动接入。为避免两张卡重复出现,
|
|
8
|
+
共享模式下页内卡不自动弹出,但仍可用 `Ctrl+Alt+V` 手动呼出。运行中断线时
|
|
9
|
+
保留当前词并自动重连,不会误跳到首次下载界面。
|
|
9
10
|
|
|
10
11
|
## 安装
|
|
11
12
|
|
|
@@ -46,6 +47,8 @@ dsh plugin --profile web remove dsh-vibegap
|
|
|
46
47
|
本机 Core 可用时,单词和进度改由 `http://127.0.0.1:8765/panel/*` 提供,
|
|
47
48
|
插件每 5 秒低频探测一次,且仅允许来自 `localhost` 或 `127.0.0.1` 的
|
|
48
49
|
HTTP(S) 浏览器 Origin。DSH 单独使用时不会因此启动 Python/Core 进程。
|
|
50
|
+
共享模式目前只发现默认端口 `8765`;若桌面端修改了 `daemon_port`,DSH 会保持
|
|
51
|
+
独立本地模式。
|
|
49
52
|
|
|
50
53
|
## 使用
|
|
51
54
|
|
|
@@ -53,6 +56,7 @@ HTTP(S) 浏览器 Origin。DSH 单独使用时不会因此启动 Python/Core 进
|
|
|
53
56
|
- 输入错误会清空当前拼写并计数。
|
|
54
57
|
- `Tab` 显示或隐藏答案;看过答案后,本词按 fail 完成并前进。
|
|
55
58
|
- `Esc` 或右上角 `×` 隐藏卡片;同一批运行中的会话不会再次弹出。
|
|
59
|
+
- `Ctrl+Alt+V` 手动呼出或隐藏卡片(dsh 标签页处于前台时有效,不受 18 秒规则限制)。
|
|
56
60
|
- “发音”可手动播放美式发音,“自动发音”可随时开关。
|
|
57
61
|
|
|
58
62
|
## 开发
|
|
@@ -61,6 +65,12 @@ HTTP(S) 浏览器 Origin。DSH 单独使用时不会因此启动 Python/Core 进
|
|
|
61
65
|
或其他构建步骤。修改 bundle 后刷新页面;修改 `package.json` 或
|
|
62
66
|
`cordis.patch.yml` 后重启 `dsh web`。
|
|
63
67
|
|
|
68
|
+
纯逻辑与 manifest 兼容性测试:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm test
|
|
72
|
+
```
|
|
73
|
+
|
|
64
74
|
服务端 `lib/index.js` 会在本机 VibeGap daemon 存在时继续上报 agent 生命周期;
|
|
65
75
|
连接失败会静默降级,不影响 dsh。
|
|
66
76
|
|
package/lib/client.js
CHANGED
|
@@ -35,12 +35,17 @@ window.__ModuleLoader__.load({
|
|
|
35
35
|
{ autoPronounce: true },
|
|
36
36
|
{ persist: { name: "vibegap.prefs" } },
|
|
37
37
|
);
|
|
38
|
+
var posStore = runtime.createSnapshotStore(
|
|
39
|
+
{ x: null, y: null },
|
|
40
|
+
{ persist: { name: "vibegap.pos" } },
|
|
41
|
+
);
|
|
38
42
|
|
|
39
43
|
var css = [
|
|
40
44
|
".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}",
|
|
41
45
|
".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)}",
|
|
42
|
-
".vg-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}",
|
|
46
|
+
".vg-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px;cursor:move;user-select:none;touch-action:none}",
|
|
43
47
|
".vg-title{font-size:13px;font-weight:650;letter-spacing:.02em}",
|
|
48
|
+
".vg-prog{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right;font-size:11px;color:var(--dsw-alias-label-tertiary,#888)}",
|
|
44
49
|
".vg-close{min-width:28px!important;padding:0!important;font-size:18px!important}",
|
|
45
50
|
".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}",
|
|
46
51
|
".vg-word{display:flex;flex-wrap:wrap;justify-content:center;gap:4px;min-height:48px;margin:16px 0 12px}",
|
|
@@ -54,6 +59,7 @@ window.__ModuleLoader__.load({
|
|
|
54
59
|
".vg-hint{margin-top:12px;text-align:center;color:var(--dsw-alias-label-tertiary,#888);font-size:11px}",
|
|
55
60
|
".vg-empty{padding:18px 4px 8px;text-align:center;color:var(--dsw-alias-label-secondary,#666);font-size:13px;line-height:1.6}",
|
|
56
61
|
".vg-error{margin-top:10px;color:var(--dsw-alias-state-danger-primary,#c93c37);font-size:12px}",
|
|
62
|
+
".vg-reconnect{margin:8px 0 0;text-align:center;color:var(--dsw-alias-state-warning-primary,#9a6700);font-size:12px}",
|
|
57
63
|
".vg-shake{animation:vg-shake .28s ease-in-out}",
|
|
58
64
|
"@keyframes vg-blink{50%{border-color:transparent}}",
|
|
59
65
|
"@keyframes vg-shake{25%{transform:translateX(-5px)}75%{transform:translateX(5px)}}",
|
|
@@ -189,6 +195,13 @@ window.__ModuleLoader__.load({
|
|
|
189
195
|
draft.cursor = Math.min(Math.max(Number(remoteProgress.cursor), 0), draft.words.length - 1);
|
|
190
196
|
});
|
|
191
197
|
}
|
|
198
|
+
function daemonFailureState(current) {
|
|
199
|
+
if (current.mode === "daemon" || current.mode === "disconnected") {
|
|
200
|
+
return { mode: "disconnected", word: current.word };
|
|
201
|
+
}
|
|
202
|
+
if (current.mode === "probing") return { mode: "local", word: null };
|
|
203
|
+
return current;
|
|
204
|
+
}
|
|
192
205
|
function useDaemonBackend() {
|
|
193
206
|
var backend = React.useState({ mode: "probing", word: null });
|
|
194
207
|
var current = useCell({ mode: "probing", word: null });
|
|
@@ -208,14 +221,15 @@ window.__ModuleLoader__.load({
|
|
|
208
221
|
if (active) setBackend({ mode: "daemon", word: word });
|
|
209
222
|
}
|
|
210
223
|
} catch (_) {
|
|
211
|
-
|
|
224
|
+
var failed = daemonFailureState(current.current);
|
|
225
|
+
if (active && failed !== current.current) setBackend(failed);
|
|
212
226
|
}
|
|
213
227
|
if (active) timer = setTimeout(probe, DAEMON_PROBE_MS);
|
|
214
228
|
}
|
|
215
229
|
probe();
|
|
216
230
|
return function () { active = false; if (timer) clearTimeout(timer); };
|
|
217
231
|
}, []);
|
|
218
|
-
return backend;
|
|
232
|
+
return [backend[0], setBackend];
|
|
219
233
|
}
|
|
220
234
|
|
|
221
235
|
function advanceProgress(total) {
|
|
@@ -253,10 +267,16 @@ window.__ModuleLoader__.load({
|
|
|
253
267
|
);
|
|
254
268
|
}
|
|
255
269
|
|
|
256
|
-
function
|
|
270
|
+
function canAutoPopup(backendMode) {
|
|
271
|
+
return backendMode !== "daemon" && backendMode !== "disconnected";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function useLifecycle(sessionState, visible, setVisible, showBanner, onRestart, allowAutoPopup) {
|
|
257
275
|
var ref = useCell({ running: false, rows: {}, suppressed: false, arm: null, pending: null });
|
|
258
276
|
var runningRef = useCell(false);
|
|
259
277
|
var visibleRef = useCell(visible);
|
|
278
|
+
var autoPopupRef = useCell(allowAutoPopup);
|
|
279
|
+
autoPopupRef.current = allowAutoPopup;
|
|
260
280
|
React.useEffect(function () { visibleRef.current = visible; }, [visible]);
|
|
261
281
|
React.useEffect(function () {
|
|
262
282
|
var anyRunning = sessionState.ids.some(function (id) {
|
|
@@ -268,7 +288,7 @@ window.__ModuleLoader__.load({
|
|
|
268
288
|
life.suppressed = false; life.pending = null;
|
|
269
289
|
onRestart();
|
|
270
290
|
if (!visibleRef.current) life.arm = setTimeout(function () {
|
|
271
|
-
if (!runningRef.current || life.suppressed) return;
|
|
291
|
+
if (!runningRef.current || life.suppressed || !autoPopupRef.current) return;
|
|
272
292
|
setVisible(true);
|
|
273
293
|
if (life.pending) { showBanner(life.pending); life.pending = null; }
|
|
274
294
|
}, POPUP_DELAY_MS);
|
|
@@ -288,34 +308,47 @@ window.__ModuleLoader__.load({
|
|
|
288
308
|
return { state: ref, running: runningRef };
|
|
289
309
|
}
|
|
290
310
|
|
|
311
|
+
function createTypingHandler(configRef) {
|
|
312
|
+
return function onKey(event) {
|
|
313
|
+
var config = configRef.current;
|
|
314
|
+
if (document.activeElement !== config.card.current) return;
|
|
315
|
+
if (event.key === "Escape") { event.preventDefault(); config.hide(); return; }
|
|
316
|
+
if (event.key === "Tab") {
|
|
317
|
+
event.preventDefault(); config.setPeeked(true); config.setTyped(0);
|
|
318
|
+
config.setRevealed(function (value) { return !value; }); return;
|
|
319
|
+
}
|
|
320
|
+
if (event.key.length !== 1 || !/[a-zA-Z'\- ]/.test(event.key)) return;
|
|
321
|
+
event.preventDefault();
|
|
322
|
+
if (config.revealed) config.setRevealed(false);
|
|
323
|
+
if (event.key.toLowerCase() === config.word[config.typed].toLowerCase()) {
|
|
324
|
+
var next = config.typed + 1; config.setTyped(next);
|
|
325
|
+
if (next === config.word.length) config.complete();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
config.setTypos(function (value) { return value + 1; });
|
|
329
|
+
config.setTyped(0); config.shake();
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
291
333
|
function useTyping(config) {
|
|
334
|
+
var configRef = useCell(config);
|
|
335
|
+
configRef.current = config;
|
|
292
336
|
React.useEffect(function () {
|
|
293
337
|
if (!config.visible || !config.focused || !config.word || config.busy) return;
|
|
294
|
-
|
|
295
|
-
if (document.activeElement !== config.card.current) return;
|
|
296
|
-
if (event.key === "Escape") { event.preventDefault(); config.hide(); return; }
|
|
297
|
-
if (event.key === "Tab") {
|
|
298
|
-
event.preventDefault(); config.setPeeked(true); config.setTyped(0);
|
|
299
|
-
config.setRevealed(function (value) { return !value; }); return;
|
|
300
|
-
}
|
|
301
|
-
if (event.key.length !== 1 || !/[a-zA-Z'\- ]/.test(event.key)) return;
|
|
302
|
-
event.preventDefault();
|
|
303
|
-
if (config.revealed) config.setRevealed(false);
|
|
304
|
-
if (event.key.toLowerCase() === config.word[config.typed].toLowerCase()) {
|
|
305
|
-
var next = config.typed + 1; config.setTyped(next);
|
|
306
|
-
if (next === config.word.length) config.complete();
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
config.setTypos(function (value) { return value + 1; });
|
|
310
|
-
config.setTyped(0); config.shake();
|
|
311
|
-
}
|
|
338
|
+
var onKey = createTypingHandler(configRef);
|
|
312
339
|
document.addEventListener("keydown", onKey);
|
|
313
340
|
return function () { document.removeEventListener("keydown", onKey); };
|
|
314
|
-
}, [config]);
|
|
341
|
+
}, [config.visible, config.focused, config.word, config.busy]);
|
|
315
342
|
}
|
|
316
343
|
|
|
317
344
|
function CardBody(props) {
|
|
318
|
-
if (!props.word)
|
|
345
|
+
if (!props.word) {
|
|
346
|
+
if (props.connectionLost) {
|
|
347
|
+
return h("div", { className: "vg-empty", role: "status" },
|
|
348
|
+
"桌面端连接已断开,正在自动重连");
|
|
349
|
+
}
|
|
350
|
+
return h(EmptyCard, { status: props.download, setStatus: props.setDownload });
|
|
351
|
+
}
|
|
319
352
|
var translation = props.word.trans.join(";");
|
|
320
353
|
return h(React.Fragment, null,
|
|
321
354
|
h(LetterGrid, {
|
|
@@ -325,15 +358,17 @@ window.__ModuleLoader__.load({
|
|
|
325
358
|
h("div", { className: "vg-trans", title: translation }, translation.slice(0, TRANSLATION_MAX_CHARS)),
|
|
326
359
|
h("div", { className: "vg-meta" },
|
|
327
360
|
props.word.usphone ? "/" + props.word.usphone + "/" : null,
|
|
328
|
-
props.typos ? "
|
|
361
|
+
props.typos ? "错 " + props.typos : null,
|
|
329
362
|
props.outcome ? h("span", { role: "status" }, props.outcome === "fail" ? "已记为需复习" : "拼写正确") : null,
|
|
330
363
|
),
|
|
364
|
+
props.connectionLost ? h("div", { className: "vg-reconnect", role: "status" },
|
|
365
|
+
"桌面端连接已断开,当前词会在重连后继续") : null,
|
|
331
366
|
h("div", { className: "vg-actions" },
|
|
332
367
|
h(Button, { size: "sm", variant: "outline", onClick: props.pronounce }, "发音"),
|
|
333
368
|
h(Button, { size: "sm", variant: "ghost", onClick: props.toggleAuto },
|
|
334
369
|
"自动发音:" + (props.autoPronounce ? "开" : "关")),
|
|
335
370
|
),
|
|
336
|
-
h("div", { className: "vg-hint" }, "点击卡片后拼写 · Tab 查看答案 · Esc 隐藏"),
|
|
371
|
+
h("div", { className: "vg-hint" }, "点击卡片后拼写 · Tab 查看答案 · Esc 隐藏 · Ctrl+Alt+V 呼出"),
|
|
337
372
|
);
|
|
338
373
|
}
|
|
339
374
|
|
|
@@ -344,6 +379,7 @@ window.__ModuleLoader__.load({
|
|
|
344
379
|
peeked: React.useState(false), shaking: React.useState(false), busy: React.useState(false),
|
|
345
380
|
outcome: React.useState(""), holdWord: React.useState(null),
|
|
346
381
|
download: React.useState({ busy: false, error: "" }),
|
|
382
|
+
pos: React.useState(null),
|
|
347
383
|
};
|
|
348
384
|
}
|
|
349
385
|
|
|
@@ -362,12 +398,18 @@ window.__ModuleLoader__.load({
|
|
|
362
398
|
function activeSelection(local, backend) {
|
|
363
399
|
var remote = backend[0];
|
|
364
400
|
if (remote.mode === "daemon" && remote.word) {
|
|
365
|
-
return { ordered: [remote.word], word: remote.word, isDaemon: true
|
|
401
|
+
return { ordered: [remote.word], word: remote.word, isDaemon: true,
|
|
402
|
+
connectionLost: false, remoteUnavailable: false };
|
|
366
403
|
}
|
|
367
|
-
|
|
404
|
+
if (remote.mode === "disconnected") {
|
|
405
|
+
return { ordered: remote.word ? [remote.word] : [], word: remote.word, isDaemon: true,
|
|
406
|
+
connectionLost: true, remoteUnavailable: true };
|
|
407
|
+
}
|
|
408
|
+
return { ordered: local.ordered, word: local.word, isDaemon: false,
|
|
409
|
+
connectionLost: false, remoteUnavailable: false };
|
|
368
410
|
}
|
|
369
411
|
|
|
370
|
-
function useLifecycleBridge(sessionState, states, refs) {
|
|
412
|
+
function useLifecycleBridge(sessionState, states, refs, allowAutoPopup) {
|
|
371
413
|
var showBanner = function (message) {
|
|
372
414
|
refs.banner.current = message; states.banner[1](message);
|
|
373
415
|
};
|
|
@@ -376,7 +418,9 @@ window.__ModuleLoader__.load({
|
|
|
376
418
|
clearTimeout(refs.hideTimer.current); refs.hideTimer.current = null;
|
|
377
419
|
showBanner(null); resetInput(states);
|
|
378
420
|
};
|
|
379
|
-
var tracker = useLifecycle(
|
|
421
|
+
var tracker = useLifecycle(
|
|
422
|
+
sessionState, states.visible[0], states.visible[1], showBanner, onRestart, allowAutoPopup,
|
|
423
|
+
);
|
|
380
424
|
return { tracker: tracker, showBanner: showBanner };
|
|
381
425
|
}
|
|
382
426
|
function resetInput(states) {
|
|
@@ -429,7 +473,7 @@ window.__ModuleLoader__.load({
|
|
|
429
473
|
syncLocalProgress(result);
|
|
430
474
|
var word = await panelRequest("/next-word");
|
|
431
475
|
model.backend[1]({ mode: "daemon", word: word });
|
|
432
|
-
} catch (_) { model.backend[1](
|
|
476
|
+
} catch (_) { model.backend[1](daemonFailureState(model.backend[0])); }
|
|
433
477
|
}
|
|
434
478
|
|
|
435
479
|
function shakeCard(states) {
|
|
@@ -437,6 +481,31 @@ window.__ModuleLoader__.load({
|
|
|
437
481
|
setTimeout(function () { states.shaking[1](false); }, SHAKE_MS);
|
|
438
482
|
}
|
|
439
483
|
|
|
484
|
+
function startDrag(model, event) {
|
|
485
|
+
if (event.target.closest("button,a,input,textarea,select")) return;
|
|
486
|
+
var card = model.refs.card.current;
|
|
487
|
+
if (!card || event.button !== 0) return;
|
|
488
|
+
event.preventDefault();
|
|
489
|
+
var rect = card.getBoundingClientRect();
|
|
490
|
+
var offsetX = event.clientX - rect.left;
|
|
491
|
+
var offsetY = event.clientY - rect.top;
|
|
492
|
+
var last = null;
|
|
493
|
+
function move(ev) {
|
|
494
|
+
last = {
|
|
495
|
+
x: Math.min(Math.max(ev.clientX - offsetX, 0), Math.max(window.innerWidth - rect.width, 0)),
|
|
496
|
+
y: Math.min(Math.max(ev.clientY - offsetY, 0), Math.max(window.innerHeight - 48, 0)),
|
|
497
|
+
};
|
|
498
|
+
model.states.pos[1](last);
|
|
499
|
+
}
|
|
500
|
+
function up() {
|
|
501
|
+
document.removeEventListener("pointermove", move);
|
|
502
|
+
document.removeEventListener("pointerup", up);
|
|
503
|
+
if (last) try { posStore.set(last); } catch (_) {}
|
|
504
|
+
}
|
|
505
|
+
document.addEventListener("pointermove", move);
|
|
506
|
+
document.addEventListener("pointerup", up);
|
|
507
|
+
}
|
|
508
|
+
|
|
440
509
|
function useCardEffects(model) {
|
|
441
510
|
var word = displayedWord(model);
|
|
442
511
|
React.useEffect(function () { resetInput(model.states); }, [word && word.name]);
|
|
@@ -446,6 +515,29 @@ window.__ModuleLoader__.load({
|
|
|
446
515
|
React.useEffect(function () {
|
|
447
516
|
return function () { if (model.refs.hideTimer.current) clearTimeout(model.refs.hideTimer.current); };
|
|
448
517
|
}, []);
|
|
518
|
+
React.useEffect(function () {
|
|
519
|
+
// 页面内手动唤醒:Ctrl+Alt+V 唤出/隐藏(dsh 标签页聚焦时有效)
|
|
520
|
+
function onSummon(event) {
|
|
521
|
+
if (!event.ctrlKey || !event.altKey || event.key.toLowerCase() !== "v") return;
|
|
522
|
+
event.preventDefault();
|
|
523
|
+
if (model.states.visible[0]) { hideCard(model, true); return; }
|
|
524
|
+
model.lifecycle.tracker.state.current.suppressed = false;
|
|
525
|
+
model.states.visible[1](true);
|
|
526
|
+
}
|
|
527
|
+
document.addEventListener("keydown", onSummon);
|
|
528
|
+
return function () { document.removeEventListener("keydown", onSummon); };
|
|
529
|
+
}, [model.states.visible[0]]);
|
|
530
|
+
React.useEffect(function () {
|
|
531
|
+
// 恢复上次拖到的位置;越出当前视口则放弃,回默认右下角
|
|
532
|
+
try {
|
|
533
|
+
var saved = posStore.getSnapshot();
|
|
534
|
+
if (saved && Number.isFinite(saved.x) && Number.isFinite(saved.y) &&
|
|
535
|
+
saved.x >= 0 && saved.y >= 0 &&
|
|
536
|
+
saved.x < window.innerWidth - 60 && saved.y < window.innerHeight - 48) {
|
|
537
|
+
model.states.pos[1]({ x: saved.x, y: saved.y });
|
|
538
|
+
}
|
|
539
|
+
} catch (_) {}
|
|
540
|
+
}, []);
|
|
449
541
|
}
|
|
450
542
|
|
|
451
543
|
function useCardTyping(model) {
|
|
@@ -454,7 +546,8 @@ window.__ModuleLoader__.load({
|
|
|
454
546
|
useTyping({
|
|
455
547
|
visible: states.visible[0], focused: states.focused[0],
|
|
456
548
|
word: word && word.name,
|
|
457
|
-
busy: states.busy[0]
|
|
549
|
+
busy: states.busy[0] || model.selection.remoteUnavailable,
|
|
550
|
+
typed: states.typed[0], revealed: states.revealed[0],
|
|
458
551
|
card: model.refs.card, setTyped: states.typed[1], setTypos: states.typos[1],
|
|
459
552
|
setRevealed: states.revealed[1], setPeeked: states.peeked[1],
|
|
460
553
|
hide: function () { hideCard(model, true); },
|
|
@@ -468,13 +561,21 @@ window.__ModuleLoader__.load({
|
|
|
468
561
|
return h(React.Fragment, null, h("style", { id: STYLE_ID }, css),
|
|
469
562
|
h("section", {
|
|
470
563
|
className: "vg-card", tabIndex: 0, ref: model.refs.card, "aria-label": "VibeGap 单词卡",
|
|
564
|
+
style: states.pos[0]
|
|
565
|
+
? { left: states.pos[0].x + "px", top: states.pos[0].y + "px", right: "auto", bottom: "auto" }
|
|
566
|
+
: undefined,
|
|
471
567
|
onFocus: function (event) { if (event.target === event.currentTarget) states.focused[1](true); },
|
|
472
568
|
onBlur: function () { states.focused[1](false); },
|
|
473
569
|
onMouseDown: function (event) {
|
|
474
570
|
if (!event.target.closest("button,a,input,textarea,select")) model.refs.card.current.focus();
|
|
475
571
|
},
|
|
476
572
|
},
|
|
477
|
-
h("div", {
|
|
573
|
+
h("div", {
|
|
574
|
+
className: "vg-head", title: "按住拖动",
|
|
575
|
+
onPointerDown: function (event) { startDrag(model, event); },
|
|
576
|
+
},
|
|
577
|
+
h("div", { className: "vg-title" }, "VibeGap"),
|
|
578
|
+
model.progressText ? h("span", { className: "vg-prog" }, model.progressText) : null,
|
|
478
579
|
h(Button, { className: "vg-close", size: "sm", variant: "ghost", title: "隐藏",
|
|
479
580
|
onClick: function () { hideCard(model, true); } }, "×")),
|
|
480
581
|
states.banner[0] ? h("div", { className: "vg-banner", role: "status" }, states.banner[0]) : null,
|
|
@@ -489,6 +590,7 @@ window.__ModuleLoader__.load({
|
|
|
489
590
|
word: word, typed: states.typed[0], typos: states.typos[0], revealed: states.revealed[0],
|
|
490
591
|
focused: states.focused[0], shaking: states.shaking[0], download: states.download[0],
|
|
491
592
|
outcome: states.outcome[0], setDownload: states.download[1], autoPronounce: model.autoPronounce,
|
|
593
|
+
connectionLost: model.selection.connectionLost,
|
|
492
594
|
pronounce: function () { if (word) safePronounce(word.name); },
|
|
493
595
|
toggleAuto: function () { prefsStore.set({ autoPronounce: !model.autoPronounce }); },
|
|
494
596
|
};
|
|
@@ -502,12 +604,24 @@ window.__ModuleLoader__.load({
|
|
|
502
604
|
var prefs = useSnapshot(prefsStore);
|
|
503
605
|
var states = useCardStates();
|
|
504
606
|
var refs = { card: useCell(null), hideTimer: useCell(null), banner: useCell(null) };
|
|
505
|
-
var lifecycle = useLifecycleBridge(sessionState, states, refs);
|
|
506
607
|
var backend = useDaemonBackend();
|
|
608
|
+
var lifecycle = useLifecycleBridge(
|
|
609
|
+
sessionState, states, refs, canAutoPopup(backend[0].mode),
|
|
610
|
+
);
|
|
507
611
|
var selection = activeSelection(useWordSelection(progress), backend);
|
|
612
|
+
var word = selection.word;
|
|
613
|
+
var progressText = "";
|
|
614
|
+
if (selection.isDaemon && word && Number.isFinite(Number(word.total))) {
|
|
615
|
+
progressText = word.position + "/" + word.total +
|
|
616
|
+
(selection.connectionLost ? " · 桌面端重连中" : " · 共享桌面进度");
|
|
617
|
+
} else if (selection.ordered.length) {
|
|
618
|
+
var cursorShown = Math.min(Math.max(Number(progress.cursor) || 0, 0), selection.ordered.length - 1);
|
|
619
|
+
progressText = cursorShown + "/" + selection.ordered.length + " · 本地进度";
|
|
620
|
+
}
|
|
508
621
|
var model = {
|
|
509
622
|
states: states, refs: refs, lifecycle: lifecycle, selection: selection, backend: backend,
|
|
510
623
|
autoPronounce: !prefs || prefs.autoPronounce !== false,
|
|
624
|
+
progressText: progressText,
|
|
511
625
|
};
|
|
512
626
|
useCardEffects(model);
|
|
513
627
|
useCardTyping(model);
|
|
@@ -516,6 +630,15 @@ window.__ModuleLoader__.load({
|
|
|
516
630
|
|
|
517
631
|
exports.apply = apply;
|
|
518
632
|
exports.inject = inject;
|
|
633
|
+
exports.__test = {
|
|
634
|
+
activeSelection: activeSelection,
|
|
635
|
+
canAutoPopup: canAutoPopup,
|
|
636
|
+
createTypingHandler: createTypingHandler,
|
|
637
|
+
daemonFailureState: daemonFailureState,
|
|
638
|
+
normalizeWords: normalizeWords,
|
|
639
|
+
transitionNotice: transitionNotice,
|
|
640
|
+
useTyping: useTyping,
|
|
641
|
+
};
|
|
519
642
|
return module.exports;
|
|
520
643
|
},
|
|
521
644
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vibegap",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Self-contained vocabulary flashcards for DeepSeek Harness web sessions",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
},
|
|
11
11
|
"keywords": ["dsh-plugin", "vibegap", "vocabulary", "productivity"],
|
|
12
12
|
"files": ["lib", "cordis.patch.yml", "README.md"],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node --test test/*.test.js"
|
|
15
|
+
},
|
|
13
16
|
"repository": {
|
|
14
17
|
"type": "git",
|
|
15
18
|
"url": "git+https://github.com/ktao732084-arch/vibegap.git",
|