dsh-focus-overlay 0.10.0 → 0.11.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 +48 -34
- package/README.md +50 -35
- package/lib/client.js +1370 -377
- package/package.json +10 -4
package/lib/client.js
CHANGED
|
@@ -7,6 +7,69 @@ window.__ModuleLoader__.load({
|
|
|
7
7
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
8
8
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
9
|
//#region src/client/model.ts
|
|
10
|
+
function legacySliceOf(chatView, snap) {
|
|
11
|
+
const legacy = chatView && chatView.legacy ? chatView.legacy : null;
|
|
12
|
+
return {
|
|
13
|
+
nodes: legacy && legacy.nodes || snap && snap.nodes || [],
|
|
14
|
+
partial: legacy ? legacy.partial ?? null : snap && snap.partial || null,
|
|
15
|
+
runningCalls: legacy && legacy.runningCalls || snap && snap.runningCalls || []
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** The current session's pending user interaction (question / plan review /
|
|
19
|
+
* approval) from the uiSession pending map — dsh 0.1.2 replaced the session
|
|
20
|
+
* snapshot's flat `pending` array with this service-owned map. Null when the
|
|
21
|
+
* map (or the interaction for this session) is absent. */
|
|
22
|
+
function pendingInteractionOf(map, sessionId) {
|
|
23
|
+
try {
|
|
24
|
+
if (map && typeof map.get === "function") return map.get(sessionId) || null;
|
|
25
|
+
} catch {}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
/** Legacy pending interaction — pre-0.1.2 dsh carried a flat `pending` array
|
|
29
|
+
* on the session snapshot, with a different carrier shape (`payload`
|
|
30
|
+
* envelope, answers via `wait.respond({ok, value})` returning a `receipt`).
|
|
31
|
+
* This adapter reshapes the first legacy item into the 0.1.2 carrier face the
|
|
32
|
+
* overlay's answer card renders (kind / questions / reason / toolName) and
|
|
33
|
+
* shims `wait.answer(...)` onto `wait.respond(...)`, throwing a rejected-
|
|
34
|
+
* marker error when the legacy receipt says the answer was refused. Null
|
|
35
|
+
* when nothing is pending or no legacy carrier exists. */
|
|
36
|
+
function legacyPendingOf(snap) {
|
|
37
|
+
const old = (snap && Array.isArray(snap.pending) ? snap.pending : [])[0];
|
|
38
|
+
if (!old) return null;
|
|
39
|
+
const payload = old.payload || {};
|
|
40
|
+
const kind = old.kind === "question" ? "question" : "approval";
|
|
41
|
+
const shaped = {
|
|
42
|
+
...old,
|
|
43
|
+
kind,
|
|
44
|
+
questions: Array.isArray(payload.questions) ? payload.questions : [],
|
|
45
|
+
toolName: payload.toolName,
|
|
46
|
+
reason: payload.reason ?? payload.detail,
|
|
47
|
+
approvalId: payload.approvalId
|
|
48
|
+
};
|
|
49
|
+
if (typeof old.respond === "function" && typeof old.answer !== "function") shaped.answer = (answerPayload) => Promise.resolve(old.respond(kind === "approval" ? {
|
|
50
|
+
ok: true,
|
|
51
|
+
value: {
|
|
52
|
+
sessionId: old.sessionId,
|
|
53
|
+
approvalId: payload.approvalId,
|
|
54
|
+
outcome: answerPayload
|
|
55
|
+
}
|
|
56
|
+
} : {
|
|
57
|
+
ok: true,
|
|
58
|
+
value: {
|
|
59
|
+
sessionId: old.sessionId,
|
|
60
|
+
answer: answerPayload
|
|
61
|
+
}
|
|
62
|
+
})).then((receipt) => {
|
|
63
|
+
if (receipt && receipt.accepted === false) {
|
|
64
|
+
const err = new Error(receipt.reason || "rejected");
|
|
65
|
+
err.rejected = true;
|
|
66
|
+
err.reason = receipt.reason;
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
return receipt;
|
|
70
|
+
});
|
|
71
|
+
return shaped;
|
|
72
|
+
}
|
|
10
73
|
function flattenText(content) {
|
|
11
74
|
if (!content || !content.length) return "";
|
|
12
75
|
let out = "";
|
|
@@ -100,7 +163,120 @@ window.__ModuleLoader__.load({
|
|
|
100
163
|
for (const b of blocks || []) if (b && (b.kind === "text" || b.kind === "image")) return true;
|
|
101
164
|
return false;
|
|
102
165
|
}
|
|
103
|
-
|
|
166
|
+
/** Assemble the official TurnProcess disclosure label: "N 次工具调用 · M 条消息
|
|
167
|
+
* · K 个 subagent", falling back to "已思考" when the segment has nothing
|
|
168
|
+
* countable — mirroring the official `message.turnProcess.*` assembly. */
|
|
169
|
+
function processLabel(counts, t) {
|
|
170
|
+
const labels = [];
|
|
171
|
+
if (counts.tools > 0) labels.push(t("process.toolCalls", { n: counts.tools }));
|
|
172
|
+
if (counts.messages > 0) labels.push(t("process.messages", { n: counts.messages }));
|
|
173
|
+
if (counts.subagents > 0) labels.push(t("process.subagents", { n: counts.subagents }));
|
|
174
|
+
return labels.length === 0 ? t("process.thought") : labels.join(t("process.separator"));
|
|
175
|
+
}
|
|
176
|
+
function buildItems(nodes, runningCalls, t, transcriptView = "compact", stable = true) {
|
|
177
|
+
if (transcriptView === "normal") return segmentItems(nodes, runningCalls, t);
|
|
178
|
+
const buckets = [];
|
|
179
|
+
for (const n of nodes) {
|
|
180
|
+
if (!n) continue;
|
|
181
|
+
if (n.kind === "user") buckets.push([n]);
|
|
182
|
+
else if (buckets.length === 0) buckets.push([n]);
|
|
183
|
+
else buckets[buckets.length - 1].push(n);
|
|
184
|
+
}
|
|
185
|
+
if (buckets.length === 0) return segmentItems(nodes, runningCalls, t);
|
|
186
|
+
const items = [];
|
|
187
|
+
let turn = 0;
|
|
188
|
+
for (let b = 0; b < buckets.length; b++) {
|
|
189
|
+
const bucket = buckets[b];
|
|
190
|
+
const isLast = b === buckets.length - 1;
|
|
191
|
+
const head = bucket[0];
|
|
192
|
+
if (head.kind !== "user") {
|
|
193
|
+
items.push(...segmentItems(bucket, isLast ? runningCalls : [], t));
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
const headText = flattenText(head.content);
|
|
197
|
+
if (headText.trim() !== "") items.push({
|
|
198
|
+
kind: "user",
|
|
199
|
+
text: headText,
|
|
200
|
+
seq: head.seq
|
|
201
|
+
});
|
|
202
|
+
const body = bucket.slice(1);
|
|
203
|
+
let closingIdx = -1;
|
|
204
|
+
for (let k = body.length - 1; k >= 0; k--) {
|
|
205
|
+
const n = body[k];
|
|
206
|
+
if (n && n.kind === "assistant" && hasAssistantContent(n.blocks)) {
|
|
207
|
+
closingIdx = k;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (!(closingIdx >= 0 && closingIdx === body.length - 1 && (stable || !isLast))) {
|
|
212
|
+
items.push(...segmentItems(body, isLast ? runningCalls : [], t));
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const closing = body[closingIdx];
|
|
216
|
+
let workNodes = [];
|
|
217
|
+
const flushWork = () => {
|
|
218
|
+
if (workNodes.length > 0) {
|
|
219
|
+
items.push({
|
|
220
|
+
kind: "turnProcess",
|
|
221
|
+
turn: ++turn,
|
|
222
|
+
seq: head.seq,
|
|
223
|
+
collapsed: true,
|
|
224
|
+
counts: countProcess(workNodes),
|
|
225
|
+
workItems: segmentItems(workNodes, [], t)
|
|
226
|
+
});
|
|
227
|
+
workNodes = [];
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
for (let k = 0; k < closingIdx; k++) {
|
|
231
|
+
const n = body[k];
|
|
232
|
+
if (n.kind === "steering") {
|
|
233
|
+
flushWork();
|
|
234
|
+
if (flattenText(n.content).trim() !== "") items.push({
|
|
235
|
+
kind: "steering",
|
|
236
|
+
text: flattenText(n.content),
|
|
237
|
+
seq: n.seq
|
|
238
|
+
});
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
workNodes.push(n);
|
|
242
|
+
}
|
|
243
|
+
flushWork();
|
|
244
|
+
items.push({
|
|
245
|
+
kind: "assistant",
|
|
246
|
+
blocks: closing.blocks,
|
|
247
|
+
seq: closing.seq,
|
|
248
|
+
turn: closing.turn,
|
|
249
|
+
step: closing.step
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return items;
|
|
253
|
+
}
|
|
254
|
+
/** Per-turn process counts for the official disclosure label. */
|
|
255
|
+
function countProcess(workNodes) {
|
|
256
|
+
const metrics = {};
|
|
257
|
+
let assistantMessages = 0;
|
|
258
|
+
for (const n of workNodes) {
|
|
259
|
+
if (!n) continue;
|
|
260
|
+
if (n.kind === "assistant") {
|
|
261
|
+
if (hasAssistantContent(n.blocks)) assistantMessages++;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (n.kind === "context" || n.kind === "compaction") {
|
|
265
|
+
metrics.context = (metrics.context || 0) + 1;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
addMetric(metrics, n.call && n.call.name ? n.call.name : n.name || "");
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
tools: (metrics.commands || 0) + (metrics.edits || 0) + (metrics.searches || 0) + (metrics.files || 0) + (metrics.dirs || 0) + (metrics.todos || 0) + (metrics.goals || 0) + (metrics.workflows || 0) + (metrics.skills || 0) + (metrics.questions || 0) + (metrics.plans || 0) + (metrics.jobs || 0) + (metrics.others || 0),
|
|
272
|
+
messages: (metrics.context || 0) + assistantMessages,
|
|
273
|
+
subagents: metrics.subagents || 0
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/** The classic (pre-0.1.2) segmentation: content rows with tool/context runs
|
|
277
|
+
* folded into metric summary lines. This IS the normal-mode presentation,
|
|
278
|
+
* and the expanded work-part presentation in compact mode. */
|
|
279
|
+
function segmentItems(nodes, runningCalls, t) {
|
|
104
280
|
const items = [];
|
|
105
281
|
let metrics = {};
|
|
106
282
|
const flush = () => {
|
|
@@ -180,10 +356,11 @@ window.__ModuleLoader__.load({
|
|
|
180
356
|
return items;
|
|
181
357
|
}
|
|
182
358
|
function resolveAnchorSeq(chat, key) {
|
|
183
|
-
if (!key || !chat
|
|
359
|
+
if (!key || !chat) return null;
|
|
184
360
|
try {
|
|
185
|
-
const node = chat.nodes.get(key);
|
|
361
|
+
const node = typeof chat.get === "function" ? chat.get(key) : chat.nodes ? chat.nodes.get(key) : null;
|
|
186
362
|
if (!node) return null;
|
|
363
|
+
if (typeof node.anchorSeq === "number") return node.anchorSeq;
|
|
187
364
|
const d = node.data || node.node || node;
|
|
188
365
|
return d && typeof d.seq === "number" ? d.seq : null;
|
|
189
366
|
} catch {
|
|
@@ -197,6 +374,72 @@ window.__ModuleLoader__.load({
|
|
|
197
374
|
}
|
|
198
375
|
return -1;
|
|
199
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* Decide the overlay's opening scroll from the entry intent, the item list
|
|
379
|
+
* assembled so far, and the entry deadline. Pure so the decision is
|
|
380
|
+
* unit-testable at the seam the effect actually drives:
|
|
381
|
+
*
|
|
382
|
+
* - `auto` — a just-finished turn (auto-focus): open at the question that
|
|
383
|
+
* started it (`seq` known from the turn edge, no key resolution needed).
|
|
384
|
+
* - `anchor` — a reading-position preserve: `seq` is resolved from the captured
|
|
385
|
+
* chat anchor key by the caller (null while the node store is not resolvable
|
|
386
|
+
* yet). The overlay scrolls to the ROW WITH THAT SEQ — never to a different
|
|
387
|
+
* row (a regression here used to read the key-less `seq` field off anchor
|
|
388
|
+
* entries and silently fall back to the newest message).
|
|
389
|
+
* - `lastUser` — position sync off / no anchor captured: open at the newest
|
|
390
|
+
* user prompt.
|
|
391
|
+
*
|
|
392
|
+
* Waiting rules (the 0.1.2 chat view materializes lazily AND pages history
|
|
393
|
+
* asynchronously, so neither `items` nor the resolved seq may exist at mount):
|
|
394
|
+
* - While the history loader is still paging (`hasMore`) we wait — pages
|
|
395
|
+
* PREPEND above any anchor, so scrolling mid-load would drift.
|
|
396
|
+
* - While the row list itself has not landed yet (`items` empty on the mount
|
|
397
|
+
* render, before the chat-view state delivers) we wait until the entry
|
|
398
|
+
* deadline instead of latching — latching on an empty list is what stranded
|
|
399
|
+
* the overlay at the first message on every entry.
|
|
400
|
+
* - Once rows HAVE landed but the target row is absent (blank prompt skipped
|
|
401
|
+
* by {@link buildItems}, steering-only session, …) the row list is final, so
|
|
402
|
+
* we fall back immediately to the classic behaviors — no pointless wait.
|
|
403
|
+
* - A pass never reports "done" without actually scrolling somewhere.
|
|
404
|
+
*
|
|
405
|
+
* A successful target scroll carries the resolved `seq` back (the caller arms
|
|
406
|
+
* its anti-drift corrector with it); fallback scrolls carry null.
|
|
407
|
+
*/
|
|
408
|
+
function decideOpenScroll(o) {
|
|
409
|
+
if (o.hasMore) return { action: "wait" };
|
|
410
|
+
if (o.kind === "lastUser") {
|
|
411
|
+
const lastIdx = lastUserIndex(o.items);
|
|
412
|
+
if (lastIdx >= 0) return {
|
|
413
|
+
action: "scroll",
|
|
414
|
+
index: lastIdx,
|
|
415
|
+
seq: null
|
|
416
|
+
};
|
|
417
|
+
if (o.items.length === 0 && !o.deadlinePassed) return { action: "wait" };
|
|
418
|
+
return { action: "bottom" };
|
|
419
|
+
}
|
|
420
|
+
if (o.seq != null) {
|
|
421
|
+
const idx = findSeqIndex(o.items, o.seq);
|
|
422
|
+
if (idx >= 0) return {
|
|
423
|
+
action: "scroll",
|
|
424
|
+
index: idx,
|
|
425
|
+
seq: o.seq
|
|
426
|
+
};
|
|
427
|
+
if (o.items.length === 0 && !o.deadlinePassed) return { action: "wait" };
|
|
428
|
+
const lastIdx = lastUserIndex(o.items);
|
|
429
|
+
return lastIdx >= 0 ? {
|
|
430
|
+
action: "scroll",
|
|
431
|
+
index: lastIdx,
|
|
432
|
+
seq: null
|
|
433
|
+
} : { action: "bottom" };
|
|
434
|
+
}
|
|
435
|
+
if (!o.deadlinePassed) return { action: "wait" };
|
|
436
|
+
const lastIdx = lastUserIndex(o.items);
|
|
437
|
+
return lastIdx >= 0 ? {
|
|
438
|
+
action: "scroll",
|
|
439
|
+
index: lastIdx,
|
|
440
|
+
seq: null
|
|
441
|
+
} : { action: "bottom" };
|
|
442
|
+
}
|
|
200
443
|
/** Seq of the message that started the latest turn: the last `user` node, or a
|
|
201
444
|
* `steering` node as a fallback when no user message exists (e.g. a
|
|
202
445
|
* command-only session). Returns null when neither is present. */
|
|
@@ -216,6 +459,18 @@ window.__ModuleLoader__.load({
|
|
|
216
459
|
for (let i = items.length - 1; i >= 0; i--) if (items[i].kind === "user") return i;
|
|
217
460
|
return -1;
|
|
218
461
|
}
|
|
462
|
+
/** Seq of the newest prompt row (user OR steering) in an already-built focus
|
|
463
|
+
* list — the row a just-sent message occupies once it renders. Unlike
|
|
464
|
+
* {@link lastUserSeq} (raw nodes), this reads the rendered list, so a blank
|
|
465
|
+
* prompt or an unsent queue entry is already filtered out. Null when no
|
|
466
|
+
* prompt row exists. */
|
|
467
|
+
function lastPromptSeq(items) {
|
|
468
|
+
for (let i = items.length - 1; i >= 0; i--) {
|
|
469
|
+
const it = items[i];
|
|
470
|
+
if (it.kind === "user" || it.kind === "steering") return it.seq;
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
219
474
|
/**
|
|
220
475
|
* Decide whether a settled conversation snapshot (`running === false`) ends in
|
|
221
476
|
* a *normal* completed reply, and which user message started it.
|
|
@@ -315,6 +570,18 @@ window.__ModuleLoader__.load({
|
|
|
315
570
|
return prev;
|
|
316
571
|
}
|
|
317
572
|
/**
|
|
573
|
+
* Whether the overlay should scroll to uncover the just-sent prompt row. The
|
|
574
|
+
* send path arms the reveal only while the reader sits in the bottom zone
|
|
575
|
+
* (sending from up in history must never yank the view), and the reveal fires
|
|
576
|
+
* only once the newest prompt seq has advanced — the sent row has actually
|
|
577
|
+
* rendered; delivery can lag the click by a snapshot or two through queue
|
|
578
|
+
* adjudication. An unchanged seq (ordinary streaming snapshots, mount) or an
|
|
579
|
+
* unarmed state never scrolls.
|
|
580
|
+
*/
|
|
581
|
+
function shouldRevealSentPrompt(o) {
|
|
582
|
+
return o.armed && o.nextSeq != null && o.nextSeq !== o.prevSeq;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
318
585
|
* Index of the conversation-turn dot the nav rail should highlight, from live
|
|
319
586
|
* scroll geometry. The rail is a position indicator, so the decision is pure
|
|
320
587
|
* math over measured anchor tops (document order) plus the distance to the
|
|
@@ -576,8 +843,8 @@ window.__ModuleLoader__.load({
|
|
|
576
843
|
}
|
|
577
844
|
function AnswerCard(props) {
|
|
578
845
|
const { t, width, wait, onClose } = props;
|
|
579
|
-
const isQuestion = wait.kind === "
|
|
580
|
-
const questions = isQuestion
|
|
846
|
+
const isQuestion = !(wait.kind === "approval");
|
|
847
|
+
const questions = isQuestion && Array.isArray(wait.questions) ? wait.questions : [];
|
|
581
848
|
const [drafts, setDrafts] = (0, react.useState)({});
|
|
582
849
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
583
850
|
const [error, setError] = (0, react.useState)(null);
|
|
@@ -617,17 +884,18 @@ window.__ModuleLoader__.load({
|
|
|
617
884
|
}
|
|
618
885
|
};
|
|
619
886
|
});
|
|
620
|
-
const respond = async (
|
|
887
|
+
const respond = async (p) => {
|
|
621
888
|
setBusy(true);
|
|
622
889
|
setError(null);
|
|
623
890
|
try {
|
|
624
|
-
const receipt = await
|
|
891
|
+
const receipt = await p;
|
|
625
892
|
if (receipt && receipt.accepted === false) {
|
|
626
893
|
setError(t("answer.rejected") + (receipt.reason ? `: ${receipt.reason}` : ""));
|
|
627
894
|
setBusy(false);
|
|
628
895
|
}
|
|
629
896
|
} catch (err) {
|
|
630
|
-
|
|
897
|
+
const rej = err;
|
|
898
|
+
setError(rej && rej.rejected ? t("answer.rejected") + (rej.reason ? `: ${rej.reason}` : "") : err instanceof Error ? err.message : String(err));
|
|
631
899
|
setBusy(false);
|
|
632
900
|
}
|
|
633
901
|
};
|
|
@@ -636,22 +904,9 @@ window.__ModuleLoader__.load({
|
|
|
636
904
|
setError(t("answer.incomplete"));
|
|
637
905
|
return;
|
|
638
906
|
}
|
|
639
|
-
respond(
|
|
640
|
-
ok: true,
|
|
641
|
-
value: {
|
|
642
|
-
sessionId: wait.sessionId,
|
|
643
|
-
answer: encodeAnswer(questions, drafts)
|
|
644
|
-
}
|
|
645
|
-
});
|
|
907
|
+
respond(wait.answer(encodeAnswer(questions, drafts)));
|
|
646
908
|
};
|
|
647
|
-
const decide = (outcome) => respond(
|
|
648
|
-
ok: true,
|
|
649
|
-
value: {
|
|
650
|
-
sessionId: wait.sessionId,
|
|
651
|
-
approvalId: wait.payload.approvalId,
|
|
652
|
-
outcome
|
|
653
|
-
}
|
|
654
|
-
});
|
|
909
|
+
const decide = (outcome) => respond(wait.answer(outcome));
|
|
655
910
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
656
911
|
className: "fm-card" + (isQuestion ? "" : " fm-card-approval"),
|
|
657
912
|
style: { maxWidth: Math.min(width, 748) },
|
|
@@ -738,12 +993,12 @@ window.__ModuleLoader__.load({
|
|
|
738
993
|
}, q.id);
|
|
739
994
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
740
995
|
className: "fm-card-q",
|
|
741
|
-
children: [wait.
|
|
996
|
+
children: [wait.reason ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
742
997
|
className: "fm-card-q-text",
|
|
743
|
-
children: wait.
|
|
744
|
-
}) : null, wait.
|
|
998
|
+
children: wait.reason
|
|
999
|
+
}) : null, wait.toolName ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
745
1000
|
className: "fm-card-detail",
|
|
746
|
-
children: wait.
|
|
1001
|
+
children: wait.toolName
|
|
747
1002
|
}) : null]
|
|
748
1003
|
})
|
|
749
1004
|
}),
|
|
@@ -884,47 +1139,560 @@ window.__ModuleLoader__.load({
|
|
|
884
1139
|
});
|
|
885
1140
|
}
|
|
886
1141
|
//#endregion
|
|
887
|
-
//#region src/client/
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1142
|
+
//#region src/client/styles.ts
|
|
1143
|
+
/** Package-owned stylesheet text (injected by the client apply, cleaned up on unload). */
|
|
1144
|
+
const FOCUS_CSS = `
|
|
1145
|
+
.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}
|
|
1146
|
+
.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}
|
|
1147
|
+
.fm-title{font-size:calc(14px + var(--dsh-content-font-delta,0px));font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1148
|
+
.fm-body-wrap{position:relative;flex:1 1 auto;display:flex;min-height:0}
|
|
1149
|
+
.fm-body{flex:1 1 auto;min-width:0;overflow-y:auto;padding:28px 28px 110px}
|
|
1150
|
+
.fm-inner{max-width:760px;margin:0 auto}
|
|
1151
|
+
.fm-msg{margin:0}
|
|
1152
|
+
.fm-user-msg{margin:0}
|
|
1153
|
+
.fm-inner>*{margin:var(--dsh-chat-flow-gap,16px) 0 0}
|
|
1154
|
+
.fm-inner>*:first-child{margin-top:0}
|
|
1155
|
+
.fm-user{background:var(--dsw-specific-bubble,#eaf2ff);color:var(--dsw-alias-label-primary,#1a1a1a);font-size:var(--dsh-content-font-size,14px);line-height:calc(22px + var(--dsh-content-font-delta,0px));white-space:pre-wrap;word-break:break-word;border-radius:22px;padding:10px 16px;max-width:min(calc(var(--fm-content-width,748px) * .702),82%);margin-left:auto}
|
|
1156
|
+
.fm-steering{opacity:.85}
|
|
1157
|
+
.fm-assistant{word-break:break-word;font:var(--dsw-font-markdown-base,400 var(--dsh-content-font-size,14px)/calc(24px + var(--dsh-content-font-delta,0px)) var(--dsw-font-family,sans-serif))}
|
|
1158
|
+
.fm-image{max-width:100%;border-radius:8px;margin:8px 0}
|
|
1159
|
+
/* Classic process fold (normal mode, and the expanded work-part rows in
|
|
1160
|
+
compact): centered, muted, secondary typography. */
|
|
1161
|
+
.fm-hidden{text-align:center;color:var(--dsw-alias-label-secondary,#888);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));padding:2px 0;user-select:none}
|
|
1162
|
+
/* Turn process disclosure (compact 对话显示) — replica of the official
|
|
1163
|
+
TurnProcessNodeView row: 33px button, bottom hairline, chevron that
|
|
1164
|
+
rotates from -90deg to 0 when open, 8px breathing room when closed. */
|
|
1165
|
+
.fm-process-root{box-sizing:border-box;border:none;border-bottom:.5px solid var(--dsw-alias-border-l2);width:100%;min-width:0;height:33px;color:var(--dsw-alias-label-secondary);cursor:pointer;text-align:left;background:0 0;align-items:center;padding:0 0 8px;display:flex;font:inherit}
|
|
1166
|
+
.fm-process-root:not([data-open]){margin-bottom:8px}
|
|
1167
|
+
.fm-process-label{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:14px;line-height:24px;overflow:hidden}
|
|
1168
|
+
.fm-process-chevron{width:16px;height:16px;color:var(--dsw-alias-label-tertiary);flex:none;margin-left:6px;transition:transform .1s;transform:rotate(-90deg)}
|
|
1169
|
+
.fm-process-root[data-open] .fm-process-chevron{transform:rotate(0)}
|
|
1170
|
+
@media (prefers-reduced-motion:reduce){.fm-process-chevron{transition:none}}
|
|
1171
|
+
.fm-error{color:var(--dsw-alias-state-error-primary,#d23);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px));white-space:pre-wrap}
|
|
1172
|
+
/* "正在工作" live line — the shimmer is a 1:1 replica of the official
|
|
1173
|
+
generating indicator in dsh-client-ui-conversation (the "Deep Diving"
|
|
1174
|
+
turn-status): brand-blue text (--dsw-static-deepseek-500) with a light
|
|
1175
|
+
highlight (--dsw-static-deepseek-200) sweeping through — gradient stops
|
|
1176
|
+
0/40/50/60/100 on a 250% tile, background-position 100%→0, 1.8s linear,
|
|
1177
|
+
font --dsw-font-s-strong-14 (500 14px/22px). The official class name
|
|
1178
|
+
(Md3f7G_turnStatus) is a per-build CSS-modules hash, so reusing it would
|
|
1179
|
+
break silently on dsh updates — the declarations are copied verbatim onto
|
|
1180
|
+
our own class using the same theme tokens instead. The flanking short
|
|
1181
|
+
dashes are part of the same shimmer span, so the sweep covers dashes and
|
|
1182
|
+
label as one unit. Fallbacks: no background-clip:text → secondary gray
|
|
1183
|
+
label; prefers-reduced-motion → animation off, label stays solid
|
|
1184
|
+
deepseek-500 (the official base color). */
|
|
1185
|
+
.fm-running{text-align:center;user-select:none;animation:fm-up .18s ease}
|
|
1186
|
+
.fm-running-text{color:var(--dsw-alias-label-secondary,#888);font:500 var(--dsh-content-font-size,14px)/calc(22px + var(--dsh-content-font-delta,0px)) var(--dsw-font-family,sans-serif);white-space:nowrap}
|
|
1187
|
+
/* Dashes: separate spans with fixed margins instead of space characters — in
|
|
1188
|
+
CJK/Latin mixed runs the browser itemizes the two U+0020 spaces into
|
|
1189
|
+
different fonts (one Latin-narrow, one Han-wide), and the Han font's en
|
|
1190
|
+
dash glyph is left-biased, which made the two gaps visually unequal. The
|
|
1191
|
+
forced Latin-first stack renders a symmetric en dash; margins are exact. */
|
|
1192
|
+
.fm-running-dash{font-family:system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;margin:0 8px}
|
|
1193
|
+
@supports ((-webkit-background-clip:text) or (background-clip:text)){
|
|
1194
|
+
.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}
|
|
1195
|
+
}
|
|
1196
|
+
@keyframes fm-sweep{to{background-position:0 0}}
|
|
1197
|
+
@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}}
|
|
1198
|
+
.fm-empty{color:var(--dsw-alias-label-secondary,#888);padding:48px 16px;text-align:center}
|
|
1199
|
+
.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:var(--dsw-shadow-lv2,0 4px 16px rgba(0,0,0,.12));font-size:var(--dsh-content-font-size-secondary,13px)}
|
|
1200
|
+
.fm-reply-toast-dot{width:8px;height:8px;border-radius:999px;background:var(--dsw-static-deepseek-500,#4176e6);flex:0 0 auto}
|
|
1201
|
+
.fm-reply-toast-text{white-space:nowrap}
|
|
1202
|
+
/* ---- bottom dock: bar / pill / jump-to-bottom / answer card ----
|
|
1203
|
+
Visual language mirrors the official composer cards token-for-token
|
|
1204
|
+
(ui-conversation InputBar/QuestionComposer, ui-user-questions
|
|
1205
|
+
PlanReviewPanel): --dsw-specific-input-major surfaces, 20–22px radii,
|
|
1206
|
+
--dsw-shadow-lv2, the 34px round info-fill primary button, interactive-
|
|
1207
|
+
hover option tinting. */
|
|
1208
|
+
.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}
|
|
1209
|
+
.fm-dock>*{pointer-events:auto;animation:fm-up .18s ease}
|
|
1210
|
+
.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:calc(var(--dsh-content-font-size,14px) + 2px);line-height:calc(24px + var(--dsh-content-font-delta,0px))}
|
|
1211
|
+
.fm-bar-row{display:flex;align-items:flex-end;gap:12px;min-width:0;padding:2px 8px 6px}
|
|
1212
|
+
/* flex-end anchors the send button to the row's bottom edge, so a multi-line
|
|
1213
|
+
draft grows the textarea upward and the button's distance to the bar's
|
|
1214
|
+
bottom-right corner never moves. */
|
|
1215
|
+
.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}
|
|
1216
|
+
.fm-bar-text::placeholder{color:var(--dsw-alias-label-caption,#999);user-select:none}
|
|
1217
|
+
.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)}
|
|
1218
|
+
.fm-bar-note-error{color:var(--dsw-alias-state-error-primary,#d23);word-break:break-word}
|
|
1219
|
+
.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}
|
|
1220
|
+
.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}
|
|
1221
|
+
.fm-bar-send:hover:not(:disabled){background:var(--dsw-alias-button-info-hover,#3560c4)}
|
|
1222
|
+
.fm-bar-send:disabled{opacity:.4;cursor:default}
|
|
1223
|
+
.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}
|
|
1224
|
+
.fm-pill:hover{color:var(--dsw-alias-label-primary,#1a1a1a)}
|
|
1225
|
+
.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))}
|
|
1226
|
+
.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)}
|
|
1227
|
+
.fm-card *{box-sizing:border-box}
|
|
1228
|
+
.fm-card-approval{border-color:var(--dsw-alias-state-warn-secondary,#e6a23c)}
|
|
1229
|
+
.fm-card-head{flex:0 0 auto;display:flex;justify-content:space-between;align-items:flex-start;gap:16px;padding:20px 16px 0 24px}
|
|
1230
|
+
.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}
|
|
1231
|
+
.fm-card-heading{min-width:0}
|
|
1232
|
+
.fm-card-eyebrow{color:var(--dsw-alias-label-tertiary,#999);margin:0 0 5px;font-size:11px;line-height:16px}
|
|
1233
|
+
.fm-card-title{margin:0;font-size:16px;font-weight:500;line-height:22px}
|
|
1234
|
+
.fm-card-strip-title{font-size:13px;line-height:18px;flex:1;min-width:0}
|
|
1235
|
+
.fm-card-dot{background:var(--dsw-alias-state-warn-primary,#9a6b12);border-radius:50%;width:8px;height:8px;flex:none}
|
|
1236
|
+
.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}
|
|
1237
|
+
.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)}
|
|
1238
|
+
.fm-card-approval .fm-card-close,.fm-card-approval .fm-card-close:hover:not(:disabled){color:inherit}
|
|
1239
|
+
.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:var(--dsh-content-font-size,14px);line-height:calc(22px + var(--dsh-content-font-delta,0px))}
|
|
1240
|
+
.fm-card-q{display:flex;flex-direction:column}
|
|
1241
|
+
.fm-card-q-text{margin:0 2px 8px;padding:0 22px;white-space:pre-wrap;word-break:break-word;font-size:calc(var(--dsh-content-font-size,14px) + 2px);font-weight:500;line-height:calc(22px + var(--dsh-content-font-delta,0px))}
|
|
1242
|
+
.fm-card-detail{color:var(--dsw-alias-label-secondary,#888);margin:0 2px 8px;padding:0 22px;font-size:var(--dsh-content-font-size,14px);line-height:calc(22px + var(--dsh-content-font-delta,0px))}
|
|
1243
|
+
.fm-card-opts{display:flex;flex-direction:column;gap:1px;margin:8px 0 0;padding:4px 12px}
|
|
1244
|
+
.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}
|
|
1245
|
+
.fm-opt:hover:not(:disabled),.fm-opt-on{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06))}
|
|
1246
|
+
.fm-opt-on{border-color:var(--dsw-alias-border-l2,rgba(0,0,0,.18))}
|
|
1247
|
+
.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}
|
|
1248
|
+
.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)}
|
|
1249
|
+
.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}
|
|
1250
|
+
.fm-opt-check-on::before{border-color:var(--dsw-alias-label-primary,#1a1a1a);background:var(--dsw-alias-label-primary,#1a1a1a)}
|
|
1251
|
+
.fm-opt-check>*{grid-area:1/1}
|
|
1252
|
+
.fm-opt-copy{flex:1;min-width:0}
|
|
1253
|
+
.fm-opt-line{display:flex;flex-wrap:wrap;align-items:baseline;gap:2px 6px}
|
|
1254
|
+
.fm-opt-label{font-size:var(--dsh-content-font-size,14px);font-weight:500;line-height:calc(24px + var(--dsh-content-font-delta,0px));word-break:break-word}
|
|
1255
|
+
.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}
|
|
1256
|
+
.fm-opt-desc{color:var(--dsw-alias-label-tertiary,#999);font-size:var(--dsh-content-font-size,14px);line-height:calc(24px + var(--dsh-content-font-delta,0px));word-break:break-word}
|
|
1257
|
+
.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}
|
|
1258
|
+
.fm-card-custom:hover,.fm-card-custom:focus-within{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06))}
|
|
1259
|
+
.fm-card-custom:focus-within{border-color:var(--dsw-alias-border-l2,rgba(0,0,0,.18))}
|
|
1260
|
+
.fm-card-field{min-width:0;width:100%;border:none;outline:none;background:0 0;color:inherit;font:inherit;font-size:var(--dsh-content-font-size,14px);line-height:calc(24px + var(--dsh-content-font-delta,0px));padding:0}
|
|
1261
|
+
.fm-card-field::placeholder{color:var(--dsw-alias-label-caption,#999)}
|
|
1262
|
+
.fm-card-foot{flex:0 0 auto;display:flex;justify-content:space-between;align-items:center;gap:12px;padding:8px 16px 12px}
|
|
1263
|
+
.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}
|
|
1264
|
+
.fm-card-actions{display:flex;align-items:center;gap:8px;flex:none}
|
|
1265
|
+
@keyframes fm-up{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
|
|
1266
|
+
.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}
|
|
1267
|
+
.fm-plugin-card:hover{border-color:var(--dsw-alias-label-dimmed)}
|
|
1268
|
+
.fm-plugin-card-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
|
|
1269
|
+
.fm-plugin-card-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
|
|
1270
|
+
.fm-plugin-card-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
|
|
1271
|
+
.fm-plugin-card-headtext{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
|
|
1272
|
+
.fm-plugin-card-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
|
|
1273
|
+
.fm-plugin-card-desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
|
|
1274
|
+
.fm-plugin-card-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}
|
|
1275
|
+
.fm-plugin-card-chevron-open{transform:rotate(180deg)}
|
|
1276
|
+
.fm-plugin-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}
|
|
1277
|
+
.fm-plugin-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
|
|
1278
|
+
.fm-plugin-field+.fm-plugin-field{border-top:1px solid var(--dsw-alias-border-l2)}
|
|
1279
|
+
.fm-plugin-field-head{align-items:center;gap:8px;display:flex}
|
|
1280
|
+
.fm-plugin-field-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
|
|
1281
|
+
.fm-plugin-field-value{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}
|
|
1282
|
+
.fm-plugin-field-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
|
|
1283
|
+
.fm-plugin-check{align-items:center;gap:8px;display:flex;cursor:pointer;user-select:none}
|
|
1284
|
+
.fm-plugin-check input[type="checkbox"]{width:16px;height:16px;margin:0;cursor:pointer;accent-color:var(--dsw-static-deepseek-500,#4176e6)}
|
|
1285
|
+
.fm-plugin-check-label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5}
|
|
1286
|
+
.fm-plugin-range{width:100%;max-width:280px;cursor:pointer;accent-color:var(--dsw-static-deepseek-500,#4176e6)}
|
|
1287
|
+
/* DSH-better-sidebar compatibility: its panel host (top-right toggle cluster +
|
|
1288
|
+
right/bottom panels) is appended to document.body at z-index 40/45, above
|
|
1289
|
+
shell.overlay's z-20 layer, so this overlay cannot cover it. Hide the host
|
|
1290
|
+
while focus is on, and neutralize the #root layout push it applies when its
|
|
1291
|
+
panels are open. Selectors mirror better-sidebar's own layout.css exactly. */
|
|
1292
|
+
body[data-fm-focus] [data-dsh-panel-host]{display:none!important}
|
|
1293
|
+
body[data-fm-focus] #root{margin-right:0!important;width:100%!important}
|
|
1294
|
+
body[data-fm-focus] #root [data-dsh-frame] > [data-pane="conversation"],
|
|
1295
|
+
body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!important}
|
|
1296
|
+
/* First-run onboarding (settings.onboarding step) — content inside the official
|
|
1297
|
+
Modal primitive; the primitive owns the mask/dialog chrome, these style our body. */
|
|
1298
|
+
.fm-onboard{width:min(600px,100%);padding:0}
|
|
1299
|
+
.fm-onboard-content{box-sizing:border-box;flex-direction:column;max-height:calc(100vh - 48px);padding:28px;display:flex;overflow-y:auto}
|
|
1300
|
+
.fm-onboard-title{color:var(--dsw-alias-label-primary);outline:none;margin:0;font-size:20px;font-weight:500;line-height:28px}
|
|
1301
|
+
.fm-onboard-intro{color:var(--dsw-alias-label-secondary);margin:8px 0 0;font-size:14px;line-height:22px}
|
|
1302
|
+
.fm-onboard-subtitle{color:var(--dsw-alias-label-primary);margin:20px 0 8px;font-size:14px;font-weight:600;line-height:22px}
|
|
1303
|
+
.fm-onboard-features{margin:0;padding:0 0 0 18px;list-style:disc;display:flex;flex-direction:column;gap:6px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}
|
|
1304
|
+
.fm-onboard-note{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:18px}
|
|
1305
|
+
.fm-onboard-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}
|
|
1306
|
+
@media (width<=560px){.fm-onboard-content{padding:24px}}
|
|
1307
|
+
`;
|
|
1308
|
+
const injectedStyles = /* @__PURE__ */ new Map();
|
|
1309
|
+
/** Inject a named package stylesheet once and return a dispose function. The
|
|
1310
|
+
* map keeps duplicates out across mount/unmount cycles; the dispose removes
|
|
1311
|
+
* the element so plugin unload (or the component's effect cleanup) never
|
|
1312
|
+
* leaks <style> nodes. */
|
|
1313
|
+
function injectStyle(id, css) {
|
|
1314
|
+
if (typeof document === "undefined") return () => {};
|
|
1315
|
+
let el = injectedStyles.get(id);
|
|
1316
|
+
if (!el) {
|
|
1317
|
+
el = document.createElement("style");
|
|
1318
|
+
el.setAttribute("data-plugin", "dsh-focus-overlay");
|
|
1319
|
+
el.setAttribute("data-fm-style", id);
|
|
1320
|
+
el.textContent = css;
|
|
1321
|
+
document.head.appendChild(el);
|
|
1322
|
+
injectedStyles.set(id, el);
|
|
1323
|
+
}
|
|
1324
|
+
const node = el;
|
|
1325
|
+
return () => {
|
|
1326
|
+
node.remove();
|
|
1327
|
+
injectedStyles.delete(id);
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
//#endregion
|
|
1331
|
+
//#region src/client/TurnRail.tsx
|
|
1332
|
+
/**
|
|
1333
|
+
* TurnRail — a 1:1 replica of the official dsh-client-ui-chat TurnNavigator
|
|
1334
|
+
* (the conversation view's right-side turn rail), rebuilt for the focus
|
|
1335
|
+
* overlay because the official component is internal to the chat view module
|
|
1336
|
+
* and not reachable through any runtime module id. Markup, geometry, styling,
|
|
1337
|
+
* and motion mirror the official `TurnNavigator.module.css` + `TurnNavigator.js`:
|
|
1338
|
+
*
|
|
1339
|
+
* - fixed pitch between marks (TURN_SPACING_PX), rail insets, 24px scroll
|
|
1340
|
+
* fade bands via mask-image gradients at each scrollable end;
|
|
1341
|
+
* - the frame is vertically centered over the reading band, its height
|
|
1342
|
+
* clamped to `min(natural, band - 64px, 420px)` like the official frame;
|
|
1343
|
+
* - a hover/focus preview card (1-line prompt + 3-line response) that maps
|
|
1344
|
+
* the pointer's Y to the nearest mark — including while the inner scroller
|
|
1345
|
+
* has scrolled — and clamps itself inside the rail band;
|
|
1346
|
+
* - the active mark keeps itself in view while the pointer is elsewhere;
|
|
1347
|
+
* - enter/pulse keyframes and the prefers-reduced-motion opt-out.
|
|
1348
|
+
*
|
|
1349
|
+
* Differences imposed by the overlay context: every mark is "loaded" (the
|
|
1350
|
+
* overlay pages the full history in on open, so there are no unloaded marks
|
|
1351
|
+
* to render), and navigation scrolls the overlay's own scrollport instead of
|
|
1352
|
+
* the conversation view's DOM anchor.
|
|
1353
|
+
*/
|
|
1354
|
+
/** Fixed pitch between neighbouring marks; overflow scrolls inside the frame. */
|
|
1355
|
+
const TURN_SPACING_PX = 10;
|
|
1356
|
+
/** Rail padding above the first mark and below the last one, per end. */
|
|
1357
|
+
const RAIL_INSET_PX = 6;
|
|
1358
|
+
/** Fade band the mask reserves at a scrollable end. */
|
|
1359
|
+
const FADE_PX = 24;
|
|
1360
|
+
const RAIL_CSS = `
|
|
1361
|
+
.fm-rail-slot{position:absolute;inset:0;pointer-events:none;z-index:2}
|
|
1362
|
+
.fm-rail{--turn-rail-band:calc(var(--dsh-conversation-viewport-height,100dvh) - var(--dsh-composer-height,152px));--turn-preview-height:100px;top:calc(var(--turn-rail-band) / 2);right:12px;width:28px;height:min(var(--turn-natural-height),max(0px,calc(var(--turn-rail-band) - 64px)),420px);cursor:pointer;pointer-events:auto;transition:height .22s cubic-bezier(.2,.8,.2,1);position:absolute;transform:translateY(-50%)}
|
|
1363
|
+
.fm-rail-scroller{overscroll-behavior:contain;scrollbar-width:none;position:absolute;inset:0;overflow-y:auto}
|
|
1364
|
+
.fm-rail-scroller::-webkit-scrollbar{display:none}
|
|
1365
|
+
.fm-rail-fade-top{mask-image:linear-gradient(#0000 0,#000 24px 100%)}
|
|
1366
|
+
.fm-rail-fade-bottom{mask-image:linear-gradient(#000 0 calc(100% - 24px),#0000 100%)}
|
|
1367
|
+
.fm-rail-fade-top.fm-rail-fade-bottom{mask-image:linear-gradient(#0000 0,#000 24px calc(100% - 24px),#0000 100%)}
|
|
1368
|
+
.fm-rail-marks{height:var(--turn-natural-height);position:relative}
|
|
1369
|
+
.fm-rail-mark-position{top:calc(var(--turn-natural-position) + var(--turn-rail-inset));height:10px;transition:top .22s cubic-bezier(.2,.8,.2,1);animation:.15s ease-out fm-turn-mark-enter;position:absolute;left:0;right:0;transform:translateY(-50%)}
|
|
1370
|
+
.fm-rail-mark{cursor:pointer;pointer-events:none;background:0 0;border:0;border-radius:8px;width:20px;padding:0;position:absolute;inset:0 0 0 auto}
|
|
1371
|
+
.fm-rail-mark:before{background:var(--dsw-alias-border-l4);content:"";border-radius:2px;width:12px;height:2px;transition:width .14s,background-color .14s;position:absolute;top:50%;right:0;transform:translateY(-50%)}
|
|
1372
|
+
.fm-rail-mark-preview:before{background:var(--dsw-alias-label-tertiary);width:18px}
|
|
1373
|
+
.fm-rail-mark-active:before{background:var(--dsw-alias-label-primary);width:20px}
|
|
1374
|
+
.fm-rail-mark:focus-visible:before{background:var(--dsw-alias-state-business-primary);width:20px}
|
|
1375
|
+
.fm-rail-mark:focus-visible{outline:1px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
|
|
1376
|
+
.fm-rail-preview{top:clamp(0px,calc(var(--turn-natural-position) + var(--turn-rail-inset) - var(--turn-scroll-top,0px) - var(--turn-preview-height) / 2),calc(100% - var(--turn-preview-height)));box-sizing:border-box;width:min(300px,100cqw - 120px);max-height:var(--turn-preview-height);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);box-shadow:var(--dsw-elevation-panel,0 4px 16px rgba(0,0,0,.12));pointer-events:none;border:0;border-radius:10px;padding:10px 12px;transition:top .14s cubic-bezier(.2,.8,.2,1);animation:.12s ease-out fm-turn-preview-enter;position:absolute;right:calc(100% + 10px);overflow:hidden}
|
|
1377
|
+
.fm-rail-preview-prompt,.fm-rail-preview-response{-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}
|
|
1378
|
+
.fm-rail-preview-prompt{font:var(--dsw-font-xs-strong-13,600 13px/18px var(--dsw-font-family,sans-serif));-webkit-line-clamp:1}
|
|
1379
|
+
.fm-rail-preview-response{color:var(--dsw-alias-label-caption);font:var(--dsw-font-xxs-12,400 12px/18px var(--dsw-font-family,sans-serif));-webkit-line-clamp:3;margin-top:4px}
|
|
1380
|
+
@keyframes fm-turn-mark-enter{0%{opacity:0}to{opacity:1}}
|
|
1381
|
+
@keyframes fm-turn-preview-enter{0%{opacity:0;transform:translate(4px)}to{opacity:1;transform:translate(0)}}
|
|
1382
|
+
@media (prefers-reduced-motion:reduce){.fm-rail,.fm-rail-scroller,.fm-rail-mark-position,.fm-rail-mark:before,.fm-rail-preview{scroll-behavior:auto;transition:none;animation:none}}
|
|
1383
|
+
`;
|
|
1384
|
+
function itemPosition(index) {
|
|
1385
|
+
return { "--turn-natural-position": `${index * TURN_SPACING_PX}px` };
|
|
1386
|
+
}
|
|
1387
|
+
function frameStyle(count, scrollTop) {
|
|
1388
|
+
return {
|
|
1389
|
+
"--turn-natural-height": `${(count - 1) * TURN_SPACING_PX + 2 * RAIL_INSET_PX}px`,
|
|
1390
|
+
"--turn-rail-inset": `${RAIL_INSET_PX}px`,
|
|
1391
|
+
"--turn-scroll-top": `${scrollTop}px`
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
function itemAtPointer(items, frame, scrollTop, clientY) {
|
|
1395
|
+
const offset = clientY - frame.getBoundingClientRect().top + scrollTop - RAIL_INSET_PX;
|
|
1396
|
+
return items[Math.max(0, Math.min(items.length - 1, Math.round(offset / TURN_SPACING_PX)))];
|
|
1397
|
+
}
|
|
1398
|
+
const RAIL_AT_REST = {
|
|
1399
|
+
top: 0,
|
|
1400
|
+
canScrollUp: false,
|
|
1401
|
+
canScrollDown: false
|
|
895
1402
|
};
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1403
|
+
function railScrollState(scroller) {
|
|
1404
|
+
const top = scroller.scrollTop;
|
|
1405
|
+
return {
|
|
1406
|
+
top,
|
|
1407
|
+
canScrollUp: top > 1,
|
|
1408
|
+
canScrollDown: top < scroller.scrollHeight - scroller.clientHeight - 1
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
function sameRailScrollState(left, right) {
|
|
1412
|
+
return left.top === right.top && left.canScrollUp === right.canScrollUp && left.canScrollDown === right.canScrollDown;
|
|
1413
|
+
}
|
|
1414
|
+
function TurnRail(props) {
|
|
1415
|
+
const { items, activeTurn, onNavigate, label, jumpLabel, turnLabel } = props;
|
|
1416
|
+
const [previewTurn, setPreviewTurn] = (0, react.useState)(null);
|
|
1417
|
+
const [scrollState, setScrollState] = (0, react.useState)(RAIL_AT_REST);
|
|
1418
|
+
const scrollerRef = (0, react.useRef)(null);
|
|
1419
|
+
const pointerInsideRef = (0, react.useRef)(false);
|
|
1420
|
+
const previewId = (0, react.useId)();
|
|
1421
|
+
(0, react.useEffect)(() => injectStyle("fm-turn-rail", RAIL_CSS), []);
|
|
1422
|
+
const syncScrollState = () => {
|
|
1423
|
+
const scroller = scrollerRef.current;
|
|
1424
|
+
if (scroller === null) return;
|
|
1425
|
+
const next = railScrollState(scroller);
|
|
1426
|
+
setScrollState((current) => sameRailScrollState(current, next) ? current : next);
|
|
1427
|
+
};
|
|
1428
|
+
(0, react.useEffect)(() => {
|
|
1429
|
+
const scroller = scrollerRef.current;
|
|
1430
|
+
if (scroller === null || typeof ResizeObserver === "undefined") return;
|
|
1431
|
+
const observer = new ResizeObserver(syncScrollState);
|
|
1432
|
+
observer.observe(scroller);
|
|
904
1433
|
return () => {
|
|
905
|
-
|
|
906
|
-
if (i >= 0) focusListeners.splice(i, 1);
|
|
1434
|
+
observer.disconnect();
|
|
907
1435
|
};
|
|
908
|
-
},
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1436
|
+
}, []);
|
|
1437
|
+
(0, react.useEffect)(() => {
|
|
1438
|
+
syncScrollState();
|
|
1439
|
+
}, [items.length]);
|
|
1440
|
+
(0, react.useEffect)(() => {
|
|
1441
|
+
const scroller = scrollerRef.current;
|
|
1442
|
+
const index = items.findIndex((item) => item.turn === activeTurn);
|
|
1443
|
+
if (scroller === null || index < 0 || pointerInsideRef.current) return;
|
|
1444
|
+
const markTop = index * TURN_SPACING_PX + RAIL_INSET_PX;
|
|
1445
|
+
const viewTop = scroller.scrollTop;
|
|
1446
|
+
const viewHeight = scroller.clientHeight;
|
|
1447
|
+
if (viewHeight <= 0 || markTop >= viewTop + FADE_PX && markTop <= viewTop + viewHeight - FADE_PX) return;
|
|
1448
|
+
const target = Math.max(0, markTop - viewHeight / 2);
|
|
1449
|
+
const reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1450
|
+
if (typeof scroller.scrollTo === "function") scroller.scrollTo({
|
|
1451
|
+
top: target,
|
|
1452
|
+
behavior: reduced ? "auto" : "smooth"
|
|
1453
|
+
});
|
|
1454
|
+
else scroller.scrollTop = target;
|
|
1455
|
+
syncScrollState();
|
|
1456
|
+
}, [activeTurn, items]);
|
|
1457
|
+
if (items.length < 2) return null;
|
|
1458
|
+
const previewIndex = items.findIndex((item) => item.turn === previewTurn);
|
|
1459
|
+
const preview = previewIndex < 0 ? void 0 : items[previewIndex];
|
|
1460
|
+
const previewPosition = previewIndex < 0 ? void 0 : itemPosition(previewIndex);
|
|
1461
|
+
const previewAtPointer = (event) => {
|
|
1462
|
+
const scrollTop = scrollerRef.current ? scrollerRef.current.scrollTop : 0;
|
|
1463
|
+
const item = itemAtPointer(items, event.currentTarget, scrollTop, event.clientY);
|
|
1464
|
+
setPreviewTurn(item ? item.turn : null);
|
|
1465
|
+
};
|
|
1466
|
+
const navigateAtPointer = (event) => {
|
|
1467
|
+
const scrollTop = scrollerRef.current ? scrollerRef.current.scrollTop : 0;
|
|
1468
|
+
const item = itemAtPointer(items, event.currentTarget, scrollTop, event.clientY);
|
|
1469
|
+
if (item !== void 0) onNavigate(item);
|
|
1470
|
+
};
|
|
1471
|
+
const fadeClasses = ["fm-rail-scroller"];
|
|
1472
|
+
if (scrollState.canScrollUp) fadeClasses.push("fm-rail-fade-top");
|
|
1473
|
+
if (scrollState.canScrollDown) fadeClasses.push("fm-rail-fade-bottom");
|
|
1474
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1475
|
+
className: "fm-rail-slot",
|
|
1476
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
|
|
1477
|
+
className: "fm-rail",
|
|
1478
|
+
style: frameStyle(items.length, scrollState.top),
|
|
1479
|
+
"aria-label": label,
|
|
1480
|
+
onClick: navigateAtPointer,
|
|
1481
|
+
onPointerMove: previewAtPointer,
|
|
1482
|
+
onPointerEnter: () => {
|
|
1483
|
+
pointerInsideRef.current = true;
|
|
1484
|
+
},
|
|
1485
|
+
onPointerLeave: () => {
|
|
1486
|
+
pointerInsideRef.current = false;
|
|
1487
|
+
setPreviewTurn(null);
|
|
1488
|
+
},
|
|
1489
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1490
|
+
ref: scrollerRef,
|
|
1491
|
+
className: fadeClasses.join(" "),
|
|
1492
|
+
onScroll: () => {
|
|
1493
|
+
syncScrollState();
|
|
1494
|
+
},
|
|
1495
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1496
|
+
className: "fm-rail-marks",
|
|
1497
|
+
children: items.map((item, index) => {
|
|
1498
|
+
const active = item.turn === activeTurn;
|
|
1499
|
+
const showingPreview = item.turn === previewTurn;
|
|
1500
|
+
const classes = ["fm-rail-mark"];
|
|
1501
|
+
if (active) classes.push("fm-rail-mark-active");
|
|
1502
|
+
else if (showingPreview) classes.push("fm-rail-mark-preview");
|
|
1503
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1504
|
+
className: "fm-rail-mark-position",
|
|
1505
|
+
style: itemPosition(index),
|
|
1506
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1507
|
+
type: "button",
|
|
1508
|
+
className: classes.join(" "),
|
|
1509
|
+
"aria-label": jumpLabel(item.turn),
|
|
1510
|
+
"aria-current": active ? "true" : void 0,
|
|
1511
|
+
"aria-describedby": showingPreview ? previewId : void 0,
|
|
1512
|
+
onClick: (event) => {
|
|
1513
|
+
event.stopPropagation();
|
|
1514
|
+
onNavigate(item);
|
|
1515
|
+
},
|
|
1516
|
+
onFocus: () => {
|
|
1517
|
+
setPreviewTurn(item.turn);
|
|
1518
|
+
},
|
|
1519
|
+
onBlur: () => {
|
|
1520
|
+
setPreviewTurn(null);
|
|
1521
|
+
}
|
|
1522
|
+
})
|
|
1523
|
+
}, item.turn);
|
|
1524
|
+
})
|
|
1525
|
+
})
|
|
1526
|
+
}), preview !== void 0 && previewPosition !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1527
|
+
id: previewId,
|
|
1528
|
+
role: "tooltip",
|
|
1529
|
+
className: "fm-rail-preview",
|
|
1530
|
+
style: previewPosition,
|
|
1531
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1532
|
+
className: "fm-rail-preview-prompt",
|
|
1533
|
+
children: preview.prompt || turnLabel(preview.turn)
|
|
1534
|
+
}), preview.response !== "" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1535
|
+
className: "fm-rail-preview-response",
|
|
1536
|
+
children: preview.response
|
|
1537
|
+
}) : null]
|
|
1538
|
+
}) : null]
|
|
1539
|
+
})
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
//#endregion
|
|
1543
|
+
//#region src/client/WidthHandle.tsx
|
|
1544
|
+
/**
|
|
1545
|
+
* WidthHandle — a 1:1 replica of the official dsh-client-ui-conversation
|
|
1546
|
+
* transcript width handle (the drag strip beside the content column), rebuilt
|
|
1547
|
+
* for the focus overlay because the official component is internal to the
|
|
1548
|
+
* conversation module and not reachable through any runtime module id.
|
|
1549
|
+
*
|
|
1550
|
+
* Interaction model mirrors the official `WidthHandle` exactly:
|
|
1551
|
+
* - pointer capture on the strip; both sides write the ONE centered width, so
|
|
1552
|
+
* outward travel widens by 2× the pointer distance;
|
|
1553
|
+
* - rAF-throttled drag updates (live resize while the pointer moves);
|
|
1554
|
+
* - pointermove publishes the pointer's Y as a CSS variable so the glow
|
|
1555
|
+
* indicator rides it (48px falloff gradient, 3px core line);
|
|
1556
|
+
* - commit on release (only when the pointer actually moved), cancel on
|
|
1557
|
+
* pointer cancel / lost capture.
|
|
1558
|
+
*
|
|
1559
|
+
* Clamping constants mirror the official `resolveContentWidth`: a 640px floor
|
|
1560
|
+
* and a 176px edge budget per viewport (88px per side keeps the handles
|
|
1561
|
+
* placeable — a wider column would push its own handles off-screen).
|
|
1562
|
+
*/
|
|
1563
|
+
const HANDLE_CSS = `
|
|
1564
|
+
.fm-width-handles{position:absolute;top:0;bottom:0;left:0;right:0;pointer-events:none}
|
|
1565
|
+
.fm-width-handle{z-index:8;width:min(40px,calc((100% - var(--fm-content-width,748px)) / 2 - 24px - 24px));cursor:col-resize;pointer-events:auto;position:absolute;top:0;bottom:0}
|
|
1566
|
+
.fm-width-handle[data-side=left]{right:calc(50% + var(--fm-content-width,748px) / 2 + 24px)}
|
|
1567
|
+
.fm-width-handle[data-side=right]{left:calc(50% + var(--fm-content-width,748px) / 2 + 24px)}
|
|
1568
|
+
.fm-width-handle:after{content:"";background:linear-gradient(to bottom,transparent calc(var(--dsh-width-handle-pointer-y,50%) - 52px),var(--dsw-alias-scrollbar-hover-l1,rgba(0,0,0,.18)) calc(var(--dsh-width-handle-pointer-y,50%) - 12px),var(--dsw-alias-scrollbar-hover-l1,rgba(0,0,0,.18)) calc(var(--dsh-width-handle-pointer-y,50%) + 12px),transparent calc(var(--dsh-width-handle-pointer-y,50%) + 52px));opacity:0;pointer-events:none;border-radius:3px;width:3px;position:absolute;top:0;bottom:0}
|
|
1569
|
+
.fm-width-handle[data-side=left]:after{right:16px}
|
|
1570
|
+
.fm-width-handle[data-side=right]:after{left:16px}
|
|
1571
|
+
.fm-width-handle:hover:after,.fm-width-handle[data-dragging]:after{opacity:1}
|
|
1572
|
+
@media (prefers-reduced-motion:reduce){.fm-width-handle:after{transition:none}}
|
|
1573
|
+
`;
|
|
1574
|
+
function WidthHandle(props) {
|
|
1575
|
+
const [dragging, setDragging] = (0, react.useState)(false);
|
|
1576
|
+
const base = (0, react.useRef)(0);
|
|
1577
|
+
const origin = (0, react.useRef)(0);
|
|
1578
|
+
const latest = (0, react.useRef)(0);
|
|
1579
|
+
const frame = (0, react.useRef)(null);
|
|
1580
|
+
const callbacks = (0, react.useRef)(props);
|
|
1581
|
+
callbacks.current = props;
|
|
1582
|
+
const outwardWidth = () => {
|
|
1583
|
+
const dx = latest.current - origin.current;
|
|
1584
|
+
const outward = callbacks.current.side === "right" ? dx : -dx;
|
|
1585
|
+
return base.current + outward * 2;
|
|
1586
|
+
};
|
|
1587
|
+
const cancelFrame = () => {
|
|
1588
|
+
if (frame.current !== null) {
|
|
1589
|
+
cancelAnimationFrame(frame.current);
|
|
1590
|
+
frame.current = null;
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
const onPointerDown = (0, react.useCallback)((e) => {
|
|
1594
|
+
e.preventDefault();
|
|
1595
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
1596
|
+
origin.current = e.clientX;
|
|
1597
|
+
latest.current = e.clientX;
|
|
1598
|
+
base.current = callbacks.current.onStart();
|
|
1599
|
+
setDragging(true);
|
|
1600
|
+
}, []);
|
|
1601
|
+
const onPointerMove = (0, react.useCallback)((e) => {
|
|
1602
|
+
const box = e.currentTarget.getBoundingClientRect();
|
|
1603
|
+
e.currentTarget.style.setProperty("--dsh-width-handle-pointer-y", `${e.clientY - box.top}px`);
|
|
1604
|
+
if (!e.currentTarget.hasPointerCapture(e.pointerId)) return;
|
|
1605
|
+
latest.current = e.clientX;
|
|
1606
|
+
frame.current ??= requestAnimationFrame(() => {
|
|
1607
|
+
frame.current = null;
|
|
1608
|
+
callbacks.current.onDrag(outwardWidth());
|
|
1609
|
+
});
|
|
1610
|
+
}, []);
|
|
1611
|
+
const onPointerUp = (0, react.useCallback)((e) => {
|
|
1612
|
+
if (!e.currentTarget.hasPointerCapture(e.pointerId)) return;
|
|
1613
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
1614
|
+
cancelFrame();
|
|
1615
|
+
latest.current = e.clientX;
|
|
1616
|
+
if (latest.current !== origin.current) callbacks.current.onCommit(outwardWidth());
|
|
1617
|
+
setDragging(false);
|
|
1618
|
+
callbacks.current.onEnd();
|
|
1619
|
+
}, []);
|
|
1620
|
+
const onPointerCancel = (0, react.useCallback)(() => {
|
|
1621
|
+
cancelFrame();
|
|
1622
|
+
setDragging(false);
|
|
1623
|
+
callbacks.current.onEnd();
|
|
1624
|
+
}, []);
|
|
1625
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1626
|
+
className: "fm-width-handle",
|
|
1627
|
+
"data-side": props.side,
|
|
1628
|
+
"data-dragging": dragging || void 0,
|
|
1629
|
+
style: { "--fm-content-width": `${props.width}px` },
|
|
1630
|
+
onPointerDown,
|
|
1631
|
+
onPointerMove,
|
|
1632
|
+
onPointerUp,
|
|
1633
|
+
onPointerCancel,
|
|
1634
|
+
onLostPointerCapture: onPointerCancel
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
function WidthHandles(props) {
|
|
1638
|
+
(0, react.useEffect)(() => injectStyle("fm-width-handle", HANDLE_CSS), []);
|
|
1639
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1640
|
+
className: "fm-width-handles",
|
|
1641
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidthHandle, {
|
|
1642
|
+
...props,
|
|
1643
|
+
side: "left"
|
|
1644
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidthHandle, {
|
|
1645
|
+
...props,
|
|
1646
|
+
side: "right"
|
|
1647
|
+
})]
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
//#endregion
|
|
1651
|
+
//#region src/client/FocusView.tsx
|
|
1652
|
+
let focusOn = false;
|
|
1653
|
+
let pendingAnchorKey = null;
|
|
1654
|
+
let pendingAutoSeq = null;
|
|
1655
|
+
let donePing = 0;
|
|
1656
|
+
const focusListeners = [];
|
|
1657
|
+
const notify = () => {
|
|
1658
|
+
for (const l of focusListeners) try {
|
|
1659
|
+
l();
|
|
1660
|
+
} catch (err) {
|
|
1661
|
+
console.error("[dsh-focus-overlay] focus listener failed", err);
|
|
1662
|
+
}
|
|
1663
|
+
};
|
|
1664
|
+
const focusStore = {
|
|
1665
|
+
get: () => focusOn,
|
|
1666
|
+
set: (v) => {
|
|
1667
|
+
focusOn = !!v;
|
|
1668
|
+
notify();
|
|
1669
|
+
},
|
|
1670
|
+
subscribe: (l) => {
|
|
1671
|
+
focusListeners.push(l);
|
|
1672
|
+
return () => {
|
|
1673
|
+
const i = focusListeners.indexOf(l);
|
|
1674
|
+
if (i >= 0) focusListeners.splice(i, 1);
|
|
1675
|
+
};
|
|
1676
|
+
},
|
|
1677
|
+
captureChatAnchor: () => {
|
|
1678
|
+
pendingAnchorKey = null;
|
|
1679
|
+
try {
|
|
1680
|
+
const scrollport = document.querySelector("[data-conversation-scroll]");
|
|
1681
|
+
if (!scrollport) return;
|
|
1682
|
+
const vp = scrollport.getBoundingClientRect();
|
|
1683
|
+
const rows = scrollport.querySelectorAll("[data-chat-flow-kind=\"user\"], [data-chat-flow-kind=\"steering\"]");
|
|
1684
|
+
for (const row of rows) if (row.getBoundingClientRect().bottom > vp.top + 8) {
|
|
1685
|
+
pendingAnchorKey = row.dataset.chatAnchorKey || null;
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
} catch {}
|
|
1689
|
+
},
|
|
1690
|
+
consumeAnchorKey: () => {
|
|
1691
|
+
const k = pendingAnchorKey;
|
|
1692
|
+
pendingAnchorKey = null;
|
|
1693
|
+
return k;
|
|
1694
|
+
},
|
|
1695
|
+
setAutoAnchor: (seq) => {
|
|
928
1696
|
pendingAutoSeq = seq;
|
|
929
1697
|
},
|
|
930
1698
|
consumeAutoSeq: () => {
|
|
@@ -974,6 +1742,90 @@ window.__ModuleLoader__.load({
|
|
|
974
1742
|
}, [sessions, sessionId]);
|
|
975
1743
|
return snap;
|
|
976
1744
|
}
|
|
1745
|
+
/** The session's pending user interaction (question / plan review / approval),
|
|
1746
|
+
* read from the uiSession pending-interactions map — dsh 0.1.2 moved the old
|
|
1747
|
+
* snapshot `pending` array into this root-level observable. When the service
|
|
1748
|
+
* is absent (older dsh), falls back to the session snapshot's flat `pending`
|
|
1749
|
+
* array through the `legacyPendingOf` adapter. Returns null when this session
|
|
1750
|
+
* has nothing pending. */
|
|
1751
|
+
function usePendingInteraction(uiSession, sessionId, snap) {
|
|
1752
|
+
const source = uiSession && uiSession.pendingInteractions;
|
|
1753
|
+
const read = () => {
|
|
1754
|
+
try {
|
|
1755
|
+
return source ? source.getSnapshot() : null;
|
|
1756
|
+
} catch {
|
|
1757
|
+
return null;
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
const [map, setMap] = (0, react.useState)(read);
|
|
1761
|
+
(0, react.useEffect)(() => {
|
|
1762
|
+
if (!source) {
|
|
1763
|
+
setMap(null);
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
setMap(source.getSnapshot());
|
|
1767
|
+
try {
|
|
1768
|
+
return source.subscribe(() => setMap(source.getSnapshot()));
|
|
1769
|
+
} catch {
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
}, [source]);
|
|
1773
|
+
return pendingInteractionOf(map, sessionId) || legacyPendingOf(snap);
|
|
1774
|
+
}
|
|
1775
|
+
/** The session's Chat view snapshot (dsh 0.1.2 `ChatSnapshot`) from the
|
|
1776
|
+
* uiConversation assembly — the first subscriber activates the chat target,
|
|
1777
|
+
* so this hook must stay subscribed for the view to materialize its legacy
|
|
1778
|
+
* slice. Null when the service is absent (older dsh). */
|
|
1779
|
+
function useChatView(uiConversation, sessionId) {
|
|
1780
|
+
const [chat, setChat] = (0, react.useState)(null);
|
|
1781
|
+
(0, react.useEffect)(() => {
|
|
1782
|
+
if (!uiConversation || sessionId == null) {
|
|
1783
|
+
setChat(null);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
let unsub = null;
|
|
1787
|
+
try {
|
|
1788
|
+
const source = uiConversation.binding(sessionId).target("chat");
|
|
1789
|
+
setChat(source.getSnapshot() || null);
|
|
1790
|
+
unsub = source.subscribe(() => setChat(source.getSnapshot() || null));
|
|
1791
|
+
} catch {
|
|
1792
|
+
setChat(null);
|
|
1793
|
+
}
|
|
1794
|
+
return () => {
|
|
1795
|
+
if (unsub) unsub();
|
|
1796
|
+
};
|
|
1797
|
+
}, [uiConversation, sessionId]);
|
|
1798
|
+
return chat;
|
|
1799
|
+
}
|
|
1800
|
+
/** The official "对话显示" preference (`ui-chat.transcriptView`, values
|
|
1801
|
+
* normal | compact), read reactively from the settings scope the official
|
|
1802
|
+
* TranscriptViewPolicy binds. Defaults to compact — the official default —
|
|
1803
|
+
* when the scope or the section has not arrived yet. */
|
|
1804
|
+
function useTranscriptViewMode(settingsScope) {
|
|
1805
|
+
const source = settingsScope;
|
|
1806
|
+
const read = () => {
|
|
1807
|
+
try {
|
|
1808
|
+
const section = source ? source.getSnapshot().value : null;
|
|
1809
|
+
return (section ? section.transcriptView : null) === "normal" ? "normal" : "compact";
|
|
1810
|
+
} catch {
|
|
1811
|
+
return "compact";
|
|
1812
|
+
}
|
|
1813
|
+
};
|
|
1814
|
+
const [mode, setMode] = (0, react.useState)(read);
|
|
1815
|
+
(0, react.useEffect)(() => {
|
|
1816
|
+
if (!source) {
|
|
1817
|
+
setMode("compact");
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
setMode(read());
|
|
1821
|
+
try {
|
|
1822
|
+
return source.subscribe(() => setMode(read()));
|
|
1823
|
+
} catch {
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
}, [source]);
|
|
1827
|
+
return mode;
|
|
1828
|
+
}
|
|
977
1829
|
function SessionImage({ attachment, loadImage }) {
|
|
978
1830
|
const [src, setSrc] = (0, react.useState)(null);
|
|
979
1831
|
(0, react.useEffect)(() => {
|
|
@@ -992,13 +1844,28 @@ window.__ModuleLoader__.load({
|
|
|
992
1844
|
alt: ""
|
|
993
1845
|
});
|
|
994
1846
|
}
|
|
995
|
-
|
|
1847
|
+
/** Labels the official MarkdownText requires for its code-block copy buttons
|
|
1848
|
+
* and footnotes section — mirrors the official `markdownLabels(t)` shape.
|
|
1849
|
+
* Without it the renderer throws `Cannot read properties of undefined
|
|
1850
|
+
* (reading 'code')` on the first fenced code block. */
|
|
1851
|
+
function markdownLabels(t) {
|
|
1852
|
+
return {
|
|
1853
|
+
code: {
|
|
1854
|
+
copyLabel: t("markdown.code.copy"),
|
|
1855
|
+
copiedLabel: t("markdown.code.copied")
|
|
1856
|
+
},
|
|
1857
|
+
footnotes: t("markdown.footnotes")
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
function AssistantItem(props) {
|
|
1861
|
+
const { blocks, loadImage, fileMentions, labels } = props;
|
|
996
1862
|
const text = (blocks || []).filter((b) => b && b.kind === "text").map((b) => b.text).join("");
|
|
997
1863
|
const images = (blocks || []).filter((b) => b && b.kind === "image");
|
|
998
1864
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
999
1865
|
className: "fm-msg fm-assistant",
|
|
1000
1866
|
children: [text ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
|
|
1001
1867
|
text,
|
|
1868
|
+
labels,
|
|
1002
1869
|
fileMentions
|
|
1003
1870
|
}) : null, images.map((b, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionImage, {
|
|
1004
1871
|
attachment: b.attachment,
|
|
@@ -1007,21 +1874,102 @@ window.__ModuleLoader__.load({
|
|
|
1007
1874
|
});
|
|
1008
1875
|
}
|
|
1009
1876
|
function FocusContent(props) {
|
|
1010
|
-
const { useSessions, sessions, workspaces, conversation, chatFileMentions, t } = props;
|
|
1877
|
+
const { useSessions, sessions, workspaces, conversation, chatFileMentions, uiSession, uiConversation, settingsScope, t } = props;
|
|
1011
1878
|
const prefs = usePrefs();
|
|
1012
1879
|
const listState = useSessions((s) => s);
|
|
1013
1880
|
const currentId = listState ? listState.current : void 0;
|
|
1014
1881
|
const snap = useSessionSnapshot(sessions, currentId);
|
|
1015
|
-
const
|
|
1882
|
+
const chatView = useChatView(uiConversation, currentId);
|
|
1883
|
+
const transcriptView = useTranscriptViewMode(settingsScope);
|
|
1884
|
+
const legacy = (0, react.useMemo)(() => legacySliceOf(chatView, snap), [chatView, snap]);
|
|
1885
|
+
const stable = legacy.partial == null;
|
|
1886
|
+
const mdLabels = (0, react.useMemo)(() => markdownLabels(t), [t]);
|
|
1887
|
+
const items = (0, react.useMemo)(() => snap ? buildItems(legacy.nodes, legacy.runningCalls, t, transcriptView, stable) : [], [
|
|
1888
|
+
snap,
|
|
1889
|
+
legacy,
|
|
1890
|
+
t,
|
|
1891
|
+
transcriptView,
|
|
1892
|
+
stable
|
|
1893
|
+
]);
|
|
1894
|
+
const [expanded, setExpanded] = (0, react.useState)(() => /* @__PURE__ */ new Set());
|
|
1895
|
+
(0, react.useEffect)(() => {
|
|
1896
|
+
setExpanded(/* @__PURE__ */ new Set());
|
|
1897
|
+
}, [transcriptView]);
|
|
1898
|
+
const wait = usePendingInteraction(uiSession, currentId, snap);
|
|
1899
|
+
const itemsRef = (0, react.useRef)(items);
|
|
1900
|
+
itemsRef.current = items;
|
|
1901
|
+
const snapRef = (0, react.useRef)(null);
|
|
1902
|
+
snapRef.current = snap;
|
|
1903
|
+
(0, react.useEffect)(() => {
|
|
1904
|
+
let alive = true;
|
|
1905
|
+
const b = sessions && currentId != null ? sessions.binding(currentId) : null;
|
|
1906
|
+
const face = b && b.session;
|
|
1907
|
+
if (!face || typeof face.loadOlder !== "function") return;
|
|
1908
|
+
const load = async () => {
|
|
1909
|
+
let pages = 0;
|
|
1910
|
+
while (alive && pages < 500) {
|
|
1911
|
+
const s = snapRef.current;
|
|
1912
|
+
if (!s || !s.hasMore || s.openState !== "open") break;
|
|
1913
|
+
if (s.loadingOlder) {
|
|
1914
|
+
await new Promise((r) => setTimeout(r, 60));
|
|
1915
|
+
continue;
|
|
1916
|
+
}
|
|
1917
|
+
try {
|
|
1918
|
+
await face.loadOlder();
|
|
1919
|
+
} catch {
|
|
1920
|
+
break;
|
|
1921
|
+
}
|
|
1922
|
+
pages++;
|
|
1923
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
load();
|
|
1927
|
+
return () => {
|
|
1928
|
+
alive = false;
|
|
1929
|
+
};
|
|
1930
|
+
}, [sessions, currentId]);
|
|
1931
|
+
const rail = (0, react.useMemo)(() => {
|
|
1932
|
+
if (!chatView || !chatView.navigation || !chatView.navigation.items) return [];
|
|
1933
|
+
const out = [];
|
|
1934
|
+
const used = /* @__PURE__ */ new Set();
|
|
1935
|
+
let navItems = [];
|
|
1936
|
+
try {
|
|
1937
|
+
navItems = chatView.navigation.items() || [];
|
|
1938
|
+
} catch {
|
|
1939
|
+
navItems = [];
|
|
1940
|
+
}
|
|
1941
|
+
for (const it of navItems) {
|
|
1942
|
+
if (!it) continue;
|
|
1943
|
+
let seq = null;
|
|
1944
|
+
try {
|
|
1945
|
+
const node = chatView.nodes && chatView.nodes.get ? chatView.nodes.get(it.anchorKey) : null;
|
|
1946
|
+
if (node && typeof node.anchorSeq === "number") seq = node.anchorSeq;
|
|
1947
|
+
} catch {}
|
|
1948
|
+
let idx = seq != null ? findSeqIndex(items, seq) : -1;
|
|
1949
|
+
if (idx < 0 && seq != null && Number.isFinite(seq)) for (let i = items.length - 1; i >= 0; i--) {
|
|
1950
|
+
const row = items[i];
|
|
1951
|
+
if ((row.kind === "user" || row.kind === "steering") && row.seq <= seq) {
|
|
1952
|
+
idx = i;
|
|
1953
|
+
break;
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
if (idx < 0 || used.has(idx)) continue;
|
|
1957
|
+
used.add(idx);
|
|
1958
|
+
out.push({
|
|
1959
|
+
turn: it.turn,
|
|
1960
|
+
prompt: (it.prompt || "").replace(/\s+/g, " ").trim(),
|
|
1961
|
+
response: (it.response || "").replace(/\s+/g, " ").trim(),
|
|
1962
|
+
idx
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
return out;
|
|
1966
|
+
}, [chatView, items]);
|
|
1016
1967
|
const navKeys = [];
|
|
1017
1968
|
const navPreviews = {};
|
|
1018
|
-
for (
|
|
1019
|
-
const
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
navKeys.push(k);
|
|
1023
|
-
navPreviews[k] = it.text.replace(/\s+/g, " ").trim().slice(0, 80);
|
|
1024
|
-
}
|
|
1969
|
+
for (const r of rail) {
|
|
1970
|
+
const k = "fm-" + r.idx;
|
|
1971
|
+
navKeys.push(k);
|
|
1972
|
+
navPreviews[k] = r.prompt;
|
|
1025
1973
|
}
|
|
1026
1974
|
const [activeKey, setActiveKey] = (0, react.useState)(navKeys.length ? navKeys[navKeys.length - 1] : null);
|
|
1027
1975
|
const [zone, setZone] = (0, react.useState)(true);
|
|
@@ -1033,6 +1981,8 @@ window.__ModuleLoader__.load({
|
|
|
1033
1981
|
const [localDraft, setLocalDraft] = (0, react.useState)("");
|
|
1034
1982
|
const taRef = (0, react.useRef)(null);
|
|
1035
1983
|
const focusBarOnce = (0, react.useRef)(false);
|
|
1984
|
+
const revealArmed = (0, react.useRef)(false);
|
|
1985
|
+
const seenPromptSeq = (0, react.useRef)(null);
|
|
1036
1986
|
const computeActive = () => {
|
|
1037
1987
|
if (!bodyEl) return null;
|
|
1038
1988
|
const bodyTop = bodyEl.getBoundingClientRect().top;
|
|
@@ -1051,7 +2001,8 @@ window.__ModuleLoader__.load({
|
|
|
1051
2001
|
if (rafId.current !== null) return;
|
|
1052
2002
|
rafId.current = requestAnimationFrame(() => {
|
|
1053
2003
|
rafId.current = null;
|
|
1054
|
-
|
|
2004
|
+
const c = computeActive();
|
|
2005
|
+
if (c !== null) setActiveKey(c);
|
|
1055
2006
|
setZone((prev) => bottomZoneAfter(prev, bottomDistance()));
|
|
1056
2007
|
});
|
|
1057
2008
|
};
|
|
@@ -1074,47 +2025,98 @@ window.__ModuleLoader__.load({
|
|
|
1074
2025
|
behavior: "smooth"
|
|
1075
2026
|
});
|
|
1076
2027
|
};
|
|
2028
|
+
const entryRef = (0, react.useRef)(void 0);
|
|
2029
|
+
const positionedRef = (0, react.useRef)(false);
|
|
2030
|
+
const positionDeadlineRef = (0, react.useRef)(0);
|
|
2031
|
+
const correctionRef = (0, react.useRef)(null);
|
|
1077
2032
|
(0, react.useEffect)(() => {
|
|
2033
|
+
if (entryRef.current !== void 0) return;
|
|
2034
|
+
positionDeadlineRef.current = Date.now() + 6e3;
|
|
1078
2035
|
const autoSeq = focusStore.consumeAutoSeq();
|
|
1079
2036
|
if (autoSeq != null) {
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
} else if (bodyEl) {
|
|
1085
|
-
bodyEl.scrollTo({
|
|
1086
|
-
top: bodyEl.scrollHeight,
|
|
1087
|
-
behavior: "auto"
|
|
1088
|
-
});
|
|
1089
|
-
setActiveKey(navKeys.length ? navKeys[navKeys.length - 1] : null);
|
|
1090
|
-
}
|
|
2037
|
+
entryRef.current = {
|
|
2038
|
+
kind: "auto",
|
|
2039
|
+
seq: autoSeq
|
|
2040
|
+
};
|
|
1091
2041
|
return;
|
|
1092
2042
|
}
|
|
1093
2043
|
const anchorKey = focusStore.consumeAnchorKey();
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
const lastIdx = lastUserIndex(items);
|
|
1101
|
-
if (lastIdx >= 0) {
|
|
1102
|
-
scrollToKey("fm-" + lastIdx, false);
|
|
1103
|
-
setActiveKey("fm-" + lastIdx);
|
|
1104
|
-
} else if (bodyEl) {
|
|
1105
|
-
bodyEl.scrollTo({
|
|
1106
|
-
top: bodyEl.scrollHeight,
|
|
1107
|
-
behavior: "auto"
|
|
1108
|
-
});
|
|
1109
|
-
setActiveKey(navKeys.length ? navKeys[navKeys.length - 1] : null);
|
|
1110
|
-
}
|
|
1111
|
-
} else if (bodyEl) {
|
|
1112
|
-
bodyEl.scrollTo({
|
|
1113
|
-
top: bodyEl.scrollHeight,
|
|
1114
|
-
behavior: "auto"
|
|
1115
|
-
});
|
|
1116
|
-
setActiveKey(navKeys.length ? navKeys[navKeys.length - 1] : null);
|
|
2044
|
+
if (prefs.scroll === "preserve" && anchorKey) {
|
|
2045
|
+
entryRef.current = {
|
|
2046
|
+
kind: "anchor",
|
|
2047
|
+
key: anchorKey
|
|
2048
|
+
};
|
|
2049
|
+
return;
|
|
1117
2050
|
}
|
|
2051
|
+
entryRef.current = { kind: "lastUser" };
|
|
2052
|
+
}, []);
|
|
2053
|
+
(0, react.useEffect)(() => {
|
|
2054
|
+
if (positionedRef.current || entryRef.current === void 0) return;
|
|
2055
|
+
const entry = entryRef.current;
|
|
2056
|
+
let seq = null;
|
|
2057
|
+
if (entry.kind === "auto") seq = entry.seq;
|
|
2058
|
+
else if (entry.kind === "anchor") {
|
|
2059
|
+
let nodes = chatView && chatView.nodes;
|
|
2060
|
+
if (!nodes && uiConversation && currentId != null) try {
|
|
2061
|
+
nodes = uiConversation.binding(currentId).target("chat").getSnapshot()?.nodes;
|
|
2062
|
+
} catch {}
|
|
2063
|
+
seq = resolveAnchorSeq(nodes, entry.key);
|
|
2064
|
+
}
|
|
2065
|
+
const decision = decideOpenScroll({
|
|
2066
|
+
kind: entry.kind,
|
|
2067
|
+
seq,
|
|
2068
|
+
items,
|
|
2069
|
+
hasMore: !!(snap && snap.hasMore),
|
|
2070
|
+
deadlinePassed: Date.now() >= positionDeadlineRef.current
|
|
2071
|
+
});
|
|
2072
|
+
if (decision.action === "wait") return;
|
|
2073
|
+
if (decision.action === "scroll") {
|
|
2074
|
+
scrollToKey("fm-" + decision.index, false);
|
|
2075
|
+
setActiveKey("fm-" + decision.index);
|
|
2076
|
+
const el = anchors["fm-" + decision.index];
|
|
2077
|
+
if (decision.seq != null && el && bodyEl) correctionRef.current = {
|
|
2078
|
+
seq: decision.seq,
|
|
2079
|
+
top: el.getBoundingClientRect().top - bodyEl.getBoundingClientRect().top + bodyEl.scrollTop
|
|
2080
|
+
};
|
|
2081
|
+
positionedRef.current = true;
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
if (bodyEl) bodyEl.scrollTo({
|
|
2085
|
+
top: bodyEl.scrollHeight,
|
|
2086
|
+
behavior: "auto"
|
|
2087
|
+
});
|
|
2088
|
+
setActiveKey(navKeys.length ? navKeys[navKeys.length - 1] : null);
|
|
2089
|
+
positionedRef.current = true;
|
|
2090
|
+
}, [
|
|
2091
|
+
items,
|
|
2092
|
+
snap,
|
|
2093
|
+
chatView
|
|
2094
|
+
]);
|
|
2095
|
+
(0, react.useEffect)(() => {
|
|
2096
|
+
const c = correctionRef.current;
|
|
2097
|
+
if (!c) return;
|
|
2098
|
+
let tries = 0;
|
|
2099
|
+
const timer = setInterval(() => {
|
|
2100
|
+
tries++;
|
|
2101
|
+
const items = itemsRef.current;
|
|
2102
|
+
const idx = findSeqIndex(items, c.seq);
|
|
2103
|
+
const el = idx >= 0 ? anchors["fm-" + idx] : void 0;
|
|
2104
|
+
if (!el || !bodyEl) {
|
|
2105
|
+
if (tries > 40) {
|
|
2106
|
+
clearInterval(timer);
|
|
2107
|
+
correctionRef.current = null;
|
|
2108
|
+
}
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
const delta = el.getBoundingClientRect().top - bodyEl.getBoundingClientRect().top + bodyEl.scrollTop - c.top;
|
|
2112
|
+
if (Math.abs(delta) <= 2 || tries > 40) {
|
|
2113
|
+
clearInterval(timer);
|
|
2114
|
+
correctionRef.current = null;
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
bodyEl.scrollTop += delta;
|
|
2118
|
+
}, 150);
|
|
2119
|
+
return () => clearInterval(timer);
|
|
1118
2120
|
}, []);
|
|
1119
2121
|
(0, react.useEffect)(() => {
|
|
1120
2122
|
setZone(bodyEl ? bottomZoneAfter(false, bottomDistance()) : true);
|
|
@@ -1122,13 +2124,38 @@ window.__ModuleLoader__.load({
|
|
|
1122
2124
|
(0, react.useEffect)(() => {
|
|
1123
2125
|
const el = bodyEl;
|
|
1124
2126
|
if (!el || typeof ResizeObserver === "undefined") return;
|
|
1125
|
-
|
|
2127
|
+
setViewportW(el.clientWidth);
|
|
2128
|
+
const ro = new ResizeObserver(() => {
|
|
2129
|
+
setViewportW(el.clientWidth);
|
|
2130
|
+
scheduleRef.current();
|
|
2131
|
+
});
|
|
1126
2132
|
ro.observe(el);
|
|
1127
2133
|
return () => ro.disconnect();
|
|
1128
2134
|
}, []);
|
|
1129
2135
|
(0, react.useEffect)(() => {
|
|
1130
2136
|
scheduleRef.current();
|
|
1131
2137
|
}, [snap, prefs.width]);
|
|
2138
|
+
(0, react.useEffect)(() => {
|
|
2139
|
+
const seq = lastPromptSeq(items);
|
|
2140
|
+
if (seq != null && shouldRevealSentPrompt({
|
|
2141
|
+
armed: revealArmed.current,
|
|
2142
|
+
prevSeq: seenPromptSeq.current,
|
|
2143
|
+
nextSeq: seq
|
|
2144
|
+
})) {
|
|
2145
|
+
revealArmed.current = false;
|
|
2146
|
+
const idx = findSeqIndex(items, seq);
|
|
2147
|
+
const el = idx >= 0 ? anchors["fm-" + idx] : void 0;
|
|
2148
|
+
if (el && bodyEl) {
|
|
2149
|
+
const limit = bodyEl.getBoundingClientRect().bottom - 110;
|
|
2150
|
+
const hidden = el.getBoundingClientRect().bottom - limit;
|
|
2151
|
+
if (hidden > 0) bodyEl.scrollBy({
|
|
2152
|
+
top: hidden,
|
|
2153
|
+
behavior: "smooth"
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
seenPromptSeq.current = seq;
|
|
2158
|
+
}, [items]);
|
|
1132
2159
|
(0, react.useEffect)(() => () => {
|
|
1133
2160
|
if (rafId.current !== null) {
|
|
1134
2161
|
cancelAnimationFrame(rafId.current);
|
|
@@ -1162,9 +2189,22 @@ window.__ModuleLoader__.load({
|
|
|
1162
2189
|
engaged
|
|
1163
2190
|
]);
|
|
1164
2191
|
const loadImage = (0, react.useCallback)((attachment) => {
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
2192
|
+
const modern = uiConversation && currentId ? (async () => {
|
|
2193
|
+
const url = await uiConversation.imageUrl(currentId, attachment);
|
|
2194
|
+
if (url) return url;
|
|
2195
|
+
throw new Error("no url");
|
|
2196
|
+
})() : null;
|
|
2197
|
+
if (modern) return modern.catch(() => legacyResolve(attachment));
|
|
2198
|
+
return legacyResolve(attachment);
|
|
2199
|
+
}, [
|
|
2200
|
+
uiConversation,
|
|
2201
|
+
conversation,
|
|
2202
|
+
currentId
|
|
2203
|
+
]);
|
|
2204
|
+
function legacyResolve(attachment) {
|
|
2205
|
+
if (conversation && typeof conversation.resolveImage === "function") return conversation.resolveImage(currentId, attachment);
|
|
2206
|
+
return Promise.reject(/* @__PURE__ */ new Error("dsh-focus-overlay: image resolver unavailable"));
|
|
2207
|
+
}
|
|
1168
2208
|
const fileMentionsFor = (0, react.useCallback)((node) => {
|
|
1169
2209
|
if (!chatFileMentions) return void 0;
|
|
1170
2210
|
try {
|
|
@@ -1187,9 +2227,7 @@ window.__ModuleLoader__.load({
|
|
|
1187
2227
|
const inputState = useInputState(inputFace);
|
|
1188
2228
|
const draftValue = inputFace ? inputState && typeof inputState.draft === "string" ? inputState.draft : "" : localDraft;
|
|
1189
2229
|
const draftEmpty = draftValue.trim() === "";
|
|
1190
|
-
const
|
|
1191
|
-
const pendingCount = pendingList.length;
|
|
1192
|
-
const wait = pendingCount > 0 ? pendingList[0] : null;
|
|
2230
|
+
const pending = !!wait;
|
|
1193
2231
|
const onDraftChange = (v) => {
|
|
1194
2232
|
if (inputFace) try {
|
|
1195
2233
|
inputFace.setDraft(v);
|
|
@@ -1200,6 +2238,7 @@ window.__ModuleLoader__.load({
|
|
|
1200
2238
|
const send = () => {
|
|
1201
2239
|
const text = draftValue;
|
|
1202
2240
|
if (text.trim() === "") return;
|
|
2241
|
+
if (zone) revealArmed.current = true;
|
|
1203
2242
|
if (inputFace) try {
|
|
1204
2243
|
inputFace.submit("queue");
|
|
1205
2244
|
return;
|
|
@@ -1232,7 +2271,7 @@ window.__ModuleLoader__.load({
|
|
|
1232
2271
|
const promptErr = snap && snap.promptError && snap.promptError.op === "send" ? snap.promptError.error : null;
|
|
1233
2272
|
const errorLine = promptErr ? promptErr.message || promptErr.code || "send failed" : null;
|
|
1234
2273
|
const form = bottomForm({
|
|
1235
|
-
pending
|
|
2274
|
+
pending,
|
|
1236
2275
|
cardOpen,
|
|
1237
2276
|
draftEmpty,
|
|
1238
2277
|
inZone: zone,
|
|
@@ -1240,14 +2279,14 @@ window.__ModuleLoader__.load({
|
|
|
1240
2279
|
engaged
|
|
1241
2280
|
});
|
|
1242
2281
|
(0, react.useEffect)(() => {
|
|
1243
|
-
if (
|
|
2282
|
+
if (!pending) {
|
|
1244
2283
|
if (cardOpen) setCardOpen(false);
|
|
1245
2284
|
return;
|
|
1246
2285
|
}
|
|
1247
2286
|
focusBarOnce.current = false;
|
|
1248
2287
|
setDockFocused(false);
|
|
1249
2288
|
setEngaged(false);
|
|
1250
|
-
}, [
|
|
2289
|
+
}, [pending, cardOpen]);
|
|
1251
2290
|
(0, react.useEffect)(() => {
|
|
1252
2291
|
if (form === "bar" && focusBarOnce.current) {
|
|
1253
2292
|
focusBarOnce.current = false;
|
|
@@ -1262,6 +2301,22 @@ window.__ModuleLoader__.load({
|
|
|
1262
2301
|
const row = listState.byId[currentId];
|
|
1263
2302
|
if (row && row.displayTitle) title = row.displayTitle;
|
|
1264
2303
|
}
|
|
2304
|
+
const [dragWidth, setDragWidth] = (0, react.useState)(null);
|
|
2305
|
+
const [viewportW, setViewportW] = (0, react.useState)(0);
|
|
2306
|
+
const width = dragWidth ?? prefs.width;
|
|
2307
|
+
const clampWidth = (0, react.useCallback)((w) => {
|
|
2308
|
+
const vw = bodyEl ? bodyEl.clientWidth : viewportW;
|
|
2309
|
+
const max = Math.max(640, vw - 176);
|
|
2310
|
+
return Math.round(Math.min(Math.max(w, 640), max));
|
|
2311
|
+
}, [viewportW]);
|
|
2312
|
+
const onWidthStart = (0, react.useCallback)(() => clampWidth(prefs.width), [clampWidth, prefs.width]);
|
|
2313
|
+
const onWidthDrag = (0, react.useCallback)((w) => setDragWidth(clampWidth(w)), [clampWidth]);
|
|
2314
|
+
const onWidthCommit = (0, react.useCallback)((w) => {
|
|
2315
|
+
const clamped = clampWidth(w);
|
|
2316
|
+
setDragWidth(null);
|
|
2317
|
+
if (clamped !== prefs.width) prefsStore.update({ width: clamped });
|
|
2318
|
+
}, [clampWidth, prefs.width]);
|
|
2319
|
+
const onWidthEnd = (0, react.useCallback)(() => setDragWidth(null), []);
|
|
1265
2320
|
let body;
|
|
1266
2321
|
if (!currentId) body = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1267
2322
|
className: "fm-empty",
|
|
@@ -1272,57 +2327,86 @@ window.__ModuleLoader__.load({
|
|
|
1272
2327
|
children: t("loading")
|
|
1273
2328
|
});
|
|
1274
2329
|
else {
|
|
1275
|
-
const partial =
|
|
2330
|
+
const partial = legacy.partial;
|
|
1276
2331
|
const partialBlocks = partial ? partial.blocks : [];
|
|
1277
2332
|
const running = !!snap.running;
|
|
1278
2333
|
const kids = [];
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
2334
|
+
if (snap.hasMore) kids.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2335
|
+
className: "fm-hidden",
|
|
2336
|
+
children: t("loading")
|
|
2337
|
+
}, "fm-history"));
|
|
2338
|
+
const renderItem = (it, key, registerAnchor) => {
|
|
2339
|
+
if (it.kind === "user") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1283
2340
|
className: "fm-msg fm-user-msg",
|
|
1284
2341
|
ref: (el) => {
|
|
1285
|
-
if (el) anchors[key] = el;
|
|
2342
|
+
if (registerAnchor) if (el) anchors[key] = el;
|
|
1286
2343
|
else delete anchors[key];
|
|
1287
2344
|
},
|
|
1288
2345
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1289
2346
|
className: "fm-user",
|
|
1290
2347
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MessageText, { text: it.text })
|
|
1291
2348
|
})
|
|
1292
|
-
}, key)
|
|
1293
|
-
|
|
2349
|
+
}, key);
|
|
2350
|
+
if (it.kind === "steering") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1294
2351
|
className: "fm-msg fm-user-msg",
|
|
1295
2352
|
ref: (el) => {
|
|
1296
|
-
if (el) anchors[key] = el;
|
|
2353
|
+
if (registerAnchor) if (el) anchors[key] = el;
|
|
1297
2354
|
else delete anchors[key];
|
|
1298
2355
|
},
|
|
1299
2356
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1300
2357
|
className: "fm-user fm-steering",
|
|
1301
2358
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MessageText, { text: it.text })
|
|
1302
2359
|
})
|
|
1303
|
-
}, key)
|
|
1304
|
-
|
|
2360
|
+
}, key);
|
|
2361
|
+
if (it.kind === "assistant") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AssistantItem, {
|
|
1305
2362
|
blocks: it.blocks,
|
|
1306
2363
|
loadImage,
|
|
1307
|
-
fileMentions: fileMentionsFor(it)
|
|
1308
|
-
|
|
1309
|
-
|
|
2364
|
+
fileMentions: fileMentionsFor(it),
|
|
2365
|
+
labels: mdLabels
|
|
2366
|
+
}, key);
|
|
2367
|
+
if (it.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2368
|
+
className: "fm-msg fm-error",
|
|
2369
|
+
children: it.text
|
|
2370
|
+
}, key);
|
|
2371
|
+
if (it.kind === "hidden") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1310
2372
|
className: "fm-hidden",
|
|
1311
2373
|
children: [
|
|
1312
2374
|
"· ",
|
|
1313
2375
|
it.text,
|
|
1314
2376
|
" ·"
|
|
1315
2377
|
]
|
|
1316
|
-
}, key)
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
2378
|
+
}, key);
|
|
2379
|
+
if (it.kind === "turnProcess") {
|
|
2380
|
+
const open = !it.collapsed || expanded.has(it.seq);
|
|
2381
|
+
const toggle = () => setExpanded((prev) => {
|
|
2382
|
+
const next = new Set(prev);
|
|
2383
|
+
if (next.has(it.seq)) next.delete(it.seq);
|
|
2384
|
+
else next.add(it.seq);
|
|
2385
|
+
return next;
|
|
2386
|
+
});
|
|
2387
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [it.collapsed ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2388
|
+
type: "button",
|
|
2389
|
+
className: "fm-process-root",
|
|
2390
|
+
"data-open": open || void 0,
|
|
2391
|
+
"aria-expanded": open,
|
|
2392
|
+
onClick: toggle,
|
|
2393
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2394
|
+
className: "fm-process-label",
|
|
2395
|
+
children: processLabel(it.counts, t)
|
|
2396
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: "fm-process-chevron" })]
|
|
2397
|
+
}) : null, open ? it.workItems.map((wi, wi2) => renderItem(wi, key + "-w" + wi2, false)) : null] }, key);
|
|
2398
|
+
}
|
|
2399
|
+
return null;
|
|
2400
|
+
};
|
|
2401
|
+
for (let i = 0; i < items.length; i++) {
|
|
2402
|
+
const rendered = renderItem(items[i], "fm-" + i, true);
|
|
2403
|
+
if (rendered) kids.push(rendered);
|
|
1321
2404
|
}
|
|
1322
2405
|
if (partialBlocks.length) kids.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AssistantItem, {
|
|
1323
2406
|
blocks: partialBlocks,
|
|
1324
2407
|
loadImage,
|
|
1325
|
-
fileMentions: void 0
|
|
2408
|
+
fileMentions: void 0,
|
|
2409
|
+
labels: mdLabels
|
|
1326
2410
|
}, "fm-partial"));
|
|
1327
2411
|
if (running) kids.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1328
2412
|
className: "fm-running",
|
|
@@ -1346,29 +2430,24 @@ window.__ModuleLoader__.load({
|
|
|
1346
2430
|
}, "fm-running"));
|
|
1347
2431
|
body = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1348
2432
|
className: "fm-inner",
|
|
1349
|
-
style: {
|
|
2433
|
+
style: {
|
|
2434
|
+
maxWidth: width,
|
|
2435
|
+
"--fm-content-width": `${width}px`
|
|
2436
|
+
},
|
|
1350
2437
|
children: kids
|
|
1351
2438
|
});
|
|
1352
2439
|
}
|
|
1353
2440
|
const activeIndex = activeKey ? navKeys.indexOf(activeKey) : -1;
|
|
1354
|
-
const
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
className: "fm-nav-dot" + (activeKey === k ? " fm-nav-dot-active" : ""),
|
|
1363
|
-
style: { top: dotTop(i) },
|
|
1364
|
-
onClick: () => scrollToKey(k, true),
|
|
1365
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "fm-nav-dot-core" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1366
|
-
className: "fm-nav-tip",
|
|
1367
|
-
children: navPreviews[k]
|
|
1368
|
-
})]
|
|
1369
|
-
}, k))
|
|
2441
|
+
const activeTurn = rail.length ? activeIndex >= 0 && rail[activeIndex] ? rail[activeIndex].turn : rail[rail.length - 1].turn : null;
|
|
2442
|
+
const nav = prefs.navbar && rail.length >= 2 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TurnRail, {
|
|
2443
|
+
items: rail,
|
|
2444
|
+
activeTurn,
|
|
2445
|
+
onNavigate: (item) => scrollToKey("fm-" + item.idx, true),
|
|
2446
|
+
label: t("nav.label"),
|
|
2447
|
+
jumpLabel: (turn) => t("nav.jump", { n: turn }),
|
|
2448
|
+
turnLabel: (turn) => t("nav.turn", { n: turn })
|
|
1370
2449
|
}) : null;
|
|
1371
|
-
const waiting =
|
|
2450
|
+
const waiting = pending && !cardOpen;
|
|
1372
2451
|
const donePing = useDonePing();
|
|
1373
2452
|
const [showDoneNotice, setShowDoneNotice] = (0, react.useState)(false);
|
|
1374
2453
|
(0, react.useEffect)(() => {
|
|
@@ -1436,13 +2515,22 @@ window.__ModuleLoader__.load({
|
|
|
1436
2515
|
children: t("exit")
|
|
1437
2516
|
})]
|
|
1438
2517
|
}),
|
|
1439
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1440
|
-
className: "fm-body",
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
2518
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2519
|
+
className: "fm-body-wrap",
|
|
2520
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2521
|
+
className: "fm-body",
|
|
2522
|
+
ref: (el) => {
|
|
2523
|
+
bodyEl = el;
|
|
2524
|
+
},
|
|
2525
|
+
onScroll: scheduleRecompute,
|
|
2526
|
+
children: body
|
|
2527
|
+
}), currentId && snap !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WidthHandles, {
|
|
2528
|
+
width,
|
|
2529
|
+
onStart: onWidthStart,
|
|
2530
|
+
onDrag: onWidthDrag,
|
|
2531
|
+
onCommit: onWidthCommit,
|
|
2532
|
+
onEnd: onWidthEnd
|
|
2533
|
+
}) : null]
|
|
1446
2534
|
}),
|
|
1447
2535
|
nav,
|
|
1448
2536
|
currentId && snap !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -1469,9 +2557,37 @@ window.__ModuleLoader__.load({
|
|
|
1469
2557
|
]
|
|
1470
2558
|
});
|
|
1471
2559
|
}
|
|
2560
|
+
/** Error boundary around the overlay content: a render crash would otherwise
|
|
2561
|
+
* unmount the slot subtree and every later entry would crash again — the
|
|
2562
|
+
* toggle would look permanently dead. The boundary logs, renders nothing for
|
|
2563
|
+
* the failed pass, and re-arms on the next open (keyed by open count). */
|
|
2564
|
+
var FocusErrorBoundary = class extends react.Component {
|
|
2565
|
+
state = { error: null };
|
|
2566
|
+
static getDerivedStateFromError(error) {
|
|
2567
|
+
return { error };
|
|
2568
|
+
}
|
|
2569
|
+
componentDidUpdate(prev) {
|
|
2570
|
+
if (this.state.error && prev.resetKey !== this.props.resetKey) this.setState({ error: null });
|
|
2571
|
+
}
|
|
2572
|
+
render() {
|
|
2573
|
+
if (this.state.error) {
|
|
2574
|
+
console.error("[dsh-focus-overlay] overlay render failed; it will retry on the next open", this.state.error);
|
|
2575
|
+
return null;
|
|
2576
|
+
}
|
|
2577
|
+
return this.props.children;
|
|
2578
|
+
}
|
|
2579
|
+
};
|
|
1472
2580
|
function FocusOverlay(props) {
|
|
1473
|
-
|
|
1474
|
-
|
|
2581
|
+
const on = useFocus();
|
|
2582
|
+
const opens = (0, react.useRef)(0);
|
|
2583
|
+
if (!on) {
|
|
2584
|
+
opens.current++;
|
|
2585
|
+
return null;
|
|
2586
|
+
}
|
|
2587
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FocusErrorBoundary, {
|
|
2588
|
+
resetKey: opens.current,
|
|
2589
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FocusContent, { ...props })
|
|
2590
|
+
});
|
|
1475
2591
|
}
|
|
1476
2592
|
function FocusToggle({ t }) {
|
|
1477
2593
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
@@ -1555,34 +2671,6 @@ window.__ModuleLoader__.load({
|
|
|
1555
2671
|
className: "fm-plugin-field-hint",
|
|
1556
2672
|
children: t("settings.navbar.hint")
|
|
1557
2673
|
})]
|
|
1558
|
-
}),
|
|
1559
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1560
|
-
className: "fm-plugin-field",
|
|
1561
|
-
children: [
|
|
1562
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1563
|
-
className: "fm-plugin-field-head",
|
|
1564
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1565
|
-
className: "fm-plugin-field-label",
|
|
1566
|
-
children: t("settings.width")
|
|
1567
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1568
|
-
className: "fm-plugin-field-value",
|
|
1569
|
-
children: [prefs.width, "px"]
|
|
1570
|
-
})]
|
|
1571
|
-
}),
|
|
1572
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1573
|
-
type: "range",
|
|
1574
|
-
className: "fm-plugin-range",
|
|
1575
|
-
min: 480,
|
|
1576
|
-
max: 1200,
|
|
1577
|
-
step: 40,
|
|
1578
|
-
value: prefs.width,
|
|
1579
|
-
onChange: (e) => prefsStore.update({ width: Number(e.target.value) })
|
|
1580
|
-
}),
|
|
1581
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1582
|
-
className: "fm-plugin-field-hint",
|
|
1583
|
-
children: t("settings.width.hint")
|
|
1584
|
-
})
|
|
1585
|
-
]
|
|
1586
2674
|
})
|
|
1587
2675
|
] });
|
|
1588
2676
|
}
|
|
@@ -1704,169 +2792,6 @@ window.__ModuleLoader__.load({
|
|
|
1704
2792
|
})
|
|
1705
2793
|
});
|
|
1706
2794
|
}
|
|
1707
|
-
//#endregion
|
|
1708
|
-
//#region src/client/styles.ts
|
|
1709
|
-
/** Package-owned stylesheet text (injected by the client apply, cleaned up on unload). */
|
|
1710
|
-
const FOCUS_CSS = `
|
|
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}
|
|
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}
|
|
1713
|
-
.fm-title{font-size:14px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1714
|
-
.fm-body{flex:1 1 auto;overflow-y:auto;padding:28px 28px 110px}
|
|
1715
|
-
.fm-inner{max-width:760px;margin:0 auto}
|
|
1716
|
-
.fm-msg{margin:0 0 24px}
|
|
1717
|
-
.fm-user-msg{margin-bottom:44px}
|
|
1718
|
-
.fm-user{background:var(--dsw-specific-bubble,#eaf2ff);color:var(--dsw-alias-label-primary,#1a1a1a);border-radius:14px;padding:10px 14px;word-break:break-word;max-width:85%;margin-left:auto}
|
|
1719
|
-
.fm-steering{opacity:.85}
|
|
1720
|
-
.fm-assistant{word-break:break-word}
|
|
1721
|
-
.fm-image{max-width:100%;border-radius:8px;margin:8px 0}
|
|
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}
|
|
1723
|
-
.fm-error{color:var(--dsw-alias-state-error-primary,#d23);font-size:13px;white-space:pre-wrap}
|
|
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}}
|
|
1750
|
-
.fm-empty{color:var(--dsw-alias-label-secondary,#888);padding:48px 16px;text-align:center}
|
|
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}
|
|
1752
|
-
.fm-nav:hover{transform:translateY(-50%) translateX(-6px)}
|
|
1753
|
-
.fm-nav-dot{position:absolute;left:50%;transform:translate(-50%,-50%);cursor:pointer;background:transparent;border:0;padding:0;width:16px;height:auto;display:flex;align-items:center;justify-content:center;transition:top .22s ease}
|
|
1754
|
-
.fm-nav-dot::after{content:'';position:absolute;inset:-5px -8px}
|
|
1755
|
-
.fm-nav-dot-core{display:block;width:8px;height:8px;border-radius:999px;background:var(--dsw-alias-label-secondary,#999);opacity:.45;transition:opacity .15s ease,background .15s ease,height .15s ease,transform .18s ease}
|
|
1756
|
-
.fm-nav:hover .fm-nav-dot:not(.fm-nav-dot-active) .fm-nav-dot-core{transform:scale(1.15);opacity:.9}
|
|
1757
|
-
.fm-nav-dot:not(.fm-nav-dot-active):hover .fm-nav-dot-core{background:var(--dsw-static-deepseek-500,#4176e6);opacity:1;transform:scale(1.5)}
|
|
1758
|
-
.fm-nav-dot-active .fm-nav-dot-core{background:var(--dsw-static-deepseek-500,#4176e6);height:22px;opacity:1}
|
|
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}
|
|
1760
|
-
.fm-nav-dot:hover .fm-nav-tip{display:block}
|
|
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}
|
|
1762
|
-
.fm-reply-toast-dot{width:8px;height:8px;border-radius:999px;background:var(--dsw-static-deepseek-500,#4176e6);flex:0 0 auto}
|
|
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}}
|
|
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}
|
|
1829
|
-
.fm-plugin-card:hover{border-color:var(--dsw-alias-label-dimmed)}
|
|
1830
|
-
.fm-plugin-card-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
|
|
1831
|
-
.fm-plugin-card-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
|
|
1832
|
-
.fm-plugin-card-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
|
|
1833
|
-
.fm-plugin-card-headtext{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
|
|
1834
|
-
.fm-plugin-card-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
|
|
1835
|
-
.fm-plugin-card-desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
|
|
1836
|
-
.fm-plugin-card-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}
|
|
1837
|
-
.fm-plugin-card-chevron-open{transform:rotate(180deg)}
|
|
1838
|
-
.fm-plugin-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}
|
|
1839
|
-
.fm-plugin-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
|
|
1840
|
-
.fm-plugin-field+.fm-plugin-field{border-top:1px solid var(--dsw-alias-border-l2)}
|
|
1841
|
-
.fm-plugin-field-head{align-items:center;gap:8px;display:flex}
|
|
1842
|
-
.fm-plugin-field-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
|
|
1843
|
-
.fm-plugin-field-value{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}
|
|
1844
|
-
.fm-plugin-field-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
|
|
1845
|
-
.fm-plugin-check{align-items:center;gap:8px;display:flex;cursor:pointer;user-select:none}
|
|
1846
|
-
.fm-plugin-check input[type="checkbox"]{width:16px;height:16px;margin:0;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#4176e6)}
|
|
1847
|
-
.fm-plugin-check-label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5}
|
|
1848
|
-
.fm-plugin-range{width:100%;max-width:280px;cursor:pointer;accent-color:var(--dsw-alias-brand-primary,#4176e6)}
|
|
1849
|
-
/* DSH-better-sidebar compatibility: its panel host (top-right toggle cluster +
|
|
1850
|
-
right/bottom panels) is appended to document.body at z-index 40/45, above
|
|
1851
|
-
shell.overlay's z-20 layer, so this overlay cannot cover it. Hide the host
|
|
1852
|
-
while focus is on, and neutralize the #root layout push it applies when its
|
|
1853
|
-
panels are open. Selectors mirror better-sidebar's own layout.css exactly. */
|
|
1854
|
-
body[data-fm-focus] [data-dsh-panel-host]{display:none!important}
|
|
1855
|
-
body[data-fm-focus] #root{margin-right:0!important;width:100%!important}
|
|
1856
|
-
body[data-fm-focus] #root [data-dsh-frame] > [data-pane="conversation"],
|
|
1857
|
-
body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!important}
|
|
1858
|
-
/* First-run onboarding (settings.onboarding step) — content inside the official
|
|
1859
|
-
Modal primitive; the primitive owns the mask/dialog chrome, these style our body. */
|
|
1860
|
-
.fm-onboard{width:min(600px,100%);padding:0}
|
|
1861
|
-
.fm-onboard-content{box-sizing:border-box;flex-direction:column;max-height:calc(100vh - 48px);padding:28px;display:flex;overflow-y:auto}
|
|
1862
|
-
.fm-onboard-title{color:var(--dsw-alias-label-primary);outline:none;margin:0;font-size:20px;font-weight:500;line-height:28px}
|
|
1863
|
-
.fm-onboard-intro{color:var(--dsw-alias-label-secondary);margin:8px 0 0;font-size:14px;line-height:22px}
|
|
1864
|
-
.fm-onboard-subtitle{color:var(--dsw-alias-label-primary);margin:20px 0 8px;font-size:14px;font-weight:600;line-height:22px}
|
|
1865
|
-
.fm-onboard-features{margin:0;padding:0 0 0 18px;list-style:disc;display:flex;flex-direction:column;gap:6px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}
|
|
1866
|
-
.fm-onboard-note{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:18px}
|
|
1867
|
-
.fm-onboard-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}
|
|
1868
|
-
@media (width<=560px){.fm-onboard-content{padding:24px}}
|
|
1869
|
-
`;
|
|
1870
2795
|
//#endregion
|
|
1871
2796
|
//#region src/client/locales.ts
|
|
1872
2797
|
/** i18n dictionaries for the `focus` namespace. */
|
|
@@ -1875,6 +2800,9 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
1875
2800
|
"toggle.title": "进入专注模式",
|
|
1876
2801
|
"exit": "退出专注 (Esc)",
|
|
1877
2802
|
"backToLatest": "回到最新",
|
|
2803
|
+
"nav.label": "回合导航",
|
|
2804
|
+
"nav.jump": "跳转到第 {n} 回合",
|
|
2805
|
+
"nav.turn": "第 {n} 回合",
|
|
1878
2806
|
"settings.label": "专注模式",
|
|
1879
2807
|
"settings.description": "全屏阅读遮罩的偏好设置",
|
|
1880
2808
|
"settings.expand": "展开",
|
|
@@ -1887,8 +2815,12 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
1887
2815
|
"settings.autoFocus.hint": "AI 正常完成回复后自动进入并定位到你的提问",
|
|
1888
2816
|
"settings.hotkey": "按 F 键进入专注模式",
|
|
1889
2817
|
"settings.hotkey.hint": "在任意界面按下 F 立即进入专注模式;在输入框内打字不会触发",
|
|
1890
|
-
"
|
|
1891
|
-
"
|
|
2818
|
+
"process.toolCalls": "{n} 次工具调用",
|
|
2819
|
+
"process.messages": "{n} 条消息",
|
|
2820
|
+
"process.subagents": "{n} 个 subagent",
|
|
2821
|
+
"process.thought": "已思考",
|
|
2822
|
+
"process.separator": " · ",
|
|
2823
|
+
"process.toolRow": "已完成",
|
|
1892
2824
|
"empty": "暂无打开的会话",
|
|
1893
2825
|
"loading": "正在读取会话…",
|
|
1894
2826
|
"running": "正在工作",
|
|
@@ -1908,6 +2840,9 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
1908
2840
|
"answer.reject": "拒绝",
|
|
1909
2841
|
"composer.placeholder": "输入回复…",
|
|
1910
2842
|
"composer.send": "发送",
|
|
2843
|
+
"markdown.code.copy": "复制代码",
|
|
2844
|
+
"markdown.code.copied": "已复制",
|
|
2845
|
+
"markdown.footnotes": "脚注",
|
|
1911
2846
|
"composer.expand": "展开输入框",
|
|
1912
2847
|
"composer.queued": "已排队 {n}",
|
|
1913
2848
|
"composer.occurrences": "草稿中包含引用 / 命令,在此编辑可能使其失效,建议退出专注模式编辑",
|
|
@@ -1944,6 +2879,9 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
1944
2879
|
"toggle.title": "Enter focus mode",
|
|
1945
2880
|
"exit": "Exit focus (Esc)",
|
|
1946
2881
|
"backToLatest": "Back to latest",
|
|
2882
|
+
"nav.label": "Turn navigation",
|
|
2883
|
+
"nav.jump": "Jump to turn {n}",
|
|
2884
|
+
"nav.turn": "Turn {n}",
|
|
1947
2885
|
"settings.label": "Focus Mode",
|
|
1948
2886
|
"settings.description": "Preferences for the full-screen reading overlay",
|
|
1949
2887
|
"settings.expand": "Expand",
|
|
@@ -1956,8 +2894,12 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
1956
2894
|
"settings.autoFocus.hint": "Auto-open at your question after a normal reply",
|
|
1957
2895
|
"settings.hotkey": "Press F to enter focus mode",
|
|
1958
2896
|
"settings.hotkey.hint": "Press F anywhere to enter focus mode instantly; typing in an input never triggers it",
|
|
1959
|
-
"
|
|
1960
|
-
"
|
|
2897
|
+
"process.toolCalls": "{n} tool calls",
|
|
2898
|
+
"process.messages": "{n} messages",
|
|
2899
|
+
"process.subagents": "{n} subagents",
|
|
2900
|
+
"process.thought": "Thought for a while",
|
|
2901
|
+
"process.separator": " · ",
|
|
2902
|
+
"process.toolRow": "completed",
|
|
1961
2903
|
"empty": "No open session",
|
|
1962
2904
|
"loading": "Reading session…",
|
|
1963
2905
|
"running": "Working",
|
|
@@ -1977,6 +2919,9 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
1977
2919
|
"answer.reject": "Deny",
|
|
1978
2920
|
"composer.placeholder": "Type a reply…",
|
|
1979
2921
|
"composer.send": "Send",
|
|
2922
|
+
"markdown.code.copy": "Copy code",
|
|
2923
|
+
"markdown.code.copied": "Copied",
|
|
2924
|
+
"markdown.footnotes": "Footnotes",
|
|
1980
2925
|
"composer.expand": "Expand input",
|
|
1981
2926
|
"composer.queued": "Queued {n}",
|
|
1982
2927
|
"composer.occurrences": "Draft contains references / commands — editing here may break them; edit outside focus mode",
|
|
@@ -2014,11 +2959,18 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2014
2959
|
* Focus Mode, browser half: a full-screen reading overlay in `shell.overlay`
|
|
2015
2960
|
* (additive, never replaces shipped UI), a "专注" button in the session header
|
|
2016
2961
|
* action row, and a collapsible plugin card in the Plugins settings tab
|
|
2017
|
-
* (`settings.plugin.item`). Rendering reuses the official
|
|
2018
|
-
* `@deepseek-ai/dsh-client-ui-primitives` Markdown/
|
|
2019
|
-
*
|
|
2962
|
+
* (`settings.plugin.item`). Rendering reuses the official client UI primitives
|
|
2963
|
+
* module (`@deepseek-ai/dsh-client-ui-primitives` runtime id) — Markdown /
|
|
2964
|
+
* MessageText / Tooltip / Button — plus the conversation service's composer
|
|
2965
|
+
* registry and the uiSession pending-interaction service (dsh 0.1.2).
|
|
2020
2966
|
*/
|
|
2021
2967
|
const NS = "focus";
|
|
2968
|
+
let warnedLegacy = false;
|
|
2969
|
+
function warnLegacyOnce() {
|
|
2970
|
+
if (warnedLegacy) return;
|
|
2971
|
+
warnedLegacy = true;
|
|
2972
|
+
console.warn("[dsh-focus-overlay] dsh 0.1.2+ ui services not found — running in legacy compatibility mode");
|
|
2973
|
+
}
|
|
2022
2974
|
//#endregion
|
|
2023
2975
|
module.exports = {
|
|
2024
2976
|
name: "dsh-focus-overlay-client",
|
|
@@ -2026,7 +2978,10 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2026
2978
|
"slots",
|
|
2027
2979
|
"sessions",
|
|
2028
2980
|
"locale",
|
|
2029
|
-
"workspaces"
|
|
2981
|
+
"workspaces",
|
|
2982
|
+
"uiSession",
|
|
2983
|
+
"uiConversation",
|
|
2984
|
+
"settingsScope"
|
|
2030
2985
|
],
|
|
2031
2986
|
apply(ctx) {
|
|
2032
2987
|
const style = document.createElement("style");
|
|
@@ -2059,16 +3014,50 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2059
3014
|
const workspaces = ctx.workspaces;
|
|
2060
3015
|
const conversation = ctx.get("conversation");
|
|
2061
3016
|
const chatFileMentions = ctx.get("chatFileMentions");
|
|
3017
|
+
const uiSession = ctx.get("uiSession");
|
|
3018
|
+
const uiConversation = ctx.get("uiConversation");
|
|
3019
|
+
if (!uiSession || !uiConversation) warnLegacyOnce();
|
|
3020
|
+
let settingsScope = null;
|
|
3021
|
+
try {
|
|
3022
|
+
const binder = ctx.get("settingsScope");
|
|
3023
|
+
settingsScope = binder && typeof binder.bind === "function" ? binder.bind({ namespace: "ui-chat" }) : null;
|
|
3024
|
+
} catch {
|
|
3025
|
+
settingsScope = null;
|
|
3026
|
+
}
|
|
2062
3027
|
ctx.effect(() => {
|
|
2063
3028
|
let currentId = void 0;
|
|
2064
3029
|
let unsubSession = null;
|
|
3030
|
+
let unsubChat = null;
|
|
3031
|
+
let chatSlice = null;
|
|
2065
3032
|
let prevRunning = false;
|
|
2066
3033
|
let pendingSettle = false;
|
|
3034
|
+
const judge = () => {
|
|
3035
|
+
if (!pendingSettle) return;
|
|
3036
|
+
const snap = chatSlice;
|
|
3037
|
+
const outcome = detectSettledCompletion({
|
|
3038
|
+
partial: snap ? snap.partial : null,
|
|
3039
|
+
nodes: snap ? snap.nodes : [],
|
|
3040
|
+
runningCalls: snap && snap.runningCalls ? snap.runningCalls : []
|
|
3041
|
+
});
|
|
3042
|
+
if (outcome.settled) {
|
|
3043
|
+
pendingSettle = false;
|
|
3044
|
+
if (outcome.completed && prefsStore.get().autoFocus) if (focusStore.get()) focusStore.notifyDone();
|
|
3045
|
+
else {
|
|
3046
|
+
focusStore.setAutoAnchor(outcome.anchorSeq);
|
|
3047
|
+
focusStore.set(true);
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
};
|
|
2067
3051
|
const watch = (id) => {
|
|
2068
3052
|
if (unsubSession) {
|
|
2069
3053
|
unsubSession();
|
|
2070
3054
|
unsubSession = null;
|
|
2071
3055
|
}
|
|
3056
|
+
if (unsubChat) {
|
|
3057
|
+
unsubChat();
|
|
3058
|
+
unsubChat = null;
|
|
3059
|
+
}
|
|
3060
|
+
chatSlice = null;
|
|
2072
3061
|
prevRunning = false;
|
|
2073
3062
|
pendingSettle = false;
|
|
2074
3063
|
if (id == null) return;
|
|
@@ -2081,21 +3070,21 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2081
3070
|
const running = !!snap.running;
|
|
2082
3071
|
if (prevRunning && !running) pendingSettle = true;
|
|
2083
3072
|
if (running) pendingSettle = false;
|
|
2084
|
-
|
|
2085
|
-
const outcome = detectSettledCompletion(snap);
|
|
2086
|
-
if (outcome.settled) {
|
|
2087
|
-
pendingSettle = false;
|
|
2088
|
-
if (outcome.completed && prefsStore.get().autoFocus) if (focusStore.get()) focusStore.notifyDone();
|
|
2089
|
-
else {
|
|
2090
|
-
focusStore.setAutoAnchor(outcome.anchorSeq);
|
|
2091
|
-
focusStore.set(true);
|
|
2092
|
-
}
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
3073
|
+
judge();
|
|
2095
3074
|
prevRunning = running;
|
|
2096
3075
|
};
|
|
2097
3076
|
onSnap();
|
|
2098
3077
|
unsubSession = face.subscribe(onSnap);
|
|
3078
|
+
if (uiConversation) try {
|
|
3079
|
+
const chatSource = uiConversation.binding(id).target("chat");
|
|
3080
|
+
const onChat = () => {
|
|
3081
|
+
chatSlice = legacySliceOf(chatSource.getSnapshot(), face.getSnapshot());
|
|
3082
|
+
judge();
|
|
3083
|
+
};
|
|
3084
|
+
onChat();
|
|
3085
|
+
unsubChat = chatSource.subscribe(onChat);
|
|
3086
|
+
} catch {}
|
|
3087
|
+
if (!unsubChat) chatSlice = legacySliceOf(null, face.getSnapshot());
|
|
2099
3088
|
};
|
|
2100
3089
|
const onList = () => {
|
|
2101
3090
|
const ls = sessions.list.getSnapshot();
|
|
@@ -2110,6 +3099,7 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2110
3099
|
return () => {
|
|
2111
3100
|
if (unsubList) unsubList();
|
|
2112
3101
|
if (unsubSession) unsubSession();
|
|
3102
|
+
if (unsubChat) unsubChat();
|
|
2113
3103
|
};
|
|
2114
3104
|
});
|
|
2115
3105
|
ctx.effect(() => {
|
|
@@ -2139,6 +3129,9 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2139
3129
|
workspaces,
|
|
2140
3130
|
conversation,
|
|
2141
3131
|
chatFileMentions,
|
|
3132
|
+
uiSession,
|
|
3133
|
+
uiConversation,
|
|
3134
|
+
settingsScope,
|
|
2142
3135
|
t
|
|
2143
3136
|
})));
|
|
2144
3137
|
ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
|