dsh-focus-overlay 0.8.1 → 0.10.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.en.md +7 -4
- package/README.md +6 -4
- package/lib/client.js +844 -52
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
- **Hide the header and composer** — a thin top bar remains, the input area is tucked away
|
|
23
23
|
- **Fold tool calls** — an assistant turn collapses into a one-line summary
|
|
24
24
|
- **Quick navigation** — a right-side node navbar with hover preview and click-to-jump
|
|
25
|
+
- **In-focus composer** — a compact input dock appears at the live edge and shares the main composer's draft; click away and it folds into a dot, one click back; AI questions and approvals expand into an in-place answer card, so you never leave focus to reply
|
|
25
26
|
- **Auto-focus & reminders** — after a reply completes, optionally auto-enter focus at your question; while in focus, get a toast for a new reply or when the AI is waiting on you
|
|
26
27
|
- **Hotkey F** — press `F` anywhere to enter focus mode instantly (toggleable in settings); typing in an input never triggers it
|
|
27
28
|
|
|
@@ -56,13 +57,14 @@ Once in focus mode, an assistant turn no longer shows every step — it folds in
|
|
|
56
57
|
| Tool-call folding | Tool calls / commands / context injections fold into a categorized summary line (commands / edits / searches / reads / directory listings / subagents / todos / goals / workflows / skills / questions / plans / background jobs) |
|
|
57
58
|
| Precise scroll preservation | Opens at the message you're reading in the chat (aligned by `seq`), not from the top |
|
|
58
59
|
| Right-side node navbar | One dot per user message; active pill follows scroll, hover preview, click to jump, auto-hidden under 2 messages |
|
|
59
|
-
| Back to latest | A centered "↓
|
|
60
|
+
| Back to latest | A centered "↓" floating button when you scroll away from the bottom with no draft |
|
|
61
|
+
| In-focus composer | The compact input bar appears at the live edge and shares the main composer's draft — `Enter` sends (queued while the AI runs); with a draft, clicking the conversation folds it into a blue-dot pill, one click to expand |
|
|
60
62
|
| File mentions | Wires `chatFileMentions`, turning inline-code tokens that name real files into clickable links |
|
|
61
63
|
| i18n | Chinese / English copy, follows the UI language |
|
|
62
64
|
| Plugin configuration card | A collapsible card under "Settings → Plugins → Plugin configuration": navbar toggle, open-position strategy, text-area width — persisted to `localStorage` |
|
|
63
65
|
| Auto-enter focus | After a reply **completes normally**, auto-open focus at your question; abnormal endings (stop / error / max-tokens / interrupt) never fire |
|
|
64
66
|
| F hotkey | Press `F` anywhere to enter focus mode instantly (default on, toggleable in settings); never fires while typing in an input, auto-repeat ignored |
|
|
65
|
-
| Reply / waiting reminders | In focus, a "New reply ready + View" toast on completion;
|
|
67
|
+
| Reply / waiting reminders | In focus, a "New reply ready + View" toast on completion; a question/approval raises "AI is waiting for your reply + Answer", expanding an in-place answer card (options / free text / allow / deny) — auto-cleared once answered, never leaving focus |
|
|
66
68
|
| DSH-better-sidebar compatibility | While in focus, auto-hide its top-right panel toggle buttons and any open right/bottom panels (and release the squeezed layout); restored on exit |
|
|
67
69
|
|
|
68
70
|
## Install
|
|
@@ -108,8 +110,9 @@ Restart `dsh web` after install.
|
|
|
108
110
|
|
|
109
111
|
1. Open a session and click the **"Focus"** button in the session header action row (or press `F` — toggleable in settings).
|
|
110
112
|
2. You enter the full-screen focus view: a thin top bar (session title + exit) and the conversation only, with tool steps folded into summary lines.
|
|
111
|
-
3. Hover the right-side dots to preview, click to jump;
|
|
112
|
-
4.
|
|
113
|
+
3. Hover the right-side dots to preview, click to jump; the compact input bar appears when you reach the very bottom (a "↓" button shows when you scroll away with no draft).
|
|
114
|
+
4. Reply right in the bar: `Enter` sends, `Shift+Enter` newlines (queued while the AI runs), drafts shared with the main composer; clicking the conversation folds it into a blue-dot pill. AI questions/approvals raise an "Answer" card for in-place options, free text, or approvals.
|
|
115
|
+
5. `Esc` peels back layer by layer (answer card → input bar → focus mode); or click "Exit focus" to return — the original UI is untouched.
|
|
113
116
|
|
|
114
117
|
> **Compatibility note**: if [DSH-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) is also installed, entering focus mode auto-hides its two top-right panel toggle buttons and any open right/bottom panels (releasing the squeezed layout); they're restored as they were on exit, without touching that plugin's layout state.
|
|
115
118
|
|
package/README.md
CHANGED
|
@@ -54,13 +54,14 @@
|
|
|
54
54
|
| 工具调用折叠 | 工具调用 / 命令 / 上下文注入按类型计数折叠成摘要行(命令/编辑/搜索/读取/列目录/子代理/待办/目标/工作流/技能/提问/计划/后台任务) |
|
|
55
55
|
| 精确保留位置 | 进入专注时定位到聊天视图中正在阅读的消息(按 `seq` 对齐),而非从头开始 |
|
|
56
56
|
| 右侧节点导航条 | 每个 user 消息一个圆点,激活药丸跟随滚动、悬停预览、点击平滑跳转,少于 2 条自动隐藏 |
|
|
57
|
-
| 回到最新 |
|
|
57
|
+
| 回到最新 | 无草稿且离开底部时,底部居中显示「↓」悬浮按钮 |
|
|
58
|
+
| 专注内输入条 | 滚到最底部自动展开精简输入框,与主输入框共享同一份草稿;`Enter` 发送(AI 忙时自动排队);有草稿时点击对话区自动收起为带蓝点的圆钮,一键展开续写 |
|
|
58
59
|
| 文件提及 | 接 `chatFileMentions`,内联代码命中真实文件时变成可点链接 |
|
|
59
60
|
| i18n | 中 / 英文案,跟随界面语言 |
|
|
60
61
|
| 插件配置卡片 | 在「设置 → 插件 → 插件配置」中的可折叠卡片,含导航条开关、打开定位策略、文字区宽度,持久化到 `localStorage` |
|
|
61
62
|
| 自动进入专注 | 回复**正常完成**后自动打开专注并定位到本轮你的提问;异常结束(停止 / 报错 / 超 token / 打断)不触发 |
|
|
62
63
|
| F 键快捷键 | 任意界面按 `F` 立即进入专注(默认开,可在设置中关闭);输入框内打字不触发,长按自动忽略 |
|
|
63
|
-
| 回复 / 等待提醒 | 专注中回复完成弹「新回复已生成 + 查看」;AI 提问 / 审批等待时弹「AI 正在等待你的回复 +
|
|
64
|
+
| 回复 / 等待提醒 | 专注中回复完成弹「新回复已生成 + 查看」;AI 提问 / 审批等待时弹「AI 正在等待你的回复 + 去回答」,原地展开答题卡(选项 / 自由文本 / 允许 / 拒绝),作答后自动消失——全程无需退出专注 |
|
|
64
65
|
| 兼容 DSH-better-sidebar | 进入专注时自动隐藏其右上角的收放面板按钮与已开启的右侧/底部面板,并释放被挤压的布局;退出专注后按原状态还原 |
|
|
65
66
|
|
|
66
67
|
## 安装
|
|
@@ -106,8 +107,9 @@ dsh web
|
|
|
106
107
|
|
|
107
108
|
1. 打开任意会话,点击标题栏操作区的 **「专注」** 按钮(或直接按 `F` 键,可在设置中关闭该快捷键)。
|
|
108
109
|
2. 进入全屏专注视图:顶部只有一条极窄栏(会话标题 + 退出),正文只显示你与 AI 的对话,工具步骤折叠为摘要行。
|
|
109
|
-
3.
|
|
110
|
-
4.
|
|
110
|
+
3. 右侧导航圆点可悬停预览、点击跳转;滚到最底部时底部居中自动展开精简输入条(无草稿离开底部则显示「↓」按钮)。
|
|
111
|
+
4. 在输入条里直接回复:`Enter` 发送、`Shift+Enter` 换行(AI 忙时自动排队),草稿与主输入框互通;点击对话区域自动收起为带蓝点的圆钮。AI 提问 / 审批时弹「去回答」卡片,就地进行选项 / 文本 / 审批作答。
|
|
112
|
+
5. 按 `Esc` 逐层退出(答题卡 → 输入条 → 专注模式),或点「退出专注」返回原界面,原位置/状态保持不变。
|
|
111
113
|
|
|
112
114
|
> **兼容提示**:若同时安装了 [DSH-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar),进入专注模式会自动隐藏其右上角的两个收放面板按钮以及已开启的右侧/底部面板(并释放被挤压的布局);退出专注后按原状态恢复,不会改动该插件的面板布局。
|
|
113
115
|
|
package/lib/client.js
CHANGED
|
@@ -258,14 +258,6 @@ window.__ModuleLoader__.load({
|
|
|
258
258
|
anchorSeq: lastUserSeq(nodes)
|
|
259
259
|
};
|
|
260
260
|
}
|
|
261
|
-
/** True while the session has a pending interaction — the AI is blocked waiting
|
|
262
|
-
* for the user (to answer a question or approve an action). It stays true for
|
|
263
|
-
* the whole wait (`running` remains true throughout) and clears as soon as the
|
|
264
|
-
* user answers, so it is the live signal for "questions still unanswered". */
|
|
265
|
-
function hasPendingInteraction(snap) {
|
|
266
|
-
const pending = snap && snap.pending;
|
|
267
|
-
return Array.isArray(pending) && pending.length > 0;
|
|
268
|
-
}
|
|
269
261
|
/** True once the persisted onboarding flag matches the current intro version.
|
|
270
262
|
* Kept as a pure comparison so the "already seen?" decision is testable and
|
|
271
263
|
* can be versioned: bump the version to re-show the intro after a major change. */
|
|
@@ -286,6 +278,146 @@ window.__ModuleLoader__.load({
|
|
|
286
278
|
const k = typeof e.key === "string" ? e.key : "";
|
|
287
279
|
return k === "f" || k === "F";
|
|
288
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Decide the bottom dock's form from the live facts. Priority order mirrors the
|
|
283
|
+
* agreed design:
|
|
284
|
+
*
|
|
285
|
+
* 1. pending (the AI is blocked waiting) owns the region — the answer card when
|
|
286
|
+
* the user opened it (the waiting toast morphs in place), otherwise the
|
|
287
|
+
* waiting toast itself. A plain prompt cannot answer `ask_user_question`, so
|
|
288
|
+
* while pending the input bar is suppressed entirely (the official main view
|
|
289
|
+
* likewise lets the question UI take over the composer seat).
|
|
290
|
+
* 2. the input bar while the user is IN it: focused, or the one-render handoff
|
|
291
|
+
* from a pill click (`engaged`, focus not landed yet). With a draft this is
|
|
292
|
+
* the only thing that keeps the bar up — clicking the conversation area
|
|
293
|
+
* blurs the textarea and the bar folds itself into the pill (draft safe,
|
|
294
|
+
* blue dot, one click back). There is no manual hide.
|
|
295
|
+
* 3. also the bar, as ambient chrome, while empty and reading at the live
|
|
296
|
+
* edge — a slim placeholder offering the composer.
|
|
297
|
+
* 4. otherwise: a draft (unfocused) shows the pill; no draft and scrolled away
|
|
298
|
+
* shows the jump-to-bottom button.
|
|
299
|
+
*/
|
|
300
|
+
function bottomForm(o) {
|
|
301
|
+
if (o.pending) return o.cardOpen ? "card" : "toast";
|
|
302
|
+
if (o.engaged || o.focused || o.draftEmpty && o.inZone) return "bar";
|
|
303
|
+
return o.draftEmpty ? "tobottom" : "pill";
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Bottom-zone membership with hysteresis: entering requires the viewport edge
|
|
307
|
+
* within {@link BOTTOM_ENTER_PX} of the scroll end, leaving requires drifting
|
|
308
|
+
* past {@link BOTTOM_EXIT_PX}. Between the two thresholds the previous verdict
|
|
309
|
+
* stands, so slow scrolling near the end (or streaming content quietly growing
|
|
310
|
+
* the scrollHeight under a still reader) never flaps the dock between forms.
|
|
311
|
+
*/
|
|
312
|
+
function bottomZoneAfter(prev, distance) {
|
|
313
|
+
if (distance <= 48) return true;
|
|
314
|
+
if (distance >= 160) return false;
|
|
315
|
+
return prev;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Index of the conversation-turn dot the nav rail should highlight, from live
|
|
319
|
+
* scroll geometry. The rail is a position indicator, so the decision is pure
|
|
320
|
+
* math over measured anchor tops (document order) plus the distance to the
|
|
321
|
+
* scroll end:
|
|
322
|
+
*
|
|
323
|
+
* 1. Bottom force: within {@link BOTTOM_ENTER_PX} of the scroll end the last
|
|
324
|
+
* turn owns the highlight, whatever its height. Without this rule a short
|
|
325
|
+
* final turn could never light its own dot — with less than a viewport of
|
|
326
|
+
* content below its anchor, that anchor physically cannot reach the top
|
|
327
|
+
* threshold even pinned at the very bottom, so the highlight would stick
|
|
328
|
+
* to an earlier turn forever.
|
|
329
|
+
* 2. Geometric scan: the last anchor whose top sits at/above the
|
|
330
|
+
* {@link NAV_TOP_PX} line — "which turn is at the top of the viewport".
|
|
331
|
+
* Non-finite entries (a stale anchor slot after the item list shifted) are
|
|
332
|
+
* skipped without stopping the scan, matching the old ref-null behavior.
|
|
333
|
+
* 3. Top clamp: every real anchor still below the line means the reader is
|
|
334
|
+
* above the first message's threshold (scrollTop ≈ 0 with leading
|
|
335
|
+
* padding), and the first turn owns the position.
|
|
336
|
+
*
|
|
337
|
+
* A bottom-forced index may land on a stale slot; the caller maps it back to
|
|
338
|
+
* the nearest existing anchor.
|
|
339
|
+
*/
|
|
340
|
+
function activeNavIndex(tops, bottomDistance, topPx = 96, bottomPx = 48) {
|
|
341
|
+
if (!tops.length) return -1;
|
|
342
|
+
if (bottomDistance <= bottomPx) return tops.length - 1;
|
|
343
|
+
let cur = -1;
|
|
344
|
+
for (let i = 0; i < tops.length; i++) {
|
|
345
|
+
const top = tops[i];
|
|
346
|
+
if (!Number.isFinite(top)) continue;
|
|
347
|
+
if (top <= topPx) cur = i;
|
|
348
|
+
else break;
|
|
349
|
+
}
|
|
350
|
+
return cur >= 0 ? cur : 0;
|
|
351
|
+
}
|
|
352
|
+
/** A question is answered when at least one option is selected or the custom
|
|
353
|
+
* text is non-blank — the same completeness rule the official question
|
|
354
|
+
* composer applies before enabling submit. */
|
|
355
|
+
function questionAnswered(d) {
|
|
356
|
+
if (!d) return false;
|
|
357
|
+
if (d.selected.length > 0) return true;
|
|
358
|
+
return d.custom.trim() !== "";
|
|
359
|
+
}
|
|
360
|
+
/** The whole batch must be answered: one `ask()` is one answer covering every
|
|
361
|
+
* question, never split per question. An empty question list is never
|
|
362
|
+
* complete (nothing to send — submit stays disabled); malformed null entries
|
|
363
|
+
* are skipped, matching encodeAnswer, so one bad row cannot deadlock the
|
|
364
|
+
* answerable majority. */
|
|
365
|
+
function allAnswered(questions, drafts) {
|
|
366
|
+
if (!questions || questions.length === 0) return false;
|
|
367
|
+
for (const q of questions) {
|
|
368
|
+
if (!q) continue;
|
|
369
|
+
if (!questionAnswered(drafts[q.id])) return false;
|
|
370
|
+
}
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Encode the complete answer batch in the wire shape `question/respond`
|
|
375
|
+
* expects (`AskUserQuestionAnswer`). Mirrors the official encoder's
|
|
376
|
+
* exclusivity rule: a single-select question carries EITHER the selected
|
|
377
|
+
* option OR the custom text (`selected` is cleared when custom is present —
|
|
378
|
+
* never both, which would be a contradictory answer); multi-select may carry
|
|
379
|
+
* both. Option labels go back verbatim — the harness's recommendation suffix
|
|
380
|
+
* is part of the label the asker must see echoed — and custom text is sent
|
|
381
|
+
* trimmed, only when present.
|
|
382
|
+
*/
|
|
383
|
+
function encodeAnswer(questions, drafts) {
|
|
384
|
+
const answers = [];
|
|
385
|
+
for (const q of questions || []) {
|
|
386
|
+
if (!q) continue;
|
|
387
|
+
const d = drafts[q.id] || {
|
|
388
|
+
selected: [],
|
|
389
|
+
custom: ""
|
|
390
|
+
};
|
|
391
|
+
const custom = d.custom.trim();
|
|
392
|
+
const selected = custom === "" || q.multiSelect ? [...d.selected] : [];
|
|
393
|
+
const item = {
|
|
394
|
+
id: q.id,
|
|
395
|
+
selected
|
|
396
|
+
};
|
|
397
|
+
if (custom) item.custom = custom;
|
|
398
|
+
answers.push(item);
|
|
399
|
+
}
|
|
400
|
+
return { answers };
|
|
401
|
+
}
|
|
402
|
+
/** Split the harness's recommendation marker off an option label for display
|
|
403
|
+
* (the marker becomes a badge); the full label stays what the wire echoes
|
|
404
|
+
* back. Lenient like the official parser: ASCII or full-width parentheses,
|
|
405
|
+
* "recommended" in any case, or the Chinese 推荐. */
|
|
406
|
+
function parseRecommendedLabel(label) {
|
|
407
|
+
if (typeof label !== "string") return {
|
|
408
|
+
label,
|
|
409
|
+
recommended: false
|
|
410
|
+
};
|
|
411
|
+
const rest = label.replace(/\s*[((]\s*(?:recommended|推荐)\s*[))]\s*$/i, "");
|
|
412
|
+
if (rest === label || rest === "") return {
|
|
413
|
+
label,
|
|
414
|
+
recommended: false
|
|
415
|
+
};
|
|
416
|
+
return {
|
|
417
|
+
label: rest,
|
|
418
|
+
recommended: true
|
|
419
|
+
};
|
|
420
|
+
}
|
|
289
421
|
//#endregion
|
|
290
422
|
//#region src/client/settings.ts
|
|
291
423
|
const KEY = "dsh-focus-overlay:prefs";
|
|
@@ -350,6 +482,408 @@ window.__ModuleLoader__.load({
|
|
|
350
482
|
}
|
|
351
483
|
};
|
|
352
484
|
//#endregion
|
|
485
|
+
//#region src/client/Composer.tsx
|
|
486
|
+
/**
|
|
487
|
+
* The focus-mode bottom dock: the compact composer region that replaces the
|
|
488
|
+
* old lone "jump to bottom" button at the bottom center of the overlay.
|
|
489
|
+
*
|
|
490
|
+
* Forms (mutually exclusive, decided by the pure `bottomForm` selector):
|
|
491
|
+
* - `bar` the compact input bar — the shared official draft, Enter sends
|
|
492
|
+
* - `pill` the collapsed affordance after a manual hide (blue dot = draft)
|
|
493
|
+
* - `tobottom` the classic jump-to-bottom button
|
|
494
|
+
* - `card` the pending answer card (options / free text / approve-deny),
|
|
495
|
+
* opened in place from the waiting toast
|
|
496
|
+
* (`toast` is decided here too, but rendered by the overlay's toast element.)
|
|
497
|
+
*
|
|
498
|
+
* The bar shares the MAIN composer's draft through the official per-session
|
|
499
|
+
* input machine (`conversation.input.for(binding.ctx)`): setDraft / submit are
|
|
500
|
+
* the same public action face every session-scope component gets, so whatever
|
|
501
|
+
* the user typed before entering focus mode is already here, and whatever they
|
|
502
|
+
* type here is still in the main composer after leaving — one draft, both
|
|
503
|
+
* views, persisted by the host like always. When the input service is absent
|
|
504
|
+
* (older dsh), the bar degrades to a plugin-local draft sent through the
|
|
505
|
+
* session face's `prompt` verb — the same wire call the official default sink
|
|
506
|
+
* makes.
|
|
507
|
+
*/
|
|
508
|
+
/** Resolve the per-session input facade for a session's agent scope. Returns
|
|
509
|
+
* null when the conversation service (or its input registry) is absent so the
|
|
510
|
+
* caller can fall back to a plugin-local draft. Pure resolution, memoized per
|
|
511
|
+
* binding — the registry hands back the same stable facade every time. */
|
|
512
|
+
function useInputFace(conversation, actx) {
|
|
513
|
+
return (0, react.useMemo)(() => {
|
|
514
|
+
try {
|
|
515
|
+
if (conversation && conversation.input && actx) return conversation.input.for(actx);
|
|
516
|
+
} catch {}
|
|
517
|
+
return null;
|
|
518
|
+
}, [conversation, actx]);
|
|
519
|
+
}
|
|
520
|
+
/** Subscribe to the input machine's published state (draft, queue, reference
|
|
521
|
+
* occurrences). Mirrors the overlay's session-snapshot hook: read once, then
|
|
522
|
+
* re-read on every store flush. */
|
|
523
|
+
function useInputState(input) {
|
|
524
|
+
const [snap, setSnap] = (0, react.useState)(() => {
|
|
525
|
+
try {
|
|
526
|
+
return input && input.state ? input.state.getSnapshot() : null;
|
|
527
|
+
} catch {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
(0, react.useEffect)(() => {
|
|
532
|
+
if (!input || !input.state) {
|
|
533
|
+
setSnap(null);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
try {
|
|
537
|
+
setSnap(input.state.getSnapshot());
|
|
538
|
+
return input.state.subscribe(() => setSnap(input.state.getSnapshot()));
|
|
539
|
+
} catch {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
}, [input]);
|
|
543
|
+
return snap;
|
|
544
|
+
}
|
|
545
|
+
function OptionRow(props) {
|
|
546
|
+
const { t, index, label, description, multi, on, onToggle } = props;
|
|
547
|
+
const parsed = parseRecommendedLabel(label);
|
|
548
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
549
|
+
type: "button",
|
|
550
|
+
className: "fm-opt" + (on ? " fm-opt-on" : ""),
|
|
551
|
+
onClick: onToggle,
|
|
552
|
+
"aria-pressed": on,
|
|
553
|
+
children: [multi ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
554
|
+
className: "fm-opt-check" + (on ? " fm-opt-check-on" : ""),
|
|
555
|
+
children: on ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCheckOutline14, {}) : null
|
|
556
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
557
|
+
className: "fm-opt-num",
|
|
558
|
+
children: index + 1
|
|
559
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
560
|
+
className: "fm-opt-copy",
|
|
561
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
562
|
+
className: "fm-opt-line",
|
|
563
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
564
|
+
className: "fm-opt-label",
|
|
565
|
+
children: parsed.label
|
|
566
|
+
}), parsed.recommended ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
567
|
+
className: "fm-opt-badge",
|
|
568
|
+
children: t("answer.recommended")
|
|
569
|
+
}) : null]
|
|
570
|
+
}), description ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
571
|
+
className: "fm-opt-desc",
|
|
572
|
+
children: description
|
|
573
|
+
}) : null]
|
|
574
|
+
})]
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
function AnswerCard(props) {
|
|
578
|
+
const { t, width, wait, onClose } = props;
|
|
579
|
+
const isQuestion = wait.kind === "question";
|
|
580
|
+
const questions = isQuestion ? wait.payload && wait.payload.questions ? wait.payload.questions : [] : [];
|
|
581
|
+
const [drafts, setDrafts] = (0, react.useState)({});
|
|
582
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
583
|
+
const [error, setError] = (0, react.useState)(null);
|
|
584
|
+
const patch = (id, next) => setDrafts((prev) => {
|
|
585
|
+
const cur = prev[id] || {
|
|
586
|
+
selected: [],
|
|
587
|
+
custom: ""
|
|
588
|
+
};
|
|
589
|
+
return {
|
|
590
|
+
...prev,
|
|
591
|
+
[id]: {
|
|
592
|
+
...cur,
|
|
593
|
+
...next
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
});
|
|
597
|
+
const toggle = (qid, multi, label) => setDrafts((prev) => {
|
|
598
|
+
const cur = prev[qid] || {
|
|
599
|
+
selected: [],
|
|
600
|
+
custom: ""
|
|
601
|
+
};
|
|
602
|
+
if (multi) {
|
|
603
|
+
const selected = cur.selected.includes(label) ? cur.selected.filter((l) => l !== label) : [...cur.selected, label];
|
|
604
|
+
return {
|
|
605
|
+
...prev,
|
|
606
|
+
[qid]: {
|
|
607
|
+
...cur,
|
|
608
|
+
selected
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
return {
|
|
613
|
+
...prev,
|
|
614
|
+
[qid]: {
|
|
615
|
+
selected: [label],
|
|
616
|
+
custom: ""
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
});
|
|
620
|
+
const respond = async (result) => {
|
|
621
|
+
setBusy(true);
|
|
622
|
+
setError(null);
|
|
623
|
+
try {
|
|
624
|
+
const receipt = await wait.respond(result);
|
|
625
|
+
if (receipt && receipt.accepted === false) {
|
|
626
|
+
setError(t("answer.rejected") + (receipt.reason ? `: ${receipt.reason}` : ""));
|
|
627
|
+
setBusy(false);
|
|
628
|
+
}
|
|
629
|
+
} catch (err) {
|
|
630
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
631
|
+
setBusy(false);
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
const submitAnswers = () => {
|
|
635
|
+
if (!allAnswered(questions, drafts)) {
|
|
636
|
+
setError(t("answer.incomplete"));
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
respond({
|
|
640
|
+
ok: true,
|
|
641
|
+
value: {
|
|
642
|
+
sessionId: wait.sessionId,
|
|
643
|
+
answer: encodeAnswer(questions, drafts)
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
};
|
|
647
|
+
const decide = (outcome) => respond({
|
|
648
|
+
ok: true,
|
|
649
|
+
value: {
|
|
650
|
+
sessionId: wait.sessionId,
|
|
651
|
+
approvalId: wait.payload.approvalId,
|
|
652
|
+
outcome
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
656
|
+
className: "fm-card" + (isQuestion ? "" : " fm-card-approval"),
|
|
657
|
+
style: { maxWidth: Math.min(width, 748) },
|
|
658
|
+
children: [
|
|
659
|
+
isQuestion ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
660
|
+
className: "fm-card-head",
|
|
661
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
662
|
+
className: "fm-card-heading",
|
|
663
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
664
|
+
className: "fm-card-eyebrow",
|
|
665
|
+
children: t("reply.waiting")
|
|
666
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
667
|
+
className: "fm-card-title",
|
|
668
|
+
children: t("answer.title")
|
|
669
|
+
})]
|
|
670
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
671
|
+
type: "button",
|
|
672
|
+
className: "fm-card-close",
|
|
673
|
+
title: t("answer.close"),
|
|
674
|
+
"aria-label": t("answer.close"),
|
|
675
|
+
onClick: onClose,
|
|
676
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
|
|
677
|
+
})]
|
|
678
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
679
|
+
className: "fm-card-head",
|
|
680
|
+
children: [
|
|
681
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "fm-card-dot" }),
|
|
682
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
683
|
+
className: "fm-card-strip-title",
|
|
684
|
+
children: t("answer.approvalTitle")
|
|
685
|
+
}),
|
|
686
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
687
|
+
type: "button",
|
|
688
|
+
className: "fm-card-close",
|
|
689
|
+
title: t("answer.close"),
|
|
690
|
+
"aria-label": t("answer.close"),
|
|
691
|
+
onClick: onClose,
|
|
692
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
|
|
693
|
+
})
|
|
694
|
+
]
|
|
695
|
+
}),
|
|
696
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
697
|
+
className: "fm-card-body",
|
|
698
|
+
children: isQuestion ? questions.map((q, i) => {
|
|
699
|
+
const d = drafts[q.id] || {
|
|
700
|
+
selected: [],
|
|
701
|
+
custom: ""
|
|
702
|
+
};
|
|
703
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
704
|
+
className: "fm-card-q",
|
|
705
|
+
children: [
|
|
706
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
707
|
+
className: "fm-card-q-text",
|
|
708
|
+
children: [questions.length > 1 ? `${i + 1}. ` : "", q.question]
|
|
709
|
+
}),
|
|
710
|
+
q.detail ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
711
|
+
className: "fm-card-detail",
|
|
712
|
+
children: q.detail
|
|
713
|
+
}) : null,
|
|
714
|
+
q.options && q.options.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
715
|
+
className: "fm-card-opts",
|
|
716
|
+
role: q.multiSelect ? "group" : "radiogroup",
|
|
717
|
+
children: q.options.map((o, oi) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OptionRow, {
|
|
718
|
+
t,
|
|
719
|
+
index: oi,
|
|
720
|
+
label: o.label,
|
|
721
|
+
description: o.description,
|
|
722
|
+
multi: !!q.multiSelect,
|
|
723
|
+
on: d.selected.includes(o.label),
|
|
724
|
+
onToggle: () => toggle(q.id, !!q.multiSelect, o.label)
|
|
725
|
+
}, o.label))
|
|
726
|
+
}) : null,
|
|
727
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
728
|
+
className: "fm-card-custom",
|
|
729
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
730
|
+
className: "fm-card-field",
|
|
731
|
+
type: "text",
|
|
732
|
+
value: d.custom,
|
|
733
|
+
placeholder: t("answer.custom"),
|
|
734
|
+
onChange: (e) => patch(q.id, { custom: e.target.value })
|
|
735
|
+
})
|
|
736
|
+
})
|
|
737
|
+
]
|
|
738
|
+
}, q.id);
|
|
739
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
740
|
+
className: "fm-card-q",
|
|
741
|
+
children: [wait.payload && wait.payload.reason ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
742
|
+
className: "fm-card-q-text",
|
|
743
|
+
children: wait.payload.reason
|
|
744
|
+
}) : null, wait.payload && wait.payload.toolName ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
745
|
+
className: "fm-card-detail",
|
|
746
|
+
children: wait.payload.toolName
|
|
747
|
+
}) : null]
|
|
748
|
+
})
|
|
749
|
+
}),
|
|
750
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
751
|
+
className: "fm-card-foot",
|
|
752
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
753
|
+
className: "fm-card-err",
|
|
754
|
+
children: error
|
|
755
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
756
|
+
className: "fm-card-actions",
|
|
757
|
+
children: isQuestion ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
758
|
+
variant: "primary",
|
|
759
|
+
size: "sm",
|
|
760
|
+
disabled: busy,
|
|
761
|
+
onClick: submitAnswers,
|
|
762
|
+
children: t("answer.submit")
|
|
763
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
764
|
+
variant: "primary",
|
|
765
|
+
size: "sm",
|
|
766
|
+
disabled: busy,
|
|
767
|
+
onClick: () => decide("allowed-once"),
|
|
768
|
+
children: t("answer.allow")
|
|
769
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
770
|
+
variant: "outline",
|
|
771
|
+
size: "sm",
|
|
772
|
+
disabled: busy,
|
|
773
|
+
onClick: () => decide("rejected"),
|
|
774
|
+
children: t("answer.reject")
|
|
775
|
+
})] })
|
|
776
|
+
})]
|
|
777
|
+
})
|
|
778
|
+
]
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
function InputBar(props) {
|
|
782
|
+
const { t, width, value, queueCount, occCount, errorLine, textareaRef, onFocusChange, onChange, onSend } = props;
|
|
783
|
+
const empty = value.trim() === "";
|
|
784
|
+
const onKeyDown = (e) => {
|
|
785
|
+
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing && e.nativeEvent.keyCode !== 229) {
|
|
786
|
+
e.preventDefault();
|
|
787
|
+
if (!empty) onSend();
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
const autoGrow = (el) => {
|
|
791
|
+
el.style.height = "auto";
|
|
792
|
+
el.style.height = `${Math.min(el.scrollHeight, 120)}px`;
|
|
793
|
+
};
|
|
794
|
+
(0, react.useEffect)(() => {
|
|
795
|
+
if (textareaRef.current) autoGrow(textareaRef.current);
|
|
796
|
+
}, [value, textareaRef]);
|
|
797
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
798
|
+
className: "fm-bar",
|
|
799
|
+
style: { maxWidth: Math.min(width, 680) },
|
|
800
|
+
children: [
|
|
801
|
+
errorLine ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
802
|
+
className: "fm-bar-note fm-bar-note-error",
|
|
803
|
+
children: errorLine
|
|
804
|
+
}) : null,
|
|
805
|
+
occCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
806
|
+
className: "fm-bar-note",
|
|
807
|
+
children: t("composer.occurrences")
|
|
808
|
+
}) : null,
|
|
809
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
810
|
+
className: "fm-bar-row",
|
|
811
|
+
children: [
|
|
812
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
813
|
+
ref: textareaRef,
|
|
814
|
+
className: "fm-bar-text",
|
|
815
|
+
rows: 1,
|
|
816
|
+
value,
|
|
817
|
+
placeholder: t("composer.placeholder"),
|
|
818
|
+
onChange: (e) => {
|
|
819
|
+
onChange(e.target.value);
|
|
820
|
+
autoGrow(e.target);
|
|
821
|
+
},
|
|
822
|
+
onKeyDown,
|
|
823
|
+
onFocus: () => onFocusChange(true),
|
|
824
|
+
onBlur: () => onFocusChange(false)
|
|
825
|
+
}),
|
|
826
|
+
queueCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
827
|
+
className: "fm-bar-queue",
|
|
828
|
+
children: t("composer.queued", { n: queueCount })
|
|
829
|
+
}) : null,
|
|
830
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
831
|
+
type: "button",
|
|
832
|
+
className: "fm-bar-send",
|
|
833
|
+
title: t("composer.send"),
|
|
834
|
+
"aria-label": t("composer.send"),
|
|
835
|
+
disabled: empty,
|
|
836
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
837
|
+
onClick: onSend,
|
|
838
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSendOutline14, {})
|
|
839
|
+
})
|
|
840
|
+
]
|
|
841
|
+
})
|
|
842
|
+
]
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
function FocusBottomDock(props) {
|
|
846
|
+
const { t, width, form, wait, onCardClose, draft, queueCount, occCount, errorLine, textareaRef, onFocusChange, onDraftChange, onSend, onExpand, onJumpBottom } = props;
|
|
847
|
+
if (form === "toast") return null;
|
|
848
|
+
if (form === "card") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnswerCard, {
|
|
849
|
+
t,
|
|
850
|
+
width,
|
|
851
|
+
wait,
|
|
852
|
+
onClose: onCardClose
|
|
853
|
+
});
|
|
854
|
+
if (form === "bar") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InputBar, {
|
|
855
|
+
t,
|
|
856
|
+
width,
|
|
857
|
+
value: draft,
|
|
858
|
+
queueCount,
|
|
859
|
+
occCount,
|
|
860
|
+
errorLine,
|
|
861
|
+
textareaRef,
|
|
862
|
+
onFocusChange,
|
|
863
|
+
onChange: onDraftChange,
|
|
864
|
+
onSend
|
|
865
|
+
});
|
|
866
|
+
if (form === "pill") {
|
|
867
|
+
const hasDraft = draft.trim() !== "";
|
|
868
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
869
|
+
type: "button",
|
|
870
|
+
className: "fm-pill",
|
|
871
|
+
title: t("composer.expand"),
|
|
872
|
+
"aria-label": t("composer.expand"),
|
|
873
|
+
onClick: onExpand,
|
|
874
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, {}), hasDraft ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "fm-pill-dot" }) : null]
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
878
|
+
type: "button",
|
|
879
|
+
className: "fm-pill",
|
|
880
|
+
title: t("backToLatest"),
|
|
881
|
+
"aria-label": t("backToLatest"),
|
|
882
|
+
onClick: onJumpBottom,
|
|
883
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { size: 16 })
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
//#endregion
|
|
353
887
|
//#region src/client/FocusView.tsx
|
|
354
888
|
let focusOn = false;
|
|
355
889
|
let pendingAnchorKey = null;
|
|
@@ -490,32 +1024,41 @@ window.__ModuleLoader__.load({
|
|
|
490
1024
|
}
|
|
491
1025
|
}
|
|
492
1026
|
const [activeKey, setActiveKey] = (0, react.useState)(navKeys.length ? navKeys[navKeys.length - 1] : null);
|
|
493
|
-
const [
|
|
1027
|
+
const [zone, setZone] = (0, react.useState)(true);
|
|
494
1028
|
const rafId = (0, react.useRef)(null);
|
|
495
1029
|
const overlayRef = (0, react.useRef)(null);
|
|
1030
|
+
const [cardOpen, setCardOpen] = (0, react.useState)(false);
|
|
1031
|
+
const [dockFocused, setDockFocused] = (0, react.useState)(false);
|
|
1032
|
+
const [engaged, setEngaged] = (0, react.useState)(false);
|
|
1033
|
+
const [localDraft, setLocalDraft] = (0, react.useState)("");
|
|
1034
|
+
const taRef = (0, react.useRef)(null);
|
|
1035
|
+
const focusBarOnce = (0, react.useRef)(false);
|
|
496
1036
|
const computeActive = () => {
|
|
497
1037
|
if (!bodyEl) return null;
|
|
498
|
-
|
|
499
|
-
|
|
1038
|
+
const bodyTop = bodyEl.getBoundingClientRect().top;
|
|
1039
|
+
const i = activeNavIndex(navKeys.map((k) => {
|
|
500
1040
|
const el = anchors[k];
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
return cur;
|
|
1041
|
+
return el ? el.getBoundingClientRect().top - bodyTop : Number.POSITIVE_INFINITY;
|
|
1042
|
+
}), bottomDistance());
|
|
1043
|
+
for (let j = i; j >= 0; j--) if (anchors[navKeys[j]]) return navKeys[j];
|
|
1044
|
+
return null;
|
|
506
1045
|
};
|
|
507
|
-
const
|
|
508
|
-
if (!bodyEl) return
|
|
509
|
-
return bodyEl.scrollHeight - bodyEl.scrollTop - bodyEl.clientHeight
|
|
1046
|
+
const bottomDistance = () => {
|
|
1047
|
+
if (!bodyEl) return Number.POSITIVE_INFINITY;
|
|
1048
|
+
return bodyEl.scrollHeight - bodyEl.scrollTop - bodyEl.clientHeight;
|
|
510
1049
|
};
|
|
511
|
-
const
|
|
1050
|
+
const scheduleRecompute = () => {
|
|
512
1051
|
if (rafId.current !== null) return;
|
|
513
1052
|
rafId.current = requestAnimationFrame(() => {
|
|
514
1053
|
rafId.current = null;
|
|
515
1054
|
setActiveKey(computeActive());
|
|
516
|
-
|
|
1055
|
+
setZone((prev) => bottomZoneAfter(prev, bottomDistance()));
|
|
517
1056
|
});
|
|
518
1057
|
};
|
|
1058
|
+
const scheduleRef = (0, react.useRef)(scheduleRecompute);
|
|
1059
|
+
(0, react.useEffect)(() => {
|
|
1060
|
+
scheduleRef.current = scheduleRecompute;
|
|
1061
|
+
});
|
|
519
1062
|
const scrollToKey = (key, smooth) => {
|
|
520
1063
|
const el = anchors[key];
|
|
521
1064
|
if (!el || !bodyEl) return;
|
|
@@ -573,18 +1116,51 @@ window.__ModuleLoader__.load({
|
|
|
573
1116
|
setActiveKey(navKeys.length ? navKeys[navKeys.length - 1] : null);
|
|
574
1117
|
}
|
|
575
1118
|
}, []);
|
|
1119
|
+
(0, react.useEffect)(() => {
|
|
1120
|
+
setZone(bodyEl ? bottomZoneAfter(false, bottomDistance()) : true);
|
|
1121
|
+
}, []);
|
|
1122
|
+
(0, react.useEffect)(() => {
|
|
1123
|
+
const el = bodyEl;
|
|
1124
|
+
if (!el || typeof ResizeObserver === "undefined") return;
|
|
1125
|
+
const ro = new ResizeObserver(() => scheduleRef.current());
|
|
1126
|
+
ro.observe(el);
|
|
1127
|
+
return () => ro.disconnect();
|
|
1128
|
+
}, []);
|
|
1129
|
+
(0, react.useEffect)(() => {
|
|
1130
|
+
scheduleRef.current();
|
|
1131
|
+
}, [snap, prefs.width]);
|
|
1132
|
+
(0, react.useEffect)(() => () => {
|
|
1133
|
+
if (rafId.current !== null) {
|
|
1134
|
+
cancelAnimationFrame(rafId.current);
|
|
1135
|
+
rafId.current = null;
|
|
1136
|
+
}
|
|
1137
|
+
}, []);
|
|
576
1138
|
(0, react.useEffect)(() => {
|
|
577
1139
|
overlayRef.current?.focus();
|
|
1140
|
+
}, []);
|
|
1141
|
+
(0, react.useEffect)(() => {
|
|
578
1142
|
const onKey = (e) => {
|
|
579
|
-
if (e.key
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
1143
|
+
if (e.key !== "Escape") return;
|
|
1144
|
+
if (e.isComposing || e.keyCode === 229) return;
|
|
1145
|
+
e.preventDefault();
|
|
1146
|
+
e.stopPropagation();
|
|
1147
|
+
if (cardOpen) {
|
|
1148
|
+
setCardOpen(false);
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
if (dockFocused || engaged) {
|
|
1152
|
+
hideBar();
|
|
1153
|
+
return;
|
|
583
1154
|
}
|
|
1155
|
+
focusStore.set(false);
|
|
584
1156
|
};
|
|
585
1157
|
window.addEventListener("keydown", onKey, true);
|
|
586
1158
|
return () => window.removeEventListener("keydown", onKey, true);
|
|
587
|
-
}, [
|
|
1159
|
+
}, [
|
|
1160
|
+
cardOpen,
|
|
1161
|
+
dockFocused,
|
|
1162
|
+
engaged
|
|
1163
|
+
]);
|
|
588
1164
|
const loadImage = (0, react.useCallback)((attachment) => {
|
|
589
1165
|
if (!conversation || !currentId) return Promise.reject(/* @__PURE__ */ new Error("dsh-focus-overlay: conversation service unavailable"));
|
|
590
1166
|
return conversation.resolveImage(currentId, attachment);
|
|
@@ -606,6 +1182,81 @@ window.__ModuleLoader__.load({
|
|
|
606
1182
|
return;
|
|
607
1183
|
}
|
|
608
1184
|
}, [chatFileMentions, workspaces]);
|
|
1185
|
+
const binding = (0, react.useMemo)(() => sessions && currentId != null ? sessions.binding(currentId) : void 0, [sessions, currentId]);
|
|
1186
|
+
const inputFace = useInputFace(conversation, binding && binding.ctx);
|
|
1187
|
+
const inputState = useInputState(inputFace);
|
|
1188
|
+
const draftValue = inputFace ? inputState && typeof inputState.draft === "string" ? inputState.draft : "" : localDraft;
|
|
1189
|
+
const draftEmpty = draftValue.trim() === "";
|
|
1190
|
+
const pendingList = snap && snap.pending ? snap.pending : [];
|
|
1191
|
+
const pendingCount = pendingList.length;
|
|
1192
|
+
const wait = pendingCount > 0 ? pendingList[0] : null;
|
|
1193
|
+
const onDraftChange = (v) => {
|
|
1194
|
+
if (inputFace) try {
|
|
1195
|
+
inputFace.setDraft(v);
|
|
1196
|
+
return;
|
|
1197
|
+
} catch {}
|
|
1198
|
+
setLocalDraft(v);
|
|
1199
|
+
};
|
|
1200
|
+
const send = () => {
|
|
1201
|
+
const text = draftValue;
|
|
1202
|
+
if (text.trim() === "") return;
|
|
1203
|
+
if (inputFace) try {
|
|
1204
|
+
inputFace.submit("queue");
|
|
1205
|
+
return;
|
|
1206
|
+
} catch {}
|
|
1207
|
+
const face = binding && binding.session;
|
|
1208
|
+
if (face) face.prompt([{
|
|
1209
|
+
type: "text",
|
|
1210
|
+
text
|
|
1211
|
+
}], "queue").then((r) => {
|
|
1212
|
+
if (r && r.ok) setLocalDraft("");
|
|
1213
|
+
}).catch(() => {});
|
|
1214
|
+
};
|
|
1215
|
+
const hideBar = () => {
|
|
1216
|
+
try {
|
|
1217
|
+
taRef.current && taRef.current.blur();
|
|
1218
|
+
} catch {}
|
|
1219
|
+
setEngaged(false);
|
|
1220
|
+
setDockFocused(false);
|
|
1221
|
+
};
|
|
1222
|
+
const expandBar = () => {
|
|
1223
|
+
focusBarOnce.current = true;
|
|
1224
|
+
setEngaged(true);
|
|
1225
|
+
};
|
|
1226
|
+
const onDockFocusChange = (f) => {
|
|
1227
|
+
setDockFocused(f);
|
|
1228
|
+
if (!f) setEngaged(false);
|
|
1229
|
+
};
|
|
1230
|
+
const queueCount = snap && snap.queue ? snap.queue.length : 0;
|
|
1231
|
+
const occCount = inputState && inputState.occurrences ? inputState.occurrences.length : 0;
|
|
1232
|
+
const promptErr = snap && snap.promptError && snap.promptError.op === "send" ? snap.promptError.error : null;
|
|
1233
|
+
const errorLine = promptErr ? promptErr.message || promptErr.code || "send failed" : null;
|
|
1234
|
+
const form = bottomForm({
|
|
1235
|
+
pending: pendingCount > 0,
|
|
1236
|
+
cardOpen,
|
|
1237
|
+
draftEmpty,
|
|
1238
|
+
inZone: zone,
|
|
1239
|
+
focused: dockFocused,
|
|
1240
|
+
engaged
|
|
1241
|
+
});
|
|
1242
|
+
(0, react.useEffect)(() => {
|
|
1243
|
+
if (pendingCount === 0) {
|
|
1244
|
+
if (cardOpen) setCardOpen(false);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
focusBarOnce.current = false;
|
|
1248
|
+
setDockFocused(false);
|
|
1249
|
+
setEngaged(false);
|
|
1250
|
+
}, [pendingCount, cardOpen]);
|
|
1251
|
+
(0, react.useEffect)(() => {
|
|
1252
|
+
if (form === "bar" && focusBarOnce.current) {
|
|
1253
|
+
focusBarOnce.current = false;
|
|
1254
|
+
try {
|
|
1255
|
+
taRef.current && taRef.current.focus();
|
|
1256
|
+
} catch {}
|
|
1257
|
+
setEngaged(false);
|
|
1258
|
+
}
|
|
1259
|
+
}, [form]);
|
|
609
1260
|
let title = "Focus Mode";
|
|
610
1261
|
if (currentId && listState && listState.byId && listState.byId[currentId]) {
|
|
611
1262
|
const row = listState.byId[currentId];
|
|
@@ -675,7 +1326,23 @@ window.__ModuleLoader__.load({
|
|
|
675
1326
|
}, "fm-partial"));
|
|
676
1327
|
if (running) kids.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
677
1328
|
className: "fm-running",
|
|
678
|
-
|
|
1329
|
+
role: "status",
|
|
1330
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1331
|
+
className: "fm-running-text",
|
|
1332
|
+
children: [
|
|
1333
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1334
|
+
className: "fm-running-dash",
|
|
1335
|
+
"aria-hidden": "true",
|
|
1336
|
+
children: "–"
|
|
1337
|
+
}),
|
|
1338
|
+
t("running"),
|
|
1339
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1340
|
+
className: "fm-running-dash",
|
|
1341
|
+
"aria-hidden": "true",
|
|
1342
|
+
children: "–"
|
|
1343
|
+
})
|
|
1344
|
+
]
|
|
1345
|
+
})
|
|
679
1346
|
}, "fm-running"));
|
|
680
1347
|
body = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
681
1348
|
className: "fm-inner",
|
|
@@ -701,18 +1368,7 @@ window.__ModuleLoader__.load({
|
|
|
701
1368
|
})]
|
|
702
1369
|
}, k))
|
|
703
1370
|
}) : null;
|
|
704
|
-
const
|
|
705
|
-
className: "fm-tobottom-wrap",
|
|
706
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
707
|
-
variant: "outline",
|
|
708
|
-
size: "sm",
|
|
709
|
-
className: "fm-tobottom",
|
|
710
|
-
title: t("backToLatest"),
|
|
711
|
-
onClick: scrollToBottom,
|
|
712
|
-
children: "↓"
|
|
713
|
-
})
|
|
714
|
-
}) : null;
|
|
715
|
-
const waiting = hasPendingInteraction(snap);
|
|
1371
|
+
const waiting = pendingCount > 0 && !cardOpen;
|
|
716
1372
|
const donePing = useDonePing();
|
|
717
1373
|
const [showDoneNotice, setShowDoneNotice] = (0, react.useState)(false);
|
|
718
1374
|
(0, react.useEffect)(() => {
|
|
@@ -740,8 +1396,8 @@ window.__ModuleLoader__.load({
|
|
|
740
1396
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
741
1397
|
variant: "primary",
|
|
742
1398
|
size: "sm",
|
|
743
|
-
onClick: () =>
|
|
744
|
-
children: t("
|
|
1399
|
+
onClick: () => setCardOpen(true),
|
|
1400
|
+
children: t("answer.open")
|
|
745
1401
|
})
|
|
746
1402
|
]
|
|
747
1403
|
}) : showDoneNotice ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -785,11 +1441,30 @@ window.__ModuleLoader__.load({
|
|
|
785
1441
|
ref: (el) => {
|
|
786
1442
|
bodyEl = el;
|
|
787
1443
|
},
|
|
788
|
-
onScroll:
|
|
1444
|
+
onScroll: scheduleRecompute,
|
|
789
1445
|
children: body
|
|
790
1446
|
}),
|
|
791
1447
|
nav,
|
|
792
|
-
|
|
1448
|
+
currentId && snap !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1449
|
+
className: "fm-dock",
|
|
1450
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FocusBottomDock, {
|
|
1451
|
+
t,
|
|
1452
|
+
width: prefs.width,
|
|
1453
|
+
form,
|
|
1454
|
+
wait,
|
|
1455
|
+
onCardClose: () => setCardOpen(false),
|
|
1456
|
+
draft: draftValue,
|
|
1457
|
+
queueCount,
|
|
1458
|
+
occCount,
|
|
1459
|
+
errorLine,
|
|
1460
|
+
textareaRef: taRef,
|
|
1461
|
+
onFocusChange: onDockFocusChange,
|
|
1462
|
+
onDraftChange,
|
|
1463
|
+
onSend: send,
|
|
1464
|
+
onExpand: expandBar,
|
|
1465
|
+
onJumpBottom: scrollToBottom
|
|
1466
|
+
})
|
|
1467
|
+
}) : null,
|
|
793
1468
|
replyNotice
|
|
794
1469
|
]
|
|
795
1470
|
});
|
|
@@ -1036,7 +1711,7 @@ window.__ModuleLoader__.load({
|
|
|
1036
1711
|
.fm-overlay{position:fixed;inset:0;z-index:2147483000;background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#1a1a1a);display:flex;flex-direction:column;pointer-events:auto;font-family:var(--dsw-font-family,sans-serif);outline:none}
|
|
1037
1712
|
.fm-topbar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.12));flex:0 0 auto}
|
|
1038
1713
|
.fm-title{font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1039
|
-
.fm-body{flex:1 1 auto;overflow-y:auto;padding:28px 28px
|
|
1714
|
+
.fm-body{flex:1 1 auto;overflow-y:auto;padding:28px 28px 110px}
|
|
1040
1715
|
.fm-inner{max-width:760px;margin:0 auto}
|
|
1041
1716
|
.fm-msg{margin:0 0 24px}
|
|
1042
1717
|
.fm-user-msg{margin-bottom:44px}
|
|
@@ -1046,7 +1721,32 @@ window.__ModuleLoader__.load({
|
|
|
1046
1721
|
.fm-image{max-width:100%;border-radius:8px;margin:8px 0}
|
|
1047
1722
|
.fm-hidden{text-align:center;color:var(--dsw-alias-label-secondary,#888);font-size:12px;line-height:1.6;margin:6px 0 18px;user-select:none}
|
|
1048
1723
|
.fm-error{color:var(--dsw-alias-state-error-primary,#d23);font-size:13px;white-space:pre-wrap}
|
|
1049
|
-
|
|
1724
|
+
/* "正在工作" live line — the shimmer is a 1:1 replica of the official
|
|
1725
|
+
generating indicator in dsh-client-ui-conversation (the "Deep Diving"
|
|
1726
|
+
turn-status): brand-blue text (--dsw-static-deepseek-500) with a light
|
|
1727
|
+
highlight (--dsw-static-deepseek-200) sweeping through — gradient stops
|
|
1728
|
+
0/40/50/60/100 on a 250% tile, background-position 100%→0, 1.8s linear,
|
|
1729
|
+
font --dsw-font-s-strong-14 (500 14px/22px). The official class name
|
|
1730
|
+
(Md3f7G_turnStatus) is a per-build CSS-modules hash, so reusing it would
|
|
1731
|
+
break silently on dsh updates — the declarations are copied verbatim onto
|
|
1732
|
+
our own class using the same theme tokens instead. The flanking short
|
|
1733
|
+
dashes are part of the same shimmer span, so the sweep covers dashes and
|
|
1734
|
+
label as one unit. Fallbacks: no background-clip:text → secondary gray
|
|
1735
|
+
label; prefers-reduced-motion → animation off, label stays solid
|
|
1736
|
+
deepseek-500 (the official base color). */
|
|
1737
|
+
.fm-running{text-align:center;margin:10px 0 20px;user-select:none;animation:fm-up .18s ease}
|
|
1738
|
+
.fm-running-text{color:var(--dsw-alias-label-secondary,#888);font:var(--dsw-font-s-strong-14,500 14px/22px var(--dsw-font-family,sans-serif));white-space:nowrap}
|
|
1739
|
+
/* Dashes: separate spans with fixed margins instead of space characters — in
|
|
1740
|
+
CJK/Latin mixed runs the browser itemizes the two U+0020 spaces into
|
|
1741
|
+
different fonts (one Latin-narrow, one Han-wide), and the Han font's en
|
|
1742
|
+
dash glyph is left-biased, which made the two gaps visually unequal. The
|
|
1743
|
+
forced Latin-first stack renders a symmetric en dash; margins are exact. */
|
|
1744
|
+
.fm-running-dash{font-family:system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;margin:0 8px}
|
|
1745
|
+
@supports ((-webkit-background-clip:text) or (background-clip:text)){
|
|
1746
|
+
.fm-running-text{background:linear-gradient(90deg,var(--dsw-static-deepseek-500,#4176e6) 0%,var(--dsw-static-deepseek-500,#4176e6) 40%,var(--dsw-static-deepseek-200,#d3e2ff) 50%,var(--dsw-static-deepseek-500,#4176e6) 60%,var(--dsw-static-deepseek-500,#4176e6) 100%);color:#0000;-webkit-text-fill-color:transparent;background-position:100% 0;background-size:250% 100%;-webkit-background-clip:text;background-clip:text;animation:fm-sweep 1.8s linear infinite}
|
|
1747
|
+
}
|
|
1748
|
+
@keyframes fm-sweep{to{background-position:0 0}}
|
|
1749
|
+
@media (prefers-reduced-motion:reduce){.fm-running{animation:none}.fm-running-text{background:0 0;color:var(--dsw-static-deepseek-500,#4176e6);-webkit-text-fill-color:currentColor;animation:none}}
|
|
1050
1750
|
.fm-empty{color:var(--dsw-alias-label-secondary,#888);padding:48px 16px;text-align:center}
|
|
1051
1751
|
.fm-nav{position:absolute;right:20px;top:50%;transform:translateY(-50%);display:block;width:16px;z-index:2;pointer-events:auto;transition:transform .18s ease}
|
|
1052
1752
|
.fm-nav:hover{transform:translateY(-50%) translateX(-6px)}
|
|
@@ -1058,11 +1758,73 @@ window.__ModuleLoader__.load({
|
|
|
1058
1758
|
.fm-nav-dot-active .fm-nav-dot-core{background:var(--dsw-static-deepseek-500,#4176e6);height:22px;opacity:1}
|
|
1059
1759
|
.fm-nav-tip{position:absolute;right:calc(100% + 8px);top:50%;transform:translateY(-50%);display:none;width:260px;box-sizing:border-box;white-space:normal;overflow-wrap:break-word;word-break:break-word;background:var(--dsw-alias-bg-overlay,#fff);color:var(--dsw-alias-label-primary,#1a1a1a);border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.12));border-radius:8px;padding:8px 10px;font-size:12px;line-height:1.5;box-shadow:0 4px 16px rgba(0,0,0,.12);pointer-events:none;text-align:left}
|
|
1060
1760
|
.fm-nav-dot:hover .fm-nav-tip{display:block}
|
|
1061
|
-
.fm-
|
|
1062
|
-
.fm-tobottom{pointer-events:auto;border-radius:100px;min-width:34px;height:34px;padding:0 10px}
|
|
1063
|
-
.fm-reply-toast{position:absolute;left:50%;transform:translateX(-50%);bottom:64px;z-index:2;display:flex;align-items:center;gap:10px;background:var(--dsw-alias-bg-overlay,#fff);color:var(--dsw-alias-label-primary,#1a1a1a);border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.12));border-radius:100px;padding:8px 8px 8px 16px;box-shadow:0 4px 16px rgba(0,0,0,.12);font-size:13px}
|
|
1761
|
+
.fm-reply-toast{position:absolute;left:50%;transform:translateX(-50%);bottom:84px;z-index:2;display:flex;align-items:center;gap:10px;background:var(--dsw-alias-bg-overlay,#fff);color:var(--dsw-alias-label-primary,#1a1a1a);border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.12));border-radius:100px;padding:8px 8px 8px 16px;box-shadow:0 4px 16px rgba(0,0,0,.12);font-size:13px}
|
|
1064
1762
|
.fm-reply-toast-dot{width:8px;height:8px;border-radius:999px;background:var(--dsw-static-deepseek-500,#4176e6);flex:0 0 auto}
|
|
1065
1763
|
.fm-reply-toast-text{white-space:nowrap}
|
|
1764
|
+
/* ---- bottom dock: bar / pill / jump-to-bottom / answer card ----
|
|
1765
|
+
Visual language mirrors the official composer cards token-for-token
|
|
1766
|
+
(ui-conversation InputBar/QuestionComposer, ui-user-questions
|
|
1767
|
+
PlanReviewPanel): --dsw-specific-input-major surfaces, 20–22px radii,
|
|
1768
|
+
--dsw-shadow-lv2, the 34px round info-fill primary button, interactive-
|
|
1769
|
+
hover option tinting. */
|
|
1770
|
+
.fm-dock{position:absolute;left:50%;transform:translateX(-50%);bottom:18px;z-index:3;width:calc(100% - 96px);display:flex;justify-content:center;pointer-events:none}
|
|
1771
|
+
.fm-dock>*{pointer-events:auto;animation:fm-up .18s ease}
|
|
1772
|
+
.fm-bar{box-sizing:border-box;width:100%;background:var(--dsw-specific-input-major,#fff);color:var(--dsw-alias-label-primary,#1a1a1a);border:1px solid var(--dsw-alias-border-l2-darkmode-thin,rgba(0,0,0,.12));box-shadow:var(--dsw-shadow-lv2,0 4px 16px rgba(0,0,0,.12));border-radius:22px;padding-top:10px;display:flex;flex-direction:column;font-size:16px;line-height:24px}
|
|
1773
|
+
.fm-bar-row{display:flex;align-items:flex-end;gap:12px;min-width:0;padding:2px 8px 6px}
|
|
1774
|
+
/* flex-end anchors the send button to the row's bottom edge, so a multi-line
|
|
1775
|
+
draft grows the textarea upward and the button's distance to the bar's
|
|
1776
|
+
bottom-right corner never moves. */
|
|
1777
|
+
.fm-bar-text{flex:1 1 auto;min-height:28px;max-height:120px;resize:none;color:inherit;caret-color:var(--dsw-alias-state-business-primary,var(--dsw-static-deepseek-500,#4176e6));background:0 0;border:none;outline:none;font:inherit;font-size:inherit;line-height:inherit;padding:4px 12px 0 16px}
|
|
1778
|
+
.fm-bar-text::placeholder{color:var(--dsw-alias-label-caption,#999);user-select:none}
|
|
1779
|
+
.fm-bar-note{margin:4px 12px 0;border-radius:8px;padding:4px 8px;font-size:12px;line-height:18px;background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.05));color:var(--dsw-alias-label-secondary,#888)}
|
|
1780
|
+
.fm-bar-note-error{color:var(--dsw-alias-state-error-primary,#d23);word-break:break-word}
|
|
1781
|
+
.fm-bar-queue{flex:0 0 auto;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary,#888);background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.05));border-radius:999px;padding:2px 10px;margin-bottom:7px;white-space:nowrap}
|
|
1782
|
+
.fm-bar-send{width:34px;height:34px;display:grid;place-items:center;flex:none;cursor:pointer;border:none;border-radius:999px;background:var(--dsw-alias-button-info-fill,#4176e6);color:#fff;transform:translateY(-2px);transition:background-color .1s;padding:0}
|
|
1783
|
+
.fm-bar-send:hover:not(:disabled){background:var(--dsw-alias-button-info-hover,#3560c4)}
|
|
1784
|
+
.fm-bar-send:disabled{opacity:.4;cursor:default}
|
|
1785
|
+
.fm-pill{position:relative;display:grid;place-items:center;width:38px;height:38px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2-darkmode-thin,rgba(0,0,0,.12));background:var(--dsw-specific-input-major,#fff);color:var(--dsw-alias-label-secondary,#666);cursor:pointer;box-shadow:var(--dsw-shadow-lv2,0 4px 16px rgba(0,0,0,.12));transition:color .12s;padding:0}
|
|
1786
|
+
.fm-pill:hover{color:var(--dsw-alias-label-primary,#1a1a1a)}
|
|
1787
|
+
.fm-pill-dot{position:absolute;top:4px;right:4px;width:7px;height:7px;border-radius:999px;background:var(--dsw-alias-state-business-primary,var(--dsw-static-deepseek-500,#4176e6))}
|
|
1788
|
+
.fm-card{box-sizing:border-box;width:100%;background:var(--dsw-specific-input-major,#fff);color:var(--dsw-alias-label-primary,#1a1a1a);border:1px solid var(--dsw-alias-border-l2-darkmode-thin,rgba(0,0,0,.12));box-shadow:var(--dsw-shadow-lv2,0 4px 16px rgba(0,0,0,.12));border-radius:20px;display:flex;flex-direction:column;max-height:min(60vh,520px);overflow:hidden;--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2)}
|
|
1789
|
+
.fm-card *{box-sizing:border-box}
|
|
1790
|
+
.fm-card-approval{border-color:var(--dsw-alias-state-warn-secondary,#e6a23c)}
|
|
1791
|
+
.fm-card-head{flex:0 0 auto;display:flex;justify-content:space-between;align-items:flex-start;gap:16px;padding:20px 16px 0 24px}
|
|
1792
|
+
.fm-card-approval .fm-card-head{background:var(--dsw-alias-state-warn-tertiary,rgba(230,162,60,.12));color:var(--dsw-alias-state-warn-primary,#9a6b12);align-items:center;padding:10px 16px}
|
|
1793
|
+
.fm-card-heading{min-width:0}
|
|
1794
|
+
.fm-card-eyebrow{color:var(--dsw-alias-label-tertiary,#999);margin:0 0 5px;font-size:11px;line-height:16px}
|
|
1795
|
+
.fm-card-title{margin:0;font-size:16px;font-weight:500;line-height:22px}
|
|
1796
|
+
.fm-card-strip-title{font-size:13px;line-height:18px;flex:1;min-width:0}
|
|
1797
|
+
.fm-card-dot{background:var(--dsw-alias-state-warn-primary,#9a6b12);border-radius:50%;width:8px;height:8px;flex:none}
|
|
1798
|
+
.fm-card-close{width:24px;height:24px;display:grid;place-items:center;flex:none;cursor:pointer;border:none;border-radius:999px;background:0 0;color:var(--dsw-alias-label-tertiary,#999);padding:0}
|
|
1799
|
+
.fm-card-close:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06));color:var(--dsw-alias-label-primary,#1a1a1a)}
|
|
1800
|
+
.fm-card-approval .fm-card-close,.fm-card-approval .fm-card-close:hover:not(:disabled){color:inherit}
|
|
1801
|
+
.fm-card-body{overscroll-behavior:contain;flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow-y:auto;padding-bottom:8px;font-size:14px;line-height:22px}
|
|
1802
|
+
.fm-card-q{display:flex;flex-direction:column}
|
|
1803
|
+
.fm-card-q-text{margin:0 2px 8px;padding:0 22px;white-space:pre-wrap;word-break:break-word;font-size:16px;font-weight:500;line-height:22px}
|
|
1804
|
+
.fm-card-detail{color:var(--dsw-alias-label-secondary,#888);margin:0 2px 8px;padding:0 22px;font-size:14px;line-height:22px}
|
|
1805
|
+
.fm-card-opts{display:flex;flex-direction:column;gap:1px;margin:8px 0 0;padding:4px 12px}
|
|
1806
|
+
.fm-opt{width:100%;min-height:40px;color:inherit;text-align:left;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:12px;flex-shrink:0;display:flex;align-items:flex-start;gap:8px;padding:8px 12px 8px 8px;transition:background-color .12s,border-color .12s;font:inherit}
|
|
1807
|
+
.fm-opt:hover:not(:disabled),.fm-opt-on{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06))}
|
|
1808
|
+
.fm-opt-on{border-color:var(--dsw-alias-border-l2,rgba(0,0,0,.18))}
|
|
1809
|
+
.fm-opt-num{background:var(--dsw-alias-bg-overlay,#fff);width:20px;height:20px;color:var(--dsw-alias-label-secondary,#888);border-radius:6px;flex:0 0 20px;display:grid;place-items:center;margin-top:2px;font-size:12px;font-weight:500;line-height:18px}
|
|
1810
|
+
.fm-opt-check{flex:0 0 20px;width:20px;height:20px;margin-top:2px;display:grid;place-items:center;position:relative;color:var(--dsw-alias-label-primary-foreground,#fff)}
|
|
1811
|
+
.fm-opt-check::before{content:'';border:1px solid var(--dsw-alias-border-l4,rgba(0,0,0,.25));border-radius:4px;width:14px;height:14px;grid-area:1/1;transition:background-color .12s,border-color .12s}
|
|
1812
|
+
.fm-opt-check-on::before{border-color:var(--dsw-alias-label-primary,#1a1a1a);background:var(--dsw-alias-label-primary,#1a1a1a)}
|
|
1813
|
+
.fm-opt-check>*{grid-area:1/1}
|
|
1814
|
+
.fm-opt-copy{flex:1;min-width:0}
|
|
1815
|
+
.fm-opt-line{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 6px}
|
|
1816
|
+
.fm-opt-label{font-size:14px;font-weight:500;line-height:24px;word-break:break-word}
|
|
1817
|
+
.fm-opt-badge{background:var(--dsw-specific-sidebar-nav-item-active-accent,rgba(65,118,230,.12));color:var(--dsw-alias-button-info-fill,#4176e6);border-radius:6px;padding:0 4px;font-size:11px;font-weight:600;line-height:18px}
|
|
1818
|
+
.fm-opt-desc{color:var(--dsw-alias-label-tertiary,#999);font-size:14px;line-height:24px;word-break:break-word}
|
|
1819
|
+
.fm-card-custom{margin:0 12px;border:1px solid #0000;border-radius:12px;flex-shrink:0;display:flex;align-items:center;min-height:40px;padding:8px 12px;transition:background-color .12s,border-color .12s}
|
|
1820
|
+
.fm-card-custom:hover,.fm-card-custom:focus-within{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06))}
|
|
1821
|
+
.fm-card-custom:focus-within{border-color:var(--dsw-alias-border-l2,rgba(0,0,0,.18))}
|
|
1822
|
+
.fm-card-field{min-width:0;width:100%;border:none;outline:none;background:0 0;color:inherit;font:inherit;font-size:14px;line-height:24px;padding:0}
|
|
1823
|
+
.fm-card-field::placeholder{color:var(--dsw-alias-label-caption,#999)}
|
|
1824
|
+
.fm-card-foot{flex:0 0 auto;display:flex;justify-content:space-between;align-items:center;gap:12px;padding:8px 16px 12px}
|
|
1825
|
+
.fm-card-err{flex:1 1 auto;min-height:16px;color:var(--dsw-alias-state-error-primary,#d23);font-size:11px;line-height:16px;word-break:break-word}
|
|
1826
|
+
.fm-card-actions{display:flex;align-items:center;gap:8px;flex:none}
|
|
1827
|
+
@keyframes fm-up{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
|
1066
1828
|
.fm-plugin-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
|
|
1067
1829
|
.fm-plugin-card:hover{border-color:var(--dsw-alias-label-dimmed)}
|
|
1068
1830
|
.fm-plugin-card-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
|
|
@@ -1129,11 +1891,26 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
1129
1891
|
"settings.width.hint": "专注模式正文的阅读列宽(480–1200px)",
|
|
1130
1892
|
"empty": "暂无打开的会话",
|
|
1131
1893
|
"loading": "正在读取会话…",
|
|
1132
|
-
"running": "
|
|
1894
|
+
"running": "正在工作",
|
|
1133
1895
|
"reply.ready": "新回复已生成",
|
|
1134
1896
|
"reply.view": "查看",
|
|
1135
1897
|
"reply.waiting": "AI 正在等待你的回复",
|
|
1136
|
-
"
|
|
1898
|
+
"answer.open": "去回答",
|
|
1899
|
+
"answer.title": "等待你的回答",
|
|
1900
|
+
"answer.approvalTitle": "请求审批",
|
|
1901
|
+
"answer.submit": "提交回答",
|
|
1902
|
+
"answer.close": "收起",
|
|
1903
|
+
"answer.custom": "其他答案(可选)",
|
|
1904
|
+
"answer.recommended": "推荐",
|
|
1905
|
+
"answer.incomplete": "请先回答所有问题",
|
|
1906
|
+
"answer.rejected": "提交被拒绝",
|
|
1907
|
+
"answer.allow": "允许",
|
|
1908
|
+
"answer.reject": "拒绝",
|
|
1909
|
+
"composer.placeholder": "输入回复…",
|
|
1910
|
+
"composer.send": "发送",
|
|
1911
|
+
"composer.expand": "展开输入框",
|
|
1912
|
+
"composer.queued": "已排队 {n}",
|
|
1913
|
+
"composer.occurrences": "草稿中包含引用 / 命令,在此编辑可能使其失效,建议退出专注模式编辑",
|
|
1137
1914
|
"sum.context": "载入了 {n} 项上下文",
|
|
1138
1915
|
"sum.commands": "运行了 {n} 个命令",
|
|
1139
1916
|
"sum.edits": "编辑了 {n} 个文件",
|
|
@@ -1183,11 +1960,26 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
1183
1960
|
"settings.width.hint": "Reading column width (480–1200px)",
|
|
1184
1961
|
"empty": "No open session",
|
|
1185
1962
|
"loading": "Reading session…",
|
|
1186
|
-
"running": "
|
|
1963
|
+
"running": "Working",
|
|
1187
1964
|
"reply.ready": "New reply ready",
|
|
1188
1965
|
"reply.view": "View",
|
|
1189
1966
|
"reply.waiting": "AI is waiting for your reply",
|
|
1190
|
-
"
|
|
1967
|
+
"answer.open": "Answer",
|
|
1968
|
+
"answer.title": "Waiting for your answer",
|
|
1969
|
+
"answer.approvalTitle": "Approval requested",
|
|
1970
|
+
"answer.submit": "Submit answer",
|
|
1971
|
+
"answer.close": "Collapse",
|
|
1972
|
+
"answer.custom": "Other (optional)",
|
|
1973
|
+
"answer.recommended": "Recommended",
|
|
1974
|
+
"answer.incomplete": "Answer every question first",
|
|
1975
|
+
"answer.rejected": "Submission rejected",
|
|
1976
|
+
"answer.allow": "Allow",
|
|
1977
|
+
"answer.reject": "Deny",
|
|
1978
|
+
"composer.placeholder": "Type a reply…",
|
|
1979
|
+
"composer.send": "Send",
|
|
1980
|
+
"composer.expand": "Expand input",
|
|
1981
|
+
"composer.queued": "Queued {n}",
|
|
1982
|
+
"composer.occurrences": "Draft contains references / commands — editing here may break them; edit outside focus mode",
|
|
1191
1983
|
"sum.context": "loaded {n} context items",
|
|
1192
1984
|
"sum.commands": "ran {n} commands",
|
|
1193
1985
|
"sum.edits": "edited {n} files",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-focus-overlay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Focus Mode for the dsh web GUI: a full-screen reading overlay that hides chrome and folds the AI tool-call flow into summaries, reusing the official Markdown / image primitives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.mjs",
|