dsh-diff-approval 0.28.0 → 0.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/README.md +12 -0
  2. package/README.zh.md +12 -0
  3. package/assets/fonts/FONTLOG.md +55 -0
  4. package/assets/fonts/OFL.txt +95 -0
  5. package/assets/fonts/README.md +123 -0
  6. package/assets/fonts/manifest.json +1784 -0
  7. package/assets/fonts/regular-cjk-punctuation.woff2 +0 -0
  8. package/assets/fonts/regular-hanzi-000.woff2 +0 -0
  9. package/assets/fonts/regular-hanzi-001.woff2 +0 -0
  10. package/assets/fonts/regular-hanzi-002.woff2 +0 -0
  11. package/assets/fonts/regular-hanzi-003.woff2 +0 -0
  12. package/assets/fonts/regular-hanzi-004.woff2 +0 -0
  13. package/assets/fonts/regular-hanzi-005.woff2 +0 -0
  14. package/assets/fonts/regular-hanzi-006.woff2 +0 -0
  15. package/assets/fonts/regular-hanzi-007.woff2 +0 -0
  16. package/assets/fonts/regular-hanzi-008.woff2 +0 -0
  17. package/assets/fonts/regular-hanzi-009.woff2 +0 -0
  18. package/assets/fonts/regular-hanzi-010.woff2 +0 -0
  19. package/assets/fonts/regular-hanzi-011.woff2 +0 -0
  20. package/assets/fonts/regular-hanzi-012.woff2 +0 -0
  21. package/assets/fonts/regular-hanzi-013.woff2 +0 -0
  22. package/assets/fonts/regular-hanzi-014.woff2 +0 -0
  23. package/assets/fonts/regular-hanzi-015.woff2 +0 -0
  24. package/assets/fonts/regular-hanzi-016.woff2 +0 -0
  25. package/assets/fonts/regular-hanzi-017.woff2 +0 -0
  26. package/assets/fonts/regular-hanzi-018.woff2 +0 -0
  27. package/assets/fonts/regular-hanzi-019.woff2 +0 -0
  28. package/assets/fonts/regular-hanzi-020.woff2 +0 -0
  29. package/assets/fonts/regular-hanzi-021.woff2 +0 -0
  30. package/assets/fonts/regular-hanzi-022.woff2 +0 -0
  31. package/assets/fonts/regular-hanzi-023.woff2 +0 -0
  32. package/assets/fonts/regular-hanzi-024.woff2 +0 -0
  33. package/assets/fonts/regular-hanzi-025.woff2 +0 -0
  34. package/assets/fonts/regular-hanzi-026.woff2 +0 -0
  35. package/assets/fonts/regular-hanzi-027.woff2 +0 -0
  36. package/assets/fonts/regular-hanzi-028.woff2 +0 -0
  37. package/assets/fonts/regular-hanzi-029.woff2 +0 -0
  38. package/assets/fonts/regular-hanzi-030.woff2 +0 -0
  39. package/assets/fonts/regular-hanzi-031.woff2 +0 -0
  40. package/assets/fonts/regular-hanzi-032.woff2 +0 -0
  41. package/assets/fonts/regular-hanzi-033.woff2 +0 -0
  42. package/assets/fonts/regular-hanzi-034.woff2 +0 -0
  43. package/assets/fonts/regular-hanzi-035.woff2 +0 -0
  44. package/assets/fonts/regular-hanzi-036.woff2 +0 -0
  45. package/assets/fonts/regular-hanzi-037.woff2 +0 -0
  46. package/assets/fonts/regular-hanzi-038.woff2 +0 -0
  47. package/assets/fonts/regular-hanzi-039.woff2 +0 -0
  48. package/assets/fonts/regular-hanzi-040.woff2 +0 -0
  49. package/assets/fonts/regular-hanzi-041.woff2 +0 -0
  50. package/assets/fonts/regular-hanzi-042.woff2 +0 -0
  51. package/assets/fonts/regular-hanzi-043.woff2 +0 -0
  52. package/assets/fonts/regular-hanzi-044.woff2 +0 -0
  53. package/assets/fonts/regular-hanzi-045.woff2 +0 -0
  54. package/assets/fonts/regular-hanzi-046.woff2 +0 -0
  55. package/assets/fonts/regular-hanzi-047.woff2 +0 -0
  56. package/assets/fonts/regular-hanzi-048.woff2 +0 -0
  57. package/assets/fonts/regular-hanzi-049.woff2 +0 -0
  58. package/assets/fonts/regular-hanzi-050.woff2 +0 -0
  59. package/assets/fonts/regular-hanzi-051.woff2 +0 -0
  60. package/assets/fonts/regular-hanzi-052.woff2 +0 -0
  61. package/assets/fonts/regular-hanzi-053.woff2 +0 -0
  62. package/assets/fonts/regular-hanzi-054.woff2 +0 -0
  63. package/assets/fonts/regular-hanzi-055.woff2 +0 -0
  64. package/assets/fonts/regular-hanzi-056.woff2 +0 -0
  65. package/assets/fonts/regular-hanzi-057.woff2 +0 -0
  66. package/assets/fonts/regular-hanzi-058.woff2 +0 -0
  67. package/assets/fonts/regular-hanzi-059.woff2 +0 -0
  68. package/assets/fonts/regular-hanzi-060.woff2 +0 -0
  69. package/assets/fonts/regular-hanzi-061.woff2 +0 -0
  70. package/assets/fonts/regular-hanzi-062.woff2 +0 -0
  71. package/assets/fonts/regular-hanzi-063.woff2 +0 -0
  72. package/assets/fonts/regular-hanzi-064.woff2 +0 -0
  73. package/assets/fonts/regular-latin.woff2 +0 -0
  74. package/assets/fonts/regular-punctuation.woff2 +0 -0
  75. package/assets/fonts/regular-symbols.woff2 +0 -0
  76. package/assets/fonts/semibold-cjk-punctuation.woff2 +0 -0
  77. package/assets/fonts/semibold-hanzi-000.woff2 +0 -0
  78. package/assets/fonts/semibold-hanzi-001.woff2 +0 -0
  79. package/assets/fonts/semibold-hanzi-002.woff2 +0 -0
  80. package/assets/fonts/semibold-hanzi-003.woff2 +0 -0
  81. package/assets/fonts/semibold-hanzi-004.woff2 +0 -0
  82. package/assets/fonts/semibold-hanzi-005.woff2 +0 -0
  83. package/assets/fonts/semibold-hanzi-006.woff2 +0 -0
  84. package/assets/fonts/semibold-hanzi-007.woff2 +0 -0
  85. package/assets/fonts/semibold-hanzi-008.woff2 +0 -0
  86. package/assets/fonts/semibold-hanzi-009.woff2 +0 -0
  87. package/assets/fonts/semibold-hanzi-010.woff2 +0 -0
  88. package/assets/fonts/semibold-hanzi-011.woff2 +0 -0
  89. package/assets/fonts/semibold-hanzi-012.woff2 +0 -0
  90. package/assets/fonts/semibold-hanzi-013.woff2 +0 -0
  91. package/assets/fonts/semibold-hanzi-014.woff2 +0 -0
  92. package/assets/fonts/semibold-hanzi-015.woff2 +0 -0
  93. package/assets/fonts/semibold-hanzi-016.woff2 +0 -0
  94. package/assets/fonts/semibold-hanzi-017.woff2 +0 -0
  95. package/assets/fonts/semibold-hanzi-018.woff2 +0 -0
  96. package/assets/fonts/semibold-hanzi-019.woff2 +0 -0
  97. package/assets/fonts/semibold-hanzi-020.woff2 +0 -0
  98. package/assets/fonts/semibold-hanzi-021.woff2 +0 -0
  99. package/assets/fonts/semibold-hanzi-022.woff2 +0 -0
  100. package/assets/fonts/semibold-hanzi-023.woff2 +0 -0
  101. package/assets/fonts/semibold-hanzi-024.woff2 +0 -0
  102. package/assets/fonts/semibold-hanzi-025.woff2 +0 -0
  103. package/assets/fonts/semibold-hanzi-026.woff2 +0 -0
  104. package/assets/fonts/semibold-hanzi-027.woff2 +0 -0
  105. package/assets/fonts/semibold-hanzi-028.woff2 +0 -0
  106. package/assets/fonts/semibold-hanzi-029.woff2 +0 -0
  107. package/assets/fonts/semibold-hanzi-030.woff2 +0 -0
  108. package/assets/fonts/semibold-hanzi-031.woff2 +0 -0
  109. package/assets/fonts/semibold-hanzi-032.woff2 +0 -0
  110. package/assets/fonts/semibold-hanzi-033.woff2 +0 -0
  111. package/assets/fonts/semibold-hanzi-034.woff2 +0 -0
  112. package/assets/fonts/semibold-hanzi-035.woff2 +0 -0
  113. package/assets/fonts/semibold-hanzi-036.woff2 +0 -0
  114. package/assets/fonts/semibold-hanzi-037.woff2 +0 -0
  115. package/assets/fonts/semibold-hanzi-038.woff2 +0 -0
  116. package/assets/fonts/semibold-hanzi-039.woff2 +0 -0
  117. package/assets/fonts/semibold-hanzi-040.woff2 +0 -0
  118. package/assets/fonts/semibold-hanzi-041.woff2 +0 -0
  119. package/assets/fonts/semibold-hanzi-042.woff2 +0 -0
  120. package/assets/fonts/semibold-hanzi-043.woff2 +0 -0
  121. package/assets/fonts/semibold-hanzi-044.woff2 +0 -0
  122. package/assets/fonts/semibold-hanzi-045.woff2 +0 -0
  123. package/assets/fonts/semibold-hanzi-046.woff2 +0 -0
  124. package/assets/fonts/semibold-hanzi-047.woff2 +0 -0
  125. package/assets/fonts/semibold-hanzi-048.woff2 +0 -0
  126. package/assets/fonts/semibold-hanzi-049.woff2 +0 -0
  127. package/assets/fonts/semibold-hanzi-050.woff2 +0 -0
  128. package/assets/fonts/semibold-hanzi-051.woff2 +0 -0
  129. package/assets/fonts/semibold-hanzi-052.woff2 +0 -0
  130. package/assets/fonts/semibold-hanzi-053.woff2 +0 -0
  131. package/assets/fonts/semibold-hanzi-054.woff2 +0 -0
  132. package/assets/fonts/semibold-hanzi-055.woff2 +0 -0
  133. package/assets/fonts/semibold-hanzi-056.woff2 +0 -0
  134. package/assets/fonts/semibold-hanzi-057.woff2 +0 -0
  135. package/assets/fonts/semibold-hanzi-058.woff2 +0 -0
  136. package/assets/fonts/semibold-hanzi-059.woff2 +0 -0
  137. package/assets/fonts/semibold-hanzi-060.woff2 +0 -0
  138. package/assets/fonts/semibold-hanzi-061.woff2 +0 -0
  139. package/assets/fonts/semibold-hanzi-062.woff2 +0 -0
  140. package/assets/fonts/semibold-hanzi-063.woff2 +0 -0
  141. package/assets/fonts/semibold-hanzi-064.woff2 +0 -0
  142. package/assets/fonts/semibold-latin.woff2 +0 -0
  143. package/assets/fonts/semibold-punctuation.woff2 +0 -0
  144. package/assets/fonts/semibold-symbols.woff2 +0 -0
  145. package/lib/client.js +577 -266
  146. package/lib/index.js +180 -2
  147. package/lib/types/client/code-font.d.ts +70 -0
  148. package/lib/types/client/locales.d.ts +12 -0
  149. package/lib/types/client/settings.d.ts +15 -0
  150. package/lib/types/comment-skill.d.ts +1 -1
  151. package/lib/types/font-slices.d.ts +58 -0
  152. package/lib/types/index.d.ts +46 -0
  153. package/package.json +8 -1
package/lib/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
- import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
2
+ import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { dshHomePath, expandHomePath } from "@deepseek-ai/dsh-home-paths";
5
6
  import "@deepseek-ai/cordis";
6
7
  import { spawn } from "node:child_process";
@@ -1318,6 +1319,10 @@ const COMMENT_SKILL = {
1318
1319
  - 只谈这几行(最多带上直接相邻的一两行),不要扩到整个文件或整个任务。
1319
1320
  - 不超过 3 行。
1320
1321
  - 行与行之间不要空行:讨论区按代码行排版,一个空行就占掉一整行高度,看起来像代码里的空洞。
1322
+ - **不要用 Markdown 的表格**(也别用列表、标题、围栏代码块这类块级语法):讨论区是**按行**排版、行高按行算的,
1323
+ 表格进来会读成一堆竖线和横线,还会把块高算错、把下面的代码顶开。要对比几项就写成一行一句,
1324
+ 例如「A 走快路径;B 走慢路径」,需要多行对比就直接说"这条在批注里说不清,去下方输入框聊"。
1325
+ 行内语法只有 \`代码\` 和 **加粗** 是被支持的。
1321
1326
  - 要引用代码就写 \`文件:行号\`,不要把整段贴回来。
1322
1327
  - 回答直接显示在批注下方的小讨论区里,所以不要写"如你所见""上面提到"这类指代。
1323
1328
  - 不确定就说不确定,并说清需要看哪一段。
@@ -1326,6 +1331,55 @@ const COMMENT_SKILL = {
1326
1331
  source: "runtime"
1327
1332
  };
1328
1333
  //#endregion
1334
+ //#region lib/types/font-slices.js
1335
+ /**
1336
+ * The bundled code font: what the panel asks for, and where the files live.
1337
+ *
1338
+ * One CJK glyph is exactly twice one Latin glyph in this face, which is what
1339
+ * puts code and Chinese comments on the same character grid. No system stack can
1340
+ * do that: the shell's code font resolves hanzi to a proportional face
1341
+ * (`"PingFang SC"`, `"Microsoft YaHei"`), so comments drift out of column on
1342
+ * Windows and Android.
1343
+ *
1344
+ * Both halves need the same names — the host serves the files under
1345
+ * {@link FONT_ROUTE}, the client asks for {@link FONT_FAMILY} — so they live
1346
+ * here rather than being written twice.
1347
+ *
1348
+ * @module dsh-diff-approval/font-slices
1349
+ */
1350
+ /**
1351
+ * The family the client declares and asks for.
1352
+ *
1353
+ * It names the actual face — this is JetBrains Maple Mono, subset and sliced by
1354
+ * this plugin — and carries the project suffix so it cannot collide with a copy
1355
+ * of the upstream font installed on the reader's machine. That collision would
1356
+ * be silent and one-sided: a locally installed font wins over a webfont of the
1357
+ * same name, so the slices would never be fetched and the bytes on screen would
1358
+ * be someone else's build. The trailing marker also keeps the upstream names out
1359
+ * of a user-visible primary name, which is what the OFL asks of a modified
1360
+ * version (Maple Mono declares `Maple Mono` as a Reserved Font Name).
1361
+ */
1362
+ const FONT_FAMILY = "JetBrains Maple Mono for DSH";
1363
+ /**
1364
+ * The URL prefix the host mounts. The client resolves it against the document's
1365
+ * own origin, so a tunnel or a non-default port needs no configuration.
1366
+ */
1367
+ const FONT_ROUTE = "/dsh-diff-approval/fonts";
1368
+ [
1369
+ `"${FONT_FAMILY}"`,
1370
+ "\"SF Mono\"",
1371
+ "\"JetBrains Mono\"",
1372
+ "\"Fira Code\"",
1373
+ "Consolas",
1374
+ "\"Liberation Mono\"",
1375
+ "Menlo",
1376
+ "Courier",
1377
+ "\"PingFang SC\"",
1378
+ "\"Microsoft YaHei\""
1379
+ ].join(", ");
1380
+ /** Where the generated slices and their manifest live, relative to `lib/`. */
1381
+ const FONT_ASSET_DIR = "../assets/fonts";
1382
+ //#endregion
1329
1383
  //#region lib/types/vcs.js
1330
1384
  /**
1331
1385
  * Version-control integration, host half: detect which VCS (git/svn/p4)
@@ -3216,6 +3270,15 @@ function apply(ctx, config) {
3216
3270
  const service = ctx.connection;
3217
3271
  return typeof service.register === "function" ? service.register(ctx, DIFF_APPROVAL_CHANNEL, handle, { authority: "trusted-host" }) : service.rpc.handle(DIFF_APPROVAL_CHANNEL, handle, { authority: "trusted-host" });
3218
3272
  }, "diff-approval: review channel");
3273
+ ctx.effect(() => {
3274
+ const route = fontRoute((message) => ctx.logger.warn(message));
3275
+ const server = ctx.get("webServer");
3276
+ if (typeof server?.register !== "function") {
3277
+ ctx.logger.debug("diff-approval: no web server, so the bundled code font is not served");
3278
+ return () => {};
3279
+ }
3280
+ return server.register(route);
3281
+ }, "diff-approval: code font slices");
3219
3282
  ctx.effect(() => ctx.on("fs/edit-intent", async (target, actor, next) => {
3220
3283
  await stashEditorIntent(target, actor, "edit");
3221
3284
  return next();
@@ -3355,5 +3418,120 @@ function openTargetOf(payload) {
3355
3418
  action
3356
3419
  };
3357
3420
  }
3421
+ /** The font slices the host is willing to serve, read once from the manifest. */
3422
+ let fontSlicesPromise;
3423
+ /** Read `assets/fonts/manifest.json` next to the built bundle. */
3424
+ async function loadFontSlices() {
3425
+ try {
3426
+ const dir = fileURLToPath(new URL(FONT_ASSET_DIR, import.meta.url));
3427
+ const text = await readFile(join(dir, "manifest.json"), "utf8");
3428
+ const manifest = JSON.parse(text);
3429
+ const slices = /* @__PURE__ */ new Map();
3430
+ for (const slice of manifest.slices ?? []) slices.set(slice.file, slice);
3431
+ return slices;
3432
+ } catch {
3433
+ return;
3434
+ }
3435
+ }
3436
+ /**
3437
+ * The manifest, cached for the life of the host — but only once it has been
3438
+ * read. A missing `assets/fonts` is deliberately not cached: a deployment that
3439
+ * puts the assets beside a running bundle (a profile reinstall after the
3440
+ * package's file list grew) starts serving slices on the next request instead
3441
+ * of needing a restart, at the cost of one failed read per request until then.
3442
+ */
3443
+ async function fontSlices() {
3444
+ if (fontSlicesPromise === void 0) {
3445
+ const pending = loadFontSlices();
3446
+ fontSlicesPromise = pending;
3447
+ const slices = await pending;
3448
+ if (slices === void 0) fontSlicesPromise = void 0;
3449
+ return slices;
3450
+ }
3451
+ return fontSlicesPromise;
3452
+ }
3453
+ /**
3454
+ * Drop the cached slice list, so the next request reads the manifest again.
3455
+ *
3456
+ * The cache exists so a served page costs one read, which makes it state that
3457
+ * outlives a test: tests that need the "no manifest" path call this between
3458
+ * cases. Nothing in the app calls it.
3459
+ */
3460
+ function resetFontSlicesForTests() {
3461
+ fontSlicesPromise = void 0;
3462
+ }
3463
+ /**
3464
+ * Answer one font request. Exported so the route's behaviour is testable
3465
+ * without a listening server: the manifest itself is answered from the parsed
3466
+ * list, only files that list names are ever read, and everything else —
3467
+ * including a path traversal attempt — is a 404.
3468
+ *
3469
+ * @param req - the request, for its path.
3470
+ * @param res - the response, whose lifecycle this owns.
3471
+ * @param nominate - how to report a missing manifest, called at most once per
3472
+ * route by {@link fontRoute}. Without the manifest every slice is a 404, and
3473
+ * that is a deployment problem the reader cannot see from the panel: the
3474
+ * switch is on, the font is simply absent, and nothing else says why.
3475
+ */
3476
+ async function serveFontSlice(req, res, nominate) {
3477
+ const slices = await fontSlices();
3478
+ if (slices === void 0) {
3479
+ nominate?.(`diff-approval: no code-font manifest at ${join(fileURLToPath(new URL(FONT_ASSET_DIR, import.meta.url)), "manifest.json")}, so the bundled font cannot be served (reinstall the plugin so its assets arrive, then restart the host)`);
3480
+ res.statusCode = 404;
3481
+ res.end("font unavailable");
3482
+ return;
3483
+ }
3484
+ const name = decodeURIComponent((req.url ?? "").split("?")[0] ?? "").split("/").pop() ?? "";
3485
+ if (name === "manifest.json") {
3486
+ res.statusCode = 200;
3487
+ res.setHeader("content-type", "application/json; charset=utf-8");
3488
+ res.setHeader("cache-control", "no-store");
3489
+ res.end(JSON.stringify({ slices: [...slices.values()].map(({ file, unicodeRange, weight }) => ({
3490
+ file,
3491
+ unicodeRange,
3492
+ weight
3493
+ })) }));
3494
+ return;
3495
+ }
3496
+ const slice = slices.get(name);
3497
+ if (slice === void 0 || !/^[a-z]+-[a-z0-9-]*\.woff2$/.test(name)) {
3498
+ res.statusCode = 404;
3499
+ res.end("no such font slice");
3500
+ return;
3501
+ }
3502
+ try {
3503
+ const path = join(fileURLToPath(new URL(FONT_ASSET_DIR, import.meta.url)), slice.file);
3504
+ const info = await stat(path);
3505
+ const body = await readFile(path);
3506
+ res.statusCode = 200;
3507
+ res.setHeader("content-type", "font/woff2");
3508
+ res.setHeader("content-length", String(info.size));
3509
+ res.setHeader("cache-control", "public, max-age=31536000, immutable");
3510
+ res.end(body);
3511
+ } catch {
3512
+ res.statusCode = 404;
3513
+ res.end("font slice missing");
3514
+ }
3515
+ }
3516
+ /**
3517
+ * The route serving the bundled font.
3518
+ *
3519
+ * @param warn - where the once-per-route "no manifest beside the bundle" line
3520
+ * goes. It is the only signal a reader's switch gives when the deployment,
3521
+ * not the switch, is what is missing.
3522
+ * @returns the prefix route.
3523
+ */
3524
+ function fontRoute(warn) {
3525
+ let reported = false;
3526
+ return {
3527
+ kind: "prefix",
3528
+ path: FONT_ROUTE,
3529
+ handler: (req, res) => serveFontSlice(req, res, (message) => {
3530
+ if (reported) return;
3531
+ reported = true;
3532
+ warn(message);
3533
+ })
3534
+ };
3535
+ }
3358
3536
  //#endregion
3359
- export { COMMENT_SKILL_NAME, DIFF_APPROVAL_CHANNEL, PendingDiffStore, PendingPersistence, apply, defaultOpenPath, defaultStorageDir, inject, name };
3537
+ export { COMMENT_SKILL_NAME, DIFF_APPROVAL_CHANNEL, PendingDiffStore, PendingPersistence, apply, defaultOpenPath, defaultStorageDir, fontRoute, inject, name, resetFontSlicesForTests, serveFontSlice };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Load the bundled code font and put the diff's code on its grid.
3
+ *
4
+ * The face is dual-width: one CJK glyph is exactly twice one Latin glyph, so
5
+ * code and Chinese comments share a character grid. The shell's own code font
6
+ * cannot do that — it resolves hanzi to a proportional face (`"PingFang SC"`,
7
+ * `"Microsoft YaHei"`), so a comment that starts in column 40 of the code ends
8
+ * somewhere else entirely on Windows and Android, where the browser has no
9
+ * monospaced CJK face at all.
10
+ *
11
+ * The rule is one selector wide: **the code column only**, `.lines`. Comment
12
+ * bubbles, the file list and the panel's chrome keep the shell's font stack; the
13
+ * code is where the grid is the point.
14
+ *
15
+ * **Off until asked for.** The slices are tens of kilobytes for ordinary text
16
+ * and up to ~120 KB for a rare character, which is traffic the reader pays for,
17
+ * so the Settings switch is the gate and its description says so. Off is not
18
+ * just "no rule": nothing is fetched at all, so the default panel is byte-for-
19
+ * byte and pixel-for-pixel what it was before this existed. A browser fetches a
20
+ * `@font-face` only when a rule uses it, which is what keeps an enabled pure-
21
+ * ASCII file down to the one Latin slice.
22
+ *
23
+ * The face's own `calt` stays on, so `->` and `=>` draw as the arrows JetBrains
24
+ * Mono already draws everywhere else in this UI. The upstream project also
25
+ * builds a no-ligature variant (`…-NL-…`) if that ever stops being wanted; it
26
+ * would mean re-slicing, not a CSS override, because `calt` is what carries the
27
+ * arrows.
28
+ *
29
+ * @module dsh-diff-approval/client/code-font
30
+ */
31
+ /** One slice as the manifest lists it. */
32
+ interface ListedSlice {
33
+ readonly file: string;
34
+ readonly unicodeRange: string;
35
+ readonly weight: number;
36
+ }
37
+ /**
38
+ * The `@font-face` rules for every slice of every weight, followed by the one
39
+ * rule that uses them: the code column.
40
+ *
41
+ * `font-display: swap` rather than `block`: the code is readable in the system
42
+ * font immediately, and swapping when the slices land only changes the column
43
+ * width, never the row height that the virtual window and the jump math are
44
+ * built on.
45
+ *
46
+ * @param slices - the manifest's slice list.
47
+ * @param codeClass - the class the code table carries, defaulting to the one the
48
+ * panel's stylesheet exports. It is a parameter so a test can pin the emitted
49
+ * selector without importing the panel.
50
+ */
51
+ export declare function codeFontCss(slices: readonly ListedSlice[], codeClass?: string): string;
52
+ /**
53
+ * Drop the cached slice list and the injected rules.
54
+ *
55
+ * The manifest is read once per page on purpose — it never changes under a live
56
+ * client — so this exists for tests, which need each case to start from the
57
+ * same empty state. Nothing in the app calls it.
58
+ */
59
+ export declare function resetCodeFontForTests(): void;
60
+ /**
61
+ * Follow the preference for as long as the client lives: apply it now, and again
62
+ * whenever the Settings switch flips. A host without the slices (a source
63
+ * checkout that never ran the font build, or an install that arrived without
64
+ * `assets/`) keeps the system stack, and says so once on the console: the
65
+ * switch is on, so silence would read as "the font does nothing".
66
+ *
67
+ * @returns a disposer that stops following and removes the injected rules.
68
+ */
69
+ export declare function attachCodeFont(): () => void;
70
+ export {};
@@ -46,6 +46,8 @@ export declare const zh: {
46
46
  'panel.diffLineHeightDesc': string;
47
47
  'panel.mdPreview': string;
48
48
  'panel.mdPreviewDesc': string;
49
+ 'panel.codeFont': string;
50
+ 'panel.codeFontDesc': string;
49
51
  'panel.mdMaxWidth': string;
50
52
  'panel.mdMaxWidthDesc': string;
51
53
  'panel.diffAddColor': string;
@@ -96,6 +98,9 @@ export declare const zh: {
96
98
  'row.create': string;
97
99
  'row.failed': string;
98
100
  'row.dismiss': string;
101
+ 'row.keepListed': string;
102
+ 'row.keepRemove': string;
103
+ 'row.revertRemove': string;
99
104
  'row.added': string;
100
105
  'row.removed': string;
101
106
  'action.keep': string;
@@ -216,6 +221,7 @@ export declare const zh: {
216
221
  'panel.addNoEntries': string;
217
222
  'panel.addMissing': string;
218
223
  'panel.addOutside': string;
224
+ 'panel.addNotAFile': string;
219
225
  'panel.addFailed': string;
220
226
  'action.cancel': string;
221
227
  'action.collapseRow': string;
@@ -278,6 +284,8 @@ export declare const en: {
278
284
  'panel.diffLineHeightDesc': string;
279
285
  'panel.mdPreview': string;
280
286
  'panel.mdPreviewDesc': string;
287
+ 'panel.codeFont': string;
288
+ 'panel.codeFontDesc': string;
281
289
  'panel.mdMaxWidth': string;
282
290
  'panel.mdMaxWidthDesc': string;
283
291
  'panel.diffAddColor': string;
@@ -328,6 +336,9 @@ export declare const en: {
328
336
  'row.create': string;
329
337
  'row.failed': string;
330
338
  'row.dismiss': string;
339
+ 'row.keepListed': string;
340
+ 'row.keepRemove': string;
341
+ 'row.revertRemove': string;
331
342
  'row.added': string;
332
343
  'row.removed': string;
333
344
  'action.keep': string;
@@ -448,6 +459,7 @@ export declare const en: {
448
459
  'panel.addNoEntries': string;
449
460
  'panel.addMissing': string;
450
461
  'panel.addOutside': string;
462
+ 'panel.addNotAFile': string;
451
463
  'panel.addFailed': string;
452
464
  'action.cancel': string;
453
465
  'action.collapseRow': string;
@@ -217,6 +217,21 @@ export declare function commentModeEnabled(): boolean;
217
217
  export declare const COMMENT_MODE_CHANGED_EVENT = "diff-approval:comment-mode";
218
218
  /** Persist the comment-mode preference. */
219
219
  export declare function setCommentModeEnabled(value: boolean): void;
220
+ /**
221
+ * Whether the diff's code uses the font this plugin bundles.
222
+ *
223
+ * Defaults to **off**: the bundled face is JetBrains Maple Mono subset and
224
+ * sliced into 138 woff2 files, and a page pulls the slices its characters land
225
+ * in — tens of kilobytes for a comment, up to ~120 KB for a rare character. That
226
+ * is real traffic, so it is the reader's decision, not a default they discover.
227
+ * Only an explicit `'1'` enables it.
228
+ * @returns whether the bundled code font is enabled.
229
+ */
230
+ export declare function codeFontEnabled(): boolean;
231
+ /** Window event: the bundled code font was turned on or off (see {@link setCodeFontEnabled}). */
232
+ export declare const CODE_FONT_CHANGED_EVENT = "diff-approval:code-font";
233
+ /** Persist the bundled-code-font preference. */
234
+ export declare function setCodeFontEnabled(value: boolean): void;
220
235
  /**
221
236
  * The Markdown-preview content max width, in pixels (single column). Defaults to
222
237
  * 800; the double-column view totals 2x this. An out-of-range or non-integer
@@ -21,7 +21,7 @@ export declare const COMMENT_SKILL_DESCRIPTION: string;
21
21
  /** Extra routing guidance for the catalog. */
22
22
  export declare const COMMENT_SKILL_WHEN_TO_USE: string;
23
23
  /** The instruction body the agent loads, in the language its comments arrive in. */
24
- export declare const COMMENT_SKILL_CONTENT = "# \u56DE\u7B54\u4EE3\u7801\u6279\u6CE8\n\n\u6D88\u606F\u4EE5 `[\u8BC4\u8BBA] (path:lines)` \u5F00\u5934\u65F6\uFF0C\u5C31\u662F diff \u9762\u677F\u91CC\u5BF9\u90A3\u51E0\u884C\u4EE3\u7801\u63D0\u7684\u6279\u6CE8\u3002\u6309\u4E0B\u9762\u7684\u89C4\u5219\u56DE\u7B54\uFF1A\n\n- \u53EA\u8C08\u8FD9\u51E0\u884C\uFF08\u6700\u591A\u5E26\u4E0A\u76F4\u63A5\u76F8\u90BB\u7684\u4E00\u4E24\u884C\uFF09\uFF0C\u4E0D\u8981\u6269\u5230\u6574\u4E2A\u6587\u4EF6\u6216\u6574\u4E2A\u4EFB\u52A1\u3002\n- \u4E0D\u8D85\u8FC7 3 \u884C\u3002\n- \u884C\u4E0E\u884C\u4E4B\u95F4\u4E0D\u8981\u7A7A\u884C\uFF1A\u8BA8\u8BBA\u533A\u6309\u4EE3\u7801\u884C\u6392\u7248\uFF0C\u4E00\u4E2A\u7A7A\u884C\u5C31\u5360\u6389\u4E00\u6574\u884C\u9AD8\u5EA6\uFF0C\u770B\u8D77\u6765\u50CF\u4EE3\u7801\u91CC\u7684\u7A7A\u6D1E\u3002\n- \u8981\u5F15\u7528\u4EE3\u7801\u5C31\u5199 `\u6587\u4EF6:\u884C\u53F7`\uFF0C\u4E0D\u8981\u628A\u6574\u6BB5\u8D34\u56DE\u6765\u3002\n- \u56DE\u7B54\u76F4\u63A5\u663E\u793A\u5728\u6279\u6CE8\u4E0B\u65B9\u7684\u5C0F\u8BA8\u8BBA\u533A\u91CC\uFF0C\u6240\u4EE5\u4E0D\u8981\u5199\"\u5982\u4F60\u6240\u89C1\"\"\u4E0A\u9762\u63D0\u5230\"\u8FD9\u7C7B\u6307\u4EE3\u3002\n- \u4E0D\u786E\u5B9A\u5C31\u8BF4\u4E0D\u786E\u5B9A\uFF0C\u5E76\u8BF4\u6E05\u9700\u8981\u770B\u54EA\u4E00\u6BB5\u3002\n- 3 \u884C\u91CC\u8BF4\u4E0D\u6E05\uFF08\u8981\u5C55\u5F00\u3001\u8981\u770B\u66F4\u591A\u4EE3\u7801\u3001\u8981\u6765\u56DE\u51E0\u8F6E\uFF09\uFF0C\u5C31\u76F4\u8BF4\u4E00\u53E5\"\u8FD9\u6761\u5728\u6279\u6CE8\u91CC\u8BF4\u4E0D\u6E05\uFF0C\u53BB\u4E0B\u65B9\u53D1\u6D88\u606F\u7684\u8F93\u5165\u6846\u91CC\u804A\"\uFF0C\u4E0D\u8981\u5728\u6279\u6CE8\u91CC\u786C\u585E\u957F\u56DE\u7B54\u3002\n";
24
+ export declare const COMMENT_SKILL_CONTENT = "# \u56DE\u7B54\u4EE3\u7801\u6279\u6CE8\n\n\u6D88\u606F\u4EE5 `[\u8BC4\u8BBA] (path:lines)` \u5F00\u5934\u65F6\uFF0C\u5C31\u662F diff \u9762\u677F\u91CC\u5BF9\u90A3\u51E0\u884C\u4EE3\u7801\u63D0\u7684\u6279\u6CE8\u3002\u6309\u4E0B\u9762\u7684\u89C4\u5219\u56DE\u7B54\uFF1A\n\n- \u53EA\u8C08\u8FD9\u51E0\u884C\uFF08\u6700\u591A\u5E26\u4E0A\u76F4\u63A5\u76F8\u90BB\u7684\u4E00\u4E24\u884C\uFF09\uFF0C\u4E0D\u8981\u6269\u5230\u6574\u4E2A\u6587\u4EF6\u6216\u6574\u4E2A\u4EFB\u52A1\u3002\n- \u4E0D\u8D85\u8FC7 3 \u884C\u3002\n- \u884C\u4E0E\u884C\u4E4B\u95F4\u4E0D\u8981\u7A7A\u884C\uFF1A\u8BA8\u8BBA\u533A\u6309\u4EE3\u7801\u884C\u6392\u7248\uFF0C\u4E00\u4E2A\u7A7A\u884C\u5C31\u5360\u6389\u4E00\u6574\u884C\u9AD8\u5EA6\uFF0C\u770B\u8D77\u6765\u50CF\u4EE3\u7801\u91CC\u7684\u7A7A\u6D1E\u3002\n- **\u4E0D\u8981\u7528 Markdown \u7684\u8868\u683C**\uFF08\u4E5F\u522B\u7528\u5217\u8868\u3001\u6807\u9898\u3001\u56F4\u680F\u4EE3\u7801\u5757\u8FD9\u7C7B\u5757\u7EA7\u8BED\u6CD5\uFF09\uFF1A\u8BA8\u8BBA\u533A\u662F**\u6309\u884C**\u6392\u7248\u3001\u884C\u9AD8\u6309\u884C\u7B97\u7684\uFF0C\n \u8868\u683C\u8FDB\u6765\u4F1A\u8BFB\u6210\u4E00\u5806\u7AD6\u7EBF\u548C\u6A2A\u7EBF\uFF0C\u8FD8\u4F1A\u628A\u5757\u9AD8\u7B97\u9519\u3001\u628A\u4E0B\u9762\u7684\u4EE3\u7801\u9876\u5F00\u3002\u8981\u5BF9\u6BD4\u51E0\u9879\u5C31\u5199\u6210\u4E00\u884C\u4E00\u53E5\uFF0C\n \u4F8B\u5982\u300CA \u8D70\u5FEB\u8DEF\u5F84\uFF1BB \u8D70\u6162\u8DEF\u5F84\u300D\uFF0C\u9700\u8981\u591A\u884C\u5BF9\u6BD4\u5C31\u76F4\u63A5\u8BF4\"\u8FD9\u6761\u5728\u6279\u6CE8\u91CC\u8BF4\u4E0D\u6E05\uFF0C\u53BB\u4E0B\u65B9\u8F93\u5165\u6846\u804A\"\u3002\n \u884C\u5185\u8BED\u6CD5\u53EA\u6709 `\u4EE3\u7801` \u548C **\u52A0\u7C97** \u662F\u88AB\u652F\u6301\u7684\u3002\n- \u8981\u5F15\u7528\u4EE3\u7801\u5C31\u5199 `\u6587\u4EF6:\u884C\u53F7`\uFF0C\u4E0D\u8981\u628A\u6574\u6BB5\u8D34\u56DE\u6765\u3002\n- \u56DE\u7B54\u76F4\u63A5\u663E\u793A\u5728\u6279\u6CE8\u4E0B\u65B9\u7684\u5C0F\u8BA8\u8BBA\u533A\u91CC\uFF0C\u6240\u4EE5\u4E0D\u8981\u5199\"\u5982\u4F60\u6240\u89C1\"\"\u4E0A\u9762\u63D0\u5230\"\u8FD9\u7C7B\u6307\u4EE3\u3002\n- \u4E0D\u786E\u5B9A\u5C31\u8BF4\u4E0D\u786E\u5B9A\uFF0C\u5E76\u8BF4\u6E05\u9700\u8981\u770B\u54EA\u4E00\u6BB5\u3002\n- 3 \u884C\u91CC\u8BF4\u4E0D\u6E05\uFF08\u8981\u5C55\u5F00\u3001\u8981\u770B\u66F4\u591A\u4EE3\u7801\u3001\u8981\u6765\u56DE\u51E0\u8F6E\uFF09\uFF0C\u5C31\u76F4\u8BF4\u4E00\u53E5\"\u8FD9\u6761\u5728\u6279\u6CE8\u91CC\u8BF4\u4E0D\u6E05\uFF0C\u53BB\u4E0B\u65B9\u53D1\u6D88\u606F\u7684\u8F93\u5165\u6846\u91CC\u804A\"\uFF0C\u4E0D\u8981\u5728\u6279\u6CE8\u91CC\u786C\u585E\u957F\u56DE\u7B54\u3002\n";
25
25
  /** One runtime skill contribution, in the shape `ctx.skills.register` takes. */
26
26
  export interface CommentSkillRegistration {
27
27
  /** Kebab-case identity the agent loads by. */
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The bundled code font: what the panel asks for, and where the files live.
3
+ *
4
+ * One CJK glyph is exactly twice one Latin glyph in this face, which is what
5
+ * puts code and Chinese comments on the same character grid. No system stack can
6
+ * do that: the shell's code font resolves hanzi to a proportional face
7
+ * (`"PingFang SC"`, `"Microsoft YaHei"`), so comments drift out of column on
8
+ * Windows and Android.
9
+ *
10
+ * Both halves need the same names — the host serves the files under
11
+ * {@link FONT_ROUTE}, the client asks for {@link FONT_FAMILY} — so they live
12
+ * here rather than being written twice.
13
+ *
14
+ * @module dsh-diff-approval/font-slices
15
+ */
16
+ /**
17
+ * The family the client declares and asks for.
18
+ *
19
+ * It names the actual face — this is JetBrains Maple Mono, subset and sliced by
20
+ * this plugin — and carries the project suffix so it cannot collide with a copy
21
+ * of the upstream font installed on the reader's machine. That collision would
22
+ * be silent and one-sided: a locally installed font wins over a webfont of the
23
+ * same name, so the slices would never be fetched and the bytes on screen would
24
+ * be someone else's build. The trailing marker also keeps the upstream names out
25
+ * of a user-visible primary name, which is what the OFL asks of a modified
26
+ * version (Maple Mono declares `Maple Mono` as a Reserved Font Name).
27
+ */
28
+ export declare const FONT_FAMILY = "JetBrains Maple Mono for DSH";
29
+ /**
30
+ * The URL prefix the host mounts. The client resolves it against the document's
31
+ * own origin, so a tunnel or a non-default port needs no configuration.
32
+ */
33
+ export declare const FONT_ROUTE = "/dsh-diff-approval/fonts";
34
+ /** One weight of the face. Both are the same subset, so the ranges are shared. */
35
+ export interface FontFace {
36
+ /** File-name infix: `regular-hanzi-000.woff2`. */
37
+ readonly name: string;
38
+ /** The weight the CSS declares; the slice files are per weight. */
39
+ readonly weight: number;
40
+ }
41
+ /** Regular carries the code; the Semibold only backs emphasised text. */
42
+ export declare const FONT_FACES: readonly FontFace[];
43
+ /**
44
+ * The code font stack. The bundled family comes first so its 2:1 grid wins,
45
+ * and the previous stack stays behind it for anything the slices do not cover —
46
+ * Korean, CJK Ext-A/B, and the handful of codepoints this face leaves unmapped.
47
+ */
48
+ export declare const FONT_STACK: string;
49
+ /** One slice file as the manifest records it. */
50
+ export interface FontSlice {
51
+ readonly file: string;
52
+ readonly bytes: number;
53
+ readonly unicodeRange: string;
54
+ readonly kind: 'latin' | 'punctuation' | 'symbols' | 'cjk-punctuation' | 'hanzi';
55
+ readonly weight: number;
56
+ }
57
+ /** Where the generated slices and their manifest live, relative to `lib/`. */
58
+ export declare const FONT_ASSET_DIR = "../assets/fonts";
@@ -73,3 +73,49 @@ export interface DiffApprovalConfig {
73
73
  * @param config - optional plugin configuration (`storageDir` relocates durable state).
74
74
  */
75
75
  export declare function apply(ctx: Context, config?: DiffApprovalConfig): void;
76
+ /** The parts of node's request the font handler reads. */
77
+ interface WebServerRequest {
78
+ readonly url?: string | undefined;
79
+ readonly method?: string | undefined;
80
+ }
81
+ /** The parts of node's response the font handler writes. */
82
+ interface WebServerResponse {
83
+ statusCode: number;
84
+ setHeader(name: string, value: string): unknown;
85
+ end(body?: Uint8Array | string): unknown;
86
+ }
87
+ /**
88
+ * Drop the cached slice list, so the next request reads the manifest again.
89
+ *
90
+ * The cache exists so a served page costs one read, which makes it state that
91
+ * outlives a test: tests that need the "no manifest" path call this between
92
+ * cases. Nothing in the app calls it.
93
+ */
94
+ export declare function resetFontSlicesForTests(): void;
95
+ /**
96
+ * Answer one font request. Exported so the route's behaviour is testable
97
+ * without a listening server: the manifest itself is answered from the parsed
98
+ * list, only files that list names are ever read, and everything else —
99
+ * including a path traversal attempt — is a 404.
100
+ *
101
+ * @param req - the request, for its path.
102
+ * @param res - the response, whose lifecycle this owns.
103
+ * @param nominate - how to report a missing manifest, called at most once per
104
+ * route by {@link fontRoute}. Without the manifest every slice is a 404, and
105
+ * that is a deployment problem the reader cannot see from the panel: the
106
+ * switch is on, the font is simply absent, and nothing else says why.
107
+ */
108
+ export declare function serveFontSlice(req: WebServerRequest, res: WebServerResponse, nominate?: (message: string) => void): Promise<void>;
109
+ /**
110
+ * The route serving the bundled font.
111
+ *
112
+ * @param warn - where the once-per-route "no manifest beside the bundle" line
113
+ * goes. It is the only signal a reader's switch gives when the deployment,
114
+ * not the switch, is what is missing.
115
+ * @returns the prefix route.
116
+ */
117
+ export declare function fontRoute(warn: (message: string) => void): {
118
+ kind: 'prefix';
119
+ path: string;
120
+ handler: (req: WebServerRequest, res: WebServerResponse) => Promise<void>;
121
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-diff-approval",
3
- "version": "0.28.0",
3
+ "version": "0.29.1",
4
4
  "description": "DeepSeek Harness plugin: pending-edit review with whole-file diff and Keep/Revert",
5
5
  "packageManager": "pnpm@11.21.0",
6
6
  "author": "Wu Zhiwei",
@@ -31,6 +31,11 @@
31
31
  "lib/index.js",
32
32
  "lib/client.js",
33
33
  "lib/types/**/*.d.ts",
34
+ "assets/fonts/*.woff2",
35
+ "assets/fonts/manifest.json",
36
+ "assets/fonts/OFL.txt",
37
+ "assets/fonts/FONTLOG.md",
38
+ "assets/fonts/README.md",
34
39
  "cordis.patch.yml",
35
40
  "docs/images/**"
36
41
  ],
@@ -53,6 +58,8 @@
53
58
  "build": "tsc -b tsconfig.json && tsdown --env.DSH_BUILD_FACE client",
54
59
  "test": "vitest run",
55
60
  "compat": "node scripts/dsh-compat.mjs",
61
+ "fonts:check": "python -m scripts.fonts.build --check",
62
+ "fonts:plan": "python -m scripts.fonts.build --plan",
56
63
  "prepare": "tsc -b tsconfig.json && tsdown --env.DSH_BUILD_FACE client",
57
64
  "release": "release-it"
58
65
  },