@johnnren/dsh-zen 1.0.0 → 1.1.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/README.md CHANGED
@@ -78,11 +78,24 @@ The settings panel also shows your **今日禅 / 本周禅** scores and the curr
78
78
  npm install
79
79
  npm run build # bundle src/client.ts + src/index.ts → lib/, deploy into web profile
80
80
  npm run typecheck # tsc --noEmit against the local shims in src/dts-shim.d.ts
81
+ npm run e2e:zen # GUI probe against a disposable dsh-e2e instance (see e2e/)
81
82
  ```
82
83
 
83
84
  The deploy target is `$DSH_HOME/profiles/web/node_modules` (default `~/.dsh`). Client-only
84
85
  changes need a page refresh; host-side changes need a DSH restart.
85
86
 
87
+ The GUI probe needs a disposable instance started with the provider layer in
88
+ `e2e/profile/e2e-profile.patch.yml`, otherwise agent turns fail with `NO_ADAPTER` and the
89
+ live Chat surfaces cannot be exercised:
90
+
91
+ ```sh
92
+ export CPA_API_KEY=... # cpa gateway key (see ~/.config/api-vault)
93
+ export DSH_E2E_PLAYWRIGHT_CORE=<path to an installed playwright-core entry>
94
+ dsh-e2e start --patch e2e/profile/e2e-profile.patch.yml --wait-ready
95
+ npm run e2e:zen
96
+ dsh-e2e stop
97
+ ```
98
+
86
99
  ### Repository layout
87
100
 
88
101
  ```
@@ -103,8 +116,15 @@ changes need a page refresh; host-side changes need a DSH restart.
103
116
  ## Compatibility
104
117
 
105
118
  - Requires DeepSeek Harness with the Web client module system (default `web` profile).
119
+ - **Verified against DeepSeek Harness `0.2.0-rc.2`** (see
120
+ [docs/issues/261011-dsh-02-compat](docs/issues/261011-dsh-02-compat/261011-dsh-02-compat.summary.md)).
106
121
  - Slot contracts used: `conversation.view`, `conversation.session.header.actions`,
107
- `settings.section`. Verify against your Harness version's source snapshot if upgrading.
122
+ `settings.section`.
123
+ - Client services used: `slots`, `sessions`, `locale`, `uiConversation`.
124
+ - The Zen view reads the Chat target snapshot through
125
+ `ctx.uiConversation.binding(sessionId).target("chat")`; the current Session is derived from
126
+ `sessions.list` main-view retention. Both are 0.2-era contracts — re-verify them against the
127
+ target version's source when upgrading.
108
128
  - **Zero permissions**: no network requests, no file system access, no external
109
129
  services — the host face is an empty `apply()` and all data stays in browser
110
130
  `localStorage` (`dsh.zen.*`). 无网络请求、无文件写入、无外部服务、无遥测。
@@ -121,3 +141,7 @@ can be cleared by deleting keys prefixed `dsh.zen.`.
121
141
  ## License
122
142
 
123
143
  MIT — see [LICENSE](LICENSE).
144
+
145
+ ## Community
146
+
147
+ - Thanks to the [LINUX DO](https://linux.do/) community for providing the discussion environment.
package/README.zh-CN.md CHANGED
@@ -70,11 +70,23 @@ dsh plugin --profile web add @johnnren/dsh-zen
70
70
  npm install
71
71
  npm run build # 打包 src/client.ts + src/index.ts → lib/,部署到 web profile
72
72
  npm run typecheck # 基于 src/dts-shim.d.ts 本地 shim 做 tsc --noEmit
73
+ npm run e2e:zen # 对一次性 dsh-e2e 实例跑 GUI 探针(见 e2e/)
73
74
  ```
74
75
 
75
76
  部署目标为 `$DSH_HOME/profiles/web/node_modules`(默认 `~/.dsh`)。仅客户端改动刷新页面即可;
76
77
  host 侧改动需要重启 DSH。
77
78
 
79
+ GUI 探针需要带 provider 层的一次性实例,否则 agent 轮次会以 `NO_ADAPTER` 失败,Zen 的实时
80
+ Chat 面无法被覆盖:
81
+
82
+ ```sh
83
+ export CPA_API_KEY=... # cpa 网关 key(见 ~/.config/api-vault)
84
+ export DSH_E2E_PLAYWRIGHT_CORE=<已安装的 playwright-core 入口路径>
85
+ dsh-e2e start --patch e2e/profile/e2e-profile.patch.yml --wait-ready
86
+ npm run e2e:zen
87
+ dsh-e2e stop
88
+ ```
89
+
78
90
  ### 仓库结构
79
91
 
80
92
  ```
@@ -95,8 +107,14 @@ host 侧改动需要重启 DSH。
95
107
  ## 兼容性
96
108
 
97
109
  - 需要带 Web 客户端模块系统的 DeepSeek Harness(默认 `web` profile)。
110
+ - **已在 DeepSeek Harness `0.2.0-rc.2` 上实测通过**(见
111
+ [docs/issues/261011-dsh-02-compat](docs/issues/261011-dsh-02-compat/261011-dsh-02-compat.summary.md))。
98
112
  - 使用的 slot 契约:`conversation.view`、`conversation.session.header.actions`、
99
- `settings.section`。升级 Harness 版本时请对照对应源码快照核验。
113
+ `settings.section`。
114
+ - 使用的客户端服务:`slots`、`sessions`、`locale`、`uiConversation`。
115
+ - Zen 视图通过 `ctx.uiConversation.binding(sessionId).target("chat")` 读取 Chat 目标快照;
116
+ 当前会话由 `sessions.list` 的 main-view 引用推导。二者都是 0.2 时代的契约,升级时请对照
117
+ 目标版本源码重新核验。
100
118
  - **零权限**:无网络请求、无文件写入、无外部服务——host 面是空的 `apply()`,
101
119
  所有数据保存在浏览器 `localStorage`(`dsh.zen.*`),无遥测。
102
120
 
@@ -112,3 +130,7 @@ dsh plugin --profile web remove @johnnren/dsh-zen
112
130
  ## License
113
131
 
114
132
  MIT — 见 [LICENSE](LICENSE)。
133
+
134
+ ## 社区
135
+
136
+ - 感谢 [LINUX DO](https://linux.do/) 社区提供的交流环境。
package/lib/client.js CHANGED
@@ -333,29 +333,6 @@ function createTracker(ctx) {
333
333
  }
334
334
  store.set({ sessions: kept });
335
335
  }
336
- function ensureSession(sessionId, title, running) {
337
- store.update((prev) => {
338
- if (prev.sessions[sessionId]) {
339
- const existing = prev.sessions[sessionId];
340
- if (existing.sessionEndMs === null && !running) {
341
- return { sessions: { ...prev.sessions, [sessionId]: { ...existing, title, sessionEndMs: Date.now() } } };
342
- }
343
- if (existing.sessionEndMs !== null && running) {
344
- return { sessions: { ...prev.sessions, [sessionId]: { ...existing, title, sessionEndMs: null } } };
345
- }
346
- if (existing.title !== title) {
347
- return { sessions: { ...prev.sessions, [sessionId]: { ...existing, title } } };
348
- }
349
- return prev;
350
- }
351
- return {
352
- sessions: {
353
- ...prev.sessions,
354
- [sessionId]: { foregroundMs: 0, runningMs: 0, sessionStartMs: Date.now(), sessionEndMs: running ? null : Date.now(), title, days: {} }
355
- }
356
- };
357
- });
358
- }
359
336
  function reevaluate() {
360
337
  const isZenTabActive = typeof document !== "undefined" && document.querySelector(".dsh-zen-root") !== null;
361
338
  const isWindowVisible = !isZenTabActive && typeof document !== "undefined" && !document.hidden;
@@ -363,9 +340,27 @@ function createTracker(ctx) {
363
340
  const currentId = listSnap.current ?? null;
364
341
  const currentSession = currentId ? listSnap.byId[currentId] : null;
365
342
  const isRunning = (currentSession?.running ?? false) && !currentSession?.pendingInteraction;
343
+ let next = state;
366
344
  for (const [id, summary] of Object.entries(listSnap.byId)) {
367
- ensureSession(id, summary.displayTitle, summary.running);
345
+ const title = summary.displayTitle;
346
+ const existing = next.sessions[id];
347
+ if (!existing) {
348
+ next = { sessions: { ...next.sessions, [id]: { foregroundMs: 0, runningMs: 0, sessionStartMs: Date.now(), sessionEndMs: summary.running ? null : Date.now(), title, days: {} } } };
349
+ continue;
350
+ }
351
+ if (existing.sessionEndMs === null && !summary.running) {
352
+ next = { sessions: { ...next.sessions, [id]: { ...existing, title, sessionEndMs: Date.now() } } };
353
+ continue;
354
+ }
355
+ if (existing.sessionEndMs !== null && summary.running) {
356
+ next = { sessions: { ...next.sessions, [id]: { ...existing, title, sessionEndMs: null } } };
357
+ continue;
358
+ }
359
+ if (existing.title !== title) {
360
+ next = { sessions: { ...next.sessions, [id]: { ...existing, title } } };
361
+ }
368
362
  }
363
+ if (next !== state) store.set(next);
369
364
  if (currentId !== activeSessionId) {
370
365
  if (activeSessionId !== null) flush(activeSessionId, runSegStart, fgSegStart);
371
366
  activeSessionId = currentId;
@@ -924,11 +919,29 @@ function renderMarkdown(MarkdownText, text) {
924
919
  return import_react.default.createElement("div", { style: { whiteSpace: "pre-wrap", wordBreak: "break-word" } }, text);
925
920
  }
926
921
  var ZenView = import_react.default.memo(function ZenView2(props) {
927
- const { useSession, sessionId, t, foregroundStore, MarkdownText, zenSettingsStore } = props;
922
+ const { useSession, sessionId, t, foregroundStore, MarkdownText, zenSettingsStore, bindChat } = props;
928
923
  const rawRunning = useSession((s) => s.running);
929
924
  const blank = useSession((s) => s.blank);
930
- const chat = useSession((s) => s.chat);
931
- const runningCalls = useSession((s) => s?.runningCalls);
925
+ const bindChatRef = import_react.default.useRef(bindChat);
926
+ bindChatRef.current = bindChat;
927
+ const chatSource = import_react.default.useMemo(() => {
928
+ try {
929
+ return bindChatRef.current();
930
+ } catch {
931
+ return null;
932
+ }
933
+ }, [sessionId]);
934
+ const subscribeChat = import_react.default.useCallback(
935
+ (onChange) => chatSource ? chatSource.subscribe(onChange) : () => {
936
+ },
937
+ [chatSource]
938
+ );
939
+ const getChat = import_react.default.useCallback(
940
+ () => chatSource ? chatSource.getSnapshot() : void 0,
941
+ [chatSource]
942
+ );
943
+ const chat = import_react.default.useSyncExternalStore(subscribeChat, getChat);
944
+ const runningCalls = chat?.legacy?.runningCalls;
932
945
  const openState = useSession((s) => s?.openState);
933
946
  const isLoading = !blank && (openState === "cold" || openState === "loading");
934
947
  const [statusRunning, setStatusRunning] = import_react.default.useState(rawRunning);
@@ -1152,19 +1165,22 @@ body[data-chat-hide="on"] [data-chat-flow-kind="compaction"] { display: none !im
1152
1165
  body[data-chat-hide="on"] [data-chat-flow-kind="model-retry"] { display: none !important; }
1153
1166
  body[data-chat-hide="on"] [data-chat-flow-kind="turn-error"] { display: none !important; }
1154
1167
  body[data-chat-hide="on"] [data-chat-flow-kind="turn-max-tokens"] { display: none !important; }
1168
+ /* 0.2 turn-level process disclosure ("\u5DF2\u5904\u7406 N \u6B65") \u2014 hide the control with the rest of the process */
1169
+ body[data-chat-hide="on"] [data-chat-flow-kind="turn-process"] { display: none !important; }
1155
1170
 
1156
1171
  /* Hide reasoning rows inside assistant-step */
1157
1172
  body[data-chat-hide="on"] [data-chat-flow-kind="assistant-step"] [data-variant="think"] { display: none !important; }
1158
1173
 
1159
- /* Hide assistant-step that only contains think (no reply) \u2014 prevents extra flex gap */
1160
- body[data-chat-hide="on"] [data-chat-flow-kind="assistant-step"]:has(.Sxvs8a_body > [data-variant="think"]:only-child) { display: none !important; }
1174
+ /* Hide assistant-step that only contains think (no reply) \u2014 prevents extra flex gap.
1175
+ Hash-agnostic: CSS-module class names end in _body but their prefix changes per build. */
1176
+ body[data-chat-hide="on"] [data-chat-flow-kind="assistant-step"]:has([class*="_body"] > [data-variant="think"]:only-child) { display: none !important; }
1161
1177
 
1162
- /* Collapse context/compaction rows inside user messages (they have padding:2px 0) */
1163
- body[data-chat-hide="on"] .gdEzaW_contextRow:empty,
1164
- body[data-chat-hide="on"] .gdEzaW_compactionRow:empty { display: none !important; }
1178
+ /* Collapse empty context/compaction rows inside user messages (they have padding:2px 0) */
1179
+ body[data-chat-hide="on"] [class*="_contextRow"]:empty,
1180
+ body[data-chat-hide="on"] [class*="_compactionRow"]:empty { display: none !important; }
1165
1181
 
1166
1182
  /* Zero out gap in AssistantMarkdown body when reasoning hidden */
1167
- body[data-chat-hide="on"] [data-chat-flow-kind="assistant-step"] .Sxvs8a_body { gap: 0 !important; }
1183
+ body[data-chat-hide="on"] [data-chat-flow-kind="assistant-step"] [class*="_body"]:has([data-variant="think"]) { gap: 0 !important; }
1168
1184
  `;
1169
1185
  if (typeof document !== "undefined" && !document.querySelector('style[data-plugin-css="dsh-zen/chat-hide"]')) {
1170
1186
  const tag = document.createElement("style");
@@ -1563,14 +1579,21 @@ var ZenSettingsSection = import_react3.default.memo(function ZenSettingsSection2
1563
1579
 
1564
1580
  // src/client.ts
1565
1581
  var name = "zen";
1566
- var inject = ["slots", "sessions", "locale"];
1582
+ var inject = ["slots", "sessions", "locale", "uiConversation"];
1583
+ function resolveCurrentSessionId(snap, previousId) {
1584
+ if (previousId && (snap.byId[previousId]?.retainedBy?.mainView ?? 0) > 0) return previousId;
1585
+ for (const row of Object.values(snap.byId)) {
1586
+ if ((row?.retainedBy?.mainView ?? 0) > 0) return row.id;
1587
+ }
1588
+ return null;
1589
+ }
1567
1590
  function initAutoZenSwitcher(ctx, zenSettingsStore) {
1568
1591
  const sessions = ctx.sessions;
1569
1592
  let prevRunning = null;
1570
1593
  let prevSessionId = null;
1571
1594
  function check() {
1572
1595
  const snap = sessions.list.getSnapshot();
1573
- const currentId = snap.current;
1596
+ const currentId = resolveCurrentSessionId(snap, prevSessionId);
1574
1597
  const session = currentId ? snap.byId[currentId] : null;
1575
1598
  const running = session?.running ?? false;
1576
1599
  if (currentId !== prevSessionId) {
@@ -1621,10 +1644,14 @@ function apply(ctx) {
1621
1644
  order: 5,
1622
1645
  label: () => t("view.zen"),
1623
1646
  locale: NS,
1624
- inject: (_sessionId) => ({
1647
+ inject: (sessionId) => ({
1625
1648
  foregroundStore,
1626
1649
  MarkdownText,
1627
- zenSettingsStore
1650
+ zenSettingsStore,
1651
+ // The Chat target snapshot is an independently activated Conversation
1652
+ // view; subscribing through this source also activates the target, so
1653
+ // Zen keeps reading Chat data while it is the visible view.
1654
+ bindChat: () => ctx.uiConversation.binding(sessionId).target("chat")
1628
1655
  })
1629
1656
  }, ZenView));
1630
1657
  ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnnren/dsh-zen",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Zen Mode view tab + foreground time tracker for DeepSeek Harness: a calm minimal surface (status message, latest user message, live activity hints, markdown AI reply, one-line turn stats) while the agent works, with screen-watching time tracking, daily/weekly Zen scores, a chat-hide toggle, auto enter/exit and an Alt+Z shortcut — pure client-side, no network. dsh-zen 禅模式插件:智能体后台运行时提供极简的 Zen 视图标签页(状态文案、用户消息、动态提示、Markdown 最终回复、单行轮次统计),并统计前台盯屏时长、给出每日/每周禅值,支持隐藏中间过程、自动进出禅与 Alt+Z 快捷键——纯前端、无网络请求。",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -22,7 +22,8 @@
22
22
  ],
23
23
  "scripts": {
24
24
  "build": "node build.mjs",
25
- "typecheck": "tsc --noEmit"
25
+ "typecheck": "tsc --noEmit",
26
+ "e2e:zen": "dsh-e2e run e2e/zen-view.mjs"
26
27
  },
27
28
  "keywords": [
28
29
  "dsh",
@@ -52,10 +53,10 @@
52
53
  },
53
54
  "client": {
54
55
  "inject": [
55
- "@deepseek-ai/dsh-client-runtime",
56
+ "@deepseek-ai/dsh-api-session-controller",
57
+ "@deepseek-ai/dsh-client-locale",
56
58
  "@deepseek-ai/dsh-client-ui-conversation",
57
- "@deepseek-ai/dsh-client-ui-settings",
58
- "@deepseek-ai/dsh-client-locale"
59
+ "@deepseek-ai/dsh-client-ui-settings"
59
60
  ],
60
61
  "platform": "web"
61
62
  },