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.
Files changed (2) hide show
  1. package/lib/client.js +163 -7
  2. 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 110px}
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 - 110;
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 && taRef.current.blur();
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.11.3",
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",