dsh-diff-approval 0.29.1 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/README.zh.md +4 -4
- package/lib/client.js +3145 -1522
- package/lib/index.js +2655 -173
- package/lib/types/annotate-skill.d.ts +34 -0
- package/lib/types/annotate-tool.d.ts +216 -0
- package/lib/types/annotate.d.ts +158 -0
- package/lib/types/atomic-write.d.ts +77 -0
- package/lib/types/client/PendingPanel.d.ts +66 -4
- package/lib/types/client/discussion.d.ts +69 -16
- package/lib/types/client/dock.d.ts +0 -5
- package/lib/types/client/dsh-icons.d.ts +60 -0
- package/lib/types/client/header-entry.d.ts +20 -5
- package/lib/types/client/ids.d.ts +18 -0
- package/lib/types/client/locales.d.ts +50 -28
- package/lib/types/client/panel-memory.d.ts +94 -29
- package/lib/types/client/port.d.ts +50 -1
- package/lib/types/client/presentation-menu.d.ts +1 -1
- package/lib/types/client/produced-diff.d.ts +57 -20
- package/lib/types/client/session-seat.d.ts +53 -0
- package/lib/types/client/slots.d.ts +71 -11
- package/lib/types/client/store.d.ts +35 -2
- package/lib/types/comment-ask.d.ts +209 -0
- package/lib/types/comment-lines.d.ts +131 -0
- package/lib/types/comments.d.ts +357 -0
- package/lib/types/index.d.ts +7 -2
- package/lib/types/pending.d.ts +23 -0
- package/lib/types/types.d.ts +176 -0
- package/lib/types/vcs.d.ts +51 -25
- package/package.json +4 -1
- 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,357 @@
|
|
|
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 { CommentRecord } from './types.ts';
|
|
20
|
+
/**
|
|
21
|
+
* The comment directory that belongs to a pending-storage root.
|
|
22
|
+
*
|
|
23
|
+
* The plugin's own state directory is `<dshHome>/diff-approval`, and inside it the
|
|
24
|
+
* pending file lives in `workspaces/` and comments in `comments/` — siblings, so a
|
|
25
|
+
* write that fails on one side cannot take the other with it. Any OTHER root is a
|
|
26
|
+
* self-contained state directory (a deployment's chosen location, a test's temp
|
|
27
|
+
* dir) and keeps its comments INSIDE it: the sibling of an arbitrary directory can
|
|
28
|
+
* be shared by two instances that were each pointed somewhere different, and two
|
|
29
|
+
* instances writing one comment file is a leak rather than a layout choice.
|
|
30
|
+
*
|
|
31
|
+
* @param storageRoot - the resolved pending-storage root.
|
|
32
|
+
* @returns the directory holding the comments that belong to that root.
|
|
33
|
+
*/
|
|
34
|
+
export declare function commentsDirFor(storageRoot: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* How a caller hears about the two ways a comment file can go wrong.
|
|
37
|
+
*
|
|
38
|
+
* The store reads and writes plain JSON files and owns no logger, so both halves are
|
|
39
|
+
* callbacks: the host wires them to `ctx.logger` and to the list read the panel draws,
|
|
40
|
+
* which is what turns "the write failed" from a fact only the filesystem knew into
|
|
41
|
+
* something the reader is told (and something a test can assert without a disk).
|
|
42
|
+
*/
|
|
43
|
+
export interface CommentStoreOptions {
|
|
44
|
+
/**
|
|
45
|
+
* One session's comments could not be read, and that file was skipped. Called once per
|
|
46
|
+
* skipped file, with the file's path, because the caller's warning has to name it.
|
|
47
|
+
*/
|
|
48
|
+
onLoadSkipped?: (file: string, error: unknown) => void;
|
|
49
|
+
/**
|
|
50
|
+
* A save failed (`message` is the reason) or a session's failure cleared (`undefined`).
|
|
51
|
+
* Called only when the report CHANGES, so a writer retrying every second cannot bury
|
|
52
|
+
* the log; the same value also rides the list read as `persistError()`. One session's
|
|
53
|
+
* healthy write does not clear another's failure — see `persistError`.
|
|
54
|
+
*/
|
|
55
|
+
onPersistError?: (message: string | undefined) => void;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* File-backed comment threads, one file per session.
|
|
59
|
+
*
|
|
60
|
+
* Every mutation is applied to memory synchronously and schedules that session's
|
|
61
|
+
* file write on its own chain, so a caller removing an entry can drop its comments
|
|
62
|
+
* in the same tick as the entry itself without awaiting anything — the same
|
|
63
|
+
* fire-and-forget persistence the pending store uses.
|
|
64
|
+
*/
|
|
65
|
+
export declare class CommentStore {
|
|
66
|
+
private readonly root;
|
|
67
|
+
private readonly options;
|
|
68
|
+
private readonly byId;
|
|
69
|
+
private readonly tails;
|
|
70
|
+
private revision;
|
|
71
|
+
/** Files `loadAll` could not read, in the order it met them (see `skippedFiles`). */
|
|
72
|
+
private readonly skipped;
|
|
73
|
+
/**
|
|
74
|
+
* The save failure of every session that has one, keyed by that session's file and in the
|
|
75
|
+
* order the failures happened (see `persistError`). Empty while every write works.
|
|
76
|
+
*/
|
|
77
|
+
private readonly persistFailures;
|
|
78
|
+
/**
|
|
79
|
+
* Sessions whose unreadable file could not even be moved aside, keyed by that file: the
|
|
80
|
+
* reason their writes are refused rather than allowed to truncate the bytes (see `save`).
|
|
81
|
+
*/
|
|
82
|
+
private readonly unmovable;
|
|
83
|
+
/**
|
|
84
|
+
* @param root - directory holding one `<sessionId>.json` per session.
|
|
85
|
+
* @param options - the error seam (see {@link CommentStoreOptions}).
|
|
86
|
+
*/
|
|
87
|
+
constructor(root: string, options?: CommentStoreOptions);
|
|
88
|
+
/**
|
|
89
|
+
* Load every session's comments. A malformed row is skipped rather than
|
|
90
|
+
* failing the load: one bad comment must not cost the whole workspace's
|
|
91
|
+
* comments (or the plugin's activation).
|
|
92
|
+
*
|
|
93
|
+
* A file that cannot be READ is skipped the same way, because the alternative is
|
|
94
|
+
* worse in both directions: the old all-or-nothing load threw, the caller logged
|
|
95
|
+
* once and carried on with an EMPTY store, and the next save then wrote that
|
|
96
|
+
* emptiness back over every good file. One unreadable file therefore cost every
|
|
97
|
+
* session's comments. Skipping keeps the other sessions' threads, and the skipped
|
|
98
|
+
* file is moved aside (`.corrupt`) rather than dropped: it is not deleted — the
|
|
99
|
+
* bytes may be the only copy — but it is also not left where the next save for
|
|
100
|
+
* that session would silently overwrite it, which would destroy them for good.
|
|
101
|
+
*
|
|
102
|
+
* When even that fails — the file cannot be moved aside — that session's writes are
|
|
103
|
+
* refused from then on (see `save`) rather than allowed to truncate the only copy.
|
|
104
|
+
* "Skipped but still writable" is exactly the shape that turns a read failure into
|
|
105
|
+
* data loss: the store would answer "added", write the memory-only view over bytes
|
|
106
|
+
* nobody could read, and lose what the skip had just refused to guess at.
|
|
107
|
+
* @returns how many comments were loaded.
|
|
108
|
+
*/
|
|
109
|
+
loadAll(): Promise<number>;
|
|
110
|
+
/**
|
|
111
|
+
* The files a load skipped, so the caller can say what went missing instead of
|
|
112
|
+
* reporting a session whose comments silently vanished.
|
|
113
|
+
* @returns the skipped files' paths, in load order.
|
|
114
|
+
*/
|
|
115
|
+
skippedFiles(): string[];
|
|
116
|
+
/**
|
|
117
|
+
* The failure the reader has to be told about, or `undefined` while every write works.
|
|
118
|
+
* This is what rides the list read so the panel can say the threads are memory-only;
|
|
119
|
+
* a later successful save retracts it, mirroring the pending half's own report.
|
|
120
|
+
*
|
|
121
|
+
* Per SESSION, not one global slot: a deployment where one session's file cannot be
|
|
122
|
+
* written and another's can is the ordinary case (a locked file, a full quota, a
|
|
123
|
+
* directory that vanished), and letting the healthy write retract the broken one's
|
|
124
|
+
* report would say the threads are safe while that session's live in memory only —
|
|
125
|
+
* the same "a failure that looks like a success" trap, one session over. A session's
|
|
126
|
+
* own success is what retracts its own entry (see `reportPersist`).
|
|
127
|
+
* @returns the most recent outstanding failure's message, or undefined.
|
|
128
|
+
*/
|
|
129
|
+
persistError(): string | undefined;
|
|
130
|
+
/**
|
|
131
|
+
* Record a skipped file: keep it for the caller, name it, and move it aside so a later
|
|
132
|
+
* save for that session writes a fresh file rather than overwriting the unreadable
|
|
133
|
+
* bytes. A quarantine that itself fails is still a skip — the store must not be the
|
|
134
|
+
* reason a single bad file takes the whole load down — but that session's file is then
|
|
135
|
+
* the only copy of its threads, so its writes are refused (see `save`) instead of being
|
|
136
|
+
* allowed to truncate it.
|
|
137
|
+
* @param file - the file that could not be read.
|
|
138
|
+
* @param error - why it could not be read.
|
|
139
|
+
*/
|
|
140
|
+
private skip;
|
|
141
|
+
/** One session's comments, oldest first (the order threads are read in). */
|
|
142
|
+
list(sessionId: SessionId): CommentRecord[];
|
|
143
|
+
/** A comment by id, whatever session it belongs to. */
|
|
144
|
+
get(id: string): CommentRecord | undefined;
|
|
145
|
+
/**
|
|
146
|
+
* A counter bumped on every change, so a poller can tell "nothing new" from
|
|
147
|
+
* "re-read": the list endpoint ships the session's comments anyway, and this is
|
|
148
|
+
* what says whether anything moved since the last read.
|
|
149
|
+
*/
|
|
150
|
+
commentsRevision(): number;
|
|
151
|
+
/**
|
|
152
|
+
* Record one annotation.
|
|
153
|
+
*
|
|
154
|
+
* Writing a comment that is already here is the SAME comment written again — a client
|
|
155
|
+
* retrying a request whose response was dropped, or a second client sending the id it
|
|
156
|
+
* was handed — so the questions already asked in it are kept and only the annotation's
|
|
157
|
+
* own fields move. Overwriting the whole record would erase the identity of a question
|
|
158
|
+
* that is in flight, and its answer could then never be matched back to this thread;
|
|
159
|
+
* `createdAt` is kept for the same reason, since it is what a list read orders by.
|
|
160
|
+
*
|
|
161
|
+
* @param record - the comment to store.
|
|
162
|
+
* @returns the stored comment, which is the existing one when the id was already here.
|
|
163
|
+
*/
|
|
164
|
+
add(record: CommentRecord): CommentRecord;
|
|
165
|
+
/**
|
|
166
|
+
* Record several annotations at once, as ONE write per affected session.
|
|
167
|
+
*
|
|
168
|
+
* This is what putting a batch of restored comments back needs and what a loop of `add` calls
|
|
169
|
+
* cannot give it: each `add` saves its session's file, so an undo that restored ten threads wrote
|
|
170
|
+
* that file ten times — ten chances for a transient failure to leave the store and the disk
|
|
171
|
+
* disagreeing about an action the reader already watched happen. The records are stored by the SAME
|
|
172
|
+
* rule as `add` (a new id verbatim, an existing one merged with its `createdAt` and its questions
|
|
173
|
+
* kept), which is what lets a restore be an ordinary write rather than a special case: a snapshot
|
|
174
|
+
* that came back as a new comment, with a fresh id and no questions, would be a look-alike of the
|
|
175
|
+
* reader's thread.
|
|
176
|
+
*
|
|
177
|
+
* A batch that carries nothing changes nothing, so a no-op restore does not rewrite a file.
|
|
178
|
+
*
|
|
179
|
+
* @param records - the comments to store, in the caller's order.
|
|
180
|
+
* @returns the stored record for each input, index-aligned: what landed, which is not always what
|
|
181
|
+
* was handed over (a merge keeps the fields the existing comment owns). A count would drop that,
|
|
182
|
+
* and a caller needing it would have to `get` each id back and race a later change.
|
|
183
|
+
*/
|
|
184
|
+
addMany(records: readonly CommentRecord[]): CommentRecord[];
|
|
185
|
+
/**
|
|
186
|
+
* The record as the store keeps it: a new id VERBATIM, an id already here merged.
|
|
187
|
+
*
|
|
188
|
+
* The one place the merge rule lives, so `add` and `addMany` cannot drift: writing a comment that
|
|
189
|
+
* is already here is the same comment written again — a client retrying a request whose response
|
|
190
|
+
* was dropped, or a second client sending the id it was handed — so the questions already asked in
|
|
191
|
+
* it are kept and only the annotation's own fields move. Overwriting the whole record would erase
|
|
192
|
+
* the identity of a question that is in flight, and its answer could then never be matched back to
|
|
193
|
+
* this thread; `createdAt` is kept for the same reason, since it is what a list read orders by.
|
|
194
|
+
* @param record - the comment as the caller wrote it.
|
|
195
|
+
* @returns the record to store under its id.
|
|
196
|
+
*/
|
|
197
|
+
private mergeOf;
|
|
198
|
+
/**
|
|
199
|
+
* Drop one comment.
|
|
200
|
+
* @param sessionId - the session the caller believes the comment belongs to.
|
|
201
|
+
* @param id - the comment to drop.
|
|
202
|
+
* @returns whether it was there (and belonged to that session).
|
|
203
|
+
*/
|
|
204
|
+
remove(sessionId: SessionId, id: string): boolean;
|
|
205
|
+
/**
|
|
206
|
+
* Drop several comments of one session in ONE write.
|
|
207
|
+
*
|
|
208
|
+
* This is what a batch action needs and what a loop of `remove` calls cannot give it: each
|
|
209
|
+
* `remove` saves the session's file, so ending ten comments wrote that file ten times — ten
|
|
210
|
+
* chances for a transient failure to leave the store and the disk disagreeing about an action
|
|
211
|
+
* the reader asked for once. The save happens only when something was actually dropped, so a
|
|
212
|
+
* batch that matches nothing does not rewrite the file.
|
|
213
|
+
*
|
|
214
|
+
* An id of another session, or one that is not here at all, is skipped rather than refused:
|
|
215
|
+
* a batch is one request and the comments it names are the ones it can act on, and a comment
|
|
216
|
+
* another client already ended is the state the caller was asking for anyway.
|
|
217
|
+
*
|
|
218
|
+
* @param sessionId - the session the caller believes the comments belong to.
|
|
219
|
+
* @param ids - the comments to drop; a repeated id is one comment.
|
|
220
|
+
* @returns the ids that were dropped, in the order given.
|
|
221
|
+
*/
|
|
222
|
+
removeMany(sessionId: SessionId, ids: readonly string[]): string[];
|
|
223
|
+
/**
|
|
224
|
+
* Every comment one pending entry carries, oldest first.
|
|
225
|
+
*
|
|
226
|
+
* The read half of `removeForEntry` and kept beside it, because they are two answers about the same
|
|
227
|
+
* fact: which comments an entry owns. A caller that is about to drop the entry asks this first, so
|
|
228
|
+
* the undo pair can carry them back; the two must agree on that set, or a restore would put back
|
|
229
|
+
* fewer (or more) threads than the drop took.
|
|
230
|
+
* @param entryId - the entry id (= path).
|
|
231
|
+
* @returns that entry's comments, in list order.
|
|
232
|
+
*/
|
|
233
|
+
forEntry(entryId: string): CommentRecord[];
|
|
234
|
+
/**
|
|
235
|
+
* Drop every comment of one pending entry. This is what "a comment dies with
|
|
236
|
+
* its entry" means, and the entry knows nothing about comments, so the caller
|
|
237
|
+
* runs it beside the entry's own removal.
|
|
238
|
+
* @param entryId - the entry id (= path) that left the list.
|
|
239
|
+
* @returns how many comments went with it.
|
|
240
|
+
*/
|
|
241
|
+
removeForEntry(entryId: string): number;
|
|
242
|
+
/**
|
|
243
|
+
* The orphan sweep: drop every comment whose entry is not in `entryIds`.
|
|
244
|
+
*
|
|
245
|
+
* The explicit `removeForEntry` is the primary path; this is the backstop that
|
|
246
|
+
* catches a comment left behind by a crash between the two writes. Run it when
|
|
247
|
+
* the store loads and on every list read, and the one race it could lose — an
|
|
248
|
+
* entry re-added after its comments were already swept — cannot happen: entries
|
|
249
|
+
* only leave through this host, so an entry absent at load time was removed
|
|
250
|
+
* while the host was running, and the sweep at load runs before anything can
|
|
251
|
+
* add it back.
|
|
252
|
+
* @param entryIds - every entry id the pending store currently holds.
|
|
253
|
+
* @returns how many orphaned comments were dropped.
|
|
254
|
+
*/
|
|
255
|
+
retain(entryIds: ReadonlySet<string>): number;
|
|
256
|
+
/**
|
|
257
|
+
* Record one question asked in a thread. The answer itself is never stored, and the
|
|
258
|
+
* request id is minted by the caller before it submits, so this runs as part of the
|
|
259
|
+
* ask rather than waiting for the session to answer. A thread keeps its questions in
|
|
260
|
+
* order: the first is the annotation's own, and each follow-up appends.
|
|
261
|
+
*
|
|
262
|
+
* The reader's own words are stored WITH the question (`text`), because they exist
|
|
263
|
+
* nowhere else: the transcript holds the prompt, which is those words wrapped in the
|
|
264
|
+
* marker, the reference and the rules, and a thread that showed the prompt would show
|
|
265
|
+
* the scaffolding rather than the question.
|
|
266
|
+
*
|
|
267
|
+
* @param sessionId - the session the comment belongs to.
|
|
268
|
+
* @param id - the comment that was asked in.
|
|
269
|
+
* @param requestId - the identity the submission carries into the transcript.
|
|
270
|
+
* @param turn - the turn that claimed it, when that is already known.
|
|
271
|
+
* @param text - the reader's own words, without the prompt's wrapper.
|
|
272
|
+
* @returns whether the comment existed.
|
|
273
|
+
*/
|
|
274
|
+
recordAsk(sessionId: SessionId, id: string, requestId: string, turn: number | undefined, text: string): boolean;
|
|
275
|
+
/**
|
|
276
|
+
* Record the turn that claimed one question, as the agent's inbox reported it. Two
|
|
277
|
+
* questions claimed by the same turn were answered together, which is a fact the
|
|
278
|
+
* panel can state instead of guessing from transcript positions.
|
|
279
|
+
* @param sessionId - the session the submission belongs to.
|
|
280
|
+
* @param requestId - the request identity the inbox reported back.
|
|
281
|
+
* @param turn - the turn that claimed it.
|
|
282
|
+
* @returns how many threads the patch touched.
|
|
283
|
+
*/
|
|
284
|
+
recordTurnForRequest(sessionId: SessionId, requestId: string, turn: number): number;
|
|
285
|
+
/**
|
|
286
|
+
* Record that the session dropped one question before a turn claimed it: nothing is
|
|
287
|
+
* coming, and the reader is told rather than left waiting.
|
|
288
|
+
* @param sessionId - the session the submission belongs to.
|
|
289
|
+
* @param requestId - the request identity the inbox reported back.
|
|
290
|
+
* @returns how many threads the patch touched.
|
|
291
|
+
*/
|
|
292
|
+
markDroppedForRequest(sessionId: SessionId, requestId: string): number;
|
|
293
|
+
/**
|
|
294
|
+
* Record that a turn is over, so the questions it claimed stop looking pending.
|
|
295
|
+
*
|
|
296
|
+
* The turn number is the only handle the stopping event carries — no message, no
|
|
297
|
+
* request id — so the questions that were claimed by that turn are the ones patched
|
|
298
|
+
* (see `recordTurnForRequest`, which wrote the number down when the inbox claimed
|
|
299
|
+
* them). A question whose turn is over and that the transcript shows no answer for
|
|
300
|
+
* was cut off; the client says so instead of waiting forever. Nothing here decides
|
|
301
|
+
* that: whether an answer exists is read from the transcript on each list read, so a
|
|
302
|
+
* turn that stopped AFTER writing its answer keeps showing the answer.
|
|
303
|
+
*
|
|
304
|
+
* @param sessionId - the session whose turn ended.
|
|
305
|
+
* @param turn - the turn that stopped.
|
|
306
|
+
* @returns how many threads the patch touched.
|
|
307
|
+
*/
|
|
308
|
+
markTurnEnded(sessionId: SessionId, turn: number): number;
|
|
309
|
+
/**
|
|
310
|
+
* Apply one patch to every comment it matches, and schedule the affected sessions'
|
|
311
|
+
* writes. The one mutation seam, so no caller has to remember to bump the revision
|
|
312
|
+
* or to save: a patch that matches nothing changes nothing.
|
|
313
|
+
* @param match - which comments to patch.
|
|
314
|
+
* @param patch - the change to apply.
|
|
315
|
+
* @returns how many comments changed.
|
|
316
|
+
*/
|
|
317
|
+
private patch;
|
|
318
|
+
/**
|
|
319
|
+
* Wait for every scheduled write to settle.
|
|
320
|
+
*
|
|
321
|
+
* The mutators return as soon as a change is in memory — that is what lets an
|
|
322
|
+
* entry's removal take its comments with it in the same tick — so this is how a
|
|
323
|
+
* caller that must not lose the write (a dispose path, a test asserting the file)
|
|
324
|
+
* waits for the disk.
|
|
325
|
+
* @returns resolution after every session file's write chain has drained.
|
|
326
|
+
*/
|
|
327
|
+
settled(): Promise<void>;
|
|
328
|
+
/**
|
|
329
|
+
* Write one session's comments, serialized against that file's own writes.
|
|
330
|
+
*
|
|
331
|
+
* A rejected write is reported, not swallowed: the mutation already answered
|
|
332
|
+
* "added" and the panel is drawing the thread from memory, so a silent failure means
|
|
333
|
+
* the reader keeps writing into a thread that a restart will erase. The report rides
|
|
334
|
+
* the list read (`persistError`) and the caller's logger, and a later write that works
|
|
335
|
+
* retracts it the way the pending half retracts its own.
|
|
336
|
+
*
|
|
337
|
+
* A session whose file could not be read at load AND could not be moved aside is not
|
|
338
|
+
* written at all: the only copy of that session's threads is the bytes the load could
|
|
339
|
+
* not parse, and the store's memory holds just what has been added since. Writing the
|
|
340
|
+
* memory-only view would TRUNCATE that copy — turning a read failure into a permanent
|
|
341
|
+
* loss — so the save is refused and reported instead, which is what tells the reader
|
|
342
|
+
* those comments are memory-only.
|
|
343
|
+
* @param sessionId - the session whose file to rewrite.
|
|
344
|
+
*/
|
|
345
|
+
private save;
|
|
346
|
+
/**
|
|
347
|
+
* Publish the write state of one session's file, once per distinct report. Kept as its own
|
|
348
|
+
* step so both the success and the failure of the chain above land in one place — the
|
|
349
|
+
* retraction is what arms a later failure to be reported again.
|
|
350
|
+
*
|
|
351
|
+
* Keyed per file, so a healthy session's write cannot retract a broken one's report (see
|
|
352
|
+
* `persistError`); the callback sees the aggregate, which is what rides the list read.
|
|
353
|
+
* @param file - the session file the write was for.
|
|
354
|
+
* @param message - the failure's text, or undefined when the write worked.
|
|
355
|
+
*/
|
|
356
|
+
private reportPersist;
|
|
357
|
+
}
|
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, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, DiffApprovalRefreshOutcome, DiffApprovalRefreshValue, CommentAnchor, CommentQuoteLine, CommentRecord, DiffApprovalCommentAddValue, DiffApprovalCommentRemoveManyValue, DiffApprovalCommentRemoveValue, PendingEntry, PendingEntryKind, PendingFileDiff, } 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. */
|
package/lib/types/pending.d.ts
CHANGED
|
@@ -18,6 +18,29 @@ import type { PendingEntry } from './types.ts';
|
|
|
18
18
|
*/
|
|
19
19
|
export declare class PendingDiffStore {
|
|
20
20
|
private readonly entries;
|
|
21
|
+
/**
|
|
22
|
+
* The content version of each entry: bumped whenever its tracked `newText` moves, and never for a
|
|
23
|
+
* change that leaves the content alone (a block keep advances `oldText` only).
|
|
24
|
+
*
|
|
25
|
+
* It is what lets derived, content-dependent state — the lines a comment's quote resolves to, read
|
|
26
|
+
* off `newText` — be cached and refreshed when the content moved rather than recomputed on every
|
|
27
|
+
* read. The versions come from one monotonic counter and are never reused, so an entry that leaves
|
|
28
|
+
* the list and comes back cannot hand back the number a stale cache was keyed to.
|
|
29
|
+
*/
|
|
30
|
+
private readonly contentVersions;
|
|
31
|
+
/** The last version handed out; only ever climbs (see `contentVersions`). */
|
|
32
|
+
private contentVersionCounter;
|
|
33
|
+
/** Note that one path's tracked content moved, and give it a fresh version. */
|
|
34
|
+
private bumpContent;
|
|
35
|
+
/**
|
|
36
|
+
* The version of one path's tracked content, or 0 while it has none.
|
|
37
|
+
*
|
|
38
|
+
* Two calls that return the same number describe the same `newText`: a caller caching something
|
|
39
|
+
* derived from that content may serve its copy until the number changes.
|
|
40
|
+
* @param path - the file path (the global entry key).
|
|
41
|
+
* @returns the path's content version.
|
|
42
|
+
*/
|
|
43
|
+
contentVersion(path: string): number;
|
|
21
44
|
/**
|
|
22
45
|
* Merge one captured operation into its file's entry. A no-op (equal before
|
|
23
46
|
* and after) folds nothing.
|