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,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a comment's quote sits in a file's CURRENT content: the rule that turns the lines a comment
|
|
3
|
+
* was WRITTEN on into the lines it is about now.
|
|
4
|
+
*
|
|
5
|
+
* Pure and free of both Node and the DOM, so the two halves of the plugin import the very same rule:
|
|
6
|
+
* the host resolves each comment against the entry content it hangs off and ships the figures on the
|
|
7
|
+
* list read, and the browser half draws them. The panel's own row-level re-anchor
|
|
8
|
+
* (`src/client/discussion.ts`) is this same rule seen through the diff's rows — with one row per
|
|
9
|
+
* line, a removed line among them, and the quote's context taken from the rows — and it stays the
|
|
10
|
+
* browser's business: where a card is DRAWN is a layout fact. What a reader is TOLD (the list's
|
|
11
|
+
* label, the card's own chip, the jump's target line) comes from here, once, so two panes cannot
|
|
12
|
+
* name two lines for one comment.
|
|
13
|
+
*
|
|
14
|
+
* The two models can disagree in one situation, and it is worth knowing which: the host has the
|
|
15
|
+
* file's own lines, while the client's rows also carry the lines this file DELETED. A quote whose
|
|
16
|
+
* recorded `quoteContext` was captured across a deleted row therefore matches a context window the
|
|
17
|
+
* host cannot reproduce, and the host answers `undefined` where the client would re-anchor. The
|
|
18
|
+
* caller then falls back to `record.anchor` — the line the comment was written on — which is what an
|
|
19
|
+
* outdated thread shows anyway: the number stays honest rather than drifting.
|
|
20
|
+
*
|
|
21
|
+
* What the two may NOT disagree about is how far a match is allowed to be from the stored line: a
|
|
22
|
+
* look-alike elsewhere in the file is refused by both, through the one bound they share
|
|
23
|
+
* (`REANCHOR_MAX_LINES`), so the list's figure, the card's chip and the card's place cannot name two
|
|
24
|
+
* different lines for one comment.
|
|
25
|
+
* @module dsh-diff-approval/comment-lines
|
|
26
|
+
*/
|
|
27
|
+
/** The new-file lines one comment's quote settles on now (1-based, inclusive). */
|
|
28
|
+
export interface CommentLineRange {
|
|
29
|
+
/** First new-file line the quote occupies in this content. */
|
|
30
|
+
start: number;
|
|
31
|
+
/** Last one. A one-line quote has `end === start`. */
|
|
32
|
+
end: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The record fields resolution reads: everything a re-anchor needs and nothing else.
|
|
36
|
+
*
|
|
37
|
+
* Structurally satisfied by `CommentRecord`, so a caller passes the record itself. The reader's own
|
|
38
|
+
* words, the questions asked and the timestamps play no part in where the code is.
|
|
39
|
+
*/
|
|
40
|
+
export interface CommentLineSource {
|
|
41
|
+
/** The lines the annotation was made on, as new-file numbers. */
|
|
42
|
+
anchor: {
|
|
43
|
+
startLine: number;
|
|
44
|
+
endLine: number;
|
|
45
|
+
};
|
|
46
|
+
/** The anchored lines as they read when the comment was written. */
|
|
47
|
+
quote: string;
|
|
48
|
+
/**
|
|
49
|
+
* `quote` with one line of context on each side, as it read then, when the record kept one. It is
|
|
50
|
+
* what tells a genuine move of the quoted code from another line elsewhere in the file that
|
|
51
|
+
* happens to read the same — without it a comment on a blank line or a closing brace followed that
|
|
52
|
+
* line wherever it appeared.
|
|
53
|
+
*/
|
|
54
|
+
quoteContext?: string | undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* One file's content lines, numbered exactly as the diff numbers them.
|
|
58
|
+
*
|
|
59
|
+
* Both line endings are normalized first (`\r\n` and a lone `\r` become `\n`), and a single trailing
|
|
60
|
+
* newline is a line TERMINATOR rather than an extra empty line — the same convention the browser
|
|
61
|
+
* half's whole-file diff splits on (its `contentLines`), so line 1 here is the line the diff calls
|
|
62
|
+
* line 1 and the two cannot drift by a trailing newline.
|
|
63
|
+
*
|
|
64
|
+
* @param text - the file's content.
|
|
65
|
+
* @returns its lines, without the terminating newline; an empty file has none.
|
|
66
|
+
*/
|
|
67
|
+
export declare function commentFileLines(text: string): string[];
|
|
68
|
+
/**
|
|
69
|
+
* How far a comment's quoted code may sit from the line the comment was written on and still count as
|
|
70
|
+
* that code having MOVED, in lines.
|
|
71
|
+
*
|
|
72
|
+
* A comment is re-anchored by its quote, and a quote is not unique: two declarations in one file can
|
|
73
|
+
* read exactly alike. The recorded context tells such places apart only as far as it reaches — one
|
|
74
|
+
* line each side — which two identically-shaped fields (`UPROPERTY` line, field, blank line) defeat
|
|
75
|
+
* completely. Distance is the other half of the answer, and the half that does not depend on what the
|
|
76
|
+
* record happened to keep: this code MOVING is a shift of a few lines, the insert or delete directly
|
|
77
|
+
* above it that every edit produces, while a match tens or hundreds of lines away is another place in
|
|
78
|
+
* the file that reads the same. The report's two cases bracket the number: the same declaration moved
|
|
79
|
+
* four lines when four lines above it were deleted, and a declaration that had been DELETED matched an
|
|
80
|
+
* identically-shaped one 74 lines below. Thirty keeps the first and refuses the second.
|
|
81
|
+
*
|
|
82
|
+
* Thirty is a screenful of code rather than a fact about the code: farther than that the reader is
|
|
83
|
+
* looking at another part of the file, and following it is a teleport rather than a correction — the
|
|
84
|
+
* comment ends up shown against code it was never written about, which is the bug this bound fixes.
|
|
85
|
+
* Any bound between the two real cases separates them; this one leaves a genuine shift of a
|
|
86
|
+
* function-sized block room to be followed, and nothing more.
|
|
87
|
+
*
|
|
88
|
+
* Both halves compare against this one number: the host in the file's own lines, the browser half in
|
|
89
|
+
* the rows of its diff model (a row is a line, or a line the file no longer has). A row distance is
|
|
90
|
+
* never smaller than the line distance it stands for, so a match the browser half refuses is one the
|
|
91
|
+
* host refused too, and the two panes cannot name a line the other will not draw.
|
|
92
|
+
*/
|
|
93
|
+
export declare const REANCHOR_MAX_LINES = 30;
|
|
94
|
+
/**
|
|
95
|
+
* Whether a match that far from where the comment was written is that code having moved.
|
|
96
|
+
*
|
|
97
|
+
* The rule the two halves share, so neither can widen or narrow the bound on its own (see
|
|
98
|
+
* `REANCHOR_MAX_LINES` for the number and for what it separates). The bound is inclusive: at exactly
|
|
99
|
+
* the limit the code is taken to have moved.
|
|
100
|
+
*
|
|
101
|
+
* @param stored - the line (the host's content) or row (the browser half's model) the comment names.
|
|
102
|
+
* @param found - the line or row the quote was found on.
|
|
103
|
+
* @returns whether that match is close enough to be the same code.
|
|
104
|
+
*/
|
|
105
|
+
export declare function reanchorWithinLimit(stored: number, found: number): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Where one comment's quote sits in a file's current content, or `undefined` when it is nowhere.
|
|
108
|
+
*
|
|
109
|
+
* The rule, in the order it is applied — the same order the browser half's `remapDiscussion` applies
|
|
110
|
+
* it in, so the same file and the same record produce the same number in both halves:
|
|
111
|
+
*
|
|
112
|
+
* 1. The lines the comment names have to exist in this file at all. When even the anchor's first
|
|
113
|
+
* line is past the end, nothing here can be re-anchored: `undefined`.
|
|
114
|
+
* 2. The anchor's own line still reads the quote: the comment has not moved. The stored range is the
|
|
115
|
+
* answer, `endLine` included — a range whose quote covers a different number of lines (a removed
|
|
116
|
+
* row was quoted among them) still belongs to the reader's own numbers.
|
|
117
|
+
* 3. Otherwise the quote is followed: every window of as many lines as the quote has is compared to
|
|
118
|
+
* it, the recorded context has to agree where the record carries one, and the occurrence CLOSEST
|
|
119
|
+
* to the stored anchor wins — that is where the reader last saw the code. An equal distance keeps
|
|
120
|
+
* the earlier occurrence, which is the first window an ascending scan meets.
|
|
121
|
+
* 4. The occurrence also has to be NEAR the stored anchor (see `REANCHOR_MAX_LINES`): a window
|
|
122
|
+
* farther than that from the line the comment was written on is not this code having moved but
|
|
123
|
+
* another place in the file that reads the same — however exactly its recorded context agrees.
|
|
124
|
+
* 5. Nothing matched: `undefined`. The caller falls back to `record.anchor`, the line the comment was
|
|
125
|
+
* written on — the same answer the code view gives an outdated thread.
|
|
126
|
+
*
|
|
127
|
+
* @param text - the entry's current content (the file as it reads now).
|
|
128
|
+
* @param comment - the stored comment's anchor and quote.
|
|
129
|
+
* @returns the new-file line range the quote occupies now, or `undefined` when it is gone.
|
|
130
|
+
*/
|
|
131
|
+
export declare function resolveCommentLines(text: string, comment: CommentLineSource): CommentLineRange | undefined;
|
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable comment threads, one JSON file per session under
|
|
3
|
+
* `<commentsDir>/<sessionId>.json`, beside the pending-entry storage but never in
|
|
4
|
+
* the same file: a comment that outlived its entry is exactly the leak this
|
|
5
|
+
* module exists to prevent, so a pending write that fails must not be able to
|
|
6
|
+
* take comments with it (or the other way round).
|
|
7
|
+
*
|
|
8
|
+
* A comment belongs to ONE pending entry (`entryId`, which is the entry's path —
|
|
9
|
+
* the store's global identity) and to ONE session. It carries the annotation and
|
|
10
|
+
* the identity of the turn that answered it (`messageId`, `turn`); it does NOT
|
|
11
|
+
* carry the answer's text, because the session transcript stays the single source
|
|
12
|
+
* of truth for what the agent said — a client reads the answer out of the
|
|
13
|
+
* transcript by that id. Saves rewrite the whole session file, staged as a sibling
|
|
14
|
+
* temp file and atomically renamed, so a crash leaves either the old or the new
|
|
15
|
+
* file. A missing file reads as empty; unknown versions and non-JSON content throw.
|
|
16
|
+
* @module dsh-diff-approval/src/comments
|
|
17
|
+
*/
|
|
18
|
+
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
19
|
+
import type { CommentAsk, CommentRecord } from './types.ts';
|
|
20
|
+
import type { AskRead } from './comment-ask.ts';
|
|
21
|
+
/**
|
|
22
|
+
* The comment directory that belongs to a pending-storage root.
|
|
23
|
+
*
|
|
24
|
+
* The plugin's own state directory is `<dshHome>/diff-approval`, and inside it the
|
|
25
|
+
* pending file lives in `workspaces/` and comments in `comments/` — siblings, so a
|
|
26
|
+
* write that fails on one side cannot take the other with it. Any OTHER root is a
|
|
27
|
+
* self-contained state directory (a deployment's chosen location, a test's temp
|
|
28
|
+
* dir) and keeps its comments INSIDE it: the sibling of an arbitrary directory can
|
|
29
|
+
* be shared by two instances that were each pointed somewhere different, and two
|
|
30
|
+
* instances writing one comment file is a leak rather than a layout choice.
|
|
31
|
+
*
|
|
32
|
+
* @param storageRoot - the resolved pending-storage root.
|
|
33
|
+
* @returns the directory holding the comments that belong to that root.
|
|
34
|
+
*/
|
|
35
|
+
export declare function commentsDirFor(storageRoot: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* The scope one request may act on: which comment AUTHORS its lineage covers.
|
|
38
|
+
*
|
|
39
|
+
* This module knows nothing about lineages — the host walks them — so the rule arrives as a predicate,
|
|
40
|
+
* built once per request from the lineage view the endpoint already has (`sameRoot`). Every read and
|
|
41
|
+
* every guard takes it, which is what lets the store answer for a whole lineage without a second
|
|
42
|
+
* lineage rule living down here beside the first.
|
|
43
|
+
*/
|
|
44
|
+
export type CommentScope = (author: SessionId) => boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The transcript one question's answer will be written in.
|
|
47
|
+
*
|
|
48
|
+
* `ask.sessionId` when the question recorded one — every question asked since a lineage could read more
|
|
49
|
+
* than one seat's threads does — otherwise the comment's own session, which is where a question asked
|
|
50
|
+
* before that field existed necessarily went: the only session that could ask it was the one that wrote
|
|
51
|
+
* the thread. ONE definition, shared by the answer fold, the turn-end match and the asker's grouping,
|
|
52
|
+
* so the three cannot disagree about which transcript a question belongs to.
|
|
53
|
+
* @param ask - the question.
|
|
54
|
+
* @param comment - the thread it was asked in.
|
|
55
|
+
* @returns the session whose transcript holds its answer.
|
|
56
|
+
*/
|
|
57
|
+
export declare function askTranscript(ask: CommentAsk, comment: CommentRecord): SessionId;
|
|
58
|
+
/**
|
|
59
|
+
* How a caller hears about the two ways a comment file can go wrong.
|
|
60
|
+
*
|
|
61
|
+
* The store reads and writes plain JSON files and owns no logger, so both halves are
|
|
62
|
+
* callbacks: the host wires them to `ctx.logger` and to the list read the panel draws,
|
|
63
|
+
* which is what turns "the write failed" from a fact only the filesystem knew into
|
|
64
|
+
* something the reader is told (and something a test can assert without a disk).
|
|
65
|
+
*/
|
|
66
|
+
export interface CommentStoreOptions {
|
|
67
|
+
/**
|
|
68
|
+
* One session's comments could not be read, and that file was skipped. Called once per
|
|
69
|
+
* skipped file, with the file's path, because the caller's warning has to name it.
|
|
70
|
+
*/
|
|
71
|
+
onLoadSkipped?: (file: string, error: unknown) => void;
|
|
72
|
+
/**
|
|
73
|
+
* A save failed (`message` is the reason) or a session's failure cleared (`undefined`).
|
|
74
|
+
* Called only when the report CHANGES, so a writer retrying every second cannot bury
|
|
75
|
+
* the log; the same value also rides the list read as `persistError()`. One session's
|
|
76
|
+
* healthy write does not clear another's failure — see `persistError`.
|
|
77
|
+
*/
|
|
78
|
+
onPersistError?: (message: string | undefined) => void;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* File-backed comment threads, one file per session.
|
|
82
|
+
*
|
|
83
|
+
* Every mutation is applied to memory synchronously and schedules that session's
|
|
84
|
+
* file write on its own chain, so a caller removing an entry can drop its comments
|
|
85
|
+
* in the same tick as the entry itself without awaiting anything — the same
|
|
86
|
+
* fire-and-forget persistence the pending store uses.
|
|
87
|
+
*/
|
|
88
|
+
export declare class CommentStore {
|
|
89
|
+
private readonly root;
|
|
90
|
+
private readonly options;
|
|
91
|
+
private readonly byId;
|
|
92
|
+
private readonly tails;
|
|
93
|
+
private revision;
|
|
94
|
+
/** Files `loadAll` could not read, in the order it met them (see `skippedFiles`). */
|
|
95
|
+
private readonly skipped;
|
|
96
|
+
/**
|
|
97
|
+
* The save failure of every session that has one, keyed by that session's file and in the
|
|
98
|
+
* order the failures happened (see `persistError`). Empty while every write works.
|
|
99
|
+
*/
|
|
100
|
+
private readonly persistFailures;
|
|
101
|
+
/**
|
|
102
|
+
* Sessions whose unreadable file could not even be moved aside, keyed by that file: the
|
|
103
|
+
* reason their writes are refused rather than allowed to truncate the bytes (see `save`).
|
|
104
|
+
*/
|
|
105
|
+
private readonly unmovable;
|
|
106
|
+
/**
|
|
107
|
+
* @param root - directory holding one `<sessionId>.json` per session.
|
|
108
|
+
* @param options - the error seam (see {@link CommentStoreOptions}).
|
|
109
|
+
*/
|
|
110
|
+
constructor(root: string, options?: CommentStoreOptions);
|
|
111
|
+
/**
|
|
112
|
+
* Load every session's comments. A malformed row is skipped rather than
|
|
113
|
+
* failing the load: one bad comment must not cost the whole workspace's
|
|
114
|
+
* comments (or the plugin's activation).
|
|
115
|
+
*
|
|
116
|
+
* A file that cannot be READ is skipped the same way, because the alternative is
|
|
117
|
+
* worse in both directions: the old all-or-nothing load threw, the caller logged
|
|
118
|
+
* once and carried on with an EMPTY store, and the next save then wrote that
|
|
119
|
+
* emptiness back over every good file. One unreadable file therefore cost every
|
|
120
|
+
* session's comments. Skipping keeps the other sessions' threads, and the skipped
|
|
121
|
+
* file is moved aside (`.corrupt`) rather than dropped: it is not deleted — the
|
|
122
|
+
* bytes may be the only copy — but it is also not left where the next save for
|
|
123
|
+
* that session would silently overwrite it, which would destroy them for good.
|
|
124
|
+
*
|
|
125
|
+
* When even that fails — the file cannot be moved aside — that session's writes are
|
|
126
|
+
* refused from then on (see `save`) rather than allowed to truncate the only copy.
|
|
127
|
+
* "Skipped but still writable" is exactly the shape that turns a read failure into
|
|
128
|
+
* data loss: the store would answer "added", write the memory-only view over bytes
|
|
129
|
+
* nobody could read, and lose what the skip had just refused to guess at.
|
|
130
|
+
* @returns how many comments were loaded.
|
|
131
|
+
*/
|
|
132
|
+
loadAll(): Promise<number>;
|
|
133
|
+
/**
|
|
134
|
+
* The files a load skipped, so the caller can say what went missing instead of
|
|
135
|
+
* reporting a session whose comments silently vanished.
|
|
136
|
+
* @returns the skipped files' paths, in load order.
|
|
137
|
+
*/
|
|
138
|
+
skippedFiles(): string[];
|
|
139
|
+
/**
|
|
140
|
+
* The failure the reader has to be told about, or `undefined` while every write works.
|
|
141
|
+
* This is what rides the list read so the panel can say the threads are memory-only;
|
|
142
|
+
* a later successful save retracts it, mirroring the pending half's own report.
|
|
143
|
+
*
|
|
144
|
+
* Per SESSION, not one global slot: a deployment where one session's file cannot be
|
|
145
|
+
* written and another's can is the ordinary case (a locked file, a full quota, a
|
|
146
|
+
* directory that vanished), and letting the healthy write retract the broken one's
|
|
147
|
+
* report would say the threads are safe while that session's live in memory only —
|
|
148
|
+
* the same "a failure that looks like a success" trap, one session over. A session's
|
|
149
|
+
* own success is what retracts its own entry (see `reportPersist`).
|
|
150
|
+
* @returns the most recent outstanding failure's message, or undefined.
|
|
151
|
+
*/
|
|
152
|
+
persistError(): string | undefined;
|
|
153
|
+
/**
|
|
154
|
+
* Record a skipped file: keep it for the caller, name it, and move it aside so a later
|
|
155
|
+
* save for that session writes a fresh file rather than overwriting the unreadable
|
|
156
|
+
* bytes. A quarantine that itself fails is still a skip — the store must not be the
|
|
157
|
+
* reason a single bad file takes the whole load down — but that session's file is then
|
|
158
|
+
* the only copy of its threads, so its writes are refused (see `save`) instead of being
|
|
159
|
+
* allowed to truncate it.
|
|
160
|
+
* @param file - the file that could not be read.
|
|
161
|
+
* @param error - why it could not be read.
|
|
162
|
+
*/
|
|
163
|
+
private skip;
|
|
164
|
+
/**
|
|
165
|
+
* The comments one SCOPE may read, oldest first (the order threads are read in).
|
|
166
|
+
*
|
|
167
|
+
* The scope is the caller's lineage rule (`CommentScope`), so a seat reads every thread of the seats
|
|
168
|
+
* it shares a lineage root with — not only the ones it wrote. The files do not move for that: each
|
|
169
|
+
* comment still lives in its author's file (see `save`), and the fan-out happens here, in memory,
|
|
170
|
+
* over the one map `loadAll` filled from every file. That is what keeps a teammate's already-written
|
|
171
|
+
* threads visible, with no migration.
|
|
172
|
+
* @param scope - which comment authors this caller may read.
|
|
173
|
+
* @returns the comments those authors wrote, oldest first.
|
|
174
|
+
*/
|
|
175
|
+
list(scope: CommentScope): CommentRecord[];
|
|
176
|
+
/** One author's own comments, oldest first: the WRITE-SET of that author's file (see `save`). */
|
|
177
|
+
private authoredBy;
|
|
178
|
+
/** A comment by id, whatever session it belongs to. */
|
|
179
|
+
get(id: string): CommentRecord | undefined;
|
|
180
|
+
/**
|
|
181
|
+
* A counter bumped on every change, so a poller can tell "nothing new" from
|
|
182
|
+
* "re-read": the list endpoint ships the session's comments anyway, and this is
|
|
183
|
+
* what says whether anything moved since the last read.
|
|
184
|
+
*/
|
|
185
|
+
commentsRevision(): number;
|
|
186
|
+
/**
|
|
187
|
+
* Fold what ONE transcript now says into the threads that asked there, so a thread whose answer has
|
|
188
|
+
* arrived (or been rewritten) wears the dot.
|
|
189
|
+
*
|
|
190
|
+
* The answers are DERIVED on every list read and never trusted from the file (see
|
|
191
|
+
* `CommentRecord.answerNow`): the read is the only moment the host knows what the agent has said, and
|
|
192
|
+
* the comparison it makes is against a baseline that survives a restart — a restart must not light a
|
|
193
|
+
* dot the reader already cleared, and a read that changed nothing must not light one at all. That is
|
|
194
|
+
* what `answerSeen` is, and why this writes a baseline only when the text actually moved.
|
|
195
|
+
*
|
|
196
|
+
* A question the transcript shows no answer for goes back to ABSENT rather than looking like an
|
|
197
|
+
* empty answer: a log this process cannot read (or a turn that has not written yet) then leaves the
|
|
198
|
+
* dot exactly as it was, instead of raising it on every poll.
|
|
199
|
+
*
|
|
200
|
+
* The patch's own key is the TRANSCRIPT, not the comment's author: a thread written in one seat can
|
|
201
|
+
* hold a question asked from another (see `askTranscript`), and this read is that other seat's log.
|
|
202
|
+
* Only the questions whose answer lives in `sessionId` are folded; a thread that also holds questions
|
|
203
|
+
* asked elsewhere keeps those exactly as that transcript's own fold left them.
|
|
204
|
+
*
|
|
205
|
+
* @param sessionId - the transcript being folded, which owns the questions it is handed.
|
|
206
|
+
* @param answers - the answer text per question id, as that transcript read it.
|
|
207
|
+
* @returns whether any thread changed.
|
|
208
|
+
*/
|
|
209
|
+
syncAnswers(sessionId: SessionId, answers: Readonly<Record<string, AskRead>>): boolean;
|
|
210
|
+
/**
|
|
211
|
+
* Mark one comment as looked at: the dot goes out, and the answers it currently shows become the
|
|
212
|
+
* baseline a later rewrite is measured against — otherwise the next read would find the same answer
|
|
213
|
+
* "new" again and put the dot straight back up.
|
|
214
|
+
*
|
|
215
|
+
* Deliberately its own seam rather than something a read does: only a reader action (the card coming
|
|
216
|
+
* into view) may clear attention, and a read that cleared it would make the dot impossible to see.
|
|
217
|
+
*
|
|
218
|
+
* @param id - the comment the reader has in front of them.
|
|
219
|
+
* @returns whether the comment existed.
|
|
220
|
+
*/
|
|
221
|
+
markSeen(id: string): boolean;
|
|
222
|
+
/**
|
|
223
|
+
* Record one annotation.
|
|
224
|
+
*
|
|
225
|
+
* Writing a comment that is already here is the SAME comment written again — a client
|
|
226
|
+
* retrying a request whose response was dropped, or a second client sending the id it
|
|
227
|
+
* was handed — so the questions already asked in it are kept and only the annotation's
|
|
228
|
+
* own fields move. Overwriting the whole record would erase the identity of a question
|
|
229
|
+
* that is in flight, and its answer could then never be matched back to this thread;
|
|
230
|
+
* `createdAt` is kept for the same reason, since it is what a list read orders by.
|
|
231
|
+
*
|
|
232
|
+
* @param record - the comment to store.
|
|
233
|
+
* @returns the stored comment, which is the existing one when the id was already here.
|
|
234
|
+
*/
|
|
235
|
+
add(record: CommentRecord): CommentRecord;
|
|
236
|
+
/**
|
|
237
|
+
* Record several annotations at once, as ONE write per affected session.
|
|
238
|
+
*
|
|
239
|
+
* This is what putting a batch of restored comments back needs and what a loop of `add` calls
|
|
240
|
+
* cannot give it: each `add` saves its session's file, so an undo that restored ten threads wrote
|
|
241
|
+
* that file ten times — ten chances for a transient failure to leave the store and the disk
|
|
242
|
+
* disagreeing about an action the reader already watched happen. The records are stored by the SAME
|
|
243
|
+
* rule as `add` (a new id verbatim, an existing one merged with its `createdAt` and its questions
|
|
244
|
+
* kept), which is what lets a restore be an ordinary write rather than a special case: a snapshot
|
|
245
|
+
* that came back as a new comment, with a fresh id and no questions, would be a look-alike of the
|
|
246
|
+
* reader's thread.
|
|
247
|
+
*
|
|
248
|
+
* A batch that carries nothing changes nothing, so a no-op restore does not rewrite a file.
|
|
249
|
+
*
|
|
250
|
+
* @param records - the comments to store, in the caller's order.
|
|
251
|
+
* @returns the stored record for each input, index-aligned: what landed, which is not always what
|
|
252
|
+
* was handed over (a merge keeps the fields the existing comment owns). A count would drop that,
|
|
253
|
+
* and a caller needing it would have to `get` each id back and race a later change.
|
|
254
|
+
*/
|
|
255
|
+
addMany(records: readonly CommentRecord[]): CommentRecord[];
|
|
256
|
+
/**
|
|
257
|
+
* The record as the store keeps it: a new id VERBATIM, an id already here merged.
|
|
258
|
+
*
|
|
259
|
+
* The one place the merge rule lives, so `add` and `addMany` cannot drift: writing a comment that
|
|
260
|
+
* is already here is the same comment written again — a client retrying a request whose response
|
|
261
|
+
* was dropped, or a second client sending the id it was handed — so the questions already asked in
|
|
262
|
+
* it are kept and only the annotation's own fields move. Overwriting the whole record would erase
|
|
263
|
+
* the identity of a question that is in flight, and its answer could then never be matched back to
|
|
264
|
+
* this thread; `createdAt` is kept for the same reason, since it is what a list read orders by.
|
|
265
|
+
* @param record - the comment as the caller wrote it.
|
|
266
|
+
* @returns the record to store under its id.
|
|
267
|
+
*/
|
|
268
|
+
private mergeOf;
|
|
269
|
+
/**
|
|
270
|
+
* Drop one comment the caller's scope covers.
|
|
271
|
+
* @param scope - which comment authors the caller may act on.
|
|
272
|
+
* @param id - the comment to drop.
|
|
273
|
+
* @returns whether it was there (and was in scope).
|
|
274
|
+
*/
|
|
275
|
+
remove(scope: CommentScope, id: string): boolean;
|
|
276
|
+
/**
|
|
277
|
+
* Drop several comments in one write PER FILE that holds them.
|
|
278
|
+
*
|
|
279
|
+
* This is what a batch action needs and what a loop of `remove` calls cannot give it: each
|
|
280
|
+
* `remove` saves the file, so ending ten comments wrote that file ten times — ten chances for a
|
|
281
|
+
* transient failure to leave the store and the disk disagreeing about an action the reader asked
|
|
282
|
+
* for once. The save happens only when something was actually dropped, so a batch that matches
|
|
283
|
+
* nothing does not rewrite a file.
|
|
284
|
+
*
|
|
285
|
+
* Each record is removed from the file that HOLDS it, which is its author's (`save`), not the
|
|
286
|
+
* caller's: once a lineage reads every seat's threads, a batch can name comments written by several
|
|
287
|
+
* seats, and routing them all to the caller's file would leave the real files holding records the
|
|
288
|
+
* store no longer has — the next load would resurrect every one of them.
|
|
289
|
+
*
|
|
290
|
+
* An id outside the scope, or one that is not here at all, is skipped rather than refused:
|
|
291
|
+
* a batch is one request and the comments it names are the ones it can act on, and a comment
|
|
292
|
+
* another client already ended is the state the caller was asking for anyway.
|
|
293
|
+
*
|
|
294
|
+
* @param scope - which comment authors the caller may act on.
|
|
295
|
+
* @param ids - the comments to drop; a repeated id is one comment.
|
|
296
|
+
* @returns the ids that were dropped, in the order given.
|
|
297
|
+
*/
|
|
298
|
+
removeMany(scope: CommentScope, ids: readonly string[]): string[];
|
|
299
|
+
/**
|
|
300
|
+
* Every comment one pending entry carries, oldest first.
|
|
301
|
+
*
|
|
302
|
+
* The read half of `removeForEntry` and kept beside it, because they are two answers about the same
|
|
303
|
+
* fact: which comments an entry owns. A caller that is about to drop the entry asks this first, so
|
|
304
|
+
* the undo pair can carry them back; the two must agree on that set, or a restore would put back
|
|
305
|
+
* fewer (or more) threads than the drop took.
|
|
306
|
+
* @param entryId - the entry id (= path).
|
|
307
|
+
* @returns that entry's comments, in list order.
|
|
308
|
+
*/
|
|
309
|
+
forEntry(entryId: string): CommentRecord[];
|
|
310
|
+
/**
|
|
311
|
+
* Drop every comment of one pending entry. This is what "a comment dies with
|
|
312
|
+
* its entry" means, and the entry knows nothing about comments, so the caller
|
|
313
|
+
* runs it beside the entry's own removal.
|
|
314
|
+
* @param entryId - the entry id (= path) that left the list.
|
|
315
|
+
* @returns how many comments went with it.
|
|
316
|
+
*/
|
|
317
|
+
removeForEntry(entryId: string): number;
|
|
318
|
+
/**
|
|
319
|
+
* The orphan sweep: prune every comment whose entry is not in `entryIds`, in MEMORY ONLY.
|
|
320
|
+
*
|
|
321
|
+
* The explicit `removeForEntry` is the primary path and the ONLY one that erases; this is the view
|
|
322
|
+
* filter that guarantees no read is ever handed a comment naming a file the list does not carry. Run it
|
|
323
|
+
* when the store loads and on every list read.
|
|
324
|
+
*
|
|
325
|
+
* WHY IT DOES NOT SAVE (2026-10-06, a data loss measured on the live host). The sweep used to write the
|
|
326
|
+
* pruned state to the files, on the assumption that an entry absent here had been removed while the host
|
|
327
|
+
* was running. That is only one of two ways an entry can be missing; the other is that its ADD never
|
|
328
|
+
* reached `pending.json` — a pending entry is folded in memory and persisted on a later flush — and then
|
|
329
|
+
* the sweep deleted the comments hanging on it AND saved the deletion. Three real annotations were gone
|
|
330
|
+
* from the disk after a restart, not merely hidden: the reader's cards were written first and their
|
|
331
|
+
* entries never landed. An entry that exists but is not on disk yet is not an orphan.
|
|
332
|
+
*
|
|
333
|
+
* A write would not even help the case it was added for. A second client must never be handed a comment
|
|
334
|
+
* naming a file the same read no longer lists — that is a VIEW requirement, and the prune satisfies it.
|
|
335
|
+
* Erasure belongs to the paths where a removal was actually ASKED FOR: `removeForEntry` (what the panel's
|
|
336
|
+
* own drop calls, and it removes the comments in the same tick), and `remove`/`removeMany` for one
|
|
337
|
+
* thread. A genuine orphan therefore stays on the disk, hidden, until the next boot that sees the entry
|
|
338
|
+
* again — a row nobody reads is a smaller price than a reader's annotation nobody can recover.
|
|
339
|
+
*
|
|
340
|
+
* @param entryIds - every entry id the pending store currently holds.
|
|
341
|
+
* @returns how many comments the view dropped.
|
|
342
|
+
*/
|
|
343
|
+
retain(entryIds: ReadonlySet<string>): number;
|
|
344
|
+
/**
|
|
345
|
+
* Record one question asked in a thread. The answer itself is never stored, and the
|
|
346
|
+
* request id is minted by the caller before it submits, so this runs as part of the
|
|
347
|
+
* ask rather than waiting for the session to answer. A thread keeps its questions in
|
|
348
|
+
* order: the first is the annotation's own, and each follow-up appends.
|
|
349
|
+
*
|
|
350
|
+
* The reader's own words are stored WITH the question (`text`), because they exist
|
|
351
|
+
* nowhere else: the transcript holds the prompt, which is those words wrapped in the
|
|
352
|
+
* marker, the reference and the rules, and a thread that showed the prompt would show
|
|
353
|
+
* the scaffolding rather than the question.
|
|
354
|
+
*
|
|
355
|
+
* The ASKING session is recorded on the question, not taken from the comment: a thread another
|
|
356
|
+
* seat wrote can be asked from this one, and the prompt then goes into the seat the human is
|
|
357
|
+
* using, so that session's transcript is where the answer will appear (see `askTranscript`).
|
|
358
|
+
*
|
|
359
|
+
* Matched by comment ID alone: the request id is minted here and is globally unique, and the
|
|
360
|
+
* caller has already checked that the thread is in its scope.
|
|
361
|
+
*
|
|
362
|
+
* @param sessionId - the transcript the question was submitted into.
|
|
363
|
+
* @param id - the comment that was asked in.
|
|
364
|
+
* @param requestId - the identity the submission carries into the transcript.
|
|
365
|
+
* @param turn - the turn that claimed it, when that is already known.
|
|
366
|
+
* @param text - the reader's own words, without the prompt's wrapper.
|
|
367
|
+
* @returns whether the comment existed.
|
|
368
|
+
*/
|
|
369
|
+
recordAsk(sessionId: SessionId, id: string, requestId: string, turn: number | undefined, text: string): boolean;
|
|
370
|
+
/**
|
|
371
|
+
* Record the turn that claimed one question, as the agent's inbox reported it. Two
|
|
372
|
+
* questions claimed by the same turn were answered together, which is a fact the
|
|
373
|
+
* panel can state instead of guessing from transcript positions.
|
|
374
|
+
*
|
|
375
|
+
* Matched by REQUEST ID alone: it is minted per submission and globally unique, so it names the
|
|
376
|
+
* question on its own — and a thread another seat wrote can hold a question asked from this seat,
|
|
377
|
+
* which the old `comment.sessionId === sessionId` guard would refuse to write down.
|
|
378
|
+
* @param requestId - the request identity the inbox reported back.
|
|
379
|
+
* @param turn - the turn that claimed it.
|
|
380
|
+
* @returns how many threads the patch touched.
|
|
381
|
+
*/
|
|
382
|
+
recordTurnForRequest(requestId: string, turn: number): number;
|
|
383
|
+
/**
|
|
384
|
+
* Record that a session dropped one question before a turn claimed it: nothing is
|
|
385
|
+
* coming, and the reader is told rather than left waiting.
|
|
386
|
+
*
|
|
387
|
+
* Matched by REQUEST ID alone, for the same reason as `recordTurnForRequest`.
|
|
388
|
+
* @param requestId - the request identity the inbox reported back.
|
|
389
|
+
* @returns how many threads the patch touched.
|
|
390
|
+
*/
|
|
391
|
+
markDroppedForRequest(requestId: string): number;
|
|
392
|
+
/**
|
|
393
|
+
* Record that a turn is over, so the questions it claimed stop looking pending.
|
|
394
|
+
*
|
|
395
|
+
* The turn number is the only handle the stopping event carries — no message, no
|
|
396
|
+
* request id — so the questions that were claimed by that turn are the ones patched
|
|
397
|
+
* (see `recordTurnForRequest`, which wrote the number down when the inbox claimed
|
|
398
|
+
* them). A question whose turn is over and that the transcript shows no answer for
|
|
399
|
+
* was cut off; the client says so instead of waiting forever. Nothing here decides
|
|
400
|
+
* that: whether an answer exists is read from the transcript on each list read, so a
|
|
401
|
+
* turn that stopped AFTER writing its answer keeps showing the answer.
|
|
402
|
+
*
|
|
403
|
+
* Matched by TURN **within one transcript** (`askTranscript`), never by the thread's author: turn
|
|
404
|
+
* numbers are only unique inside a session, so two sessions can both have a turn 7, and a thread that
|
|
405
|
+
* holds questions from both would otherwise have one session's ending mark the other's questions
|
|
406
|
+
* over — the reader would be told a question was cut off while its answer was still coming.
|
|
407
|
+
* @param sessionId - the transcript whose turn ended.
|
|
408
|
+
* @param turn - the turn that stopped.
|
|
409
|
+
* @returns how many threads the patch touched.
|
|
410
|
+
*/
|
|
411
|
+
markTurnEnded(sessionId: SessionId, turn: number): number;
|
|
412
|
+
/**
|
|
413
|
+
* Apply one patch to every comment it matches, and schedule the affected sessions'
|
|
414
|
+
* writes. The one mutation seam, so no caller has to remember to bump the revision
|
|
415
|
+
* or to save: a patch that matches nothing changes nothing.
|
|
416
|
+
* @param match - which comments to patch.
|
|
417
|
+
* @param patch - the change to apply.
|
|
418
|
+
* @returns how many comments changed.
|
|
419
|
+
*/
|
|
420
|
+
private patch;
|
|
421
|
+
/**
|
|
422
|
+
* Wait for every scheduled write to settle.
|
|
423
|
+
*
|
|
424
|
+
* The mutators return as soon as a change is in memory — that is what lets an
|
|
425
|
+
* entry's removal take its comments with it in the same tick — so this is how a
|
|
426
|
+
* caller that must not lose the write (a dispose path, a test asserting the file)
|
|
427
|
+
* waits for the disk.
|
|
428
|
+
* @returns resolution after every session file's write chain has drained.
|
|
429
|
+
*/
|
|
430
|
+
settled(): Promise<void>;
|
|
431
|
+
/**
|
|
432
|
+
* Write one session's comments, serialized against that file's own writes.
|
|
433
|
+
*
|
|
434
|
+
* A rejected write is reported, not swallowed: the mutation already answered
|
|
435
|
+
* "added" and the panel is drawing the thread from memory, so a silent failure means
|
|
436
|
+
* the reader keeps writing into a thread that a restart will erase. The report rides
|
|
437
|
+
* the list read (`persistError`) and the caller's logger, and a later write that works
|
|
438
|
+
* retracts it the way the pending half retracts its own.
|
|
439
|
+
*
|
|
440
|
+
* A session whose file could not be read at load AND could not be moved aside is not
|
|
441
|
+
* written at all: the only copy of that session's threads is the bytes the load could
|
|
442
|
+
* not parse, and the store's memory holds just what has been added since. Writing the
|
|
443
|
+
* memory-only view would TRUNCATE that copy — turning a read failure into a permanent
|
|
444
|
+
* loss — so the save is refused and reported instead, which is what tells the reader
|
|
445
|
+
* those comments are memory-only.
|
|
446
|
+
*
|
|
447
|
+
* The write-set is the AUTHOR's own comments (`authoredBy`), never the wider read scope
|
|
448
|
+
* (`list`): a read spans the lineage, but a file must hold exactly the comments whose
|
|
449
|
+
* author it is named for — writing the scope's comments into one seat's file would
|
|
450
|
+
* duplicate every other seat's threads on disk and, on the next load, in memory.
|
|
451
|
+
* @param sessionId - the author whose file to rewrite.
|
|
452
|
+
*/
|
|
453
|
+
private save;
|
|
454
|
+
/**
|
|
455
|
+
* Publish the write state of one session's file, once per distinct report. Kept as its own
|
|
456
|
+
* step so both the success and the failure of the chain above land in one place — the
|
|
457
|
+
* retraction is what arms a later failure to be reported again.
|
|
458
|
+
*
|
|
459
|
+
* Keyed per file, so a healthy session's write cannot retract a broken one's report (see
|
|
460
|
+
* `persistError`); the callback sees the aggregate, which is what rides the list read.
|
|
461
|
+
* @param file - the session file the write was for.
|
|
462
|
+
* @param message - the failure's text, or undefined when the write worked.
|
|
463
|
+
*/
|
|
464
|
+
private reportPersist;
|
|
465
|
+
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -35,9 +35,11 @@
|
|
|
35
35
|
*/
|
|
36
36
|
import { Context } from '@deepseek-ai/cordis';
|
|
37
37
|
import type { DiffApprovalOpenAction } from './types.ts';
|
|
38
|
-
export type { DiffApprovalActionOutcome, DiffApprovalActionValue, DiffApprovalBlockRange, DiffApprovalBlockTarget, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, DiffApprovalRefreshOutcome, DiffApprovalRefreshValue, PendingEntry, PendingEntryKind, PendingFileDiff, } from './types.ts';
|
|
38
|
+
export type { DiffApprovalActionOutcome, DiffApprovalActionValue, DiffApprovalBlockRange, DiffApprovalBlockTarget, DiffApprovalListCountValue, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, DiffApprovalRefreshOutcome, DiffApprovalRefreshValue, CommentAnchor, CommentQuoteLine, CommentRecord, DiffApprovalCommentAddValue, DiffApprovalCommentRemoveManyValue, DiffApprovalCommentRemoveValue, LineageDirection, PendingEntry, PendingEntryKind, PendingFileDiff, SessionLineage, } from './types.ts';
|
|
39
39
|
export { PendingDiffStore } from './pending.ts';
|
|
40
40
|
export { PendingPersistence, defaultStorageDir } from './persist.ts';
|
|
41
|
+
export { CommentStore, commentsDirFor } from './comments.ts';
|
|
42
|
+
export { CommentAsker, answerForRequest } from './comment-ask.ts';
|
|
41
43
|
export { defaultOpenPath } from './open.ts';
|
|
42
44
|
/** Stable Cordis plugin name. */
|
|
43
45
|
export declare const name = "diff-approval";
|
|
@@ -45,7 +47,10 @@ export declare const name = "diff-approval";
|
|
|
45
47
|
* Services required before the review surface activates. `webServer` rides with
|
|
46
48
|
* `connection` (both come from the web bundle), and is named explicitly because
|
|
47
49
|
* the channel's owner context must be able to resolve it — see
|
|
48
|
-
* `ConnectionServiceSurface`.
|
|
50
|
+
* `ConnectionServiceSurface`. `sessionController` and `agents` are what a comment is
|
|
51
|
+
* asked through and what reports the turn that claimed it: both come from the same
|
|
52
|
+
* base composition this plugin already requires for `sessions`, so a host that could
|
|
53
|
+
* list pending changes at all has them.
|
|
49
54
|
*/
|
|
50
55
|
export declare const inject: string[];
|
|
51
56
|
/** The connection RPC channel this plugin serves. */
|