dsh-focus-overlay 0.11.3 → 0.12.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/lib/client.js +163 -7
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -557,6 +557,14 @@ window.__ModuleLoader__.load({
|
|
|
557
557
|
if (o.engaged || o.focused || o.draftEmpty && o.inZone) return "bar";
|
|
558
558
|
return o.draftEmpty ? "tobottom" : "pill";
|
|
559
559
|
}
|
|
560
|
+
/** Whether the bottom dock should render the OFFICIAL composer (CSS-lifted
|
|
561
|
+
* above the overlay) as its bar form: the user wants the borrow, the
|
|
562
|
+
* composer DOM probed available, and the dock currently wants the bar.
|
|
563
|
+
* Pure so the decision is unit-testable at the seam the lift effect
|
|
564
|
+
* drives — the e2e loop exercises the DOM consequences. */
|
|
565
|
+
function shouldLiftOfficialComposer(o) {
|
|
566
|
+
return o.wanted && o.available && o.form === "bar";
|
|
567
|
+
}
|
|
560
568
|
/**
|
|
561
569
|
* Bottom-zone membership with hysteresis: entering requires the viewport edge
|
|
562
570
|
* within {@link BOTTOM_ENTER_PX} of the scroll end, leaving requires drifting
|
|
@@ -693,7 +701,8 @@ window.__ModuleLoader__.load({
|
|
|
693
701
|
scroll: "preserve",
|
|
694
702
|
width: 760,
|
|
695
703
|
autoFocus: false,
|
|
696
|
-
hotkey: true
|
|
704
|
+
hotkey: true,
|
|
705
|
+
borrow: true
|
|
697
706
|
};
|
|
698
707
|
function load() {
|
|
699
708
|
try {
|
|
@@ -1147,8 +1156,9 @@ window.__ModuleLoader__.load({
|
|
|
1147
1156
|
});
|
|
1148
1157
|
}
|
|
1149
1158
|
function FocusBottomDock(props) {
|
|
1150
|
-
const { t, width, form, wait, onCardClose, draft, queueCount, occCount, errorLine, textareaRef, onFocusChange, onDraftChange, onSend, onExpand, onJumpBottom } = props;
|
|
1159
|
+
const { t, width, form, borrowed, wait, onCardClose, draft, queueCount, occCount, errorLine, textareaRef, onFocusChange, onDraftChange, onSend, onExpand, onJumpBottom } = props;
|
|
1151
1160
|
if (form === "toast") return null;
|
|
1161
|
+
if (form === "bar" && borrowed) return null;
|
|
1152
1162
|
if (form === "card") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnswerCard, {
|
|
1153
1163
|
t,
|
|
1154
1164
|
width,
|
|
@@ -1195,7 +1205,7 @@ window.__ModuleLoader__.load({
|
|
|
1195
1205
|
.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}
|
|
1196
1206
|
.fm-title{font-size:calc(14px + var(--dsh-content-font-delta,0px));font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1197
1207
|
.fm-body-wrap{position:relative;flex:1 1 auto;display:flex;min-height:0}
|
|
1198
|
-
.fm-body{flex:1 1 auto;min-width:0;overflow-y:auto;padding:28px 28px
|
|
1208
|
+
.fm-body{flex:1 1 auto;min-width:0;overflow-y:auto;padding:28px 28px 160px}
|
|
1199
1209
|
.fm-inner{max-width:760px;margin:0 auto}
|
|
1200
1210
|
.fm-msg{margin:0}
|
|
1201
1211
|
.fm-user-msg{margin:0}
|
|
@@ -1248,6 +1258,31 @@ window.__ModuleLoader__.load({
|
|
|
1248
1258
|
.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)}
|
|
1249
1259
|
.fm-reply-toast-dot{width:8px;height:8px;border-radius:999px;background:var(--dsw-static-deepseek-500,#4176e6);flex:0 0 auto}
|
|
1250
1260
|
.fm-reply-toast-text{white-space:nowrap}
|
|
1261
|
+
/* ---- official-composer borrow: the lifted input bar ----
|
|
1262
|
+
The main view's composer seat ([data-composer-seat], normally in the
|
|
1263
|
+
conversation scroll flow UNDER this overlay) is re-anchored above the
|
|
1264
|
+
overlay while the dock wants the bar form. Its ancestor chain has no
|
|
1265
|
+
transform/filter/positioned-z traps (verified against the 0.1.2 build),
|
|
1266
|
+
so position:fixed escapes to the viewport; the z-index sits just above
|
|
1267
|
+
.fm-overlay's 2147483000 and below the int32 ceiling. The slash/@ menus
|
|
1268
|
+
are descendants of the composer card, so they lift with it.
|
|
1269
|
+
The seat itself must paint NOTHING: it is a layout box larger than the
|
|
1270
|
+
composer card (hero variant stacks brand/workspace rows above it), so a
|
|
1271
|
+
seat-level background or shadow would draw a translucent SQUARE behind
|
|
1272
|
+
the rounded card. Only the card's own skin shows; the seat's empty area
|
|
1273
|
+
is click-transparent so the reading surface beneath stays interactive. */
|
|
1274
|
+
.fm-lift{position:fixed!important;left:50%!important;right:auto!important;top:auto!important;bottom:20px!important;transform:translateX(-50%)!important;width:min(var(--fm-lift-width,680px),calc(100vw - 48px))!important;max-width:calc(100vw - 48px)!important;z-index:2147483200!important;margin:0!important;box-shadow:none!important;background:0 0!important;pointer-events:none}
|
|
1275
|
+
/* The composer's dock row (queue/references rail) is hidden while lifted on
|
|
1276
|
+
purpose: focus mode's dock is the plugin's own bottom-center region, and a
|
|
1277
|
+
second rail inside the floating bar would duplicate it. Noteboard's canvas
|
|
1278
|
+
borrow does the same. This drops that row's affordances IN FOCUS MODE only
|
|
1279
|
+
(the main view keeps them); revisit if a focus-mode user needs them. */
|
|
1280
|
+
.fm-lift [data-slot="conversation.composer.dock"]{display:none!important}
|
|
1281
|
+
.fm-lift [data-composer-card]{pointer-events:auto}
|
|
1282
|
+
/* Cap the lifted editor's scroll area: a long multi-line draft must not grow
|
|
1283
|
+
the floating card over half the reading surface (the main view allows the
|
|
1284
|
+
full composer column height, which focus mode cannot afford). */
|
|
1285
|
+
.fm-lift [data-input-scroll]{max-height:180px!important;overscroll-behavior:contain}
|
|
1251
1286
|
/* ---- bottom dock: bar / pill / jump-to-bottom / answer card ----
|
|
1252
1287
|
Visual language mirrors the official composer cards token-for-token
|
|
1253
1288
|
(ui-conversation InputBar/QuestionComposer, ui-user-questions
|
|
@@ -1922,6 +1957,20 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
1922
1957
|
}, i))]
|
|
1923
1958
|
});
|
|
1924
1959
|
}
|
|
1960
|
+
/** The main view's composer seat — the DOM node the borrow path CSS-lifts
|
|
1961
|
+
* above the overlay. ONE selector shared by the probe, the lift, the focus
|
|
1962
|
+
* bridge, and the Esc peel, so they can never disagree on WHAT they lift. */
|
|
1963
|
+
function composerSeat() {
|
|
1964
|
+
return document.querySelector("[data-composer-seat]");
|
|
1965
|
+
}
|
|
1966
|
+
/** The official editor's editable surface inside the seat. The SAME node the
|
|
1967
|
+
* availability probe verifies and the pill→bar handoff focuses: probe
|
|
1968
|
+
* success therefore implies a focusable editor. If a dsh update moves either
|
|
1969
|
+
* surface, the probe fails → the dock degrades to the built-in textarea bar
|
|
1970
|
+
* — never to a silently un-focusable lifted composer. */
|
|
1971
|
+
function composerEditable(seat) {
|
|
1972
|
+
return seat ? seat.querySelector("[contenteditable=\"true\"]") : null;
|
|
1973
|
+
}
|
|
1925
1974
|
function FocusContent(props) {
|
|
1926
1975
|
const { useSessions, sessions, workspaces, conversation, chatFileMentions, uiSession, uiConversation, settingsScope, t } = props;
|
|
1927
1976
|
const prefs = usePrefs();
|
|
@@ -2195,7 +2244,7 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2195
2244
|
const idx = findSeqIndex(items, seq);
|
|
2196
2245
|
const el = idx >= 0 ? anchors["fm-" + idx] : void 0;
|
|
2197
2246
|
if (el && bodyEl) {
|
|
2198
|
-
const limit = bodyEl.getBoundingClientRect().bottom -
|
|
2247
|
+
const limit = bodyEl.getBoundingClientRect().bottom - 160;
|
|
2199
2248
|
const hidden = el.getBoundingClientRect().bottom - limit;
|
|
2200
2249
|
if (hidden > 0) bodyEl.scrollBy({
|
|
2201
2250
|
top: hidden,
|
|
@@ -2218,6 +2267,7 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2218
2267
|
const onKey = (e) => {
|
|
2219
2268
|
if (e.key !== "Escape") return;
|
|
2220
2269
|
if (e.isComposing || e.keyCode === 229) return;
|
|
2270
|
+
if (composerSeat()?.querySelector("[role=\"listbox\"]")) return;
|
|
2221
2271
|
e.preventDefault();
|
|
2222
2272
|
e.stopPropagation();
|
|
2223
2273
|
if (cardOpen) {
|
|
@@ -2302,7 +2352,10 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2302
2352
|
};
|
|
2303
2353
|
const hideBar = () => {
|
|
2304
2354
|
try {
|
|
2305
|
-
taRef.current
|
|
2355
|
+
if (taRef.current) taRef.current.blur();
|
|
2356
|
+
const seat = composerSeat();
|
|
2357
|
+
const active = document.activeElement;
|
|
2358
|
+
if (seat && active && seat.contains(active)) active.blur();
|
|
2306
2359
|
} catch {}
|
|
2307
2360
|
setEngaged(false);
|
|
2308
2361
|
setDockFocused(false);
|
|
@@ -2327,6 +2380,87 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2327
2380
|
focused: dockFocused,
|
|
2328
2381
|
engaged
|
|
2329
2382
|
});
|
|
2383
|
+
const [borrowAvailable, setBorrowAvailable] = (0, react.useState)(false);
|
|
2384
|
+
const borrowWanted = prefs.borrow && !!currentId && snap !== null;
|
|
2385
|
+
const borrowedBar = shouldLiftOfficialComposer({
|
|
2386
|
+
wanted: borrowWanted,
|
|
2387
|
+
available: borrowAvailable,
|
|
2388
|
+
form
|
|
2389
|
+
});
|
|
2390
|
+
(0, react.useEffect)(() => {
|
|
2391
|
+
if (!borrowWanted) {
|
|
2392
|
+
setBorrowAvailable(false);
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
let tries = 0;
|
|
2396
|
+
let timer = null;
|
|
2397
|
+
const probe = () => {
|
|
2398
|
+
if (composerEditable(composerSeat())) {
|
|
2399
|
+
setBorrowAvailable(true);
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
if (++tries > 30) return;
|
|
2403
|
+
timer = setTimeout(probe, 100);
|
|
2404
|
+
};
|
|
2405
|
+
probe();
|
|
2406
|
+
return () => {
|
|
2407
|
+
if (timer) clearTimeout(timer);
|
|
2408
|
+
};
|
|
2409
|
+
}, [borrowWanted]);
|
|
2410
|
+
(0, react.useEffect)(() => {
|
|
2411
|
+
if (!borrowedBar) return;
|
|
2412
|
+
const seat = composerSeat();
|
|
2413
|
+
if (!seat) return;
|
|
2414
|
+
const inSeat = (t) => t instanceof Node && seat.contains(t);
|
|
2415
|
+
const onFocusIn = (e) => {
|
|
2416
|
+
if (inSeat(e.target)) onDockFocusChange(true);
|
|
2417
|
+
};
|
|
2418
|
+
const onFocusOut = (e) => {
|
|
2419
|
+
if (inSeat(e.relatedTarget)) return;
|
|
2420
|
+
setTimeout(() => {
|
|
2421
|
+
if (!inSeat(document.activeElement)) onDockFocusChange(false);
|
|
2422
|
+
}, 0);
|
|
2423
|
+
};
|
|
2424
|
+
seat.addEventListener("focusin", onFocusIn);
|
|
2425
|
+
seat.addEventListener("focusout", onFocusOut);
|
|
2426
|
+
return () => {
|
|
2427
|
+
seat.removeEventListener("focusin", onFocusIn);
|
|
2428
|
+
seat.removeEventListener("focusout", onFocusOut);
|
|
2429
|
+
onDockFocusChange(false);
|
|
2430
|
+
};
|
|
2431
|
+
}, [borrowedBar]);
|
|
2432
|
+
(0, react.useEffect)(() => {
|
|
2433
|
+
const seat = composerSeat();
|
|
2434
|
+
if (!seat) return;
|
|
2435
|
+
if (!borrowedBar) return;
|
|
2436
|
+
seat.style.setProperty("--fm-lift-width", `${Math.min(prefs.width, 680)}px`);
|
|
2437
|
+
seat.classList.add("fm-lift");
|
|
2438
|
+
if (focusBarOnce.current) {
|
|
2439
|
+
const editable = composerEditable(seat);
|
|
2440
|
+
if (!editable) {
|
|
2441
|
+
setBorrowAvailable(false);
|
|
2442
|
+
setDockFocused(false);
|
|
2443
|
+
} else {
|
|
2444
|
+
focusBarOnce.current = false;
|
|
2445
|
+
let landed = false;
|
|
2446
|
+
try {
|
|
2447
|
+
editable.focus();
|
|
2448
|
+
landed = seat.contains(document.activeElement);
|
|
2449
|
+
} catch {}
|
|
2450
|
+
onDockFocusChange(landed);
|
|
2451
|
+
setEngaged(false);
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
return () => {
|
|
2455
|
+
seat.classList.remove("fm-lift");
|
|
2456
|
+
seat.style.removeProperty("--fm-lift-width");
|
|
2457
|
+
};
|
|
2458
|
+
}, [borrowedBar, prefs.width]);
|
|
2459
|
+
const prevDraftRef = (0, react.useRef)(draftValue);
|
|
2460
|
+
(0, react.useEffect)(() => {
|
|
2461
|
+
if (prevDraftRef.current !== "" && draftValue === "" && zone) revealArmed.current = true;
|
|
2462
|
+
prevDraftRef.current = draftValue;
|
|
2463
|
+
}, [draftValue, zone]);
|
|
2330
2464
|
(0, react.useEffect)(() => {
|
|
2331
2465
|
if (!pending) {
|
|
2332
2466
|
if (cardOpen) setCardOpen(false);
|
|
@@ -2337,14 +2471,14 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2337
2471
|
setEngaged(false);
|
|
2338
2472
|
}, [pending, cardOpen]);
|
|
2339
2473
|
(0, react.useEffect)(() => {
|
|
2340
|
-
if (form === "bar" && focusBarOnce.current) {
|
|
2474
|
+
if (form === "bar" && !borrowedBar && focusBarOnce.current) {
|
|
2341
2475
|
focusBarOnce.current = false;
|
|
2342
2476
|
try {
|
|
2343
2477
|
taRef.current && taRef.current.focus();
|
|
2344
2478
|
} catch {}
|
|
2345
2479
|
setEngaged(false);
|
|
2346
2480
|
}
|
|
2347
|
-
}, [form]);
|
|
2481
|
+
}, [form, borrowedBar]);
|
|
2348
2482
|
let title = "Focus Mode";
|
|
2349
2483
|
if (currentId && listState && listState.byId && listState.byId[currentId]) {
|
|
2350
2484
|
const row = listState.byId[currentId];
|
|
@@ -2588,6 +2722,7 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2588
2722
|
t,
|
|
2589
2723
|
width: prefs.width,
|
|
2590
2724
|
form,
|
|
2725
|
+
borrowed: borrowedBar,
|
|
2591
2726
|
wait,
|
|
2592
2727
|
onCardClose: () => setCardOpen(false),
|
|
2593
2728
|
draft: draftValue,
|
|
@@ -2687,6 +2822,23 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2687
2822
|
children: t("settings.hotkey.hint")
|
|
2688
2823
|
})]
|
|
2689
2824
|
}),
|
|
2825
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2826
|
+
className: "fm-plugin-field",
|
|
2827
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2828
|
+
className: "fm-plugin-check",
|
|
2829
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2830
|
+
type: "checkbox",
|
|
2831
|
+
checked: prefs.borrow,
|
|
2832
|
+
onChange: (e) => prefsStore.update({ borrow: e.target.checked })
|
|
2833
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2834
|
+
className: "fm-plugin-check-label",
|
|
2835
|
+
children: t("settings.borrow")
|
|
2836
|
+
})]
|
|
2837
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2838
|
+
className: "fm-plugin-field-hint",
|
|
2839
|
+
children: t("settings.borrow.hint")
|
|
2840
|
+
})]
|
|
2841
|
+
}),
|
|
2690
2842
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2691
2843
|
className: "fm-plugin-field",
|
|
2692
2844
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
@@ -2864,6 +3016,8 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2864
3016
|
"settings.autoFocus.hint": "AI 正常完成回复后自动进入并定位到你的提问",
|
|
2865
3017
|
"settings.hotkey": "按 F 键进入专注模式",
|
|
2866
3018
|
"settings.hotkey.hint": "在任意界面按下 F 立即进入专注模式;在输入框内打字不会触发",
|
|
3019
|
+
"settings.borrow": "使用官方输入框",
|
|
3020
|
+
"settings.borrow.hint": "专注模式直接借用主界面的官方输入框(支持 / 指令、@ 引用文件与会话);关闭时使用插件内置输入框。官方输入框不可用时自动回退。",
|
|
2867
3021
|
"process.toolCalls": "{n} 次工具调用",
|
|
2868
3022
|
"process.messages": "{n} 条消息",
|
|
2869
3023
|
"process.subagents": "{n} 个 subagent",
|
|
@@ -2943,6 +3097,8 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2943
3097
|
"settings.autoFocus.hint": "Auto-open at your question after a normal reply",
|
|
2944
3098
|
"settings.hotkey": "Press F to enter focus mode",
|
|
2945
3099
|
"settings.hotkey.hint": "Press F anywhere to enter focus mode instantly; typing in an input never triggers it",
|
|
3100
|
+
"settings.borrow": "Use the official composer",
|
|
3101
|
+
"settings.borrow.hint": "Focus mode borrows the main view's official composer (slash commands, @ file/session references included); turn off to use the built-in input bar. Falls back automatically when the official composer is unavailable.",
|
|
2946
3102
|
"process.toolCalls": "{n} tool calls",
|
|
2947
3103
|
"process.messages": "{n} messages",
|
|
2948
3104
|
"process.subagents": "{n} subagents",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-focus-overlay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Focus Mode for the dsh web GUI: a full-screen reading overlay that hides chrome and folds the AI tool-call flow into summaries, reusing the official Markdown / image primitives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.mjs",
|