dsh-diff-approval 0.29.1 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/README.zh.md +7 -4
- package/assets/fonts/README.md +10 -8
- package/assets/fonts/manifest.json +1 -1
- package/lib/client.js +5161 -1972
- package/lib/index.js +4331 -442
- package/lib/types/annotate-skill.d.ts +34 -0
- package/lib/types/annotate-tool.d.ts +227 -0
- package/lib/types/annotate.d.ts +186 -0
- package/lib/types/atomic-write.d.ts +77 -0
- package/lib/types/client/PendingPanel.d.ts +92 -24
- package/lib/types/client/category-color.d.ts +62 -0
- package/lib/types/client/chords.d.ts +70 -17
- package/lib/types/client/discussion.d.ts +76 -16
- package/lib/types/client/dock.d.ts +8 -8
- package/lib/types/client/dsh-icons.d.ts +98 -0
- package/lib/types/client/header-entry.d.ts +27 -11
- package/lib/types/client/ids.d.ts +18 -0
- package/lib/types/client/locales.d.ts +88 -36
- package/lib/types/client/panel-memory.d.ts +118 -35
- package/lib/types/client/path-match.d.ts +50 -0
- package/lib/types/client/port.d.ts +77 -1
- package/lib/types/client/presentation-menu.d.ts +1 -1
- package/lib/types/client/produced-diff.d.ts +136 -22
- package/lib/types/client/reference.d.ts +0 -2
- package/lib/types/client/remap-sync.d.ts +18 -1
- package/lib/types/client/session-seat.d.ts +93 -0
- package/lib/types/client/settings.d.ts +40 -14
- package/lib/types/client/slots.d.ts +184 -12
- package/lib/types/client/store.d.ts +85 -2
- package/lib/types/comment-ask.d.ts +236 -0
- package/lib/types/comment-lines.d.ts +131 -0
- package/lib/types/comments.d.ts +465 -0
- package/lib/types/index.d.ts +7 -2
- package/lib/types/pending.d.ts +53 -4
- package/lib/types/types.d.ts +356 -4
- package/lib/types/vcs.d.ts +53 -26
- package/lib/types/version.d.ts +162 -0
- package/package.json +7 -2
- package/lib/types/client/chat-bridge.d.ts +0 -70
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The skill that teaches the agent to annotate code with the panel's cards.
|
|
3
|
+
*
|
|
4
|
+
* The plugin offers two skills, and they are the two halves of one conversation. `-comment` is how to
|
|
5
|
+
* ANSWER a card the reader wrote; this one is how to PLACE a card of the agent's own — for the moments
|
|
6
|
+
* where the thing worth saying is "these lines, here" rather than a paragraph in the reply.
|
|
7
|
+
*
|
|
8
|
+
* Like the comment skill, the long form lives here rather than in the system prompt: the harness's
|
|
9
|
+
* `skill` tool advertises the catalog (name, description, when-to-use) and loads the body on demand, so
|
|
10
|
+
* a rule about annotating costs nothing until an agent is actually about to annotate.
|
|
11
|
+
*/
|
|
12
|
+
/** Kebab-case identity, prefixed with the package for the same flat-namespace reason as the comment skill. */
|
|
13
|
+
export declare const ANNOTATE_SKILL_NAME = "dsh-diff-approval-annotate";
|
|
14
|
+
/** Short routing line the catalog shows beside the name. */
|
|
15
|
+
export declare const ANNOTATE_SKILL_DESCRIPTION: string;
|
|
16
|
+
/** Extra routing guidance for the catalog. */
|
|
17
|
+
export declare const ANNOTATE_SKILL_WHEN_TO_USE: string;
|
|
18
|
+
/** The instruction body the agent loads. */
|
|
19
|
+
export declare const ANNOTATE_SKILL_CONTENT = "# \u7528\u5361\u7247\u7ED9\u4EE3\u7801\u505A\u6279\u6CE8\n\n\u6539\u52A8\u5BA1\u6279\u9762\u677F\u91CC\u7684\u5361\u7247 = \u9488\u5BF9**\u5177\u4F53\u51E0\u884C\u4EE3\u7801**\u7684\u5BF9\u8BDD\u3002\u4F60\u5199\u7684\u6279\u6CE8\u4F1A\u6210\u4E3A\u5361\u7247\u7684\u7B2C\u4E00\u6761\u5185\u5BB9\uFF08\u4F60\u5728\u5DE5\u5177\u91CC\u5199\u7684 `note`\uFF09\uFF0C\u7528\u6237\u5C31\u5728\u4EE3\u7801\u65C1\u8FB9\u770B\u5230\u5B83\uFF0C\u5E76\u4E14\u53EF\u4EE5\u5728\u8FD9\u5F20\u5361\u7247\u91CC\u56DE\u4F60\uFF1B\u4ED6\u7684\u56DE\u590D\u4F1A\u4EE5 `[\u8BC4\u8BBA] (path:lines)` \u7684\u6D88\u606F\u56DE\u5230\u4F60\u3002\n\n## \u4EC0\u4E48\u65F6\u5019\u8BE5\u7528\n\n- \u7528\u6237\u60F3\u8BFB\u4EE3\u7801\uFF1A\u628A**\u8C03\u7528\u94FE**\u4E00\u6B65\u6B65\u6279\u6CE8\u51FA\u6765\uFF08\u6BCF\u4E2A\u5173\u952E\u8C03\u7528\u70B9\u4E00\u5F20\u5361\uFF0C\u6309\u9605\u8BFB\u987A\u5E8F\uFF09\uFF0C\u6BD4\u5728\u56DE\u590D\u91CC\u8D34\u4E00\u5927\u6BB5\u4EE3\u7801\u6216\u884C\u53F7\u6E05\u5355\u6E05\u695A\u5F97\u591A\u3002\n- \u4F60\u8981\u6307\u51FA\u67D0\u6BB5\u4EE3\u7801\u7684\u95EE\u9898\u3001\u7406\u7531\u3001\u6216\u9700\u8981\u6CE8\u610F\u7684\u5730\u65B9\uFF08Review \u573A\u666F\uFF09\uFF0C\u8BA9\u8BC1\u636E\u5C31\u8D34\u5728\u8BC1\u636E\u6240\u5728\u7684\u884C\u4E0A\u3002\n- \u4F60\u6B63\u5728\u8BA8\u8BBA\u67D0\u6BB5\u4EE3\u7801\uFF0C\u60F3\u628A\u8BDD\u76F4\u63A5\u6302\u5728\u90A3\u4E9B\u884C\u4E0A\uFF0C\u800C\u4E0D\u662F\u8BA9\u7528\u6237\u5728\u56DE\u590D\u91CC\u5BF9\u7740\u884C\u53F7\u627E\u3002\n\n\u4E0D\u8981\u5728\u7528\u6237\u6CA1\u8BA9\u4F60\u8BB2\u4EE3\u7801\u65F6\u5237\u5361\u7247\uFF0C\u4E5F\u4E0D\u8981\u7528\u5B83\u4EE3\u66FF\u56DE\u590D\uFF1A\u5361\u7247\u662F**\u8865\u5145**\uFF0C\u4F60\u7684\u56DE\u7B54\u8FD8\u662F\u8981\u5199\u5728\u56DE\u590D\u91CC\uFF08\u53EF\u4EE5\u7B80\u77ED\u8BF4\"\u5DF2\u5728\u4EE3\u7801\u4E0A\u6807\u4E86 3 \u5904\"\u5E76\u7ED9\u51FA\u8981\u70B9\uFF09\u3002\n\n## \u600E\u4E48\u8C03\u7528 `diff_approval_annotate`\n\n- `path`\uFF1A\u9762\u677F\u91CC\u90A3\u4E2A\u6587\u4EF6\u3002\u4F60\u8BFB\u6587\u4EF6\u65F6\u7528\u7684\u8DEF\u5F84\u4E5F\u884C\uFF08\u6309\u5C3E\u90E8\u5339\u914D\uFF09\uFF1B\u5339\u914D\u5230\u591A\u4E2A\u4F1A\u88AB\u62D2\uFF0C\u628A\u5B8C\u6574\u8DEF\u5F84\u7ED9\u5B83\u3002\n **\u6587\u4EF6\u8FD8\u4E0D\u5728\u9762\u677F\u7684\u5217\u8868\u91CC\u4E5F\u6CA1\u5173\u7CFB\uFF1A\u4F1A\u81EA\u52A8\u52A0\u8FDB\u53BB**\uFF08\u5DE5\u4F5C\u533A\u91CC\u80FD\u8BFB\u6210\u6587\u672C\u7684\u6587\u4EF6\uFF09\uFF0C\u6240\u4EE5\"\u7528\u6237\u60F3\u8BFB\u61C2\u67D0\u4E2A\u6587\u4EF6\"\u4E0D\u5FC5\u5148\u7B49\u6539\u52A8\u53D1\u751F\u3002\u5DE5\u4F5C\u533A\u4E4B\u5916\u3001\u6216\u8BFB\u4E0D\u51FA\u6587\u672C\uFF0C\u624D\u4F1A\u88AB\u62D2\u3002\n- `startLine` / `endLine`\uFF1A**\u73B0\u5728**\u7684\u884C\u53F7\uFF081 \u8D77\uFF0C\u542B\u4E24\u7AEF\uFF1B\u7701\u7565 `endLine` \u5C31\u662F\u4E00\u884C\uFF09\u3002\u5148\u8BFB\u6587\u4EF6\u518D\u5199\u53F7\uFF0C\u522B\u51ED\u8BB0\u5FC6\u3002\n- `note`\uFF1A\u5361\u7247\u4E0A\u5199\u7684\u8BDD\u3002\u8981\u77ED\u2014\u2014\u5361\u7247\u662F**\u6309\u884C**\u6392\u7248\u3001\u884C\u9AD8\u6309\u884C\u7B97\u7684\uFF08\u4E0A\u9650 1000 \u5B57\u7B26\uFF0C\u8D85\u4E86\u4F1A\u88AB\u62D2\uFF09\u3002\u8BB2\u6D41\u7A0B\u65F6\uFF0C\u628A\u8FD9\u4E00\u6B65\u7684\u5E8F\u53F7\u5199\u5728 note \u7684**\u6700\u524D\u9762**\uFF08`1. \u2026`\u3001`2. \u2026`\uFF09\u3002\n- `quote`\uFF08\u53EF\u9009\uFF0C\u5EFA\u8BAE\u7ED9\uFF09\uFF1A\u4F60\u8BFB\u5230\u7684\u90A3\u51E0\u884C\u539F\u6587\uFF08\u6362\u884C\u8FDE\u63A5\uFF09\u3002\u6587\u4EF6\u5728\u4F60\u8BFB\u5B8C\u4E4B\u540E\u53C8\u53D8\u4E86\u7684\u8BDD\uFF0C\u4F1A\u5F53\u573A\u88AB\u62D2\u5E76\u628A\u73B0\u5728\u7684\u539F\u6587\u7ED9\u4F60\uFF0C\u907F\u514D\u6279\u6CE8\u6302\u5728\u9519\u8BEF\u7684\u884C\u4E0A\u3002\n- `category`\uFF08\u53EF\u9009\uFF09\uFF1A**\u7ED9\"\u4E00\u7C7B\u6279\u6CE8\"\u8D77\u4E00\u4E2A id**\u3002\u540C\u4E00\u7C7B\uFF08\u540C\u4E00\u8F6E\u3001\u540C\u4E00\u4E3B\u9898\u3001\u540C\u4E00\u6B21\u8BB2\u89E3\uFF09\u6BCF\u6B21\u8C03\u7528\u90FD\u4F20**\u540C\u4E00\u4E2A** id\uFF0C\u6362\u6210\u53E6\u4E00\u7C7B\u5C31\u6362\u4E00\u4E2A id\u3002\u9762\u677F\u4F1A\u6309\u8FD9\u4E2A id \u7ED9\u8BC4\u8BBA\u5217\u8868\u91CC\u7684\u90A3\u6761\u8BC4\u8BBA\u753B\u4E00\u4E2A**\u5F69\u8272\u5706\u70B9**\uFF0C\u6240\u4EE5**\u540C\u4E00\u7C7B\u7684\u6279\u6CE8\u662F\u540C\u4E00\u4E2A\u989C\u8272**\uFF0C\u4E24\u7C7B\u4E4B\u95F4\u53EF\u4EE5\u9760\u989C\u8272\u533A\u5206\u3002**\u4E0D\u4F20 `category` \u5C31\u5B8C\u5168\u4E0D\u753B\u5706\u70B9**\u2014\u2014\u5B83\u662F\u53EF\u9009\u7684\uFF0C\u5706\u70B9\u53EA\u4E3A\"\u6709\u7C7B\u522B\"\u7684\u6279\u6CE8\u800C\u753B\u3002\n - \u989C\u8272\u53EA\u6709 12 \u79CD\uFF0Cid \u662F\u6563\u5217\u5230\u989C\u8272\u4E0A\u7684\uFF0C\u6240\u4EE5**\u4E24\u4E2A\u4E0D\u540C\u7684 id \u53EF\u80FD\u649E\u6210\u540C\u4E00\u4E2A\u989C\u8272**\uFF0C\u90A3\u65F6\u8FD9\u4E24\u7C7B\u770B\u8D77\u6765\u4E00\u6837\uFF1A\u8FD9\u4E0D\u662F\"\u5206\u4E86\u51E0\u7C7B\u4E00\u76EE\u4E86\u7136\"\uFF0C\u53EA\u662F\"\u540C\u4E00\u7C7B\u989C\u8272\u4E00\u81F4\"\u3002\u60F3\u8BA9\u4EBA\u5206\u5F97\u6E05\uFF0C\u5C31**\u6709\u610F\u8BC6\u5730\u590D\u7528\u540C\u4E00\u4E2A id**\uFF0C\u4E0D\u8981\u7ED9\u540C\u4E00\u7C7B\u6362\u65B0 id\u3002\n - id \u672C\u8EAB\u4E0D\u89E3\u6790\uFF1A\u968F\u4FBF\u7ED9\uFF08uuid \u4E5F\u884C\uFF09\u3002\u989C\u8272\u7531\u5B83\u7B97\u51FA\u6765\uFF1B\u4F60\u5199\u7684 id \u53EA\u51FA\u73B0\u5728\u5706\u70B9\u7684 **`title` / `aria-label`** \u91CC\uFF08\u9F20\u6807\u60AC\u505C\u3001\u6216\u8BFB\u5C4F\u8F6F\u4EF6\uFF09\uFF0C**\u4E0D\u662F\u5C4F\u5E55\u4E0A\u7684\u6587\u5B57**\u3002\u4F1A\u53BB\u6389\u4E24\u7AEF\u7A7A\u767D\uFF1B\u6700\u591A 64 \u4E2A\u5B57\u7B26\uFF0C\u8D85\u4E86**\u622A\u65AD**\uFF08\u4E0D\u4F1A\u56E0\u6B64\u62D2\u6389\u6574\u6761\u6279\u6CE8\uFF09\u3002\n\n## \u8BB2\u6D41\u7A0B\u5C31\u5728\u6B63\u6587\u5F00\u5934\u5199\u5E8F\u53F7\n\n\u5982\u679C\u8FD9\u51E0\u5F20\u5361\u662F**\u4E00\u6761\u6D41\u7A0B/\u8C03\u7528\u94FE\u7684\u6B65\u9AA4**\uFF0C\u628A\u5E8F\u53F7\u5199\u5728 note \u7684\u6700\u524D\u9762\uFF0C\u7528\u6237\u5C31\u80FD\u6309\u53F7\u8BFB\uFF1A\n\n- \u5E8F\u53F7\u6CA1\u6709\u4E13\u95E8\u53C2\u6570\uFF0C\u4E5F\u4E0D\u662F\u9762\u677F\u753B\u7684\u2014\u2014\u5B83\u5C31\u662F\u6B63\u6587\u7684\u5934\u51E0\u4E2A\u5B57\u7B26\uFF0C\u6240\u4EE5\u5361\u7247\u91CC\u3001\u5217\u8868\u91CC\u3001\u590D\u5236\u51FA\u53BB\u7684\u5185\u5BB9\u91CC\u90FD\u5E26\u7740\u5B83\uFF0C\u4F60\u5728\u56DE\u590D\u91CC\u4E5F\u80FD\u5F15\u7528\u540C\u4E00\u4E2A\u53F7\u3002\n- \u53F7\u7531\u4F60\u6392\uFF1A\u6309\u4F60\u5E0C\u671B\u7528\u6237\u9605\u8BFB\u7684\u987A\u5E8F\uFF1B1\u30012\u30013\u2026 \u8FDE\u4E0D\u8FDE\u7EED\u3001\u91CD\u4E0D\u91CD\u590D\u90FD\u968F\u4F60\uFF08**\u4E0D\u9700\u8981\u552F\u4E00**\uFF0C\u8BB2\u540C\u4E00\u4E2A\u70B9\u7684\u8865\u5145\u5361\u53EF\u4EE5\u540C\u53F7\uFF09\u3002\n- \u987A\u5E8F\u53F7\u4E5F\u5199\u8FDB\u4F60\u7684\u56DE\u590D\uFF08\"\u6211\u5728\u4EE3\u7801\u4E0A\u6807\u4E86 1\u20135\uFF0C\u4ECE X \u5F00\u59CB\"\uFF09\uFF0C\u8FD9\u6837\u9762\u677F\u91CC\u7684\u53F7\u548C\u56DE\u590D\u91CC\u7684\u53F7\u5BF9\u5F97\u4E0A\u3002\n- \u96F6\u6563\u7684\u6279\u6CE8\uFF08\u4E0D\u662F\u4E00\u4E32\u6B65\u9AA4\uFF09**\u4E0D\u8981**\u5199\u53F7\u3002\n\n## \u4E00\u5F20\u5361\u4E00\u4E2A\u70B9\n\n- \u8303\u56F4\u8981\u5C0F\uFF1A**\u6700\u591A 60 \u884C**\uFF0C\u901A\u5E38 1\u20135 \u884C\u5C31\u591F\u3002\u8981\u8BB2\u6574\u6BB5\u903B\u8F91\u5C31\u62C6\u6210\u51E0\u5F20\u5361\u3002\n- \u4E00\u6761\u8C03\u7528\u94FE = \u4E00\u5F20\u5361\u4E00\u4E2A\u8C03\u7528\u70B9\uFF0C\u6309\u4F60\u5E94\u8BE5\u8BA9\u7528\u6237\u9605\u8BFB\u7684\u987A\u5E8F\u4F9D\u6B21\u521B\u5EFA\uFF1B\u4E0D\u8981\u628A\u6240\u6709\u6B65\u9AA4\u585E\u8FDB\u4E00\u5F20\u5361\u3002\n- note \u91CC\u53EA\u8C08\u8FD9\u4E9B\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- \u5199\u6CD5\u6CBF\u7528\u6279\u6CE8\u7684\u89C4\u77E9\uFF1A\u4E0D\u8D85\u8FC7 3 \u884C\u3001\u884C\u95F4\u4E0D\u7A7A\u884C\u3001**\u4E0D\u8981 Markdown \u8868\u683C/\u5217\u8868/\u6807\u9898/\u56F4\u680F\u4EE3\u7801\u5757**\uFF08\u5361\u7247\u6309\u884C\u6392\u7248\uFF0C\u8868\u683C\u4F1A\u8BFB\u6210\u4E00\u5806\u7AD6\u7EBF\u5E76\u628A\u9AD8\u5EA6\u7B97\u9519\uFF09\uFF0C\u884C\u5185\u53EA\u7528 `\u4EE3\u7801` \u548C **\u52A0\u7C97**\u3002\u8981\u5F15\u7528\u522B\u5904\u5C31\u5199 `\u6587\u4EF6:\u884C\u53F7`\u3002\n- \u7528\u7528\u6237\u7684\u8BED\u8A00\u5199 note\u3002\n\n## \u90A3\u51E0\u884C\u5DF2\u7ECF\u88AB\u5360\u7528\u65F6\n\n\u5982\u679C\u8FD9\u4E9B\u884C\u5DF2\u7ECF\u843D\u5728**\u67D0\u5F20\u5361\u7247\u91CC**\uFF0C\u8C03\u7528\u4F1A\u88AB\u62D2\uFF0C\u62D2\u7EDD\u4FE1\u606F\u91CC\u4F1A\u5199\u660E\u662F\u54EA\u4E00\u5F20\uFF08id\u3001\u884C\u53F7\u3001\u5B83\u5F00\u5934\u7684\u8BDD\uFF09\u3002\u8FD9\u4E0D\u662F\u9519\u8BEF\uFF0C\u662F\"\u8FD9\u91CC\u5DF2\u7ECF\u6709\u8BDD\u8BF4\u4E86\"\uFF1A\n\n- \u6362\u4E00\u5904\u884C\u53F7\uFF1B\u6216\u8005\n- \u628A\u8FD9\u4EF6\u4E8B\u5199\u8FDB\u4F60\u7684\u56DE\u590D\u91CC\uFF0C\u4E0D\u8981\u8BD5\u56FE\u786C\u585E\u8FDB\u90A3\u5F20\u5361\uFF1A\u90A3\u5F20\u5361\u7684\u5BF9\u8BDD\u5C5E\u4E8E\u5199\u5B83\u7684\u4EBA\uFF08\u53EF\u80FD\u662F\u7528\u6237\uFF0C\u4E5F\u53EF\u80FD\u662F\u4F60\u65E9\u5148\u5199\u7684\uFF09\u3002\u5982\u679C\u7528\u6237\u60F3\u5728\u90A3\u5F20\u5361\u4E0A\u7EE7\u7EED\u804A\uFF0C\u4ED6\u4F1A\u5728\u5361\u7247\u91CC\u56DE\u590D\u4F60\uFF0C\u90A3\u65F6\u4F60\u4F1A\u6536\u5230 `[\u8BC4\u8BBA]`\uFF0C\u6309 `dsh-diff-approval-comment` \u7684\u89C4\u77E9\u56DE\u7B54\u5373\u53EF\u3002\n\n**\u4E0D\u8981**\u5728\u56DE\u590D\u91CC\u5047\u88C5\u5DF2\u7ECF\u6279\u6CE8\u6210\u529F\u2014\u2014\u5DE5\u5177\u8FD4\u56DE\u7684\u662F\"refused: ...\"\u5C31\u8BF4\u660E\u6CA1\u6709\u5361\u7247\u88AB\u521B\u5EFA\u3002\n";
|
|
20
|
+
/** One runtime skill contribution, in the shape `ctx.skills.register` takes. */
|
|
21
|
+
export interface AnnotateSkillRegistration {
|
|
22
|
+
/** Kebab-case identity the agent loads by. */
|
|
23
|
+
readonly name: string;
|
|
24
|
+
/** Catalog routing line. */
|
|
25
|
+
readonly description: string;
|
|
26
|
+
/** Catalog routing guidance. */
|
|
27
|
+
readonly whenToUse: string;
|
|
28
|
+
/** Instruction body. */
|
|
29
|
+
readonly content: string;
|
|
30
|
+
/** Origin bucket: this plugin contributes it at runtime, not from disk. */
|
|
31
|
+
readonly source: 'runtime';
|
|
32
|
+
}
|
|
33
|
+
/** The contribution to register with the skill registry. */
|
|
34
|
+
export declare const ANNOTATE_SKILL: AnnotateSkillRegistration;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tool the agent calls to annotate code: one card, on the lines it names.
|
|
3
|
+
*
|
|
4
|
+
* This is the half a model reads. The rule it applies lives in `annotate.ts` and is a pure function of
|
|
5
|
+
* the entry, the stored comments and the request; what is here is the model-facing surface — the name,
|
|
6
|
+
* the argument schema, the sentence each outcome is handed back as — plus the two lookups that only
|
|
7
|
+
* exist at run time (which session is calling, and which of ITS pending files the caller means).
|
|
8
|
+
*
|
|
9
|
+
* Why a tool at all, when the reader can already annotate: the reader annotates what they are looking
|
|
10
|
+
* at, and the agent annotates what it is EXPLAINING. A call chain the reader asked to have walked, a
|
|
11
|
+
* piece of code the agent wants to point at, a review finding with its evidence beside it — all of those
|
|
12
|
+
* are "quote these lines and say something about them", which is exactly the card the panel already
|
|
13
|
+
* draws. The card is the shared object: the agent's note is its first turn, the reader answers it in the
|
|
14
|
+
* card, and that answer comes back to the agent as an ordinary `[评论]` message.
|
|
15
|
+
*
|
|
16
|
+
* The refusal the caller is most likely to meet is the one that makes the feature coherent: those lines
|
|
17
|
+
* already belong to a card. Two cards hanging off the same lines cannot be told apart in the panel, so
|
|
18
|
+
* the annotation is refused and the refusal NAMES the card holding them — the agent can then say its
|
|
19
|
+
* piece in its own reply, or annotate other lines.
|
|
20
|
+
*
|
|
21
|
+
* The definition is built BY HAND rather than with the harness's `defineTool`: the host half of this
|
|
22
|
+
* plugin imports nothing from the harness at runtime (its harness packages are services it asks for by
|
|
23
|
+
* name, and types), the registry asks only for `name`, `description`, a raw argument schema and an
|
|
24
|
+
* `output` with a schema and a `render`, and this way a build whose harness has no `dsh-tools` at all
|
|
25
|
+
* still loads the plugin — the tool is simply never registered.
|
|
26
|
+
*
|
|
27
|
+
* @module dsh-diff-approval/src/annotate-tool
|
|
28
|
+
*/
|
|
29
|
+
import type { AnnotationRefusal } from './annotate.ts';
|
|
30
|
+
import type { CommentRecord, PendingEntry } from './types.ts';
|
|
31
|
+
import type { SessionId } from '@deepseek-ai/dsh-session';
|
|
32
|
+
/** The name the agent calls this by. Prefixed with the package: tool names share one flat namespace. */
|
|
33
|
+
export declare const ANNOTATE_TOOL_NAME = "diff_approval_annotate";
|
|
34
|
+
/**
|
|
35
|
+
* The routing line the model always sees, which has to carry the two things that decide whether it calls
|
|
36
|
+
* this at all: what appears for the reader, and that the lines must be free.
|
|
37
|
+
*/
|
|
38
|
+
export declare const ANNOTATE_TOOL_DESCRIPTION: string;
|
|
39
|
+
/** One call's arguments, as the raw schema declares them. */
|
|
40
|
+
export interface AnnotateToolArgs {
|
|
41
|
+
/** The file to annotate: a path of a file with pending changes, as the panel shows it. */
|
|
42
|
+
path: string;
|
|
43
|
+
/** First line to annotate, 1-based, counted in the file as it reads now. */
|
|
44
|
+
startLine: number;
|
|
45
|
+
/** Last line, inclusive. Absent means the one line `startLine` names. */
|
|
46
|
+
endLine?: number | undefined;
|
|
47
|
+
/** What the card says. Kept short: the card is drawn line by line beside the code. */
|
|
48
|
+
note: string;
|
|
49
|
+
/**
|
|
50
|
+
* Optional class of annotation — the same id for every annotation of one kind, a different one for the
|
|
51
|
+
* next. The panel draws a coloured round dot from it (see `category-color.ts`), so an agent's several
|
|
52
|
+
* passes over the same review are tellable apart at a glance. Opaque: hashed for the colour and shown
|
|
53
|
+
* back to the reader as written. Trimmed, and cut to 64 characters.
|
|
54
|
+
*/
|
|
55
|
+
category?: string | undefined;
|
|
56
|
+
/** Optional guard: the exact text the caller believes those lines hold (newline-joined). */
|
|
57
|
+
quote?: string | undefined;
|
|
58
|
+
}
|
|
59
|
+
/** What one call needs from the harness around it: whose session it belongs to, and the caller's life. */
|
|
60
|
+
export interface AnnotateToolContext {
|
|
61
|
+
/** The session whose panel will show the annotation, or `undefined` when the call has no agent. */
|
|
62
|
+
sessionId: SessionId | undefined;
|
|
63
|
+
/** Caller-owned cancellation. */
|
|
64
|
+
signal: AbortSignal | undefined;
|
|
65
|
+
}
|
|
66
|
+
/** What the plugin body does for one call: the sentence the model reads. */
|
|
67
|
+
export type AnnotateToolRun = (args: AnnotateToolArgs, context: AnnotateToolContext) => Promise<string>;
|
|
68
|
+
/**
|
|
69
|
+
* The argument schema, as raw JSON Schema.
|
|
70
|
+
*
|
|
71
|
+
* Raw rather than the harness's author DSL because the definition is hand-built (see the module
|
|
72
|
+
* comment): the registry validates a hand-built definition's arguments against this subset, and the
|
|
73
|
+
* model reads the same descriptions either way.
|
|
74
|
+
*/
|
|
75
|
+
export declare const ANNOTATE_TOOL_PARAMETERS: {
|
|
76
|
+
readonly type: "object";
|
|
77
|
+
readonly additionalProperties: false;
|
|
78
|
+
readonly required: readonly ["path", "startLine", "note"];
|
|
79
|
+
readonly properties: {
|
|
80
|
+
readonly path: {
|
|
81
|
+
readonly type: "string";
|
|
82
|
+
readonly description: string;
|
|
83
|
+
};
|
|
84
|
+
readonly startLine: {
|
|
85
|
+
readonly type: "integer";
|
|
86
|
+
readonly description: "First line to annotate, 1-based, counted in the file as it reads NOW.";
|
|
87
|
+
};
|
|
88
|
+
readonly endLine: {
|
|
89
|
+
readonly type: "integer";
|
|
90
|
+
readonly description: "Last line, inclusive. Omit to annotate the single line startLine names.";
|
|
91
|
+
};
|
|
92
|
+
readonly note: {
|
|
93
|
+
readonly type: "string";
|
|
94
|
+
readonly description: string;
|
|
95
|
+
};
|
|
96
|
+
readonly quote: {
|
|
97
|
+
readonly type: "string";
|
|
98
|
+
readonly description: string;
|
|
99
|
+
};
|
|
100
|
+
readonly category: {
|
|
101
|
+
readonly type: "string";
|
|
102
|
+
readonly description: string;
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* The definition to hand to `ctx.tools.register`.
|
|
108
|
+
*
|
|
109
|
+
* The canonical value is the sentence itself: this tool answers with prose because that is all the model
|
|
110
|
+
* needs from it, and a one-field schema keeps the wire contract trivial.
|
|
111
|
+
*
|
|
112
|
+
* @param run - what fulfils one call; it never throws for a refusal (see below), only for a call that
|
|
113
|
+
* could not be carried out at all.
|
|
114
|
+
* @returns the definition object.
|
|
115
|
+
*/
|
|
116
|
+
export declare function annotateToolDefinition(run: AnnotateToolRun): Record<string, unknown>;
|
|
117
|
+
/** How a path the caller wrote resolved against the session's pending files. */
|
|
118
|
+
export type EntryLookup = {
|
|
119
|
+
kind: 'one';
|
|
120
|
+
entry: PendingEntry;
|
|
121
|
+
} | {
|
|
122
|
+
kind: 'none';
|
|
123
|
+
} | {
|
|
124
|
+
kind: 'many';
|
|
125
|
+
paths: readonly string[];
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* One path, folded to the form two spellings of the same file share: separators unified, trailing slash
|
|
129
|
+
* dropped, case dropped (the panel and the agent can each write `C:\a\b.ts`, `C:/a/b.ts` or `c:\A\B.TS`).
|
|
130
|
+
*
|
|
131
|
+
* @param path - the path as written.
|
|
132
|
+
* @returns the comparable form.
|
|
133
|
+
*/
|
|
134
|
+
export declare function foldPath(path: string): string;
|
|
135
|
+
/**
|
|
136
|
+
* Which pending file the caller means by `path`.
|
|
137
|
+
*
|
|
138
|
+
* A model writing this argument usually hands back the path it just read — absolute, workspace-relative,
|
|
139
|
+
* or with a slash style of its own. Exact match first, then a trailing-segment match; an AMBIGUOUS tail
|
|
140
|
+
* is reported as such rather than guessed, because annotating the wrong file's lines is the one mistake
|
|
141
|
+
* the reader would see as a bug in the panel.
|
|
142
|
+
*
|
|
143
|
+
* @param entries - the session's pending files.
|
|
144
|
+
* @param path - the path the caller wrote.
|
|
145
|
+
* @returns the one entry, or why it could not be picked.
|
|
146
|
+
*/
|
|
147
|
+
export declare function lookupEntry(entries: readonly PendingEntry[], path: string): EntryLookup;
|
|
148
|
+
/**
|
|
149
|
+
* The sentence a refusal is handed back as.
|
|
150
|
+
*
|
|
151
|
+
* Every one of these is written for a model that has to choose what to do next, so each says what was
|
|
152
|
+
* wrong AND what it can do instead. The lines are named back in the panel's own form (`path:from-to`),
|
|
153
|
+
* which is the reference the reader sees in a card's header.
|
|
154
|
+
*
|
|
155
|
+
* @param refusal - what the rule refused.
|
|
156
|
+
* @param path - the file the request named, for the reference.
|
|
157
|
+
* @returns the sentence the caller reads.
|
|
158
|
+
*/
|
|
159
|
+
export declare function refusalText(refusal: AnnotationRefusal, path: string): string;
|
|
160
|
+
/**
|
|
161
|
+
* What the host answers when the tool asks for a file to be listed.
|
|
162
|
+
*
|
|
163
|
+
* Listing is the host's job, not the tool's: only the host knows the session's workspace, its file
|
|
164
|
+
* service, and the admission bookkeeping every listed entry goes through (this reuses the very seam the
|
|
165
|
+
* panel's own "add this path" uses).
|
|
166
|
+
*/
|
|
167
|
+
export type ListFileOutcome = {
|
|
168
|
+
kind: 'listed';
|
|
169
|
+
entry: PendingEntry;
|
|
170
|
+
added: boolean;
|
|
171
|
+
} | {
|
|
172
|
+
kind: 'outside';
|
|
173
|
+
} | {
|
|
174
|
+
kind: 'missing';
|
|
175
|
+
} | {
|
|
176
|
+
kind: 'no-workspace';
|
|
177
|
+
};
|
|
178
|
+
/** What the plugin body needs from the host, so one call can be fulfilled (and tested) without one. */
|
|
179
|
+
export interface AnnotateRunDeps {
|
|
180
|
+
/**
|
|
181
|
+
* Hydrate the host's stores before they are read: a call can arrive before the panel has ever listed
|
|
182
|
+
* anything, and the comment store's own file has to be loaded before its contents can be checked for
|
|
183
|
+
* an overlap. The same call the panel's own handlers make (see the `comment-add` seam).
|
|
184
|
+
*/
|
|
185
|
+
ready?: (() => Promise<void>) | undefined;
|
|
186
|
+
/**
|
|
187
|
+
* The pending entries of one session — the files that session's panel is showing.
|
|
188
|
+
*/
|
|
189
|
+
entriesOf: (sessionId: SessionId) => readonly PendingEntry[];
|
|
190
|
+
/**
|
|
191
|
+
* Put one file into that session's list, when the panel is not showing it yet.
|
|
192
|
+
*
|
|
193
|
+
* A card hangs in the diff the reader is looking at, so a file has to be listed before it can carry
|
|
194
|
+
* one — and an agent annotating the code a reader is studying is exactly the case where nothing in
|
|
195
|
+
* that file has changed. This is the reader's own "add this path", reached by the agent instead.
|
|
196
|
+
*/
|
|
197
|
+
listFile: (sessionId: SessionId, asked: string, signal: AbortSignal | undefined) => Promise<ListFileOutcome>;
|
|
198
|
+
/** Every comment the store holds for that session; the rule narrows them to the entry itself. */
|
|
199
|
+
commentsOf: (sessionId: SessionId) => readonly CommentRecord[];
|
|
200
|
+
/** Store the record: the comment store's own `add`, so the card is the same object as any other. */
|
|
201
|
+
addComment: (record: CommentRecord) => unknown;
|
|
202
|
+
/** The plugin's logger, for the one line a refusal leaves behind on the host. */
|
|
203
|
+
log?: ((message: string) => void) | undefined;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The sentence a file that could not be listed is refused with.
|
|
207
|
+
*
|
|
208
|
+
* All three are the workspace's rules rather than the annotation's: the panel can only show what the
|
|
209
|
+
* session's workspace contains and the file service can read, and naming which one failed is what lets
|
|
210
|
+
* the agent pick another file, or stop annotating that one.
|
|
211
|
+
*
|
|
212
|
+
* @param kind - why listing failed.
|
|
213
|
+
* @param path - the path the caller named.
|
|
214
|
+
* @returns the sentence the caller reads.
|
|
215
|
+
*/
|
|
216
|
+
export declare function unlistedText(kind: 'outside' | 'missing' | 'no-workspace', path: string): string;
|
|
217
|
+
/**
|
|
218
|
+
* Build the body one tool call runs: resolve the file, apply the rule, store the record.
|
|
219
|
+
*
|
|
220
|
+
* A refusal is NOT an error: "those lines belong to a card" is an answer about the code, not a broken
|
|
221
|
+
* call, and returning it as text keeps the turn going so the agent can act on it in the same step.
|
|
222
|
+
* Throwing is left for what it means to the registry: the call could not be carried out at all.
|
|
223
|
+
*
|
|
224
|
+
* @param deps - the host lookups and the store write.
|
|
225
|
+
* @returns the body to hand to {@link annotateToolDefinition}.
|
|
226
|
+
*/
|
|
227
|
+
export declare function annotateRun(deps: AnnotateRunDeps): AnnotateToolRun;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One agent-authored annotation on a pending file's own lines: the rule, and every refusal.
|
|
3
|
+
*
|
|
4
|
+
* A reader can annotate a diff by selecting rows and typing in the card's compose row. This module is
|
|
5
|
+
* the same act, asked for by the AGENT instead: it turns "annotate lines 120-128 of this file with
|
|
6
|
+
* this note" into the very record the panel's own `comment-add` writes, so an agent's annotation is a
|
|
7
|
+
* comment like any other — stored by the host, drawn as a card in the reader's panel, answerable from
|
|
8
|
+
* the card, and quoted with the rule both halves already share (`resolveCommentLines`).
|
|
9
|
+
*
|
|
10
|
+
* Three things are deliberately NOT the caller's problem, because a tool the agent calls has to be
|
|
11
|
+
* answerable in one call and honest about why it refused:
|
|
12
|
+
*
|
|
13
|
+
* - The QUOTE is read from the file the host holds, never from the caller. An agent quotes what it
|
|
14
|
+
* believes those lines say (optionally as a guard, see `AnnotationRequest.quote`); the record's
|
|
15
|
+
* `quote`/`quoteContext` are the bytes the entry actually has, which is what keeps the card anchored
|
|
16
|
+
* by the same rule as every other comment when the file moves on.
|
|
17
|
+
* - OVERLAP is checked against what the reader can SEE. A comment that no longer resolves to any line
|
|
18
|
+
* (`resolveCommentLines` is `undefined`) is outdated and owns no rows, exactly as the panel's own
|
|
19
|
+
* selection rule treats it, so those lines are free to annotate; one that does resolve blocks the
|
|
20
|
+
* annotation and the refusal names it, because two cards hanging off the same lines cannot be told
|
|
21
|
+
* apart in the panel.
|
|
22
|
+
* - The RECORD is built here, including the `author` mark the panel reads to draw the first turn as
|
|
23
|
+
* the agent's rather than the reader's (`CommentRecord.author`).
|
|
24
|
+
*
|
|
25
|
+
* @module dsh-diff-approval/src/annotate
|
|
26
|
+
*/
|
|
27
|
+
import type { SessionId } from '@deepseek-ai/dsh-session';
|
|
28
|
+
import type { CommentRecord, PendingEntry } from './types.ts';
|
|
29
|
+
/**
|
|
30
|
+
* The most lines one annotation may name.
|
|
31
|
+
*
|
|
32
|
+
* An annotation is a card that hangs under the lines it names, and its quote is drawn inside the card:
|
|
33
|
+
* past a screenful the reader is looking at a card that is bigger than the code around it, which is
|
|
34
|
+
* the opposite of what annotating a few lines is for. A call-chain tour is many cards of a few lines
|
|
35
|
+
* each, not one card over the whole function — the refusal says so, so the caller can split it.
|
|
36
|
+
*/
|
|
37
|
+
export declare const MAX_ANNOTATION_LINES = 60;
|
|
38
|
+
/**
|
|
39
|
+
* The most characters one annotation's note may carry.
|
|
40
|
+
*
|
|
41
|
+
* The card lays its text out one row per line and reserves exactly that many rows in the diff, so an
|
|
42
|
+
* essay in a card pushes the code it is about off the screen. Anything longer belongs in the agent's
|
|
43
|
+
* own reply to the user, which is not measured in code rows.
|
|
44
|
+
*/
|
|
45
|
+
export declare const MAX_ANNOTATION_CHARS = 1000;
|
|
46
|
+
/**
|
|
47
|
+
* The most characters a category id may carry.
|
|
48
|
+
*
|
|
49
|
+
* The id is a grouping key, not prose: it is hashed for a colour and shown back in the mark's own label,
|
|
50
|
+
* so anything past this is noise the reader would never see. Longer ids are TRUNCATED rather than
|
|
51
|
+
* refused (see {@link normalizeCategory}).
|
|
52
|
+
*/
|
|
53
|
+
export declare const MAX_ANNOTATION_CATEGORY_CHARS = 64;
|
|
54
|
+
/**
|
|
55
|
+
* The category id as it is stored: trimmed, and cut to {@link MAX_ANNOTATION_CATEGORY_CHARS}.
|
|
56
|
+
*
|
|
57
|
+
* TRUNCATED rather than refused: the id is only ever hashed into a colour and echoed in the mark's label,
|
|
58
|
+
* so an over-long one still groups its annotations consistently — every call of that class is cut in the
|
|
59
|
+
* same place — while refusing would fail an entire annotation over a cosmetic detail. Absent, empty and
|
|
60
|
+
* whitespace-only all mean the same thing: no class, so no dot (see `CommentRecord.category`).
|
|
61
|
+
*
|
|
62
|
+
* @param value - the id the caller passed, if any.
|
|
63
|
+
* @returns the id to store, or `undefined` when there is none.
|
|
64
|
+
*/
|
|
65
|
+
export declare function normalizeCategory(value: string | undefined): string | undefined;
|
|
66
|
+
/** What the agent asked for, once the tool layer has resolved which pending entry it means. */
|
|
67
|
+
export interface AnnotationRequest {
|
|
68
|
+
/** The session the annotation belongs to, i.e. the one whose panel will draw it. */
|
|
69
|
+
sessionId: SessionId;
|
|
70
|
+
/** The pending entry to annotate: the file as the panel is showing it. */
|
|
71
|
+
entry: PendingEntry;
|
|
72
|
+
/**
|
|
73
|
+
* The first line to annotate, 1-based, counted in the file AS IT READS NOW — the numbers an editor
|
|
74
|
+
* shows for the file on disk, which is also what the card's own header will name.
|
|
75
|
+
*/
|
|
76
|
+
startLine: number;
|
|
77
|
+
/** The last line, inclusive. Absent means the one line `startLine` names. */
|
|
78
|
+
endLine?: number | undefined;
|
|
79
|
+
/** The annotation itself: the card's first turn, written by the agent. */
|
|
80
|
+
note: string;
|
|
81
|
+
/**
|
|
82
|
+
* Optional class of annotation: the same id for every annotation of one kind, a different one for the
|
|
83
|
+
* next. Opaque to the host and to the panel — hashed into a colour and shown back to the reader in the
|
|
84
|
+
* mark's label — so an agent annotating several passes can be told apart at a glance. Normalised by
|
|
85
|
+
* {@link normalizeCategory}: trimmed, empty means absent, cut to
|
|
86
|
+
* {@link MAX_ANNOTATION_CATEGORY_CHARS}.
|
|
87
|
+
*/
|
|
88
|
+
category?: string | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* Optional guard: the text the caller believes those lines read, newline-joined.
|
|
91
|
+
*
|
|
92
|
+
* The caller read the file at some point and the file may have changed since. Given, it is compared
|
|
93
|
+
* with the lines the entry has NOW, and a mismatch is refused with the text that is actually there —
|
|
94
|
+
* which is the difference between an annotation of the code the agent meant and one pinned to
|
|
95
|
+
* whatever moved into those numbers.
|
|
96
|
+
*/
|
|
97
|
+
quote?: string | undefined;
|
|
98
|
+
/** The record's id. Defaults to a fresh uuid; injectable so a retry can reuse one. */
|
|
99
|
+
id?: string | undefined;
|
|
100
|
+
/** The record's timestamps. Defaults to now; injectable for tests. */
|
|
101
|
+
now?: number | undefined;
|
|
102
|
+
}
|
|
103
|
+
/** Why an annotation was refused, in the words the calling agent is handed. */
|
|
104
|
+
export type AnnotationRefusal =
|
|
105
|
+
/** The note was empty (after trimming): there is nothing for the card to say. */
|
|
106
|
+
{
|
|
107
|
+
outcome: 'empty-note';
|
|
108
|
+
}
|
|
109
|
+
/** The note is longer than {@link MAX_ANNOTATION_CHARS}. */
|
|
110
|
+
| {
|
|
111
|
+
outcome: 'note-too-long';
|
|
112
|
+
limit: number;
|
|
113
|
+
length: number;
|
|
114
|
+
}
|
|
115
|
+
/** The range is backwards or names more lines than {@link MAX_ANNOTATION_LINES}. */
|
|
116
|
+
| {
|
|
117
|
+
outcome: 'range-too-wide';
|
|
118
|
+
limit: number;
|
|
119
|
+
lines: number;
|
|
120
|
+
}
|
|
121
|
+
/** The range is not inside the file at all. */
|
|
122
|
+
| {
|
|
123
|
+
outcome: 'out-of-range';
|
|
124
|
+
lines: number;
|
|
125
|
+
startLine: number;
|
|
126
|
+
endLine: number;
|
|
127
|
+
}
|
|
128
|
+
/** The caller's `quote` guard did not match the lines that are there now. */
|
|
129
|
+
| {
|
|
130
|
+
outcome: 'quote-mismatch';
|
|
131
|
+
actual: string;
|
|
132
|
+
}
|
|
133
|
+
/** Another comment already covers those lines, and the refusal says which. */
|
|
134
|
+
| {
|
|
135
|
+
outcome: 'already-annotated';
|
|
136
|
+
comment: CommentRecord;
|
|
137
|
+
start: number;
|
|
138
|
+
end: number;
|
|
139
|
+
};
|
|
140
|
+
/** What one annotation request produced. */
|
|
141
|
+
export type AnnotationResult = {
|
|
142
|
+
outcome: 'annotated';
|
|
143
|
+
comment: CommentRecord;
|
|
144
|
+
} | AnnotationRefusal;
|
|
145
|
+
/**
|
|
146
|
+
* The quote, the context fingerprint and the gutter numbers a range of a file's own lines produces.
|
|
147
|
+
*
|
|
148
|
+
* The shape mirrors what the panel records when the reader annotates rows: the quote is the named lines
|
|
149
|
+
* newline-joined, the context is one line each side of them (as far as the file reaches, which is how
|
|
150
|
+
* the client's own `contextAt` clamps at the ends), and the gutter numbers are the new-file numbers of
|
|
151
|
+
* the quoted lines.
|
|
152
|
+
*
|
|
153
|
+
* The old-file side is deliberately absent, and so is the add/del/context marker: the host holds the
|
|
154
|
+
* file's current text, not a diff, so it cannot say which of those lines the change added and which it
|
|
155
|
+
* left alone. Absent is the honest answer — the panel draws such a quote with the numbers it has and
|
|
156
|
+
* no wash, exactly as it does for a thread recorded before those fields existed.
|
|
157
|
+
*
|
|
158
|
+
* @param lines - the file's lines, as `commentFileLines` splits them.
|
|
159
|
+
* @param startLine - first quoted line, 1-based.
|
|
160
|
+
* @param endLine - last quoted line, 1-based and inclusive.
|
|
161
|
+
* @returns the record's quote fields.
|
|
162
|
+
*/
|
|
163
|
+
export declare function annotationQuote(lines: readonly string[], startLine: number, endLine: number): Pick<CommentRecord, 'quote' | 'quoteContext' | 'quoteLines'>;
|
|
164
|
+
/**
|
|
165
|
+
* Turn one annotation request into the record the panel draws, or say why it cannot be made.
|
|
166
|
+
*
|
|
167
|
+
* The order of the checks is the order a caller can act on them: what it asked for (note, range), then
|
|
168
|
+
* whether the code it means is still there (`quote`), then whether the reader already has a card on
|
|
169
|
+
* those lines. Nothing is written here — the caller hands the record to the comment store — so this is
|
|
170
|
+
* a pure function of the entry, the stored comments and the request, and it can be tested as one.
|
|
171
|
+
*
|
|
172
|
+
* @param request - what to annotate and with what words.
|
|
173
|
+
* @param existing - every comment the store holds for this entry.
|
|
174
|
+
* @returns the record to store, or the refusal to hand back.
|
|
175
|
+
*/
|
|
176
|
+
export declare function annotateLines(request: AnnotationRequest, existing: readonly CommentRecord[]): AnnotationResult;
|
|
177
|
+
/**
|
|
178
|
+
* The one line a refusal is summarised with, for the caller's own log: the outcome and what it says.
|
|
179
|
+
*
|
|
180
|
+
* The agent is handed a full sentence by the tool layer (see `annotate-tool.ts`), which is where the
|
|
181
|
+
* wording lives because that is what a model reads. This is for the plugin's logger.
|
|
182
|
+
*
|
|
183
|
+
* @param refusal - the refusal to name.
|
|
184
|
+
* @returns the outcome, plus the numbers that make two of the same kind tellable apart.
|
|
185
|
+
*/
|
|
186
|
+
export declare function refusalSummary(refusal: AnnotationRefusal): string;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stage a JSON envelope beside its target and rename it into place, retrying the rename.
|
|
3
|
+
*
|
|
4
|
+
* Both stores in this plugin persist the same way — write a sibling temp file, `rename` it over the
|
|
5
|
+
* real one — because a crash must leave either the old file or the new one and never a torn one. What
|
|
6
|
+
* neither of them did was survive the rename itself being refused, and on Windows that is not rare:
|
|
7
|
+
* replacing a file another process has open at that instant answers `EPERM`, and the readers here are
|
|
8
|
+
* everyone — a second panel, a backup agent, an antivirus, the user's own editor, or this suite's own
|
|
9
|
+
* 50ms `readFile` loop waiting for the write. The refusal is transient by construction (the handle that
|
|
10
|
+
* caused it closes within a tick), but the write it killed was the ONLY copy of that update: the
|
|
11
|
+
* pending list or a session's comment file simply kept its previous content, and the failure was
|
|
12
|
+
* reported as "your threads will not survive a restart" without ever being retried.
|
|
13
|
+
*
|
|
14
|
+
* So the rename is attempted a few times with a short, growing pause. Measured on this machine with a
|
|
15
|
+
* concurrent reader, the retry turns 6 lost writes in 12 into 0 — see `tests/atomic-write.spec.ts` for
|
|
16
|
+
* the deterministic shape (an injected rename that fails once) and the commit message for the numbers.
|
|
17
|
+
*
|
|
18
|
+
* Failures that are NOT about a held handle (a missing directory, a full disk, a read-only mount) are
|
|
19
|
+
* rethrown on the first attempt: retrying those only delays the report the caller has to make.
|
|
20
|
+
*
|
|
21
|
+
* The directory is `node:path`'s own `dirname`, not a scan for the last separator: on Windows the
|
|
22
|
+
* storage root is `C:\Users\<user>\.dsh\diff-approval\workspaces`, which holds no forward slash at all —
|
|
23
|
+
* a scan answered `.`, the `mkdir` before each write became a no-op, and the write threw ENOENT, so
|
|
24
|
+
* nothing was ever persisted there.
|
|
25
|
+
*
|
|
26
|
+
* @module dsh-diff-approval/src/atomic-write
|
|
27
|
+
*/
|
|
28
|
+
/** The file verbs one atomic write uses, injectable so the retry can be tested without a real race. */
|
|
29
|
+
export interface AtomicWriteFs {
|
|
30
|
+
/** Create the target's directory, missing parents included. */
|
|
31
|
+
mkdir: (directory: string, options: {
|
|
32
|
+
recursive: true;
|
|
33
|
+
}) => Promise<unknown>;
|
|
34
|
+
/** Write the staged bytes. */
|
|
35
|
+
writeFile: (file: string, data: string, encoding: 'utf8') => Promise<unknown>;
|
|
36
|
+
/** Replace the target with the staged file. */
|
|
37
|
+
rename: (from: string, to: string) => Promise<void>;
|
|
38
|
+
/** Remove a staged file that will not be renamed. */
|
|
39
|
+
remove: (file: string) => Promise<unknown>;
|
|
40
|
+
}
|
|
41
|
+
/** How hard a refused rename is tried, and how long to wait between attempts. */
|
|
42
|
+
export interface AtomicWriteRetry {
|
|
43
|
+
/** Total attempts, the first one included. */
|
|
44
|
+
attempts: number;
|
|
45
|
+
/** Pause before the second attempt, in milliseconds; each further attempt doubles it. */
|
|
46
|
+
delayMs: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The default policy: five attempts, 25ms then 50, 100, 200 — a little over a third of a second in all.
|
|
50
|
+
*
|
|
51
|
+
* Long enough for the handle that refused the rename to be closed (a reader gets its next turn of the
|
|
52
|
+
* loop, an antivirus finishes its scan), and short enough that a caller who is waiting on the write
|
|
53
|
+
* — the panel's keep, a comment being added — cannot tell it retried at all.
|
|
54
|
+
*/
|
|
55
|
+
export declare const ATOMIC_WRITE_RETRY: AtomicWriteRetry;
|
|
56
|
+
/**
|
|
57
|
+
* Whether one rename error is the transient "somebody else has this file open" refusal.
|
|
58
|
+
*
|
|
59
|
+
* Windows answers `EPERM` for a replace whose target is open; `EACCES` and `EBUSY` are the same
|
|
60
|
+
* situation seen through a network share or a mapped drive. Nothing else is retried.
|
|
61
|
+
*
|
|
62
|
+
* @param error - whatever the rename threw.
|
|
63
|
+
* @returns whether another attempt is worth making.
|
|
64
|
+
*/
|
|
65
|
+
export declare function isHeldOpen(error: unknown): boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Write one JSON envelope atomically, retrying a rename that a held handle refused.
|
|
68
|
+
*
|
|
69
|
+
* @param file - the file to replace.
|
|
70
|
+
* @param value - the value to serialise into it.
|
|
71
|
+
* @param options - the fs verbs and the retry policy, for tests.
|
|
72
|
+
* @throws the rename's own error when the attempts run out, or at once when it is not a held handle.
|
|
73
|
+
*/
|
|
74
|
+
export declare function writeJsonAtomic(file: string, value: unknown, options?: {
|
|
75
|
+
fs?: Partial<AtomicWriteFs> | undefined;
|
|
76
|
+
retry?: AtomicWriteRetry | undefined;
|
|
77
|
+
}): Promise<void>;
|