dsh-focus-overlay 0.12.0 → 0.12.2
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 +503 -185
- 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,254 @@ 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
|
+
}
|
|
851
|
+
//#endregion
|
|
852
|
+
//#region src/client/locales.ts
|
|
853
|
+
/** i18n dictionaries for the `focus` namespace. */
|
|
854
|
+
const zh = {
|
|
855
|
+
"toggle": "专注",
|
|
856
|
+
"toggle.title": "进入专注模式",
|
|
857
|
+
"exit": "退出专注 (Esc)",
|
|
858
|
+
"backToLatest": "回到最新",
|
|
859
|
+
"nav.label": "回合导航",
|
|
860
|
+
"nav.jump": "跳转到第 {n} 回合",
|
|
861
|
+
"nav.turn": "第 {n} 回合",
|
|
862
|
+
"settings.label": "专注模式",
|
|
863
|
+
"settings.description": "全屏阅读遮罩的偏好设置",
|
|
864
|
+
"settings.expand": "展开",
|
|
865
|
+
"settings.collapse": "收起",
|
|
866
|
+
"settings.navbar": "显示右侧轮次导航条",
|
|
867
|
+
"settings.navbar.hint": "在右侧显示导航圆点,每个用户消息(一轮)对应一个,悬停预览、点击跳转",
|
|
868
|
+
"settings.scrollPreserve": "进入专注模式时,同步当前阅读位置",
|
|
869
|
+
"settings.scrollPreserve.hint": "进入时定位到你正在阅读的消息;关闭则定位到你最近一次提问",
|
|
870
|
+
"settings.autoFocus": "AI完成回复后,自动进入专注模式",
|
|
871
|
+
"settings.autoFocus.hint": "AI 正常完成回复后自动进入并定位到你的提问",
|
|
872
|
+
"settings.hotkey": "按 F 键进入专注模式",
|
|
873
|
+
"settings.hotkey.hint": "在任意界面按下 F 立即进入专注模式;在输入框内打字不会触发",
|
|
874
|
+
"settings.borrow": "使用官方输入框",
|
|
875
|
+
"settings.borrow.hint": "专注模式直接借用主界面的官方输入框(支持 / 指令、@ 引用文件与会话);关闭时使用插件内置输入框。官方输入框不可用时自动回退。",
|
|
876
|
+
"settings.entries": "您已进入专注模式 {n} 次",
|
|
877
|
+
"settings.entries.cheer": "🎉 您已进入专注模式 {n} 次 ✨",
|
|
878
|
+
"settings.entries.star": "为这个项目点一个 GitHub Star ⭐",
|
|
879
|
+
"settings.entries.repo": "前往 GitHub 项目页面",
|
|
880
|
+
"process.toolCalls": "{n} 次工具调用",
|
|
881
|
+
"process.messages": "{n} 条消息",
|
|
882
|
+
"process.subagents": "{n} 个 subagent",
|
|
883
|
+
"process.thought": "已思考",
|
|
884
|
+
"process.separator": " · ",
|
|
885
|
+
"process.toolRow": "已完成",
|
|
886
|
+
"empty": "暂无打开的会话",
|
|
887
|
+
"loading": "正在读取会话…",
|
|
888
|
+
"running": "正在工作",
|
|
889
|
+
"reply.ready": "新回复已生成",
|
|
890
|
+
"reply.view": "查看",
|
|
891
|
+
"reply.waiting": "AI 正在等待你的回复",
|
|
892
|
+
"answer.open": "去回答",
|
|
893
|
+
"answer.title": "等待你的回答",
|
|
894
|
+
"answer.approvalTitle": "请求审批",
|
|
895
|
+
"answer.planTitle": "计划待审",
|
|
896
|
+
"answer.submit": "提交回答",
|
|
897
|
+
"answer.close": "收起",
|
|
898
|
+
"answer.custom": "其他答案(可选)",
|
|
899
|
+
"answer.recommended": "推荐",
|
|
900
|
+
"answer.incomplete": "请先回答所有问题",
|
|
901
|
+
"answer.rejected": "提交被拒绝",
|
|
902
|
+
"answer.allow": "允许",
|
|
903
|
+
"answer.reject": "拒绝",
|
|
904
|
+
"composer.placeholder": "输入回复…",
|
|
905
|
+
"composer.send": "发送",
|
|
906
|
+
"markdown.code.copy": "复制代码",
|
|
907
|
+
"markdown.code.copied": "已复制",
|
|
908
|
+
"markdown.footnotes": "脚注",
|
|
909
|
+
"composer.expand": "展开输入框",
|
|
910
|
+
"composer.queued": "已排队 {n}",
|
|
911
|
+
"composer.occurrences": "草稿中包含引用 / 命令,在此编辑可能使其失效,建议退出专注模式编辑",
|
|
912
|
+
"sum.context": "载入了 {n} 项上下文",
|
|
913
|
+
"sum.commands": "运行了 {n} 个命令",
|
|
914
|
+
"sum.edits": "编辑了 {n} 个文件",
|
|
915
|
+
"sum.searches": "搜索了 {n} 个正则",
|
|
916
|
+
"sum.files": "读取了 {n} 个文件",
|
|
917
|
+
"sum.dirs": "列出了 {n} 个目录",
|
|
918
|
+
"sum.subagents": "fork 了 {n} 个子代理",
|
|
919
|
+
"sum.todos": "更新了待办",
|
|
920
|
+
"sum.goals": "更新了目标",
|
|
921
|
+
"sum.workflows": "运行了 {n} 个工作流",
|
|
922
|
+
"sum.skills": "载入了 {n} 个技能",
|
|
923
|
+
"sum.questions": "问了 {n} 个问题",
|
|
924
|
+
"sum.plans": "计划了 {n} 次",
|
|
925
|
+
"sum.jobs": "后台任务 {n} 个",
|
|
926
|
+
"sum.others": "调用了 {n} 个工具",
|
|
927
|
+
"onboarding.title": "欢迎使用专注模式",
|
|
928
|
+
"onboarding.intro": "dsh-focus-overlay 为 DeepSeek Harness Web 提供全屏专注阅读。",
|
|
929
|
+
"onboarding.features.title": "功能",
|
|
930
|
+
"onboarding.feature.fullscreen": "全屏阅读 —— 隐藏标题栏、输入区与侧栏,纵向空间尽给对话",
|
|
931
|
+
"onboarding.feature.fold": "折叠工具调用 —— 把 AI 的命令、编辑、搜索等步骤折叠成一行摘要",
|
|
932
|
+
"onboarding.feature.navbar": "右侧导航条 —— 按用户消息快速跳转,悬停预览",
|
|
933
|
+
"onboarding.feature.autoFocus": "自动专注与提醒 —— 回复完成自动进入专注,AI 提问 / 等待时提醒",
|
|
934
|
+
"onboarding.feature.hotkey": "快捷键 F —— 任意界面按 F 一键进入专注,可在设置中关闭",
|
|
935
|
+
"onboarding.configure.title": "快速配置",
|
|
936
|
+
"onboarding.laterNote": "这些设置随时可在「设置 → 插件 → 插件配置」中更改。",
|
|
937
|
+
"onboarding.openSettings": "打开设置",
|
|
938
|
+
"onboarding.done": "完成"
|
|
939
|
+
};
|
|
940
|
+
const en = {
|
|
941
|
+
"toggle": "Focus",
|
|
942
|
+
"toggle.title": "Enter focus mode",
|
|
943
|
+
"exit": "Exit focus (Esc)",
|
|
944
|
+
"backToLatest": "Back to latest",
|
|
945
|
+
"nav.label": "Turn navigation",
|
|
946
|
+
"nav.jump": "Jump to turn {n}",
|
|
947
|
+
"nav.turn": "Turn {n}",
|
|
948
|
+
"settings.label": "Focus Mode",
|
|
949
|
+
"settings.description": "Preferences for the full-screen reading overlay",
|
|
950
|
+
"settings.expand": "Expand",
|
|
951
|
+
"settings.collapse": "Collapse",
|
|
952
|
+
"settings.navbar": "Show right-side turn navbar",
|
|
953
|
+
"settings.navbar.hint": "Show one navigation dot per user message (turn) — hover to preview, click to jump",
|
|
954
|
+
"settings.scrollPreserve": "Sync the current reading position when entering focus mode",
|
|
955
|
+
"settings.scrollPreserve.hint": "Open at the message you're reading; off opens at your most recent question",
|
|
956
|
+
"settings.autoFocus": "Auto-enter focus mode when the AI finishes a reply",
|
|
957
|
+
"settings.autoFocus.hint": "Auto-open at your question after a normal reply",
|
|
958
|
+
"settings.hotkey": "Press F to enter focus mode",
|
|
959
|
+
"settings.hotkey.hint": "Press F anywhere to enter focus mode instantly; typing in an input never triggers it",
|
|
960
|
+
"settings.borrow": "Use the official composer",
|
|
961
|
+
"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.",
|
|
962
|
+
"settings.entries": "You have entered focus mode {n} times",
|
|
963
|
+
"settings.entries.cheer": "🎉 You have entered focus mode {n} times ✨",
|
|
964
|
+
"settings.entries.star": "Give this project a GitHub Star ⭐",
|
|
965
|
+
"settings.entries.repo": "Open the GitHub project page",
|
|
966
|
+
"process.toolCalls": "{n} tool calls",
|
|
967
|
+
"process.messages": "{n} messages",
|
|
968
|
+
"process.subagents": "{n} subagents",
|
|
969
|
+
"process.thought": "Thought for a while",
|
|
970
|
+
"process.separator": " · ",
|
|
971
|
+
"process.toolRow": "completed",
|
|
972
|
+
"empty": "No open session",
|
|
973
|
+
"loading": "Reading session…",
|
|
974
|
+
"running": "Working",
|
|
975
|
+
"reply.ready": "New reply ready",
|
|
976
|
+
"reply.view": "View",
|
|
977
|
+
"reply.waiting": "AI is waiting for your reply",
|
|
978
|
+
"answer.open": "Answer",
|
|
979
|
+
"answer.title": "Waiting for your answer",
|
|
980
|
+
"answer.approvalTitle": "Approval requested",
|
|
981
|
+
"answer.planTitle": "Plan review",
|
|
982
|
+
"answer.submit": "Submit answer",
|
|
983
|
+
"answer.close": "Collapse",
|
|
984
|
+
"answer.custom": "Other (optional)",
|
|
985
|
+
"answer.recommended": "Recommended",
|
|
986
|
+
"answer.incomplete": "Answer every question first",
|
|
987
|
+
"answer.rejected": "Submission rejected",
|
|
988
|
+
"answer.allow": "Allow",
|
|
989
|
+
"answer.reject": "Deny",
|
|
990
|
+
"composer.placeholder": "Type a reply…",
|
|
991
|
+
"composer.send": "Send",
|
|
992
|
+
"markdown.code.copy": "Copy code",
|
|
993
|
+
"markdown.code.copied": "Copied",
|
|
994
|
+
"markdown.footnotes": "Footnotes",
|
|
995
|
+
"composer.expand": "Expand input",
|
|
996
|
+
"composer.queued": "Queued {n}",
|
|
997
|
+
"composer.occurrences": "Draft contains references / commands — editing here may break them; edit outside focus mode",
|
|
998
|
+
"sum.context": "loaded {n} context items",
|
|
999
|
+
"sum.commands": "ran {n} commands",
|
|
1000
|
+
"sum.edits": "edited {n} files",
|
|
1001
|
+
"sum.searches": "ran {n} searches",
|
|
1002
|
+
"sum.files": "read {n} files",
|
|
1003
|
+
"sum.dirs": "listed {n} directories",
|
|
1004
|
+
"sum.subagents": "forked {n} subagents",
|
|
1005
|
+
"sum.todos": "updated todos",
|
|
1006
|
+
"sum.goals": "updated goals",
|
|
1007
|
+
"sum.workflows": "ran {n} workflows",
|
|
1008
|
+
"sum.skills": "loaded {n} skills",
|
|
1009
|
+
"sum.questions": "asked {n} questions",
|
|
1010
|
+
"sum.plans": "planned {n} times",
|
|
1011
|
+
"sum.jobs": "{n} background jobs",
|
|
1012
|
+
"sum.others": "called {n} tools",
|
|
1013
|
+
"onboarding.title": "Welcome to Focus Mode",
|
|
1014
|
+
"onboarding.intro": "dsh-focus-overlay adds full-screen reading to the DeepSeek Harness web GUI.",
|
|
1015
|
+
"onboarding.features.title": "Features",
|
|
1016
|
+
"onboarding.feature.fullscreen": "Full-screen reading — hides the header, composer and sidebars",
|
|
1017
|
+
"onboarding.feature.fold": "Folded tool calls — collapses commands, edits and searches into one summary line",
|
|
1018
|
+
"onboarding.feature.navbar": "Right-side navbar — jump between user messages with hover previews",
|
|
1019
|
+
"onboarding.feature.autoFocus": "Auto-focus & reminders — auto-enter after a reply, notify while the AI waits",
|
|
1020
|
+
"onboarding.feature.hotkey": "Hotkey F — press F anywhere to enter focus mode (toggleable in settings)",
|
|
1021
|
+
"onboarding.configure.title": "Quick setup",
|
|
1022
|
+
"onboarding.laterNote": "You can change these anytime in Settings → Plugins → Plugin configuration.",
|
|
1023
|
+
"onboarding.openSettings": "Open settings",
|
|
1024
|
+
"onboarding.done": "Done"
|
|
1025
|
+
};
|
|
1026
|
+
/** Labels the official MarkdownText requires for its code-block copy buttons
|
|
1027
|
+
* and footnotes section — mirrors the official `markdownLabels(t)` shape.
|
|
1028
|
+
* Lives here (not FocusView) so both the transcript items and the answer
|
|
1029
|
+
* card share it without an import cycle (FocusView imports Composer).
|
|
1030
|
+
* Without it the renderer throws `Cannot read properties of undefined
|
|
1031
|
+
* (reading 'code')` on the first fenced code block. */
|
|
1032
|
+
function markdownLabels(t) {
|
|
1033
|
+
return {
|
|
1034
|
+
code: {
|
|
1035
|
+
copyLabel: t("markdown.code.copy"),
|
|
1036
|
+
copiedLabel: t("markdown.code.copied")
|
|
1037
|
+
},
|
|
1038
|
+
footnotes: t("markdown.footnotes")
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
760
1041
|
//#endregion
|
|
761
1042
|
//#region src/client/Composer.tsx
|
|
762
1043
|
/**
|
|
@@ -854,6 +1135,8 @@ window.__ModuleLoader__.load({
|
|
|
854
1135
|
const { t, width, wait, onClose } = props;
|
|
855
1136
|
const isQuestion = !(wait.kind === "approval");
|
|
856
1137
|
const questions = isQuestion && Array.isArray(wait.questions) ? wait.questions : [];
|
|
1138
|
+
const isPlanReview = questions.some((q) => q && q.intent && q.intent.kind === "plan-review");
|
|
1139
|
+
const mdLabels = (0, react.useMemo)(() => markdownLabels(t), [t]);
|
|
857
1140
|
const [drafts, setDrafts] = (0, react.useState)({});
|
|
858
1141
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
859
1142
|
const [error, setError] = (0, react.useState)(null);
|
|
@@ -929,7 +1212,7 @@ window.__ModuleLoader__.load({
|
|
|
929
1212
|
children: t("reply.waiting")
|
|
930
1213
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
931
1214
|
className: "fm-card-title",
|
|
932
|
-
children: t("answer.title")
|
|
1215
|
+
children: isPlanReview ? t("answer.planTitle") : t("answer.title")
|
|
933
1216
|
})]
|
|
934
1217
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
935
1218
|
type: "button",
|
|
@@ -973,7 +1256,10 @@ window.__ModuleLoader__.load({
|
|
|
973
1256
|
}),
|
|
974
1257
|
q.detail ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
975
1258
|
className: "fm-card-detail",
|
|
976
|
-
children:
|
|
1259
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MarkdownText, {
|
|
1260
|
+
text: q.detail,
|
|
1261
|
+
labels: mdLabels
|
|
1262
|
+
})
|
|
977
1263
|
}) : null,
|
|
978
1264
|
q.options && q.options.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
979
1265
|
className: "fm-card-opts",
|
|
@@ -1198,6 +1484,172 @@ window.__ModuleLoader__.load({
|
|
|
1198
1484
|
});
|
|
1199
1485
|
}
|
|
1200
1486
|
//#endregion
|
|
1487
|
+
//#region src/client/celebrate.ts
|
|
1488
|
+
/**
|
|
1489
|
+
* Firework for the Star button: a short, self-contained canvas animation
|
|
1490
|
+
* appended to document.body with pointer-events:none and removed when done,
|
|
1491
|
+
* so it can never intercept clicks. No dependencies, no stylesheet injection:
|
|
1492
|
+
* every style is inline, so cleanup is simply removing the element.
|
|
1493
|
+
*
|
|
1494
|
+
* Five staggered bursts of ~90 trailing particles each; the GitHub page opens
|
|
1495
|
+
* only after the show ends (celebrateThenOpen).
|
|
1496
|
+
*/
|
|
1497
|
+
const BURSTS = 5;
|
|
1498
|
+
const BURST_GAP_MS = 280;
|
|
1499
|
+
const PARTICLES_PER_BURST = 90;
|
|
1500
|
+
const GRAVITY = 300;
|
|
1501
|
+
const MAX_LIFE_S = 2;
|
|
1502
|
+
const COLORS = [
|
|
1503
|
+
"#f43f5e",
|
|
1504
|
+
"#fb923c",
|
|
1505
|
+
"#facc15",
|
|
1506
|
+
"#4ade80",
|
|
1507
|
+
"#38bdf8",
|
|
1508
|
+
"#a78bfa",
|
|
1509
|
+
"#f472b6"
|
|
1510
|
+
];
|
|
1511
|
+
const FIREWORKS_TOTAL_MS = 4e3;
|
|
1512
|
+
/** Play the fireworks, then open `url` in a new tab once the sky is clear.
|
|
1513
|
+
* Browsers honor the click's transient activation for a few seconds, so the
|
|
1514
|
+
* delayed open is not popup-blocked in Chrome; stricter browsers fall back
|
|
1515
|
+
* on the repo button beside the counter, which opens the page directly. */
|
|
1516
|
+
function celebrateThenOpen(url) {
|
|
1517
|
+
const launched = launchFireworks();
|
|
1518
|
+
setTimeout(() => {
|
|
1519
|
+
try {
|
|
1520
|
+
window.open(url, "_blank", "noopener");
|
|
1521
|
+
} catch {}
|
|
1522
|
+
}, launched ? FIREWORKS_TOTAL_MS : 400);
|
|
1523
|
+
}
|
|
1524
|
+
/** Launch the bursts. Returns false when nothing was animated (reduced-motion
|
|
1525
|
+
* preference, an existing show, or an unavailable 2D context). */
|
|
1526
|
+
function launchFireworks() {
|
|
1527
|
+
try {
|
|
1528
|
+
if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return false;
|
|
1529
|
+
if (document.querySelector("[data-fm-fireworks]")) return false;
|
|
1530
|
+
const canvas = document.createElement("canvas");
|
|
1531
|
+
canvas.setAttribute("data-fm-fireworks", "");
|
|
1532
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
1533
|
+
const w = window.innerWidth;
|
|
1534
|
+
const h = window.innerHeight;
|
|
1535
|
+
canvas.width = Math.max(1, Math.floor(w * dpr));
|
|
1536
|
+
canvas.height = Math.max(1, Math.floor(h * dpr));
|
|
1537
|
+
canvas.style.cssText = "position:fixed;inset:0;width:100vw;height:100vh;pointer-events:none;z-index:2147483000";
|
|
1538
|
+
document.body.appendChild(canvas);
|
|
1539
|
+
const ctx = canvas.getContext("2d");
|
|
1540
|
+
if (!ctx) {
|
|
1541
|
+
canvas.remove();
|
|
1542
|
+
return false;
|
|
1543
|
+
}
|
|
1544
|
+
ctx.scale(dpr, dpr);
|
|
1545
|
+
const rand = (a, b) => a + Math.random() * (b - a);
|
|
1546
|
+
const rockets = [];
|
|
1547
|
+
const particles = [];
|
|
1548
|
+
const spawnRocket = (i) => {
|
|
1549
|
+
rockets.push({
|
|
1550
|
+
x: w * (.22 + .14 * i) + rand(-30, 30),
|
|
1551
|
+
y: h + 8,
|
|
1552
|
+
vy: -h * rand(1.05, 1.25),
|
|
1553
|
+
targetY: h * rand(.14, .36),
|
|
1554
|
+
color: COLORS[i % COLORS.length]
|
|
1555
|
+
});
|
|
1556
|
+
};
|
|
1557
|
+
const start = performance.now();
|
|
1558
|
+
let spawned = 0;
|
|
1559
|
+
const explode = (r) => {
|
|
1560
|
+
for (let k = 0; k < PARTICLES_PER_BURST; k++) {
|
|
1561
|
+
const angle = Math.PI * 2 * k / PARTICLES_PER_BURST + rand(-.07, .07);
|
|
1562
|
+
const speed = rand(80, 430) * (k % 4 === 0 ? .55 : 1);
|
|
1563
|
+
const maxLife = rand(1.2, MAX_LIFE_S);
|
|
1564
|
+
particles.push({
|
|
1565
|
+
x: r.x,
|
|
1566
|
+
y: r.y,
|
|
1567
|
+
px: r.x,
|
|
1568
|
+
py: r.y,
|
|
1569
|
+
vx: Math.cos(angle) * speed,
|
|
1570
|
+
vy: Math.sin(angle) * speed,
|
|
1571
|
+
life: maxLife,
|
|
1572
|
+
maxLife,
|
|
1573
|
+
color: Math.random() < .25 ? "#ffffff" : r.color
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
};
|
|
1577
|
+
let prev = performance.now();
|
|
1578
|
+
let raf = 0;
|
|
1579
|
+
const step = (now) => {
|
|
1580
|
+
const dt = Math.min((now - prev) / 1e3, .05);
|
|
1581
|
+
prev = now;
|
|
1582
|
+
ctx.clearRect(0, 0, w, h);
|
|
1583
|
+
ctx.globalCompositeOperation = "lighter";
|
|
1584
|
+
const elapsed = now - start;
|
|
1585
|
+
while (spawned < BURSTS && elapsed >= spawned * BURST_GAP_MS) spawnRocket(spawned++);
|
|
1586
|
+
for (let i = rockets.length - 1; i >= 0; i--) {
|
|
1587
|
+
const r = rockets[i];
|
|
1588
|
+
r.y += r.vy * dt;
|
|
1589
|
+
ctx.globalAlpha = .9;
|
|
1590
|
+
ctx.strokeStyle = r.color;
|
|
1591
|
+
ctx.lineWidth = 2.4;
|
|
1592
|
+
ctx.beginPath();
|
|
1593
|
+
ctx.moveTo(r.x, r.y + 9);
|
|
1594
|
+
ctx.lineTo(r.x, r.y);
|
|
1595
|
+
ctx.stroke();
|
|
1596
|
+
if (Math.random() < .6) particles.push({
|
|
1597
|
+
x: r.x + rand(-2, 2),
|
|
1598
|
+
y: r.y + rand(0, 6),
|
|
1599
|
+
px: r.x,
|
|
1600
|
+
py: r.y,
|
|
1601
|
+
vx: rand(-30, 30),
|
|
1602
|
+
vy: rand(10, 70),
|
|
1603
|
+
life: rand(.25, .5),
|
|
1604
|
+
maxLife: .5,
|
|
1605
|
+
color: r.color
|
|
1606
|
+
});
|
|
1607
|
+
if (r.y <= r.targetY) {
|
|
1608
|
+
explode(r);
|
|
1609
|
+
rockets.splice(i, 1);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
ctx.lineWidth = 1.8;
|
|
1613
|
+
for (let i = particles.length - 1; i >= 0; i--) {
|
|
1614
|
+
const p = particles[i];
|
|
1615
|
+
p.life -= dt;
|
|
1616
|
+
if (p.life <= 0) {
|
|
1617
|
+
particles.splice(i, 1);
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
p.px = p.x;
|
|
1621
|
+
p.py = p.y;
|
|
1622
|
+
p.vy += GRAVITY * dt;
|
|
1623
|
+
p.vx *= 1 - 1.1 * dt;
|
|
1624
|
+
p.vy *= 1 - .4 * dt;
|
|
1625
|
+
p.x += p.vx * dt;
|
|
1626
|
+
p.y += p.vy * dt;
|
|
1627
|
+
let a = p.life / p.maxLife;
|
|
1628
|
+
if (p.life < p.maxLife * .4 && Math.random() < .35) a *= .3;
|
|
1629
|
+
ctx.globalAlpha = Math.max(0, a);
|
|
1630
|
+
ctx.strokeStyle = p.color;
|
|
1631
|
+
ctx.beginPath();
|
|
1632
|
+
ctx.moveTo(p.px, p.py);
|
|
1633
|
+
ctx.lineTo(p.x, p.y);
|
|
1634
|
+
ctx.stroke();
|
|
1635
|
+
}
|
|
1636
|
+
ctx.globalAlpha = 1;
|
|
1637
|
+
if (elapsed < FIREWORKS_TOTAL_MS || rockets.length > 0 || particles.length > 0) {
|
|
1638
|
+
raf = requestAnimationFrame(step);
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
canvas.remove();
|
|
1642
|
+
};
|
|
1643
|
+
raf = requestAnimationFrame(step);
|
|
1644
|
+
setTimeout(() => {
|
|
1645
|
+
cancelAnimationFrame(raf);
|
|
1646
|
+
canvas.remove();
|
|
1647
|
+
}, 7e3);
|
|
1648
|
+
return true;
|
|
1649
|
+
} catch {}
|
|
1650
|
+
return false;
|
|
1651
|
+
}
|
|
1652
|
+
//#endregion
|
|
1201
1653
|
//#region src/client/styles.ts
|
|
1202
1654
|
/** Package-owned stylesheet text (injected by the client apply, cleaned up on unload). */
|
|
1203
1655
|
const FOCUS_CSS = `
|
|
@@ -1323,7 +1775,10 @@ window.__ModuleLoader__.load({
|
|
|
1323
1775
|
.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))}
|
|
1324
1776
|
.fm-card-q{display:flex;flex-direction:column}
|
|
1325
1777
|
.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))}
|
|
1326
|
-
.fm-card-detail{color:var(--dsw-alias-label-secondary,#888);margin:0 2px 8px;padding:0 22px;font
|
|
1778
|
+
.fm-card-detail{color:var(--dsw-alias-label-secondary,#888);margin:0 2px 8px;padding:0 22px;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))}
|
|
1779
|
+
.fm-card-detail>*:first-child{margin-top:0}
|
|
1780
|
+
.fm-card-detail>*:last-child{margin-bottom:0}
|
|
1781
|
+
.fm-card-detail pre{overflow-x:auto}
|
|
1327
1782
|
.fm-card-opts{display:flex;flex-direction:column;gap:1px;margin:8px 0 0;padding:4px 12px}
|
|
1328
1783
|
.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}
|
|
1329
1784
|
.fm-opt:hover:not(:disabled),.fm-opt-on{background:var(--dsw-alias-interactive-bg-hover,rgba(0,0,0,.06))}
|
|
@@ -1357,9 +1812,13 @@ window.__ModuleLoader__.load({
|
|
|
1357
1812
|
.fm-plugin-card-desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
|
|
1358
1813
|
.fm-plugin-card-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}
|
|
1359
1814
|
.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:
|
|
1815
|
+
.fm-plugin-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:16px}
|
|
1361
1816
|
.fm-plugin-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
|
|
1362
1817
|
.fm-plugin-field+.fm-plugin-field{border-top:1px solid var(--dsw-alias-border-l2)}
|
|
1818
|
+
/* The stats row ends the card body, so it carries NO bottom padding of its
|
|
1819
|
+
own — the body's 16px padding-bottom is the entire below-gap, matching the
|
|
1820
|
+
row's 16px above-gap after the divider (symmetric, one rhythm). */
|
|
1821
|
+
.fm-plugin-field-stat{padding:16px 0 0}
|
|
1363
1822
|
.fm-plugin-field-head{align-items:center;gap:8px;display:flex}
|
|
1364
1823
|
.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
1824
|
.fm-plugin-field-value{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}
|
|
@@ -1748,7 +2207,9 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
1748
2207
|
const focusStore = {
|
|
1749
2208
|
get: () => focusOn,
|
|
1750
2209
|
set: (v) => {
|
|
1751
|
-
|
|
2210
|
+
const next = !!v;
|
|
2211
|
+
if (next && !focusOn) statsStore.bumpEntries();
|
|
2212
|
+
focusOn = next;
|
|
1752
2213
|
notify();
|
|
1753
2214
|
},
|
|
1754
2215
|
subscribe: (l) => {
|
|
@@ -1928,19 +2389,6 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
1928
2389
|
alt: ""
|
|
1929
2390
|
});
|
|
1930
2391
|
}
|
|
1931
|
-
/** Labels the official MarkdownText requires for its code-block copy buttons
|
|
1932
|
-
* and footnotes section — mirrors the official `markdownLabels(t)` shape.
|
|
1933
|
-
* Without it the renderer throws `Cannot read properties of undefined
|
|
1934
|
-
* (reading 'code')` on the first fenced code block. */
|
|
1935
|
-
function markdownLabels(t) {
|
|
1936
|
-
return {
|
|
1937
|
-
code: {
|
|
1938
|
-
copyLabel: t("markdown.code.copy"),
|
|
1939
|
-
copiedLabel: t("markdown.code.copied")
|
|
1940
|
-
},
|
|
1941
|
-
footnotes: t("markdown.footnotes")
|
|
1942
|
-
};
|
|
1943
|
-
}
|
|
1944
2392
|
function AssistantItem(props) {
|
|
1945
2393
|
const { blocks, loadImage, fileMentions, labels } = props;
|
|
1946
2394
|
const text = (blocks || []).filter((b) => b && b.kind === "text").map((b) => b.text).join("");
|
|
@@ -2875,6 +3323,41 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2875
3323
|
})
|
|
2876
3324
|
] });
|
|
2877
3325
|
}
|
|
3326
|
+
/** The settings card's bottom row: the persisted focus-entry counter, and —
|
|
3327
|
+
* once the count passes the threshold — a nudge to star the repo. */
|
|
3328
|
+
const REPO_URL = "https://github.com/boogoo619/dsh-focus-overlay";
|
|
3329
|
+
function FocusEntryStat({ t }) {
|
|
3330
|
+
const entries = useEntryCount();
|
|
3331
|
+
const starred = useStarred();
|
|
3332
|
+
const star = shouldShowStar(entries);
|
|
3333
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3334
|
+
className: "fm-plugin-field fm-plugin-field-stat",
|
|
3335
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3336
|
+
className: "fm-plugin-field-head",
|
|
3337
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
3338
|
+
className: "fm-plugin-field-label",
|
|
3339
|
+
children: t(star ? "settings.entries.cheer" : "settings.entries", { n: entries })
|
|
3340
|
+
}), star ? starred ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3341
|
+
variant: "outline",
|
|
3342
|
+
size: "sm",
|
|
3343
|
+
onClick: () => {
|
|
3344
|
+
try {
|
|
3345
|
+
window.open(REPO_URL, "_blank", "noopener");
|
|
3346
|
+
} catch {}
|
|
3347
|
+
},
|
|
3348
|
+
children: t("settings.entries.repo")
|
|
3349
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
|
|
3350
|
+
variant: "primary",
|
|
3351
|
+
size: "sm",
|
|
3352
|
+
onClick: () => {
|
|
3353
|
+
statsStore.markStarred();
|
|
3354
|
+
celebrateThenOpen(REPO_URL);
|
|
3355
|
+
},
|
|
3356
|
+
children: t("settings.entries.star")
|
|
3357
|
+
}) : null]
|
|
3358
|
+
})
|
|
3359
|
+
});
|
|
3360
|
+
}
|
|
2878
3361
|
function FocusSettingsCard({ t }) {
|
|
2879
3362
|
const [open, setOpen] = (0, react.useState)(false);
|
|
2880
3363
|
const title = t("settings.label");
|
|
@@ -2896,9 +3379,9 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2896
3379
|
children: t("settings.description")
|
|
2897
3380
|
})]
|
|
2898
3381
|
}), /* @__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.
|
|
3382
|
+
}), open ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2900
3383
|
className: "fm-plugin-card-body",
|
|
2901
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FocusPrefsFields, { t })
|
|
3384
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FocusPrefsFields, { t }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FocusEntryStat, { t })]
|
|
2902
3385
|
}) : null]
|
|
2903
3386
|
});
|
|
2904
3387
|
}
|
|
@@ -2994,171 +3477,6 @@ body[data-fm-focus] #root :has(> [data-slot="conversation"]){margin-bottom:0!imp
|
|
|
2994
3477
|
});
|
|
2995
3478
|
}
|
|
2996
3479
|
//#endregion
|
|
2997
|
-
//#region src/client/locales.ts
|
|
2998
|
-
/** i18n dictionaries for the `focus` namespace. */
|
|
2999
|
-
const zh = {
|
|
3000
|
-
"toggle": "专注",
|
|
3001
|
-
"toggle.title": "进入专注模式",
|
|
3002
|
-
"exit": "退出专注 (Esc)",
|
|
3003
|
-
"backToLatest": "回到最新",
|
|
3004
|
-
"nav.label": "回合导航",
|
|
3005
|
-
"nav.jump": "跳转到第 {n} 回合",
|
|
3006
|
-
"nav.turn": "第 {n} 回合",
|
|
3007
|
-
"settings.label": "专注模式",
|
|
3008
|
-
"settings.description": "全屏阅读遮罩的偏好设置",
|
|
3009
|
-
"settings.expand": "展开",
|
|
3010
|
-
"settings.collapse": "收起",
|
|
3011
|
-
"settings.navbar": "显示右侧轮次导航条",
|
|
3012
|
-
"settings.navbar.hint": "在右侧显示导航圆点,每个用户消息(一轮)对应一个,悬停预览、点击跳转",
|
|
3013
|
-
"settings.scrollPreserve": "进入专注模式时,同步当前阅读位置",
|
|
3014
|
-
"settings.scrollPreserve.hint": "进入时定位到你正在阅读的消息;关闭则定位到你最近一次提问",
|
|
3015
|
-
"settings.autoFocus": "AI完成回复后,自动进入专注模式",
|
|
3016
|
-
"settings.autoFocus.hint": "AI 正常完成回复后自动进入并定位到你的提问",
|
|
3017
|
-
"settings.hotkey": "按 F 键进入专注模式",
|
|
3018
|
-
"settings.hotkey.hint": "在任意界面按下 F 立即进入专注模式;在输入框内打字不会触发",
|
|
3019
|
-
"settings.borrow": "使用官方输入框",
|
|
3020
|
-
"settings.borrow.hint": "专注模式直接借用主界面的官方输入框(支持 / 指令、@ 引用文件与会话);关闭时使用插件内置输入框。官方输入框不可用时自动回退。",
|
|
3021
|
-
"process.toolCalls": "{n} 次工具调用",
|
|
3022
|
-
"process.messages": "{n} 条消息",
|
|
3023
|
-
"process.subagents": "{n} 个 subagent",
|
|
3024
|
-
"process.thought": "已思考",
|
|
3025
|
-
"process.separator": " · ",
|
|
3026
|
-
"process.toolRow": "已完成",
|
|
3027
|
-
"empty": "暂无打开的会话",
|
|
3028
|
-
"loading": "正在读取会话…",
|
|
3029
|
-
"running": "正在工作",
|
|
3030
|
-
"reply.ready": "新回复已生成",
|
|
3031
|
-
"reply.view": "查看",
|
|
3032
|
-
"reply.waiting": "AI 正在等待你的回复",
|
|
3033
|
-
"answer.open": "去回答",
|
|
3034
|
-
"answer.title": "等待你的回答",
|
|
3035
|
-
"answer.approvalTitle": "请求审批",
|
|
3036
|
-
"answer.submit": "提交回答",
|
|
3037
|
-
"answer.close": "收起",
|
|
3038
|
-
"answer.custom": "其他答案(可选)",
|
|
3039
|
-
"answer.recommended": "推荐",
|
|
3040
|
-
"answer.incomplete": "请先回答所有问题",
|
|
3041
|
-
"answer.rejected": "提交被拒绝",
|
|
3042
|
-
"answer.allow": "允许",
|
|
3043
|
-
"answer.reject": "拒绝",
|
|
3044
|
-
"composer.placeholder": "输入回复…",
|
|
3045
|
-
"composer.send": "发送",
|
|
3046
|
-
"markdown.code.copy": "复制代码",
|
|
3047
|
-
"markdown.code.copied": "已复制",
|
|
3048
|
-
"markdown.footnotes": "脚注",
|
|
3049
|
-
"composer.expand": "展开输入框",
|
|
3050
|
-
"composer.queued": "已排队 {n}",
|
|
3051
|
-
"composer.occurrences": "草稿中包含引用 / 命令,在此编辑可能使其失效,建议退出专注模式编辑",
|
|
3052
|
-
"sum.context": "载入了 {n} 项上下文",
|
|
3053
|
-
"sum.commands": "运行了 {n} 个命令",
|
|
3054
|
-
"sum.edits": "编辑了 {n} 个文件",
|
|
3055
|
-
"sum.searches": "搜索了 {n} 个正则",
|
|
3056
|
-
"sum.files": "读取了 {n} 个文件",
|
|
3057
|
-
"sum.dirs": "列出了 {n} 个目录",
|
|
3058
|
-
"sum.subagents": "fork 了 {n} 个子代理",
|
|
3059
|
-
"sum.todos": "更新了待办",
|
|
3060
|
-
"sum.goals": "更新了目标",
|
|
3061
|
-
"sum.workflows": "运行了 {n} 个工作流",
|
|
3062
|
-
"sum.skills": "载入了 {n} 个技能",
|
|
3063
|
-
"sum.questions": "问了 {n} 个问题",
|
|
3064
|
-
"sum.plans": "计划了 {n} 次",
|
|
3065
|
-
"sum.jobs": "后台任务 {n} 个",
|
|
3066
|
-
"sum.others": "调用了 {n} 个工具",
|
|
3067
|
-
"onboarding.title": "欢迎使用专注模式",
|
|
3068
|
-
"onboarding.intro": "dsh-focus-overlay 为 DeepSeek Harness Web 提供全屏专注阅读。",
|
|
3069
|
-
"onboarding.features.title": "功能",
|
|
3070
|
-
"onboarding.feature.fullscreen": "全屏阅读 —— 隐藏标题栏、输入区与侧栏,纵向空间尽给对话",
|
|
3071
|
-
"onboarding.feature.fold": "折叠工具调用 —— 把 AI 的命令、编辑、搜索等步骤折叠成一行摘要",
|
|
3072
|
-
"onboarding.feature.navbar": "右侧导航条 —— 按用户消息快速跳转,悬停预览",
|
|
3073
|
-
"onboarding.feature.autoFocus": "自动专注与提醒 —— 回复完成自动进入专注,AI 提问 / 等待时提醒",
|
|
3074
|
-
"onboarding.feature.hotkey": "快捷键 F —— 任意界面按 F 一键进入专注,可在设置中关闭",
|
|
3075
|
-
"onboarding.configure.title": "快速配置",
|
|
3076
|
-
"onboarding.laterNote": "这些设置随时可在「设置 → 插件 → 插件配置」中更改。",
|
|
3077
|
-
"onboarding.openSettings": "打开设置",
|
|
3078
|
-
"onboarding.done": "完成"
|
|
3079
|
-
};
|
|
3080
|
-
const en = {
|
|
3081
|
-
"toggle": "Focus",
|
|
3082
|
-
"toggle.title": "Enter focus mode",
|
|
3083
|
-
"exit": "Exit focus (Esc)",
|
|
3084
|
-
"backToLatest": "Back to latest",
|
|
3085
|
-
"nav.label": "Turn navigation",
|
|
3086
|
-
"nav.jump": "Jump to turn {n}",
|
|
3087
|
-
"nav.turn": "Turn {n}",
|
|
3088
|
-
"settings.label": "Focus Mode",
|
|
3089
|
-
"settings.description": "Preferences for the full-screen reading overlay",
|
|
3090
|
-
"settings.expand": "Expand",
|
|
3091
|
-
"settings.collapse": "Collapse",
|
|
3092
|
-
"settings.navbar": "Show right-side turn navbar",
|
|
3093
|
-
"settings.navbar.hint": "Show one navigation dot per user message (turn) — hover to preview, click to jump",
|
|
3094
|
-
"settings.scrollPreserve": "Sync the current reading position when entering focus mode",
|
|
3095
|
-
"settings.scrollPreserve.hint": "Open at the message you're reading; off opens at your most recent question",
|
|
3096
|
-
"settings.autoFocus": "Auto-enter focus mode when the AI finishes a reply",
|
|
3097
|
-
"settings.autoFocus.hint": "Auto-open at your question after a normal reply",
|
|
3098
|
-
"settings.hotkey": "Press F to enter focus mode",
|
|
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.",
|
|
3102
|
-
"process.toolCalls": "{n} tool calls",
|
|
3103
|
-
"process.messages": "{n} messages",
|
|
3104
|
-
"process.subagents": "{n} subagents",
|
|
3105
|
-
"process.thought": "Thought for a while",
|
|
3106
|
-
"process.separator": " · ",
|
|
3107
|
-
"process.toolRow": "completed",
|
|
3108
|
-
"empty": "No open session",
|
|
3109
|
-
"loading": "Reading session…",
|
|
3110
|
-
"running": "Working",
|
|
3111
|
-
"reply.ready": "New reply ready",
|
|
3112
|
-
"reply.view": "View",
|
|
3113
|
-
"reply.waiting": "AI is waiting for your reply",
|
|
3114
|
-
"answer.open": "Answer",
|
|
3115
|
-
"answer.title": "Waiting for your answer",
|
|
3116
|
-
"answer.approvalTitle": "Approval requested",
|
|
3117
|
-
"answer.submit": "Submit answer",
|
|
3118
|
-
"answer.close": "Collapse",
|
|
3119
|
-
"answer.custom": "Other (optional)",
|
|
3120
|
-
"answer.recommended": "Recommended",
|
|
3121
|
-
"answer.incomplete": "Answer every question first",
|
|
3122
|
-
"answer.rejected": "Submission rejected",
|
|
3123
|
-
"answer.allow": "Allow",
|
|
3124
|
-
"answer.reject": "Deny",
|
|
3125
|
-
"composer.placeholder": "Type a reply…",
|
|
3126
|
-
"composer.send": "Send",
|
|
3127
|
-
"markdown.code.copy": "Copy code",
|
|
3128
|
-
"markdown.code.copied": "Copied",
|
|
3129
|
-
"markdown.footnotes": "Footnotes",
|
|
3130
|
-
"composer.expand": "Expand input",
|
|
3131
|
-
"composer.queued": "Queued {n}",
|
|
3132
|
-
"composer.occurrences": "Draft contains references / commands — editing here may break them; edit outside focus mode",
|
|
3133
|
-
"sum.context": "loaded {n} context items",
|
|
3134
|
-
"sum.commands": "ran {n} commands",
|
|
3135
|
-
"sum.edits": "edited {n} files",
|
|
3136
|
-
"sum.searches": "ran {n} searches",
|
|
3137
|
-
"sum.files": "read {n} files",
|
|
3138
|
-
"sum.dirs": "listed {n} directories",
|
|
3139
|
-
"sum.subagents": "forked {n} subagents",
|
|
3140
|
-
"sum.todos": "updated todos",
|
|
3141
|
-
"sum.goals": "updated goals",
|
|
3142
|
-
"sum.workflows": "ran {n} workflows",
|
|
3143
|
-
"sum.skills": "loaded {n} skills",
|
|
3144
|
-
"sum.questions": "asked {n} questions",
|
|
3145
|
-
"sum.plans": "planned {n} times",
|
|
3146
|
-
"sum.jobs": "{n} background jobs",
|
|
3147
|
-
"sum.others": "called {n} tools",
|
|
3148
|
-
"onboarding.title": "Welcome to Focus Mode",
|
|
3149
|
-
"onboarding.intro": "dsh-focus-overlay adds full-screen reading to the DeepSeek Harness web GUI.",
|
|
3150
|
-
"onboarding.features.title": "Features",
|
|
3151
|
-
"onboarding.feature.fullscreen": "Full-screen reading — hides the header, composer and sidebars",
|
|
3152
|
-
"onboarding.feature.fold": "Folded tool calls — collapses commands, edits and searches into one summary line",
|
|
3153
|
-
"onboarding.feature.navbar": "Right-side navbar — jump between user messages with hover previews",
|
|
3154
|
-
"onboarding.feature.autoFocus": "Auto-focus & reminders — auto-enter after a reply, notify while the AI waits",
|
|
3155
|
-
"onboarding.feature.hotkey": "Hotkey F — press F anywhere to enter focus mode (toggleable in settings)",
|
|
3156
|
-
"onboarding.configure.title": "Quick setup",
|
|
3157
|
-
"onboarding.laterNote": "You can change these anytime in Settings → Plugins → Plugin configuration.",
|
|
3158
|
-
"onboarding.openSettings": "Open settings",
|
|
3159
|
-
"onboarding.done": "Done"
|
|
3160
|
-
};
|
|
3161
|
-
//#endregion
|
|
3162
3480
|
//#region src/client/index.ts
|
|
3163
3481
|
/**
|
|
3164
3482
|
* Focus Mode, browser half: a full-screen reading overlay in `shell.overlay`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-focus-overlay",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
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",
|