dsh-focus-overlay 0.11.2 → 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 +188 -22
- package/package.json +2 -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 {
|
|
@@ -1036,13 +1045,27 @@ window.__ModuleLoader__.load({
|
|
|
1036
1045
|
function InputBar(props) {
|
|
1037
1046
|
const { t, width, value, queueCount, occCount, errorLine, textareaRef, onFocusChange, onChange, onSend } = props;
|
|
1038
1047
|
const composingRef = (0, react.useRef)(false);
|
|
1048
|
+
const echoingRef = (0, react.useRef)(false);
|
|
1039
1049
|
const [pending, setPending] = (0, react.useState)(null);
|
|
1040
1050
|
const shown = pending ?? value;
|
|
1041
1051
|
const empty = shown.trim() === "";
|
|
1042
|
-
const
|
|
1043
|
-
|
|
1052
|
+
const echo = (el, next) => {
|
|
1053
|
+
const start = el.selectionStart;
|
|
1054
|
+
const end = el.selectionEnd;
|
|
1055
|
+
echoingRef.current = true;
|
|
1056
|
+
try {
|
|
1057
|
+
onChange(next);
|
|
1058
|
+
} finally {
|
|
1059
|
+
echoingRef.current = false;
|
|
1060
|
+
}
|
|
1061
|
+
if (document.activeElement !== el) {
|
|
1062
|
+
el.focus();
|
|
1063
|
+
el.setSelectionRange(start, end);
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
const flush = (el) => {
|
|
1044
1067
|
setPending(null);
|
|
1045
|
-
|
|
1068
|
+
echo(el, el.value);
|
|
1046
1069
|
};
|
|
1047
1070
|
const onCompositionStart = () => {
|
|
1048
1071
|
composingRef.current = true;
|
|
@@ -1050,8 +1073,7 @@ window.__ModuleLoader__.load({
|
|
|
1050
1073
|
};
|
|
1051
1074
|
const onCompositionEnd = (e) => {
|
|
1052
1075
|
composingRef.current = false;
|
|
1053
|
-
flush(e.currentTarget
|
|
1054
|
-
refocusAfterEcho(e.currentTarget);
|
|
1076
|
+
flush(e.currentTarget);
|
|
1055
1077
|
};
|
|
1056
1078
|
const onKeyDown = (e) => {
|
|
1057
1079
|
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing && e.nativeEvent.keyCode !== 229) {
|
|
@@ -1063,12 +1085,6 @@ window.__ModuleLoader__.load({
|
|
|
1063
1085
|
el.style.height = "auto";
|
|
1064
1086
|
el.style.height = `${Math.min(el.scrollHeight, 120)}px`;
|
|
1065
1087
|
};
|
|
1066
|
-
const refocusAfterEcho = (el) => {
|
|
1067
|
-
if (document.activeElement === el) return;
|
|
1068
|
-
el.focus();
|
|
1069
|
-
const end = el.value.length;
|
|
1070
|
-
el.setSelectionRange(end, end);
|
|
1071
|
-
};
|
|
1072
1088
|
(0, react.useEffect)(() => {
|
|
1073
1089
|
if (textareaRef.current) autoGrow(textareaRef.current);
|
|
1074
1090
|
}, [shown, textareaRef]);
|
|
@@ -1077,10 +1093,9 @@ window.__ModuleLoader__.load({
|
|
|
1077
1093
|
if (composingRef.current) setPending(el.value);
|
|
1078
1094
|
else {
|
|
1079
1095
|
setPending(null);
|
|
1080
|
-
|
|
1096
|
+
echo(el, el.value);
|
|
1081
1097
|
}
|
|
1082
1098
|
autoGrow(el);
|
|
1083
|
-
refocusAfterEcho(el);
|
|
1084
1099
|
};
|
|
1085
1100
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1086
1101
|
className: "fm-bar",
|
|
@@ -1109,10 +1124,14 @@ window.__ModuleLoader__.load({
|
|
|
1109
1124
|
onKeyDown,
|
|
1110
1125
|
onFocus: () => onFocusChange(true),
|
|
1111
1126
|
onBlur: () => {
|
|
1127
|
+
if (echoingRef.current) return;
|
|
1112
1128
|
if (composingRef.current) {
|
|
1113
1129
|
composingRef.current = false;
|
|
1114
|
-
|
|
1115
|
-
if (
|
|
1130
|
+
const el = textareaRef.current;
|
|
1131
|
+
if (el) {
|
|
1132
|
+
flush(el);
|
|
1133
|
+
if (document.activeElement === el) return;
|
|
1134
|
+
}
|
|
1116
1135
|
}
|
|
1117
1136
|
onFocusChange(false);
|
|
1118
1137
|
}
|
|
@@ -1137,8 +1156,9 @@ window.__ModuleLoader__.load({
|
|
|
1137
1156
|
});
|
|
1138
1157
|
}
|
|
1139
1158
|
function FocusBottomDock(props) {
|
|
1140
|
-
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;
|
|
1141
1160
|
if (form === "toast") return null;
|
|
1161
|
+
if (form === "bar" && borrowed) return null;
|
|
1142
1162
|
if (form === "card") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AnswerCard, {
|
|
1143
1163
|
t,
|
|
1144
1164
|
width,
|
|
@@ -1185,7 +1205,7 @@ window.__ModuleLoader__.load({
|
|
|
1185
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}
|
|
1186
1206
|
.fm-title{font-size:calc(14px + var(--dsh-content-font-delta,0px));font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
1187
1207
|
.fm-body-wrap{position:relative;flex:1 1 auto;display:flex;min-height:0}
|
|
1188
|
-
.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}
|
|
1189
1209
|
.fm-inner{max-width:760px;margin:0 auto}
|
|
1190
1210
|
.fm-msg{margin:0}
|
|
1191
1211
|
.fm-user-msg{margin:0}
|
|
@@ -1238,6 +1258,31 @@ window.__ModuleLoader__.load({
|
|
|
1238
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)}
|
|
1239
1259
|
.fm-reply-toast-dot{width:8px;height:8px;border-radius:999px;background:var(--dsw-static-deepseek-500,#4176e6);flex:0 0 auto}
|
|
1240
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}
|
|
1241
1286
|
/* ---- bottom dock: bar / pill / jump-to-bottom / answer card ----
|
|
1242
1287
|
Visual language mirrors the official composer cards token-for-token
|
|
1243
1288
|
(ui-conversation InputBar/QuestionComposer, ui-user-questions
|
|
@@ -1912,6 +1957,20 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
1912
1957
|
}, i))]
|
|
1913
1958
|
});
|
|
1914
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
|
+
}
|
|
1915
1974
|
function FocusContent(props) {
|
|
1916
1975
|
const { useSessions, sessions, workspaces, conversation, chatFileMentions, uiSession, uiConversation, settingsScope, t } = props;
|
|
1917
1976
|
const prefs = usePrefs();
|
|
@@ -2185,7 +2244,7 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2185
2244
|
const idx = findSeqIndex(items, seq);
|
|
2186
2245
|
const el = idx >= 0 ? anchors["fm-" + idx] : void 0;
|
|
2187
2246
|
if (el && bodyEl) {
|
|
2188
|
-
const limit = bodyEl.getBoundingClientRect().bottom -
|
|
2247
|
+
const limit = bodyEl.getBoundingClientRect().bottom - 160;
|
|
2189
2248
|
const hidden = el.getBoundingClientRect().bottom - limit;
|
|
2190
2249
|
if (hidden > 0) bodyEl.scrollBy({
|
|
2191
2250
|
top: hidden,
|
|
@@ -2208,6 +2267,7 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2208
2267
|
const onKey = (e) => {
|
|
2209
2268
|
if (e.key !== "Escape") return;
|
|
2210
2269
|
if (e.isComposing || e.keyCode === 229) return;
|
|
2270
|
+
if (composerSeat()?.querySelector("[role=\"listbox\"]")) return;
|
|
2211
2271
|
e.preventDefault();
|
|
2212
2272
|
e.stopPropagation();
|
|
2213
2273
|
if (cardOpen) {
|
|
@@ -2292,7 +2352,10 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2292
2352
|
};
|
|
2293
2353
|
const hideBar = () => {
|
|
2294
2354
|
try {
|
|
2295
|
-
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();
|
|
2296
2359
|
} catch {}
|
|
2297
2360
|
setEngaged(false);
|
|
2298
2361
|
setDockFocused(false);
|
|
@@ -2317,6 +2380,87 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2317
2380
|
focused: dockFocused,
|
|
2318
2381
|
engaged
|
|
2319
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]);
|
|
2320
2464
|
(0, react.useEffect)(() => {
|
|
2321
2465
|
if (!pending) {
|
|
2322
2466
|
if (cardOpen) setCardOpen(false);
|
|
@@ -2327,14 +2471,14 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2327
2471
|
setEngaged(false);
|
|
2328
2472
|
}, [pending, cardOpen]);
|
|
2329
2473
|
(0, react.useEffect)(() => {
|
|
2330
|
-
if (form === "bar" && focusBarOnce.current) {
|
|
2474
|
+
if (form === "bar" && !borrowedBar && focusBarOnce.current) {
|
|
2331
2475
|
focusBarOnce.current = false;
|
|
2332
2476
|
try {
|
|
2333
2477
|
taRef.current && taRef.current.focus();
|
|
2334
2478
|
} catch {}
|
|
2335
2479
|
setEngaged(false);
|
|
2336
2480
|
}
|
|
2337
|
-
}, [form]);
|
|
2481
|
+
}, [form, borrowedBar]);
|
|
2338
2482
|
let title = "Focus Mode";
|
|
2339
2483
|
if (currentId && listState && listState.byId && listState.byId[currentId]) {
|
|
2340
2484
|
const row = listState.byId[currentId];
|
|
@@ -2578,6 +2722,7 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2578
2722
|
t,
|
|
2579
2723
|
width: prefs.width,
|
|
2580
2724
|
form,
|
|
2725
|
+
borrowed: borrowedBar,
|
|
2581
2726
|
wait,
|
|
2582
2727
|
onCardClose: () => setCardOpen(false),
|
|
2583
2728
|
draft: draftValue,
|
|
@@ -2677,6 +2822,23 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2677
2822
|
children: t("settings.hotkey.hint")
|
|
2678
2823
|
})]
|
|
2679
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
|
+
}),
|
|
2680
2842
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2681
2843
|
className: "fm-plugin-field",
|
|
2682
2844
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
@@ -2854,6 +3016,8 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2854
3016
|
"settings.autoFocus.hint": "AI 正常完成回复后自动进入并定位到你的提问",
|
|
2855
3017
|
"settings.hotkey": "按 F 键进入专注模式",
|
|
2856
3018
|
"settings.hotkey.hint": "在任意界面按下 F 立即进入专注模式;在输入框内打字不会触发",
|
|
3019
|
+
"settings.borrow": "使用官方输入框",
|
|
3020
|
+
"settings.borrow.hint": "专注模式直接借用主界面的官方输入框(支持 / 指令、@ 引用文件与会话);关闭时使用插件内置输入框。官方输入框不可用时自动回退。",
|
|
2857
3021
|
"process.toolCalls": "{n} 次工具调用",
|
|
2858
3022
|
"process.messages": "{n} 条消息",
|
|
2859
3023
|
"process.subagents": "{n} 个 subagent",
|
|
@@ -2933,6 +3097,8 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2933
3097
|
"settings.autoFocus.hint": "Auto-open at your question after a normal reply",
|
|
2934
3098
|
"settings.hotkey": "Press F to enter focus mode",
|
|
2935
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.",
|
|
2936
3102
|
"process.toolCalls": "{n} tool calls",
|
|
2937
3103
|
"process.messages": "{n} messages",
|
|
2938
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",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"bundle"
|
|
55
55
|
],
|
|
56
56
|
"devDependencies": {
|
|
57
|
+
"playwright-core": "^1.63.0",
|
|
57
58
|
"tsdown": "0.22.2",
|
|
58
59
|
"vitest": "^4.1.10"
|
|
59
60
|
},
|