dsh-diff-approval 0.27.0 → 0.29.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.
Files changed (157) hide show
  1. package/README.md +4 -0
  2. package/README.zh.md +4 -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 +1026 -437
  146. package/lib/index.js +132 -3
  147. package/lib/types/client/PendingPanel.d.ts +10 -0
  148. package/lib/types/client/code-font.d.ts +64 -0
  149. package/lib/types/client/locales.d.ts +14 -2
  150. package/lib/types/client/panel-memory.d.ts +12 -0
  151. package/lib/types/client/settings.d.ts +15 -0
  152. package/lib/types/client/slots.d.ts +6 -0
  153. package/lib/types/comment-skill.d.ts +1 -1
  154. package/lib/types/font-slices.d.ts +58 -0
  155. package/lib/types/index.d.ts +20 -0
  156. package/lib/types/types.d.ts +6 -0
  157. 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)
@@ -2482,7 +2536,8 @@ function apply(ctx, config) {
2482
2536
  files,
2483
2537
  workspacePath: workspaceOf(sessionId)?.path,
2484
2538
  redoCleared: redoCleared || void 0,
2485
- commentSkill: commentSkill()
2539
+ commentSkill: commentSkill(),
2540
+ persistError: persistFailureReported
2486
2541
  }
2487
2542
  };
2488
2543
  }
@@ -3215,6 +3270,15 @@ function apply(ctx, config) {
3215
3270
  const service = ctx.connection;
3216
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" });
3217
3272
  }, "diff-approval: review channel");
3273
+ ctx.effect(() => {
3274
+ const route = fontRoute();
3275
+ const server = ctx.get("webServer");
3276
+ if (route === void 0 || 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");
3218
3282
  ctx.effect(() => ctx.on("fs/edit-intent", async (target, actor, next) => {
3219
3283
  await stashEditorIntent(target, actor, "edit");
3220
3284
  return next();
@@ -3354,5 +3418,70 @@ function openTargetOf(payload) {
3354
3418
  action
3355
3419
  };
3356
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, once. */
3424
+ function fontSlices() {
3425
+ fontSlicesPromise ??= (async () => {
3426
+ try {
3427
+ const dir = fileURLToPath(new URL(FONT_ASSET_DIR, import.meta.url));
3428
+ const text = await readFile(join(dir, "manifest.json"), "utf8");
3429
+ const manifest = JSON.parse(text);
3430
+ const slices = /* @__PURE__ */ new Map();
3431
+ for (const slice of manifest.slices ?? []) slices.set(slice.file, slice);
3432
+ return slices;
3433
+ } catch {
3434
+ return;
3435
+ }
3436
+ })();
3437
+ return fontSlicesPromise;
3438
+ }
3439
+ /**
3440
+ * Answer one font request. Exported so the route's behaviour is testable
3441
+ * without a listening server: only files the manifest lists are ever read, and
3442
+ * everything else — including a path traversal attempt — is a 404.
3443
+ *
3444
+ * @param req - the request, for its path.
3445
+ * @param res - the response, whose lifecycle this owns.
3446
+ */
3447
+ async function serveFontSlice(req, res) {
3448
+ const slices = await fontSlices();
3449
+ if (slices === void 0) {
3450
+ res.statusCode = 404;
3451
+ res.end("font unavailable");
3452
+ return;
3453
+ }
3454
+ const name = decodeURIComponent((req.url ?? "").split("?")[0] ?? "").split("/").pop() ?? "";
3455
+ const slice = slices.get(name);
3456
+ if (slice === void 0 || !/^[a-z]+-[a-z0-9-]*\.woff2$/.test(name)) {
3457
+ res.statusCode = 404;
3458
+ res.end("no such font slice");
3459
+ return;
3460
+ }
3461
+ try {
3462
+ const path = join(fileURLToPath(new URL(FONT_ASSET_DIR, import.meta.url)), slice.file);
3463
+ const info = await stat(path);
3464
+ const body = await readFile(path);
3465
+ res.statusCode = 200;
3466
+ res.setHeader("content-type", "font/woff2");
3467
+ res.setHeader("content-length", String(info.size));
3468
+ res.setHeader("cache-control", "public, max-age=31536000, immutable");
3469
+ res.end(body);
3470
+ } catch {
3471
+ res.statusCode = 404;
3472
+ res.end("font slice missing");
3473
+ }
3474
+ }
3475
+ /**
3476
+ * The route serving the bundled font, or undefined when the slices were never
3477
+ * generated (a source checkout without the build step).
3478
+ */
3479
+ function fontRoute() {
3480
+ return {
3481
+ kind: "prefix",
3482
+ path: FONT_ROUTE,
3483
+ handler: serveFontSlice
3484
+ };
3485
+ }
3357
3486
  //#endregion
3358
- export { COMMENT_SKILL_NAME, DIFF_APPROVAL_CHANNEL, PendingDiffStore, PendingPersistence, apply, defaultOpenPath, defaultStorageDir, inject, name };
3487
+ export { COMMENT_SKILL_NAME, DIFF_APPROVAL_CHANNEL, PendingDiffStore, PendingPersistence, apply, defaultOpenPath, defaultStorageDir, inject, name, serveFontSlice };
@@ -155,6 +155,10 @@ export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
155
155
  renderDiscussion?: (discussion: Discussion, bodyWidth: number, split: boolean) => ReactNode;
156
156
  /** The comment action for the current selection, when the panel offers one for it. */
157
157
  selectionComment?: ReactNode;
158
+ /** The changed-line runs the overview ruler draws, in whole-file row indices. */
159
+ rulerRuns: readonly RulerRun[];
160
+ /** The go-to popup, which this view centres on its own box (see `gotoDialog`). */
161
+ gotoDialog?: ReactNode;
158
162
  } & import("react").RefAttributes<SplitDiffHandle>>;
159
163
  /**
160
164
  * Reconstruct the plain text of the current selection so auto-wrap's visual
@@ -164,6 +168,12 @@ export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
164
168
  * without a newline while the real newline between diff rows is kept.
165
169
  */
166
170
  export declare function selectedPlainText(): string | undefined;
171
+ /** One maximal run of same-kind changed rows: what both views' rulers are built from. */
172
+ interface RulerRun {
173
+ start: number;
174
+ end: number;
175
+ kind: 'del' | 'add';
176
+ }
167
177
  /** Render the pending-edit review panel and its unified footer action. */
168
178
  export declare function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onBlockKeep, onBlockRevert, onOpen, onPreviewImage, onPasteReference, onAskAgent, watchChat, onUndo, onRedo, onImportVcs, onRefreshVcs, onBrowse, onAddPath, onKeepAll, onRevertAll, onAckRedoCleared, collapseSidebar, t, docked, dockHost, onOpenDock, closeDock, useDock, }: PendingPanelProps): import("react").JSX.Element;
169
179
  export {};
@@ -0,0 +1,64 @@
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
+ export declare function codeFontCss(slices: readonly ListedSlice[]): string;
47
+ /**
48
+ * Drop the cached slice list and the injected rules.
49
+ *
50
+ * The manifest is read once per page on purpose — it never changes under a live
51
+ * client — so this exists for tests, which need each case to start from the
52
+ * same empty state. Nothing in the app calls it.
53
+ */
54
+ export declare function resetCodeFontForTests(): void;
55
+ /**
56
+ * Follow the preference for as long as the client lives: apply it now, and again
57
+ * whenever the Settings switch flips. Silent on every failure — a host without
58
+ * the slices (a source checkout that never ran the font build) simply keeps the
59
+ * system stack, which is what the panel did before this existed.
60
+ *
61
+ * @returns a disposer that stops following and removes the injected rules.
62
+ */
63
+ export declare function attachCodeFont(): () => void;
64
+ export {};
@@ -16,7 +16,6 @@ export declare const zh: {
16
16
  'panel.tab.comments': string;
17
17
  'panel.commentsEmpty': string;
18
18
  'panel.commentOutdated': string;
19
- 'panel.commentEmptyTitle': string;
20
19
  'panel.aria': string;
21
20
  'panel.stats': string;
22
21
  'panel.blockPosition': string;
@@ -47,6 +46,8 @@ export declare const zh: {
47
46
  'panel.diffLineHeightDesc': string;
48
47
  'panel.mdPreview': string;
49
48
  'panel.mdPreviewDesc': string;
49
+ 'panel.codeFont': string;
50
+ 'panel.codeFontDesc': string;
50
51
  'panel.mdMaxWidth': string;
51
52
  'panel.mdMaxWidthDesc': string;
52
53
  'panel.diffAddColor': string;
@@ -65,6 +66,7 @@ export declare const zh: {
65
66
  'action.gotoGo': string;
66
67
  'action.gotoCancel': string;
67
68
  'panel.gotoMissing': string;
69
+ 'panel.persistFailed': string;
68
70
  'settings.keybindings': string;
69
71
  'settings.keybindingsDesc': string;
70
72
  'settings.comment': 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;
@@ -248,7 +254,6 @@ export declare const en: {
248
254
  'panel.tab.comments': string;
249
255
  'panel.commentsEmpty': string;
250
256
  'panel.commentOutdated': string;
251
- 'panel.commentEmptyTitle': string;
252
257
  'panel.aria': string;
253
258
  'panel.stats': string;
254
259
  'panel.blockPosition': string;
@@ -279,6 +284,8 @@ export declare const en: {
279
284
  'panel.diffLineHeightDesc': string;
280
285
  'panel.mdPreview': string;
281
286
  'panel.mdPreviewDesc': string;
287
+ 'panel.codeFont': string;
288
+ 'panel.codeFontDesc': string;
282
289
  'panel.mdMaxWidth': string;
283
290
  'panel.mdMaxWidthDesc': string;
284
291
  'panel.diffAddColor': string;
@@ -297,6 +304,7 @@ export declare const en: {
297
304
  'action.gotoGo': string;
298
305
  'action.gotoCancel': string;
299
306
  'panel.gotoMissing': string;
307
+ 'panel.persistFailed': string;
300
308
  'settings.keybindings': string;
301
309
  'settings.keybindingsDesc': string;
302
310
  'settings.comment': 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;
@@ -47,6 +47,18 @@ export declare function rememberDiscussions(sessionId: string | undefined, byFil
47
47
  * @param discussionId - the thread to end; an id that is not there changes nothing.
48
48
  */
49
49
  export declare function forgetDiscussion(sessionId: string | undefined, fileId: string, discussionId: string): void;
50
+ /**
51
+ * Drop every thread whose file the list no longer holds.
52
+ *
53
+ * A thread is a block of rows in one file's diff, so a file the reader has kept or reverted out of the
54
+ * list leaves nothing for its comments to hang on: they go with it, and the same path coming back later
55
+ * in this visit starts clean. A new object is published only when something actually goes, so the panes
56
+ * that adopt this record (see `COMMENTS_CHANGED_EVENT`) do not re-adopt an equal object on every poll.
57
+ *
58
+ * @param sessionId - the session the panel is reviewing; nothing is dropped without one.
59
+ * @param listed - the pending entry ids the list is holding.
60
+ */
61
+ export declare function forgetDiscussionsNotIn(sessionId: string | undefined, listed: readonly string[]): void;
50
62
  /**
51
63
  * Whether the reader asked not to be asked again about removing one file.
52
64
  * @param sessionId - the session the panel is reviewing; nothing is quiet without one.
@@ -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
@@ -24,6 +24,12 @@ export interface PendingDiffSnapshot {
24
24
  * same rules inline when it is not.
25
25
  */
26
26
  commentSkill?: string | undefined;
27
+ /**
28
+ * The last failure to write the pending state to disk, or absent while writes are working. The
29
+ * panel says so once: the file is what makes the list survive a restart, so a reader who is never
30
+ * told finds the list gone later with nothing to explain it (see the wire type).
31
+ */
32
+ persistError?: string | undefined;
27
33
  /** Latched when an external change created a fresh undo checkpoint that
28
34
  * superseded the redo history; the panel surfaces it once (deferred if the
29
35
  * panel is closed) via a bottom-right notice. */
@@ -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,23 @@ 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
+ * Answer one font request. Exported so the route's behaviour is testable
89
+ * without a listening server: only files the manifest lists are ever read, and
90
+ * everything else — including a path traversal attempt — is a 404.
91
+ *
92
+ * @param req - the request, for its path.
93
+ * @param res - the response, whose lifecycle this owns.
94
+ */
95
+ export declare function serveFontSlice(req: WebServerRequest, res: WebServerResponse): Promise<void>;
@@ -66,6 +66,12 @@ export interface DiffApprovalListValue {
66
66
  * is set, and carries the same rules inline when it is not.
67
67
  */
68
68
  commentSkill?: string | undefined;
69
+ /**
70
+ * The last failure to write the pending state to disk, or absent while writes are working. The
71
+ * panel says so once: that file is what makes the list survive a restart, so a reader who is not
72
+ * told about it simply finds the list gone later, with nothing to explain it (issue #6).
73
+ */
74
+ persistError?: string | undefined;
69
75
  }
70
76
  /** What the open endpoint asks the OS to do with a file. */
71
77
  export type DiffApprovalOpenAction = 'open' | 'reveal';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-diff-approval",
3
- "version": "0.27.0",
3
+ "version": "0.29.0",
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
  },