dsh-focus-overlay 0.12.0 → 0.12.1
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 +2 -0
- package/README.md +2 -0
- package/lib/client.js +310 -4
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
- **Auto-focus & reminders** — after a reply completes normally, optionally auto-enter focus at your question; while in focus, get a toast for a new reply or when the AI is waiting on you
|
|
26
26
|
- **Width handles** — draggable strips beside the content column (official WidthHandle replica) that resize the reading column live and persist
|
|
27
27
|
- **Hotkey F** — press `F` anywhere to enter focus mode instantly (toggleable in settings); typing in an input never triggers it
|
|
28
|
+
- **Focus counter** — the settings card keeps a running total of your focus-mode entries; past 100 entries a GitHub Star nudge appears, is remembered after the first use, and then becomes a direct "open the project page" button
|
|
28
29
|
|
|
29
30
|
These features are designed to give **small screens** more content space: tucking away the persistent header/composer and tool steps lets the conversation use as much of the screen as possible.
|
|
30
31
|
|
|
@@ -71,6 +72,7 @@ Click the line to expand that turn's full work detail (per-category one-line sum
|
|
|
71
72
|
| Auto-enter focus | After a reply **completes normally**, auto-open focus at your question; abnormal endings (stop / error / max-tokens / interrupt) never fire — the judgement waits for a stable snapshot (streaming tail drained) first |
|
|
72
73
|
| Reply / waiting reminders | In focus, a one-shot "New reply ready + View" toast on completion (auto-dismissed after 6s); a question/approval raises "AI is waiting for your reply + Answer", whose button expands the in-place answer card — auto-cleared once answered, never leaving focus |
|
|
73
74
|
| F hotkey | Press `F` anywhere to enter focus mode instantly (default on, toggleable in settings); never fires while typing in an input or editable element, modifiers and auto-repeat ignored |
|
|
75
|
+
| Focus counter | A row at the bottom of the settings card totals your focus-mode entries (auto-entries included), persisted to `localStorage`; past 100 entries a Star nudge appears — after the first use it is remembered and replaced by a direct "open the project page" button |
|
|
74
76
|
| Esc peels layer by layer | Answer card → input bar → focus mode, one layer per press; Esc mid-IME-composition cancels the composition first |
|
|
75
77
|
| i18n | Chinese / English copy registered under the `focus` namespace, follows the UI language |
|
|
76
78
|
| Plugin configuration card | A collapsible card under "Settings → Plugins → Plugin configuration", preferences persisted to `localStorage`; the Node half registers the same settings namespace (schemastery schema) so the tab dispatches it |
|
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
- **自动专注与提醒** —— 回复正常完成后可自动进入专注并定位到你的提问;专注中收到新回复、或 AI 等待你回答时弹出提醒
|
|
24
24
|
- **宽度调节** —— 正文两侧的拖拽把手(官方 WidthHandle 复刻),实时拖动阅读列宽并持久化
|
|
25
25
|
- **快捷键 F** —— 任意界面按 `F` 一键进入专注模式,可在设置中关闭;输入框内打字不会误触发
|
|
26
|
+
- **专注计数** —— 设置卡片最底部累计展示你进入专注模式的次数;超过 100 次后出现 GitHub Star 引导,点过一次即被记住,之后提供「前往 GitHub 项目页面」直达按钮
|
|
26
27
|
|
|
27
28
|
这些功能专为**小屏幕**提供更多内容呈现空间:收起常驻的标题 / 输入区与工具步骤,让有限的屏幕尽可能多地展示对话。
|
|
28
29
|
|
|
@@ -69,6 +70,7 @@
|
|
|
69
70
|
| 自动进入专注 | 回复**正常完成**后自动打开专注并定位到本轮你的提问;异常结束(停止 / 报错 / 超 token / 打断)不触发——判定要求快照稳定(流式尾部排空)后才下结论 |
|
|
70
71
|
| 回复 / 等待提醒 | 专注中回复完成弹「新回复已生成 + 查看」(6 秒自动消失,一次性);AI 提问 / 审批等待时弹「AI 正在等待你的回复 + 去回答」,按钮原地展开答题卡,作答后自动消失——全程无需退出专注 |
|
|
71
72
|
| F 键快捷键 | 任意界面按 `F` 立即进入专注(默认开,可在设置中关闭);输入框 / 可编辑元素内打字不触发,忽略修饰键与长按自动重复 |
|
|
73
|
+
| 专注计数 | 设置卡片最底部一行累计进入专注模式的次数(含自动进入),持久化到 `localStorage`;超过 100 次出现 Star 引导按钮,点击前往仓库页面并记住状态,之后改为「前往 GitHub 项目页面」直达按钮 |
|
|
72
74
|
| Esc 逐层退出 | 答题卡 → 输入条 → 专注模式,一层一层退;输入法组词中的 Esc 优先取消组词 |
|
|
73
75
|
| i18n | 中 / 英文案,注册到 `focus` 命名空间,跟随界面语言 |
|
|
74
76
|
| 插件配置卡片 | 「设置 → 插件 → 插件配置」中的可折叠卡片,偏好持久化到 `localStorage`;Node 半区以 schemastery schema 注册同名设置 namespace 供其分发 |
|
package/lib/client.js
CHANGED
|
@@ -519,6 +519,39 @@ window.__ModuleLoader__.load({
|
|
|
519
519
|
function onboardingSeen(stored, version) {
|
|
520
520
|
return stored === version;
|
|
521
521
|
}
|
|
522
|
+
/** Parse the persisted focus-entry counter (`{"entries":n}` JSON). Anything
|
|
523
|
+
* unreadable — null, broken JSON, a non-number or a negative — reads as 0,
|
|
524
|
+
* so one corrupted write can never make the settings page show a bogus
|
|
525
|
+
* count; fractions floor (a counter is always whole). */
|
|
526
|
+
function readEntryCount(raw) {
|
|
527
|
+
try {
|
|
528
|
+
if (!raw) return 0;
|
|
529
|
+
const parsed = JSON.parse(raw);
|
|
530
|
+
if (!parsed || typeof parsed !== "object") return 0;
|
|
531
|
+
const n = parsed.entries;
|
|
532
|
+
if (typeof n !== "number" || !Number.isFinite(n)) return 0;
|
|
533
|
+
return Math.max(0, Math.floor(n));
|
|
534
|
+
} catch {
|
|
535
|
+
return 0;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/** Whether the settings card should show the Star nudge for this entry count. */
|
|
539
|
+
function shouldShowStar(entries) {
|
|
540
|
+
return entries > 100;
|
|
541
|
+
}
|
|
542
|
+
/** Whether the persisted stats mark the Star button as already used — the
|
|
543
|
+
* card then offers a plain "open the project page" button instead of asking
|
|
544
|
+
* for a star again. Anything unreadable reads as not-starred. */
|
|
545
|
+
function readStarred(raw) {
|
|
546
|
+
try {
|
|
547
|
+
if (!raw) return false;
|
|
548
|
+
const parsed = JSON.parse(raw);
|
|
549
|
+
if (!parsed || typeof parsed !== "object") return false;
|
|
550
|
+
return parsed.starred === true;
|
|
551
|
+
} catch {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
522
555
|
/** Whether a keydown should enter focus mode via the F hotkey. Pure decision so
|
|
523
556
|
* the guard rails are unit-testable: the pref must be enabled, focus must not
|
|
524
557
|
* already be open, the press must not land in an editable element (composer /
|
|
@@ -757,6 +790,64 @@ window.__ModuleLoader__.load({
|
|
|
757
790
|
} catch {}
|
|
758
791
|
}
|
|
759
792
|
};
|
|
793
|
+
const STATS_KEY = "dsh-focus-overlay:stats";
|
|
794
|
+
function loadStats() {
|
|
795
|
+
try {
|
|
796
|
+
const raw = localStorage.getItem(STATS_KEY);
|
|
797
|
+
return {
|
|
798
|
+
entries: readEntryCount(raw),
|
|
799
|
+
starred: readStarred(raw)
|
|
800
|
+
};
|
|
801
|
+
} catch {
|
|
802
|
+
return {
|
|
803
|
+
entries: 0,
|
|
804
|
+
starred: false
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
let stats = loadStats();
|
|
809
|
+
const statsListeners = [];
|
|
810
|
+
function persistStats(next) {
|
|
811
|
+
stats = next;
|
|
812
|
+
try {
|
|
813
|
+
localStorage.setItem(STATS_KEY, JSON.stringify(next));
|
|
814
|
+
} catch {}
|
|
815
|
+
for (const l of statsListeners) l();
|
|
816
|
+
}
|
|
817
|
+
const statsStore = {
|
|
818
|
+
getEntries: () => stats.entries,
|
|
819
|
+
isStarred: () => stats.starred,
|
|
820
|
+
/** One focus-mode open. Writes through to localStorage (best effort); when
|
|
821
|
+
* storage is unavailable the in-memory value still counts for the session. */
|
|
822
|
+
bumpEntries: () => persistStats({
|
|
823
|
+
...stats,
|
|
824
|
+
entries: stats.entries + 1
|
|
825
|
+
}),
|
|
826
|
+
/** Mark the Star button as used (idempotent). */
|
|
827
|
+
markStarred: () => {
|
|
828
|
+
if (!stats.starred) persistStats({
|
|
829
|
+
...stats,
|
|
830
|
+
starred: true
|
|
831
|
+
});
|
|
832
|
+
},
|
|
833
|
+
subscribe: (l) => {
|
|
834
|
+
statsListeners.push(l);
|
|
835
|
+
return () => {
|
|
836
|
+
const i = statsListeners.indexOf(l);
|
|
837
|
+
if (i >= 0) statsListeners.splice(i, 1);
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
function useEntryCount() {
|
|
842
|
+
const [n, setN] = (0, react.useState)(statsStore.getEntries);
|
|
843
|
+
(0, react.useEffect)(() => statsStore.subscribe(() => setN(statsStore.getEntries)), []);
|
|
844
|
+
return n;
|
|
845
|
+
}
|
|
846
|
+
function useStarred() {
|
|
847
|
+
const [s, setS] = (0, react.useState)(statsStore.isStarred);
|
|
848
|
+
(0, react.useEffect)(() => statsStore.subscribe(() => setS(statsStore.isStarred)), []);
|
|
849
|
+
return s;
|
|
850
|
+
}
|
|
760
851
|
//#endregion
|
|
761
852
|
//#region src/client/Composer.tsx
|
|
762
853
|
/**
|
|
@@ -1198,6 +1289,172 @@ window.__ModuleLoader__.load({
|
|
|
1198
1289
|
});
|
|
1199
1290
|
}
|
|
1200
1291
|
//#endregion
|
|
1292
|
+
//#region src/client/celebrate.ts
|
|
1293
|
+
/**
|
|
1294
|
+
* Firework for the Star button: a short, self-contained canvas animation
|
|
1295
|
+
* appended to document.body with pointer-events:none and removed when done,
|
|
1296
|
+
* so it can never intercept clicks. No dependencies, no stylesheet injection:
|
|
1297
|
+
* every style is inline, so cleanup is simply removing the element.
|
|
1298
|
+
*
|
|
1299
|
+
* Five staggered bursts of ~90 trailing particles each; the GitHub page opens
|
|
1300
|
+
* only after the show ends (celebrateThenOpen).
|
|
1301
|
+
*/
|
|
1302
|
+
const BURSTS = 5;
|
|
1303
|
+
const BURST_GAP_MS = 280;
|
|
1304
|
+
const PARTICLES_PER_BURST = 90;
|
|
1305
|
+
const GRAVITY = 300;
|
|
1306
|
+
const MAX_LIFE_S = 2;
|
|
1307
|
+
const COLORS = [
|
|
1308
|
+
"#f43f5e",
|
|
1309
|
+
"#fb923c",
|
|
1310
|
+
"#facc15",
|
|
1311
|
+
"#4ade80",
|
|
1312
|
+
"#38bdf8",
|
|
1313
|
+
"#a78bfa",
|
|
1314
|
+
"#f472b6"
|
|
1315
|
+
];
|
|
1316
|
+
const FIREWORKS_TOTAL_MS = 4e3;
|
|
1317
|
+
/** Play the fireworks, then open `url` in a new tab once the sky is clear.
|
|
1318
|
+
* Browsers honor the click's transient activation for a few seconds, so the
|
|
1319
|
+
* delayed open is not popup-blocked in Chrome; stricter browsers fall back
|
|
1320
|
+
* on the repo button beside the counter, which opens the page directly. */
|
|
1321
|
+
function celebrateThenOpen(url) {
|
|
1322
|
+
const launched = launchFireworks();
|
|
1323
|
+
setTimeout(() => {
|
|
1324
|
+
try {
|
|
1325
|
+
window.open(url, "_blank", "noopener");
|
|
1326
|
+
} catch {}
|
|
1327
|
+
}, launched ? FIREWORKS_TOTAL_MS : 400);
|
|
1328
|
+
}
|
|
1329
|
+
/** Launch the bursts. Returns false when nothing was animated (reduced-motion
|
|
1330
|
+
* preference, an existing show, or an unavailable 2D context). */
|
|
1331
|
+
function launchFireworks() {
|
|
1332
|
+
try {
|
|
1333
|
+
if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return false;
|
|
1334
|
+
if (document.querySelector("[data-fm-fireworks]")) return false;
|
|
1335
|
+
const canvas = document.createElement("canvas");
|
|
1336
|
+
canvas.setAttribute("data-fm-fireworks", "");
|
|
1337
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
1338
|
+
const w = window.innerWidth;
|
|
1339
|
+
const h = window.innerHeight;
|
|
1340
|
+
canvas.width = Math.max(1, Math.floor(w * dpr));
|
|
1341
|
+
canvas.height = Math.max(1, Math.floor(h * dpr));
|
|
1342
|
+
canvas.style.cssText = "position:fixed;inset:0;width:100vw;height:100vh;pointer-events:none;z-index:2147483000";
|
|
1343
|
+
document.body.appendChild(canvas);
|
|
1344
|
+
const ctx = canvas.getContext("2d");
|
|
1345
|
+
if (!ctx) {
|
|
1346
|
+
canvas.remove();
|
|
1347
|
+
return false;
|
|
1348
|
+
}
|
|
1349
|
+
ctx.scale(dpr, dpr);
|
|
1350
|
+
const rand = (a, b) => a + Math.random() * (b - a);
|
|
1351
|
+
const rockets = [];
|
|
1352
|
+
const particles = [];
|
|
1353
|
+
const spawnRocket = (i) => {
|
|
1354
|
+
rockets.push({
|
|
1355
|
+
x: w * (.22 + .14 * i) + rand(-30, 30),
|
|
1356
|
+
y: h + 8,
|
|
1357
|
+
vy: -h * rand(1.05, 1.25),
|
|
1358
|
+
targetY: h * rand(.14, .36),
|
|
1359
|
+
color: COLORS[i % COLORS.length]
|
|
1360
|
+
});
|
|
1361
|
+
};
|
|
1362
|
+
const start = performance.now();
|
|
1363
|
+
let spawned = 0;
|
|
1364
|
+
const explode = (r) => {
|
|
1365
|
+
for (let k = 0; k < PARTICLES_PER_BURST; k++) {
|
|
1366
|
+
const angle = Math.PI * 2 * k / PARTICLES_PER_BURST + rand(-.07, .07);
|
|
1367
|
+
const speed = rand(80, 430) * (k % 4 === 0 ? .55 : 1);
|
|
1368
|
+
const maxLife = rand(1.2, MAX_LIFE_S);
|
|
1369
|
+
particles.push({
|
|
1370
|
+
x: r.x,
|
|
1371
|
+
y: r.y,
|
|
1372
|
+
px: r.x,
|
|
1373
|
+
py: r.y,
|
|
1374
|
+
vx: Math.cos(angle) * speed,
|
|
1375
|
+
vy: Math.sin(angle) * speed,
|
|
1376
|
+
life: maxLife,
|
|
1377
|
+
maxLife,
|
|
1378
|
+
color: Math.random() < .25 ? "#ffffff" : r.color
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
let prev = performance.now();
|
|
1383
|
+
let raf = 0;
|
|
1384
|
+
const step = (now) => {
|
|
1385
|
+
const dt = Math.min((now - prev) / 1e3, .05);
|
|
1386
|
+
prev = now;
|
|
1387
|
+
ctx.clearRect(0, 0, w, h);
|
|
1388
|
+
ctx.globalCompositeOperation = "lighter";
|
|
1389
|
+
const elapsed = now - start;
|
|
1390
|
+
while (spawned < BURSTS && elapsed >= spawned * BURST_GAP_MS) spawnRocket(spawned++);
|
|
1391
|
+
for (let i = rockets.length - 1; i >= 0; i--) {
|
|
1392
|
+
const r = rockets[i];
|
|
1393
|
+
r.y += r.vy * dt;
|
|
1394
|
+
ctx.globalAlpha = .9;
|
|
1395
|
+
ctx.strokeStyle = r.color;
|
|
1396
|
+
ctx.lineWidth = 2.4;
|
|
1397
|
+
ctx.beginPath();
|
|
1398
|
+
ctx.moveTo(r.x, r.y + 9);
|
|
1399
|
+
ctx.lineTo(r.x, r.y);
|
|
1400
|
+
ctx.stroke();
|
|
1401
|
+
if (Math.random() < .6) particles.push({
|
|
1402
|
+
x: r.x + rand(-2, 2),
|
|
1403
|
+
y: r.y + rand(0, 6),
|
|
1404
|
+
px: r.x,
|
|
1405
|
+
py: r.y,
|
|
1406
|
+
vx: rand(-30, 30),
|
|
1407
|
+
vy: rand(10, 70),
|
|
1408
|
+
life: rand(.25, .5),
|
|
1409
|
+
maxLife: .5,
|
|
1410
|
+
color: r.color
|
|
1411
|
+
});
|
|
1412
|
+
if (r.y <= r.targetY) {
|
|
1413
|
+
explode(r);
|
|
1414
|
+
rockets.splice(i, 1);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
ctx.lineWidth = 1.8;
|
|
1418
|
+
for (let i = particles.length - 1; i >= 0; i--) {
|
|
1419
|
+
const p = particles[i];
|
|
1420
|
+
p.life -= dt;
|
|
1421
|
+
if (p.life <= 0) {
|
|
1422
|
+
particles.splice(i, 1);
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
p.px = p.x;
|
|
1426
|
+
p.py = p.y;
|
|
1427
|
+
p.vy += GRAVITY * dt;
|
|
1428
|
+
p.vx *= 1 - 1.1 * dt;
|
|
1429
|
+
p.vy *= 1 - .4 * dt;
|
|
1430
|
+
p.x += p.vx * dt;
|
|
1431
|
+
p.y += p.vy * dt;
|
|
1432
|
+
let a = p.life / p.maxLife;
|
|
1433
|
+
if (p.life < p.maxLife * .4 && Math.random() < .35) a *= .3;
|
|
1434
|
+
ctx.globalAlpha = Math.max(0, a);
|
|
1435
|
+
ctx.strokeStyle = p.color;
|
|
1436
|
+
ctx.beginPath();
|
|
1437
|
+
ctx.moveTo(p.px, p.py);
|
|
1438
|
+
ctx.lineTo(p.x, p.y);
|
|
1439
|
+
ctx.stroke();
|
|
1440
|
+
}
|
|
1441
|
+
ctx.globalAlpha = 1;
|
|
1442
|
+
if (elapsed < FIREWORKS_TOTAL_MS || rockets.length > 0 || particles.length > 0) {
|
|
1443
|
+
raf = requestAnimationFrame(step);
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
canvas.remove();
|
|
1447
|
+
};
|
|
1448
|
+
raf = requestAnimationFrame(step);
|
|
1449
|
+
setTimeout(() => {
|
|
1450
|
+
cancelAnimationFrame(raf);
|
|
1451
|
+
canvas.remove();
|
|
1452
|
+
}, 7e3);
|
|
1453
|
+
return true;
|
|
1454
|
+
} catch {}
|
|
1455
|
+
return false;
|
|
1456
|
+
}
|
|
1457
|
+
//#endregion
|
|
1201
1458
|
//#region src/client/styles.ts
|
|
1202
1459
|
/** Package-owned stylesheet text (injected by the client apply, cleaned up on unload). */
|
|
1203
1460
|
const FOCUS_CSS = `
|
|
@@ -1357,9 +1614,13 @@ window.__ModuleLoader__.load({
|
|
|
1357
1614
|
.fm-plugin-card-desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
|
|
1358
1615
|
.fm-plugin-card-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}
|
|
1359
1616
|
.fm-plugin-card-chevron-open{transform:rotate(180deg)}
|
|
1360
|
-
.fm-plugin-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:
|
|
1617
|
+
.fm-plugin-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:16px}
|
|
1361
1618
|
.fm-plugin-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
|
|
1362
1619
|
.fm-plugin-field+.fm-plugin-field{border-top:1px solid var(--dsw-alias-border-l2)}
|
|
1620
|
+
/* The stats row ends the card body, so it carries NO bottom padding of its
|
|
1621
|
+
own — the body's 16px padding-bottom is the entire below-gap, matching the
|
|
1622
|
+
row's 16px above-gap after the divider (symmetric, one rhythm). */
|
|
1623
|
+
.fm-plugin-field-stat{padding:16px 0 0}
|
|
1363
1624
|
.fm-plugin-field-head{align-items:center;gap:8px;display:flex}
|
|
1364
1625
|
.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}
|
|
1365
1626
|
.fm-plugin-field-value{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}
|
|
@@ -1748,7 +2009,9 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
1748
2009
|
const focusStore = {
|
|
1749
2010
|
get: () => focusOn,
|
|
1750
2011
|
set: (v) => {
|
|
1751
|
-
|
|
2012
|
+
const next = !!v;
|
|
2013
|
+
if (next && !focusOn) statsStore.bumpEntries();
|
|
2014
|
+
focusOn = next;
|
|
1752
2015
|
notify();
|
|
1753
2016
|
},
|
|
1754
2017
|
subscribe: (l) => {
|
|
@@ -2875,6 +3138,41 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2875
3138
|
})
|
|
2876
3139
|
] });
|
|
2877
3140
|
}
|
|
3141
|
+
/** The settings card's bottom row: the persisted focus-entry counter, and —
|
|
3142
|
+
* once the count passes the threshold — a nudge to star the repo. */
|
|
3143
|
+
const REPO_URL = "https://github.com/boogoo619/dsh-focus-overlay";
|
|
3144
|
+
function FocusEntryStat({ t }) {
|
|
3145
|
+
const entries = useEntryCount();
|
|
3146
|
+
const starred = useStarred();
|
|
3147
|
+
const star = shouldShowStar(entries);
|
|
3148
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3149
|
+
className: "fm-plugin-field fm-plugin-field-stat",
|
|
3150
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3151
|
+
className: "fm-plugin-field-head",
|
|
3152
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3153
|
+
className: "fm-plugin-field-label",
|
|
3154
|
+
children: t(star ? "settings.entries.cheer" : "settings.entries", { n: entries })
|
|
3155
|
+
}), star ? starred ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3156
|
+
variant: "outline",
|
|
3157
|
+
size: "sm",
|
|
3158
|
+
onClick: () => {
|
|
3159
|
+
try {
|
|
3160
|
+
window.open(REPO_URL, "_blank", "noopener");
|
|
3161
|
+
} catch {}
|
|
3162
|
+
},
|
|
3163
|
+
children: t("settings.entries.repo")
|
|
3164
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3165
|
+
variant: "primary",
|
|
3166
|
+
size: "sm",
|
|
3167
|
+
onClick: () => {
|
|
3168
|
+
statsStore.markStarred();
|
|
3169
|
+
celebrateThenOpen(REPO_URL);
|
|
3170
|
+
},
|
|
3171
|
+
children: t("settings.entries.star")
|
|
3172
|
+
}) : null]
|
|
3173
|
+
})
|
|
3174
|
+
});
|
|
3175
|
+
}
|
|
2878
3176
|
function FocusSettingsCard({ t }) {
|
|
2879
3177
|
const [open, setOpen] = (0, react.useState)(false);
|
|
2880
3178
|
const title = t("settings.label");
|
|
@@ -2896,9 +3194,9 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2896
3194
|
children: t("settings.description")
|
|
2897
3195
|
})]
|
|
2898
3196
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, { className: "fm-plugin-card-chevron" + (open ? " fm-plugin-card-chevron-open" : "") })]
|
|
2899
|
-
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.
|
|
3197
|
+
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2900
3198
|
className: "fm-plugin-card-body",
|
|
2901
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FocusPrefsFields, { t })
|
|
3199
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FocusPrefsFields, { t }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FocusEntryStat, { t })]
|
|
2902
3200
|
}) : null]
|
|
2903
3201
|
});
|
|
2904
3202
|
}
|
|
@@ -3018,6 +3316,10 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
3018
3316
|
"settings.hotkey.hint": "在任意界面按下 F 立即进入专注模式;在输入框内打字不会触发",
|
|
3019
3317
|
"settings.borrow": "使用官方输入框",
|
|
3020
3318
|
"settings.borrow.hint": "专注模式直接借用主界面的官方输入框(支持 / 指令、@ 引用文件与会话);关闭时使用插件内置输入框。官方输入框不可用时自动回退。",
|
|
3319
|
+
"settings.entries": "您已进入专注模式 {n} 次",
|
|
3320
|
+
"settings.entries.cheer": "🎉 您已进入专注模式 {n} 次 ✨",
|
|
3321
|
+
"settings.entries.star": "为这个项目点一个 GitHub Star ⭐",
|
|
3322
|
+
"settings.entries.repo": "前往 GitHub 项目页面",
|
|
3021
3323
|
"process.toolCalls": "{n} 次工具调用",
|
|
3022
3324
|
"process.messages": "{n} 条消息",
|
|
3023
3325
|
"process.subagents": "{n} 个 subagent",
|
|
@@ -3099,6 +3401,10 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
3099
3401
|
"settings.hotkey.hint": "Press F anywhere to enter focus mode instantly; typing in an input never triggers it",
|
|
3100
3402
|
"settings.borrow": "Use the official composer",
|
|
3101
3403
|
"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.",
|
|
3404
|
+
"settings.entries": "You have entered focus mode {n} times",
|
|
3405
|
+
"settings.entries.cheer": "🎉 You have entered focus mode {n} times ✨",
|
|
3406
|
+
"settings.entries.star": "Give this project a GitHub Star ⭐",
|
|
3407
|
+
"settings.entries.repo": "Open the GitHub project page",
|
|
3102
3408
|
"process.toolCalls": "{n} tool calls",
|
|
3103
3409
|
"process.messages": "{n} messages",
|
|
3104
3410
|
"process.subagents": "{n} subagents",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-focus-overlay",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.1",
|
|
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",
|