dsh-better-display 0.3.8 → 0.3.9

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 (182) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/DESIGN.md +43 -3
  3. package/README.en.md +9 -1
  4. package/README.md +9 -1
  5. package/docs/dependency-update-policy.md +9 -0
  6. package/docs/extension-points.md +172 -0
  7. package/docs/official-rendering-bridge.md +48 -19
  8. package/docs/openapi.yaml +147 -0
  9. package/docs/runbooks/blank-reader.md +39 -0
  10. package/lib/client.js +1417 -490
  11. package/lib/client.js.map +1 -1
  12. package/lib/dsh-better-display.js +378 -144
  13. package/lib/types/client/AssistantNode.d.ts +20 -0
  14. package/lib/types/client/AssistantNode.d.ts.map +1 -0
  15. package/lib/types/client/AssistantNode.js +46 -0
  16. package/lib/types/client/AssistantNode.js.map +1 -0
  17. package/lib/types/client/Blocks.d.ts.map +1 -1
  18. package/lib/types/client/Blocks.js +33 -10
  19. package/lib/types/client/Blocks.js.map +1 -1
  20. package/lib/types/client/ChoreographedFlow.d.ts.map +1 -1
  21. package/lib/types/client/ChoreographedFlow.js +2 -0
  22. package/lib/types/client/ChoreographedFlow.js.map +1 -1
  23. package/lib/types/client/ClosedProcessSummary.d.ts.map +1 -1
  24. package/lib/types/client/ClosedProcessSummary.js +3 -1
  25. package/lib/types/client/ClosedProcessSummary.js.map +1 -1
  26. package/lib/types/client/McpAppFrame.d.ts.map +1 -1
  27. package/lib/types/client/McpAppFrame.js +5 -11
  28. package/lib/types/client/McpAppFrame.js.map +1 -1
  29. package/lib/types/client/OfficialContent.d.ts +17 -3
  30. package/lib/types/client/OfficialContent.d.ts.map +1 -1
  31. package/lib/types/client/OfficialContent.js +33 -7
  32. package/lib/types/client/OfficialContent.js.map +1 -1
  33. package/lib/types/client/Reader.d.ts.map +1 -1
  34. package/lib/types/client/Reader.js +43 -71
  35. package/lib/types/client/Reader.js.map +1 -1
  36. package/lib/types/client/SettingsSection.d.ts.map +1 -1
  37. package/lib/types/client/SettingsSection.js +0 -5
  38. package/lib/types/client/SettingsSection.js.map +1 -1
  39. package/lib/types/client/StickyLane.d.ts +12 -0
  40. package/lib/types/client/StickyLane.d.ts.map +1 -1
  41. package/lib/types/client/StickyLane.js +78 -2
  42. package/lib/types/client/StickyLane.js.map +1 -1
  43. package/lib/types/client/ToolActivity.js.map +1 -1
  44. package/lib/types/client/index.d.ts +3 -1
  45. package/lib/types/client/index.d.ts.map +1 -1
  46. package/lib/types/client/index.js +6 -0
  47. package/lib/types/client/index.js.map +1 -1
  48. package/lib/types/client/live-turn.d.ts +17 -5
  49. package/lib/types/client/live-turn.d.ts.map +1 -1
  50. package/lib/types/client/live-turn.js +106 -76
  51. package/lib/types/client/live-turn.js.map +1 -1
  52. package/lib/types/client/motion.d.ts +26 -2
  53. package/lib/types/client/motion.d.ts.map +1 -1
  54. package/lib/types/client/motion.js +136 -10
  55. package/lib/types/client/motion.js.map +1 -1
  56. package/lib/types/client/official-slots.d.ts +9 -0
  57. package/lib/types/client/official-slots.d.ts.map +1 -1
  58. package/lib/types/client/official-slots.js +22 -7
  59. package/lib/types/client/official-slots.js.map +1 -1
  60. package/lib/types/client/projection.d.ts +15 -2
  61. package/lib/types/client/projection.d.ts.map +1 -1
  62. package/lib/types/client/projection.js +16 -4
  63. package/lib/types/client/projection.js.map +1 -1
  64. package/lib/types/client/rewind-cut.d.ts +131 -0
  65. package/lib/types/client/rewind-cut.d.ts.map +1 -0
  66. package/lib/types/client/rewind-cut.js +321 -0
  67. package/lib/types/client/rewind-cut.js.map +1 -0
  68. package/lib/types/client/text-transform.d.ts +93 -0
  69. package/lib/types/client/text-transform.d.ts.map +1 -0
  70. package/lib/types/client/text-transform.js +183 -0
  71. package/lib/types/client/text-transform.js.map +1 -0
  72. package/lib/types/client/timeline.d.ts.map +1 -1
  73. package/lib/types/client/timeline.js.map +1 -1
  74. package/lib/types/client/tool-activity.d.ts +9 -2
  75. package/lib/types/client/tool-activity.d.ts.map +1 -1
  76. package/lib/types/client/tool-activity.js +42 -2
  77. package/lib/types/client/tool-activity.js.map +1 -1
  78. package/lib/types/client/turn-slots.d.ts +29 -0
  79. package/lib/types/client/turn-slots.d.ts.map +1 -0
  80. package/lib/types/client/turn-slots.js +62 -0
  81. package/lib/types/client/turn-slots.js.map +1 -0
  82. package/lib/types/client/types.d.ts +18 -0
  83. package/lib/types/client/types.d.ts.map +1 -1
  84. package/lib/types/dsh-better-display.d.ts +4 -1
  85. package/lib/types/dsh-better-display.d.ts.map +1 -1
  86. package/lib/types/dsh-better-display.js +56 -7
  87. package/lib/types/dsh-better-display.js.map +1 -1
  88. package/lib/types/flags.d.ts +7 -0
  89. package/lib/types/flags.d.ts.map +1 -0
  90. package/lib/types/flags.js +37 -0
  91. package/lib/types/flags.js.map +1 -0
  92. package/lib/types/log.d.ts +17 -0
  93. package/lib/types/log.d.ts.map +1 -0
  94. package/lib/types/log.js +122 -0
  95. package/lib/types/log.js.map +1 -0
  96. package/lib/types/metrics.d.ts +18 -0
  97. package/lib/types/metrics.d.ts.map +1 -0
  98. package/lib/types/metrics.js +35 -0
  99. package/lib/types/metrics.js.map +1 -0
  100. package/package.json +36 -5
  101. package/scripts/changelog.mjs +20 -0
  102. package/scripts/check-agents-md.mjs +67 -0
  103. package/scripts/check-bundle-size.mjs +38 -0
  104. package/scripts/check-dead-flags.mjs +55 -0
  105. package/scripts/check-todo-debt.mjs +69 -0
  106. package/scripts/security-audit-report.mjs +92 -0
  107. package/scripts/verify-commentary-visible.mjs +627 -0
  108. package/src/client/AssistantNode.tsx +72 -0
  109. package/src/client/Blocks.tsx +49 -10
  110. package/src/client/ChoreographedFlow.tsx +3 -1
  111. package/src/client/ClosedProcessSummary.tsx +3 -1
  112. package/src/client/McpAppFrame.tsx +5 -13
  113. package/src/client/OfficialContent.tsx +47 -9
  114. package/src/client/Reader.module.css +68 -18
  115. package/src/client/Reader.tsx +36 -76
  116. package/src/client/SettingsSection.tsx +1 -7
  117. package/src/client/StickyLane.tsx +81 -2
  118. package/src/client/ToolActivity.tsx +1 -1
  119. package/src/client/index.tsx +8 -1
  120. package/src/client/live-turn.ts +100 -69
  121. package/src/client/motion.tsx +135 -7
  122. package/src/client/official-slots.tsx +23 -7
  123. package/src/client/projection.ts +16 -4
  124. package/src/client/rewind-cut.ts +340 -0
  125. package/src/client/text-transform.ts +265 -0
  126. package/src/client/timeline.ts +0 -2
  127. package/src/client/tool-activity.ts +49 -2
  128. package/src/client/turn-slots.ts +80 -0
  129. package/src/client/types.ts +19 -0
  130. package/src/dsh-better-display.ts +58 -7
  131. package/src/flags.ts +40 -0
  132. package/src/log.ts +137 -0
  133. package/src/metrics.ts +50 -0
  134. package/tests/browser-commentary.tsx +191 -0
  135. package/tests/chat-flow-hook.test.ts +1 -1
  136. package/tests/cloud-gate.test.ts +1 -1
  137. package/tests/context-tool-update.test.ts +1 -1
  138. package/tests/conversation-scroll.test.ts +1 -1
  139. package/tests/deliverables.test.ts +1 -1
  140. package/tests/entry.test.ts +1 -1
  141. package/tests/fixtures/reader-lane-owners.tsx +46 -0
  142. package/tests/fixtures/reader-official-users.tsx +64 -0
  143. package/tests/fixtures/reader-theme-overlap.html +80 -0
  144. package/tests/fixtures/reader-user-bubble.tsx +222 -0
  145. package/tests/flags.test.ts +66 -0
  146. package/tests/fold-choreography.test.ts +32 -8
  147. package/tests/fold-intensity.test.ts +11 -6
  148. package/tests/footer-precedence.test.ts +1 -1
  149. package/tests/history-word-regression.test.ts +1 -1
  150. package/tests/live-turn.test.ts +125 -54
  151. package/tests/log.test.ts +99 -0
  152. package/tests/mcp-apps.test.ts +1 -1
  153. package/tests/message-chrome.test.ts +1 -1
  154. package/tests/metrics.test.ts +52 -0
  155. package/tests/official-slots.test.ts +41 -2
  156. package/tests/open-file.test.ts +1 -1
  157. package/tests/pending-submission.test.ts +12 -1
  158. package/tests/pinned-selection.test.ts +16 -0
  159. package/tests/platform-media.test.ts +1 -1
  160. package/tests/projection.test.ts +66 -3
  161. package/tests/reader-glass-css.test.ts +1 -1
  162. package/tests/reader-official-users.test.ts +11 -1
  163. package/tests/reader-sticky-surface.test.ts +377 -0
  164. package/tests/reader-theme-lanes.test.ts +54 -0
  165. package/tests/reader-theme-overlap.test.ts +151 -0
  166. package/tests/reader-user-bubble.test.ts +137 -0
  167. package/tests/reasoning-follow.test.ts +1 -1
  168. package/tests/retiring-content.test.ts +98 -6
  169. package/tests/reveal.test.ts +3 -3
  170. package/tests/rewind-cut.test.ts +1090 -0
  171. package/tests/settings-copy.test.ts +1 -1
  172. package/tests/skill-status.test.ts +1 -1
  173. package/tests/stock-install.test.ts +1 -1
  174. package/tests/stream-buffer.test.ts +1 -1
  175. package/tests/styles.test.ts +30 -3
  176. package/tests/terminal-input-preview.test.ts +1 -1
  177. package/tests/text-transform.test.ts +272 -0
  178. package/tests/timeline.test.ts +1 -1
  179. package/tests/tool-clock.test.ts +1 -1
  180. package/tests/tool-diff.test.ts +1 -1
  181. package/tests/waiting-clock.test.ts +1 -1
  182. package/tests/word-timeline.test.ts +1 -1
@@ -1,158 +1,194 @@
1
- import { access, readdir } from "node:fs/promises";
2
- import { existsSync } from "node:fs";
3
- import { homedir } from "node:os";
1
+ import { existsSync, readFileSync } from "node:fs";
4
2
  import { dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
5
3
  import { fileURLToPath } from "node:url";
4
+ import { randomUUID } from "node:crypto";
6
5
  import { spawn } from "node:child_process";
7
- //#region src/skill-status.ts
8
- /** Shared generative-mcpapps identity. Plugin-tree presence is not installation. */
9
- const GENERATIVE_MCPAPPS_SKILL = "generative-mcpapps";
10
- /** Conventional relative roots only. Never expand $HOME or dump host `root.path`. */
11
- const CONVENTIONAL_SKILL_ROOTS = [".dsh/skills", ".agents/skills"];
12
- function skillNameMatches(name) {
13
- return typeof name === "string" && name === "generative-mcpapps";
14
- }
15
- function skillListIncludes(entries) {
16
- return Array.isArray(entries) && entries.some((entry) => skillNameMatches(entry?.name));
17
- }
18
- /** Directory bundle `name/SKILL.md` or flat `name.md` at a scanned root. */
19
- function skillRootEntryMatches(entryName) {
20
- return entryName === "generative-mcpapps" || entryName === `generative-mcpapps.md`;
21
- }
22
- function skillsFromListResult(result) {
23
- if (Array.isArray(result)) return result;
24
- if (result === null || typeof result !== "object") return [];
25
- const record = result;
26
- if (record.ok === false) return [];
27
- if (Array.isArray(record.skills)) return record.skills;
28
- if (record.value && typeof record.value === "object") {
29
- const value = record.value;
30
- if (Array.isArray(value.skills)) return value.skills;
31
- if (Array.isArray(record.value)) return record.value;
6
+ import { access, readdir } from "node:fs/promises";
7
+ import { homedir } from "node:os";
8
+ //#region src/flags.ts
9
+ const defaultFlags = { readerEnabled: true };
10
+ function locateFlags(start = dirname(fileURLToPath(import.meta.url))) {
11
+ let dir = start;
12
+ for (let depth = 0; depth < 6; depth += 1) {
13
+ const candidate = resolve(dir, "flags.json");
14
+ if (existsSync(candidate)) return candidate;
15
+ const parent = dirname(dir);
16
+ if (parent === dir) return void 0;
17
+ dir = parent;
32
18
  }
33
- return [];
34
19
  }
35
- function publicSkillRoots() {
36
- return CONVENTIONAL_SKILL_ROOTS.map((path) => ({
37
- source: "conventional",
38
- path
39
- }));
20
+ function normalize(value) {
21
+ const record = value !== null && typeof value === "object" ? value : {};
22
+ return { readerEnabled: typeof record.readerEnabled === "boolean" ? record.readerEnabled : defaultFlags.readerEnabled };
40
23
  }
41
- /**
42
- * Strip host home / pack absolutes before anything user-facing (HTTP or Settings).
43
- * Detection still uses the raw scan; this is the public face.
44
- */
45
- function toPublicSkillStatus(status) {
46
- return {
47
- name: status.name,
48
- installed: status.installed,
49
- via: status.via,
50
- roots: publicSkillRoots()
51
- };
24
+ function loadFlags(source) {
25
+ if (source !== void 0 && typeof source !== "string") return normalize(source);
26
+ try {
27
+ const path = typeof source === "string" ? source : locateFlags();
28
+ if (path === void 0 || !existsSync(path)) return { ...defaultFlags };
29
+ return normalize(JSON.parse(readFileSync(path, "utf8")));
30
+ } catch {
31
+ return { ...defaultFlags };
32
+ }
52
33
  }
53
34
  //#endregion
54
- //#region src/skill-roots.ts
55
- function expandHomePath(path, home = homedir()) {
56
- if (path === "~") return home;
57
- if (path.startsWith("~/") || path.startsWith("~\\")) return join(home, path.slice(2));
58
- return path;
35
+ //#region src/log.ts
36
+ const REDACTED = "[REDACTED]";
37
+ const REQUEST_ID_HEADER = "x-request-id";
38
+ const REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,200}$/;
39
+ const SECRET_KEYS = /* @__PURE__ */ new Set([
40
+ "authorization",
41
+ "proxyauthorization",
42
+ "cookie",
43
+ "setcookie",
44
+ "token",
45
+ "accesstoken",
46
+ "refreshtoken",
47
+ "idtoken",
48
+ "apikey",
49
+ "apisecret",
50
+ "secret",
51
+ "clientsecret",
52
+ "password",
53
+ "passwd",
54
+ "credential",
55
+ "credentials",
56
+ "signature",
57
+ "sig",
58
+ "jwt",
59
+ "bearer",
60
+ "privatekey",
61
+ "xapikey",
62
+ "xauthtoken",
63
+ "xaccesstoken",
64
+ "auth"
65
+ ]);
66
+ function normalizeKey(key) {
67
+ return key.toLowerCase().replace(/[^a-z0-9]/g, "");
59
68
  }
60
- /** `$DSH_HOME` or `~/.dsh`, matching official `resolveDshHome`. */
61
- function resolveDshHome(env = process.env, home = homedir()) {
62
- const fromEnv = env.DSH_HOME;
63
- const selected = fromEnv !== void 0 && fromEnv.trim().length > 0 ? fromEnv : join(home, ".dsh");
64
- return resolve(expandHomePath(selected, home));
69
+ function isSecretKey(key) {
70
+ const normalized = normalizeKey(key);
71
+ if (SECRET_KEYS.has(normalized)) return true;
72
+ if (normalized === "requestid" || normalized === "xrequestid") return false;
73
+ return /(?:^|[^a-z])(?:token|secret|password|api[-_]?key)$/i.test(key);
65
74
  }
66
- /** `$DSH_AGENTS_HOME` or `~/.agents`. */
67
- function resolveAgentsHome(env = process.env, home = homedir()) {
68
- const fromEnv = env.DSH_AGENTS_HOME;
69
- const selected = fromEnv !== void 0 && fromEnv.trim().length > 0 ? fromEnv : join(home, ".agents");
70
- return resolve(expandHomePath(selected, home));
75
+ function redactString(value) {
76
+ return value.replace(/([?&#])([^&#=]+)=([^&#]*)/g, (match, sep, rawKey) => {
77
+ let key;
78
+ try {
79
+ key = decodeURIComponent(rawKey.replace(/\+/g, " "));
80
+ } catch {
81
+ key = rawKey;
82
+ }
83
+ return isSecretKey(key) ? `${sep}${rawKey}=${REDACTED}` : match;
84
+ }).replace(/\b(Bearer|Basic)\s+\S+/gi, `$1 ${REDACTED}`).replace(/\b((?:access[_-]?token|refresh[_-]?token|id[_-]?token|api[_-]?key|client[_-]?secret|password|passwd|authorization|token|secret))(\s*[:=]\s*)([^\s&]+)/gi, `$1$2${REDACTED}`);
71
85
  }
72
- function userSkillRoots(env = process.env, home = homedir()) {
73
- return [{
74
- source: "user-dsh",
75
- path: join(resolveDshHome(env, home), "skills")
76
- }, {
77
- source: "user-agents",
78
- path: join(resolveAgentsHome(env, home), "skills")
79
- }];
86
+ function redact(value) {
87
+ return redactValue(void 0, value, 0, /* @__PURE__ */ new WeakSet());
80
88
  }
81
- function projectSkillRoots(projectRoot) {
82
- return [{
83
- source: "project-dsh",
84
- path: join(projectRoot, ".dsh", "skills")
85
- }, {
86
- source: "project-agents",
87
- path: join(projectRoot, ".agents", "skills")
88
- }];
89
- }
90
- async function findProjectRoot(cwd) {
91
- let current = resolve(cwd);
92
- while (true) {
93
- if (existsSync(join(current, ".git"))) return current;
94
- const parent = dirname(current);
95
- if (parent === current) return resolve(cwd);
96
- current = parent;
97
- }
89
+ function redactValue(key, value, depth, seen) {
90
+ if (key !== void 0 && isSecretKey(key)) return REDACTED;
91
+ if (depth > 8) return REDACTED;
92
+ if (typeof value === "string") return redactString(value);
93
+ if (typeof value === "number" || typeof value === "boolean" || value === null || value === void 0) return value;
94
+ if (typeof value !== "object") return REDACTED;
95
+ if (value instanceof Date) return value.toISOString();
96
+ if (value instanceof Error) return redactValue(void 0, {
97
+ name: value.name,
98
+ message: value.message
99
+ }, depth + 1, seen);
100
+ if (seen.has(value)) return REDACTED;
101
+ seen.add(value);
102
+ if (Array.isArray(value)) return value.map((item) => redactValue(void 0, item, depth + 1, seen));
103
+ const output = {};
104
+ for (const [childKey, childValue] of Object.entries(value)) output[childKey] = redactValue(childKey, childValue, depth + 1, seen);
105
+ return output;
98
106
  }
99
- async function rootHasGenerativeMcpapps(rootPath) {
100
- let entries;
101
- try {
102
- entries = await readdir(rootPath, { withFileTypes: true });
103
- } catch {
104
- return false;
105
- }
106
- for (const entry of entries) {
107
- if (entry.name === ".system") continue;
108
- if (!skillRootEntryMatches(entry.name)) continue;
109
- if (entry.isDirectory()) try {
110
- await access(join(rootPath, entry.name, "SKILL.md"));
111
- return true;
107
+ function createLogger(options = {}) {
108
+ const write = options.write ?? ((line) => {
109
+ console.log(line);
110
+ });
111
+ const clock = options.clock ?? (() => /* @__PURE__ */ new Date());
112
+ const emit = (level, msg, fields) => {
113
+ const details = redact({
114
+ ...options.base,
115
+ ...fields
116
+ });
117
+ const record = {
118
+ ...details !== null && typeof details === "object" ? details : {},
119
+ time: clock().toISOString(),
120
+ level,
121
+ msg: redactString(msg)
122
+ };
123
+ try {
124
+ write(JSON.stringify(record));
112
125
  } catch {
113
- continue;
126
+ write(JSON.stringify({
127
+ time: record.time,
128
+ level,
129
+ msg: record.msg,
130
+ error: "log record was not serializable"
131
+ }));
114
132
  }
115
- if (entry.isFile()) return true;
116
- }
117
- return false;
133
+ };
134
+ return {
135
+ info: (msg, fields) => emit("info", msg, fields),
136
+ warn: (msg, fields) => emit("warn", msg, fields),
137
+ error: (msg, fields) => emit("error", msg, fields)
138
+ };
118
139
  }
119
- /** Resolve the plugin-shipped pack. This path is never treated as a skill root. */
120
- function pluginSkillPackPath(moduleUrl = import.meta.url) {
121
- const here = dirname(fileURLToPath(moduleUrl));
122
- return [
123
- join(here, "../skills", GENERATIVE_MCPAPPS_SKILL),
124
- join(here, "../../skills", GENERATIVE_MCPAPPS_SKILL),
125
- join(here, "skills", GENERATIVE_MCPAPPS_SKILL)
126
- ].find((path) => existsSync(join(path, "SKILL.md")));
140
+ function requestIdOf(req) {
141
+ const raw = req.headers[REQUEST_ID_HEADER];
142
+ const value = Array.isArray(raw) ? raw[0] : raw;
143
+ if (typeof value === "string" && REQUEST_ID_PATTERN.test(value)) return value;
144
+ return randomUUID();
127
145
  }
128
- async function scanGenerativeMcpappsStatus(options = {}) {
129
- const roots = [...userSkillRoots(options.env ?? process.env, options.home ?? homedir())];
130
- const cwd = typeof options.cwd === "string" && options.cwd.trim().length > 0 ? isAbsolute(options.cwd) ? options.cwd : resolve(options.cwd) : void 0;
131
- if (cwd !== void 0) roots.unshift(...projectSkillRoots(await findProjectRoot(cwd)));
132
- const inspected = [];
133
- let rootHit = false;
134
- for (const root of roots) {
135
- const present = await rootHasGenerativeMcpapps(root.path);
136
- inspected.push({
137
- ...root,
138
- present
139
- });
140
- if (present) rootHit = true;
141
- }
142
- let via = null;
143
- let listHit = false;
144
- if (options.listSkills) try {
145
- listHit = skillListIncludes(await options.listSkills());
146
- if (listHit) via = "skills.list";
147
- } catch {}
148
- if (!listHit && rootHit) via = "skill-root";
149
- const packPath = options.packPath ?? pluginSkillPackPath();
146
+ //#endregion
147
+ //#region src/metrics.ts
148
+ const ROUTE_REVEAL = "/better-display/reveal";
149
+ const ROUTE_SKILL_STATUS = "/better-display/skill-status";
150
+ const ROUTE_METRICS = "/better-display/metrics";
151
+ const ROUTE_HITS = [
152
+ ROUTE_REVEAL,
153
+ ROUTE_SKILL_STATUS,
154
+ ROUTE_METRICS
155
+ ];
156
+ const hits = new Map(ROUTE_HITS.map((route) => [route, 0]));
157
+ const timings = new Map(ROUTE_HITS.map((route) => [route, {
158
+ count: 0,
159
+ totalMs: 0,
160
+ maxMs: 0
161
+ }]));
162
+ function snapshotHits() {
163
+ return Object.fromEntries(hits);
164
+ }
165
+ function snapshotTimings() {
166
+ return Object.fromEntries([...timings].map(([route, timing]) => [route, { ...timing }]));
167
+ }
168
+ function recordRouteHit(route) {
169
+ const next = (hits.get(route) ?? 0) + 1;
170
+ hits.set(route, next);
171
+ return next;
172
+ }
173
+ function recordRouteDuration(route, elapsedMs) {
174
+ const ms = Number.isFinite(elapsedMs) && elapsedMs > 0 ? Math.round(elapsedMs) : 0;
175
+ const current = timings.get(route) ?? {
176
+ count: 0,
177
+ totalMs: 0,
178
+ maxMs: 0
179
+ };
180
+ const next = {
181
+ count: current.count + 1,
182
+ totalMs: current.totalMs + ms,
183
+ maxMs: Math.max(current.maxMs, ms)
184
+ };
185
+ timings.set(route, next);
186
+ return next;
187
+ }
188
+ function metricsPayload() {
150
189
  return {
151
- name: GENERATIVE_MCPAPPS_SKILL,
152
- installed: listHit || rootHit,
153
- via,
154
- roots: inspected,
155
- ...packPath !== void 0 ? { packPath } : {}
190
+ counters: { dsh_better_display_route_hits_total: snapshotHits() },
191
+ timingsMs: snapshotTimings()
156
192
  };
157
193
  }
158
194
  //#endregion
@@ -162,7 +198,7 @@ function isAbsoluteWorkspacePath(path) {
162
198
  return /^(?:\/|[A-Za-z]:[/\\]|\\\\)/.test(path);
163
199
  }
164
200
  //#endregion
165
- //#region \0@oxc-project+runtime@0.153.0/helpers/esm/usingCtx.js
201
+ //#region \0@oxc-project+runtime@0.154.0/helpers/esm/usingCtx.js
166
202
  function _usingCtx() {
167
203
  var r = "function" == typeof SuppressedError ? SuppressedError : function(r, e) {
168
204
  var n = Error();
@@ -292,9 +328,162 @@ function createRevealHandler({ roots, reveal = revealPath }) {
292
328
  };
293
329
  }
294
330
  //#endregion
331
+ //#region src/skill-status.ts
332
+ /** Shared generative-mcpapps identity. Plugin-tree presence is not installation. */
333
+ const GENERATIVE_MCPAPPS_SKILL = "generative-mcpapps";
334
+ /** Conventional relative roots only. Never expand $HOME or dump host `root.path`. */
335
+ const CONVENTIONAL_SKILL_ROOTS = [".dsh/skills", ".agents/skills"];
336
+ function skillNameMatches(name) {
337
+ return typeof name === "string" && name === "generative-mcpapps";
338
+ }
339
+ function skillListIncludes(entries) {
340
+ return Array.isArray(entries) && entries.some((entry) => skillNameMatches(entry?.name));
341
+ }
342
+ /** Directory bundle `name/SKILL.md` or flat `name.md` at a scanned root. */
343
+ function skillRootEntryMatches(entryName) {
344
+ return entryName === "generative-mcpapps" || entryName === `generative-mcpapps.md`;
345
+ }
346
+ function skillsFromListResult(result) {
347
+ if (Array.isArray(result)) return result;
348
+ if (result === null || typeof result !== "object") return [];
349
+ const record = result;
350
+ if (record.ok === false) return [];
351
+ if (Array.isArray(record.skills)) return record.skills;
352
+ if (record.value && typeof record.value === "object") {
353
+ const value = record.value;
354
+ if (Array.isArray(value.skills)) return value.skills;
355
+ if (Array.isArray(record.value)) return record.value;
356
+ }
357
+ return [];
358
+ }
359
+ function publicSkillRoots() {
360
+ return CONVENTIONAL_SKILL_ROOTS.map((path) => ({
361
+ source: "conventional",
362
+ path
363
+ }));
364
+ }
365
+ /**
366
+ * Strip host home / pack absolutes before anything user-facing (HTTP or Settings).
367
+ * Detection still uses the raw scan; this is the public face.
368
+ */
369
+ function toPublicSkillStatus(status) {
370
+ return {
371
+ name: status.name,
372
+ installed: status.installed,
373
+ via: status.via,
374
+ roots: publicSkillRoots()
375
+ };
376
+ }
377
+ //#endregion
378
+ //#region src/skill-roots.ts
379
+ function expandHomePath(path, home = homedir()) {
380
+ if (path === "~") return home;
381
+ if (path.startsWith("~/") || path.startsWith("~\\")) return join(home, path.slice(2));
382
+ return path;
383
+ }
384
+ /** `$DSH_HOME` or `~/.dsh`, matching official `resolveDshHome`. */
385
+ function resolveDshHome(env = process.env, home = homedir()) {
386
+ const fromEnv = env.DSH_HOME;
387
+ const selected = fromEnv !== void 0 && fromEnv.trim().length > 0 ? fromEnv : join(home, ".dsh");
388
+ return resolve(expandHomePath(selected, home));
389
+ }
390
+ /** `$DSH_AGENTS_HOME` or `~/.agents`. */
391
+ function resolveAgentsHome(env = process.env, home = homedir()) {
392
+ const fromEnv = env.DSH_AGENTS_HOME;
393
+ const selected = fromEnv !== void 0 && fromEnv.trim().length > 0 ? fromEnv : join(home, ".agents");
394
+ return resolve(expandHomePath(selected, home));
395
+ }
396
+ function userSkillRoots(env = process.env, home = homedir()) {
397
+ return [{
398
+ source: "user-dsh",
399
+ path: join(resolveDshHome(env, home), "skills")
400
+ }, {
401
+ source: "user-agents",
402
+ path: join(resolveAgentsHome(env, home), "skills")
403
+ }];
404
+ }
405
+ function projectSkillRoots(projectRoot) {
406
+ return [{
407
+ source: "project-dsh",
408
+ path: join(projectRoot, ".dsh", "skills")
409
+ }, {
410
+ source: "project-agents",
411
+ path: join(projectRoot, ".agents", "skills")
412
+ }];
413
+ }
414
+ async function findProjectRoot(cwd) {
415
+ let current = resolve(cwd);
416
+ while (true) {
417
+ if (existsSync(join(current, ".git"))) return current;
418
+ const parent = dirname(current);
419
+ if (parent === current) return resolve(cwd);
420
+ current = parent;
421
+ }
422
+ }
423
+ async function rootHasGenerativeMcpapps(rootPath) {
424
+ let entries;
425
+ try {
426
+ entries = await readdir(rootPath, { withFileTypes: true });
427
+ } catch {
428
+ return false;
429
+ }
430
+ for (const entry of entries) {
431
+ if (entry.name === ".system") continue;
432
+ if (!skillRootEntryMatches(entry.name)) continue;
433
+ if (entry.isDirectory()) try {
434
+ await access(join(rootPath, entry.name, "SKILL.md"));
435
+ return true;
436
+ } catch {
437
+ continue;
438
+ }
439
+ if (entry.isFile()) return true;
440
+ }
441
+ return false;
442
+ }
443
+ /** Resolve the plugin-shipped pack. This path is never treated as a skill root. */
444
+ function pluginSkillPackPath(moduleUrl = import.meta.url) {
445
+ const here = dirname(fileURLToPath(moduleUrl));
446
+ return [
447
+ join(here, "../skills", GENERATIVE_MCPAPPS_SKILL),
448
+ join(here, "../../skills", GENERATIVE_MCPAPPS_SKILL),
449
+ join(here, "skills", GENERATIVE_MCPAPPS_SKILL)
450
+ ].find((path) => existsSync(join(path, "SKILL.md")));
451
+ }
452
+ async function scanGenerativeMcpappsStatus(options = {}) {
453
+ const roots = [...userSkillRoots(options.env ?? process.env, options.home ?? homedir())];
454
+ const cwd = typeof options.cwd === "string" && options.cwd.trim().length > 0 ? isAbsolute(options.cwd) ? options.cwd : resolve(options.cwd) : void 0;
455
+ if (cwd !== void 0) roots.unshift(...projectSkillRoots(await findProjectRoot(cwd)));
456
+ const inspected = [];
457
+ let rootHit = false;
458
+ for (const root of roots) {
459
+ const present = await rootHasGenerativeMcpapps(root.path);
460
+ inspected.push({
461
+ ...root,
462
+ present
463
+ });
464
+ if (present) rootHit = true;
465
+ }
466
+ let via = null;
467
+ let listHit = false;
468
+ if (options.listSkills) try {
469
+ listHit = skillListIncludes(await options.listSkills());
470
+ if (listHit) via = "skills.list";
471
+ } catch {}
472
+ if (!listHit && rootHit) via = "skill-root";
473
+ const packPath = options.packPath ?? pluginSkillPackPath();
474
+ return {
475
+ name: GENERATIVE_MCPAPPS_SKILL,
476
+ installed: listHit || rootHit,
477
+ via,
478
+ roots: inspected,
479
+ ...packPath !== void 0 ? { packPath } : {}
480
+ };
481
+ }
482
+ //#endregion
295
483
  //#region src/dsh-better-display.ts
296
484
  const name = "dsh-better-display";
297
485
  const inject = ["webServer"];
486
+ const log = createLogger({ base: { plugin: name } });
298
487
  function writeJson(res, status, body) {
299
488
  res.setHeader("Content-Type", "application/json");
300
489
  res.statusCode = status;
@@ -305,18 +494,45 @@ function skillLister(ctx) {
305
494
  if (typeof skills?.list !== "function") return void 0;
306
495
  return async () => skillsFromListResult(await skills.list({}));
307
496
  }
308
- function apply(ctx) {
309
- console.log("[my-plugins/dsh-better-display] loaded");
497
+ function beginRequest(req, res, route) {
498
+ const requestId = requestIdOf(req);
499
+ res.setHeader("X-Request-ID", requestId);
500
+ recordRouteHit(route);
501
+ const started = performance.now();
502
+ res.on("finish", () => {
503
+ recordRouteDuration(route, performance.now() - started);
504
+ });
505
+ log.info("http request", {
506
+ requestId,
507
+ route,
508
+ method: req.method ?? "",
509
+ url: req.url ?? "",
510
+ headers: req.headers
511
+ });
512
+ return requestId;
513
+ }
514
+ function apply(ctx, options) {
515
+ const flags = options?.flags ?? loadFlags();
516
+ log.info("[my-plugins/dsh-better-display] loaded", { readerEnabled: flags.readerEnabled });
517
+ if (!flags.readerEnabled) {
518
+ log.info("route registration skipped", { readerEnabled: false });
519
+ return;
520
+ }
310
521
  if (ctx.webServer) ctx.effect(() => {
522
+ const reveal = createRevealHandler({ roots: (sessionId) => revealRoots(ctx, sessionId) });
311
523
  const disposeReveal = ctx.webServer.register({
312
524
  kind: "exact",
313
- path: "/better-display/reveal",
314
- handler: createRevealHandler({ roots: (sessionId) => revealRoots(ctx, sessionId) })
525
+ path: ROUTE_REVEAL,
526
+ handler: (req, res) => {
527
+ beginRequest(req, res, ROUTE_REVEAL);
528
+ reveal(req, res);
529
+ }
315
530
  });
316
531
  const disposeSkill = ctx.webServer.register({
317
532
  kind: "exact",
318
- path: "/better-display/skill-status",
533
+ path: ROUTE_SKILL_STATUS,
319
534
  handler: async (req, res) => {
535
+ const requestId = beginRequest(req, res, ROUTE_SKILL_STATUS);
320
536
  if (req.method !== "GET") {
321
537
  res.statusCode = 405;
322
538
  res.end();
@@ -328,6 +544,10 @@ function apply(ctx) {
328
544
  listSkills: skillLister(ctx)
329
545
  })));
330
546
  } catch (err) {
547
+ log.error("skill-status failed", {
548
+ requestId,
549
+ error: err instanceof Error ? err.message : String(err)
550
+ });
331
551
  writeJson(res, 500, {
332
552
  ok: false,
333
553
  error: String(err)
@@ -335,11 +555,25 @@ function apply(ctx) {
335
555
  }
336
556
  }
337
557
  });
558
+ const disposeMetrics = ctx.webServer.register({
559
+ kind: "exact",
560
+ path: ROUTE_METRICS,
561
+ handler: (req, res) => {
562
+ beginRequest(req, res, ROUTE_METRICS);
563
+ if (req.method !== "GET") {
564
+ res.statusCode = 405;
565
+ res.end();
566
+ return;
567
+ }
568
+ writeJson(res, 200, metricsPayload());
569
+ }
570
+ });
338
571
  return () => {
339
572
  disposeReveal();
340
573
  disposeSkill();
574
+ disposeMetrics();
341
575
  };
342
- }, "dsh-better-display: reveal and skill-status routes");
576
+ }, "dsh-better-display: reveal, skill-status, and metrics routes");
343
577
  }
344
578
  //#endregion
345
579
  export { apply, inject, name };
@@ -0,0 +1,20 @@
1
+ import type { RefObject } from 'react';
2
+ import type { TurnBoundary } from './projection.js';
3
+ import type { BlockRenderProps, ReaderProps } from './types.js';
4
+ type SeatProps = BlockRenderProps & Pick<ReaderProps, 'useChat' | 'sessionId'> & {
5
+ nodeKey: string;
6
+ boundary: TurnBoundary;
7
+ pinned?: boolean;
8
+ processOpen?: boolean;
9
+ };
10
+ export declare const AssistantNode: import("react").MemoExoticComponent<({ useChat, nodeKey, boundary, processOpen, pinned, folded, partStart, seamSummary, seamAfter, motion, onRead, returnFocusTo, ...render }: SeatProps & {
11
+ motion: boolean;
12
+ onRead: () => void;
13
+ returnFocusTo: RefObject<HTMLButtonElement>;
14
+ partStart?: number;
15
+ folded?: boolean;
16
+ seamSummary?: string;
17
+ seamAfter?: boolean;
18
+ }) => import("react").JSX.Element | null>;
19
+ export {};
20
+ //# sourceMappingURL=AssistantNode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AssistantNode.d.ts","sourceRoot":"","sources":["../../../src/client/AssistantNode.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAQvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,KAAK,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAOhE,KAAK,SAAS,GAAG,gBAAgB,GAAG,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,WAAW,CAAC,GAAG;IAC/E,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,YAAY,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAC;CAClF,CAAC;AAEF,eAAO,MAAM,aAAa,iLAAyM,SAAS,GAAG;IAC7O,MAAM,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,IAAI,CAAC;IAAC,aAAa,EAAE,SAAS,CAAC,iBAAiB,CAAC,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IACvH,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC3C,wCA+CC,CAAC"}
@@ -0,0 +1,46 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Fragment, memo } from 'react';
3
+ import { Blocks, CopyAnswer } from './Blocks.js';
4
+ import { ReasoningCard } from './ReasoningCard.js';
5
+ import { OfficialActions, OfficialAssistantBody } from './OfficialContent.js';
6
+ import { Disclosure, ProcessFragment } from './motion.js';
7
+ import { FoldSummaryText } from './LiveFold.js';
8
+ import { assistantSegments, forkAnchorSeq, hasVisibleBody, isEarlierNarration } from './projection.js';
9
+ import css from './Reader.module.css';
10
+ function isNode(node, kind) {
11
+ return node.kind === kind;
12
+ }
13
+ export const AssistantNode = memo(function AssistantNode({ useChat, nodeKey, boundary, processOpen = false, pinned = false, folded = false, partStart, seamSummary, seamAfter = false, motion, onRead, returnFocusTo, ...render }) {
14
+ const node = useChat(snapshot => snapshot.nodes.get(nodeKey));
15
+ if (!node || node.visibility === 'hidden' || !isNode(node, 'assistant-step'))
16
+ return null;
17
+ const data = node.data;
18
+ const parts = assistantSegments(data.blocks);
19
+ const earlier = isEarlierNarration(data, boundary);
20
+ const hasToolCalls = data.blocks.some(block => block.kind === 'tool-call');
21
+ const isProcessStep = earlier || folded || hasToolCalls || (boundary.latestStep > 0 && data.step < boundary.latestStep);
22
+ const body = data.blocks.filter(block => block.kind !== 'reasoning' && block.kind !== 'tool-call');
23
+ const visible = partStart === undefined ? parts : parts.filter(part => part.start === partStart);
24
+ const bodyProps = { streaming: data.status === 'running', holdFormatting: pinned, startedAt: data.time, interrupted: data.status === 'interrupted', liveText: true };
25
+ return _jsx(_Fragment, { children: visible.map(part => {
26
+ const index = parts.findIndex(item => item.start === part.start);
27
+ const last = index === parts.length - 1;
28
+ if (part.kind === 'reasoning')
29
+ return _jsx(ProcessFragment, { open: processOpen, motion: motion, onRead: onRead, returnFocusTo: returnFocusTo, nodeKey: nodeKey, framed: true, children: _jsx(ReasoningCard, { step: data.step, active: processOpen && boundary.status === 'open' && data.step === boundary.latestStep, motion: motion, selected: pinned, onRead: onRead, children: _jsx(Blocks, { ...render, blocks: part.blocks, streaming: data.status === 'running' && last && data.blocks.at(-1)?.kind === 'reasoning', holdFormatting: pinned, startedAt: data.time, interrupted: data.status === 'interrupted', liveText: true }) }) }, part.start);
30
+ if (!hasVisibleBody(part.blocks))
31
+ return null;
32
+ // Narration stays body prose and never enters a disclosure. The seam in
33
+ // the gap summarises folded process between two body blocks. A foreign
34
+ // assistant-step renderer receives these original blocks; Reader markdown,
35
+ // including the text transform, is only the fallback.
36
+ const isFinalAnswer = !isProcessStep;
37
+ return _jsxs(Fragment, { children: [seamSummary && _jsx("div", { className: css.processSeamRow, "data-reader-process-seam": true, "data-reader-process-seam-summary": seamSummary, children: _jsx(Disclosure, { open: false, onChange: () => onRead(), ariaLabel: "\u88AB\u6298\u53E0\u7684\u8FC7\u7A0B", label: _jsx(FoldSummaryText, { summary: seamSummary, motion: false }) }) }), _jsxs("article", { className: css.answer, "data-reader-body": true, "data-reader-anchor": true, "data-reader-key": nodeKey, "data-reader-source-start": part.start, "data-reader-seam-after": seamAfter || undefined, "data-answer-status": data.status, "data-answer-phase": isFinalAnswer ? 'body' : 'process', children: [_jsx(OfficialAssistantBody, { ...render, node: node, blocks: part.blocks, ...bodyProps }), isFinalAnswer && last && data.status === 'interrupted' && _jsx("span", { className: css.stopped, children: "\u5DF2\u505C\u6B62" }), isFinalAnswer && last && data.status !== 'running' && boundary.status === 'closed' && (_jsx(CopyAnswer, { blocks: body, onFork: (() => {
38
+ // The fork anchor must be the durable closing message seq (same as
39
+ // the official turn-tail branch). AssistantChatData carries no seq
40
+ // of its own; passing it would fork the whole session instead.
41
+ const anchor = forkAnchorSeq([data.finalNode, { seq: render.forkSeq }]);
42
+ return render.forkAt && anchor !== undefined ? () => render.forkAt(anchor) : undefined;
43
+ })(), metrics: render.metrics, extraActions: _jsx(OfficialActions, { official: render.official, messageId: data.finalNode?.messageId }) }))] })] }, part.start);
44
+ }) });
45
+ });
46
+ //# sourceMappingURL=AssistantNode.js.map