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
|
@@ -2,16 +2,51 @@
|
|
|
2
2
|
import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
3
|
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client';
|
|
4
4
|
import type { DockSnapshot } from './dock.tsx';
|
|
5
|
-
import type {
|
|
6
|
-
import type {
|
|
5
|
+
import type { CommentDraft } from './port.ts';
|
|
6
|
+
import type { CommentLineRange } from '../comment-lines.ts';
|
|
7
|
+
import type { CommentRecord, DiffApprovalAddValue, DiffApprovalBlockRange, DiffApprovalBrowseValue, DiffApprovalCommentAddValue, DiffApprovalCommentAskValue, DiffApprovalCommentRemoveManyValue, DiffApprovalCommentRemoveValue, DiffApprovalOpenAction, DiffApprovalRefreshValue, DiffApprovalUpdateValue, PendingFileDiff, VcsImportValue } from '../types.ts';
|
|
7
8
|
/** What the panel reads and drives: the pending list plus in-flight entries. */
|
|
8
9
|
export interface PendingDiffSnapshot {
|
|
9
10
|
/** Whether a list read has completed at least once. */
|
|
10
11
|
read: boolean;
|
|
11
12
|
/** Pending entries, one per file, oldest capture first. */
|
|
12
13
|
files: PendingFileDiff[];
|
|
14
|
+
/**
|
|
15
|
+
* The session's comment threads, oldest first. They arrive on the same read as the
|
|
16
|
+
* entries, from the host — the one copy every client of this session shares, so two
|
|
17
|
+
* browsers show the same threads instead of two private ones.
|
|
18
|
+
*/
|
|
19
|
+
comments: CommentRecord[];
|
|
20
|
+
/**
|
|
21
|
+
* The new-file lines each comment sits on NOW, keyed by comment id, as the HOST resolved them
|
|
22
|
+
* against the entry's current content (see the wire type). One map for the whole panel: the list
|
|
23
|
+
* pane's label, the card's own chip and the jump all draw it, so a comment cannot read one line in
|
|
24
|
+
* the list and another in the code view — and a file nobody has opened is named just as rightly,
|
|
25
|
+
* which is what the reader asked for. A comment the host could not place is absent, and its
|
|
26
|
+
* callers fall back to `record.anchor`.
|
|
27
|
+
*/
|
|
28
|
+
commentLines: Readonly<Record<string, CommentLineRange>>;
|
|
29
|
+
/** The host's comment revision counter, bumped on every comment change. */
|
|
30
|
+
commentsRevision: number;
|
|
31
|
+
/** Derived answer text per comment id, read by the host from the session's transcript. */
|
|
32
|
+
commentAnswers: Record<string, string>;
|
|
13
33
|
/** A read failure's message; absent while the latest read succeeded. */
|
|
14
34
|
error?: string;
|
|
35
|
+
/**
|
|
36
|
+
* How many rows the host says this session has, from the LIGHT read (`list-count`) — the badge's
|
|
37
|
+
* question, answered without shipping any file content.
|
|
38
|
+
*
|
|
39
|
+
* It is the number a full read would have carried, but it is a separate fact with a separate age: a
|
|
40
|
+
* count lands on ticks that ask only for a count (when nothing is showing), and a FULL read clears it,
|
|
41
|
+
* because that read carries the same fact in `files` and is newer. So a reader's rule is
|
|
42
|
+
* `count ?? files.length` — the fallback covers both "no count has arrived" and "the list is fresher",
|
|
43
|
+
* and a count that FAILED leaves the previous number in place rather than showing zero.
|
|
44
|
+
*
|
|
45
|
+
* Absent on a host that predates the endpoint: there the store keeps doing full reads, the value stays
|
|
46
|
+
* absent, and every reader stays on the fallback — today's behaviour, at today's cost (see
|
|
47
|
+
* `PendingDiffStore.refreshCount`).
|
|
48
|
+
*/
|
|
49
|
+
count?: number | undefined;
|
|
15
50
|
/** Entry ids whose keep/revert is in flight; their controls are disabled. */
|
|
16
51
|
busy: ReadonlySet<string>;
|
|
17
52
|
/** Entry ids whose last keep/revert failed, mapped to the error message; the panel surfaces these inline (row tag + detail banner) instead of hiding the list. */
|
|
@@ -30,13 +65,100 @@ export interface PendingDiffSnapshot {
|
|
|
30
65
|
* told finds the list gone later with nothing to explain it (see the wire type).
|
|
31
66
|
*/
|
|
32
67
|
persistError?: string | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* The last failure to write the session's COMMENT files, or absent while those writes are
|
|
70
|
+
* working. Their own field (and their own copy) because they are their own files: a comment
|
|
71
|
+
* that only exists in the host's memory is erased by a restart exactly like an unpersisted
|
|
72
|
+
* list, and the panel says so once — the same way, and for the same reason, as `persistError`.
|
|
73
|
+
*/
|
|
74
|
+
commentPersistError?: string | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* Why the last undo/redo was refused by the host, or absent when there is nothing to say.
|
|
77
|
+
* The host answers a refused undo with its reason (the divergence guard's "the file changed
|
|
78
|
+
* outside the review"), and the reader who pressed the key has to be told: the panel surfaces
|
|
79
|
+
* this once as a toast and clears it, so the next refusal is news again.
|
|
80
|
+
*/
|
|
81
|
+
undoNotice?: string | undefined;
|
|
33
82
|
/** Latched when an external change created a fresh undo checkpoint that
|
|
34
83
|
* superseded the redo history; the panel surfaces it once (deferred if the
|
|
35
84
|
* panel is closed) via a bottom-right notice. */
|
|
36
85
|
redoCleared?: boolean;
|
|
37
86
|
}
|
|
87
|
+
/** One filter over one session's view of the pending list; the shape every view hook takes. */
|
|
88
|
+
export type PendingViewSelector<T> = (view: PendingDiffSnapshot) => T;
|
|
89
|
+
/**
|
|
90
|
+
* The number a badge, a header entry or a dock chip shows for one session: the light count when one has
|
|
91
|
+
* arrived, else the length of the list that read carried.
|
|
92
|
+
*
|
|
93
|
+
* The fallback is not a nicety — it is the whole older-host story and the state before the first count
|
|
94
|
+
* lands. `count` is absent when no count has arrived (the page just loaded, or the host predates the
|
|
95
|
+
* endpoint, where the store keeps doing full reads) and it is cleared by every full read, whose `files`
|
|
96
|
+
* are the same fact and newer. So `count` is only ever shown while it is the FRESHEST number the client
|
|
97
|
+
* has; otherwise the list speaks. See `PendingDiffSnapshot.count`.
|
|
98
|
+
* @param view - one session's view.
|
|
99
|
+
* @returns how many rows it holds.
|
|
100
|
+
*/
|
|
101
|
+
export declare function countOf(view: PendingDiffSnapshot): number;
|
|
102
|
+
/**
|
|
103
|
+
* The view a seat draws when it has NO reviewable session: a session that is absent, or one the shell has
|
|
104
|
+
* told us is blank — a brand-new session with nothing said in it yet.
|
|
105
|
+
*
|
|
106
|
+
* This exists because the page-wide fallback is the wrong answer for such a seat. `viewFor(undefined)`
|
|
107
|
+
* and `getSnapshot()` deliberately answer the newest session read on the page (the whole-page contract the
|
|
108
|
+
* remap follows), and with the store's slots keyed only by session id and no workspace comparison anywhere
|
|
109
|
+
* in this client, "the newest read" can be another session's list — even another workspace's. A seat that
|
|
110
|
+
* has nothing of its own to show must draw NOTHING, and this is the nothing: no rows (so `countOf` is 0),
|
|
111
|
+
* no comments, no workspace, and `read: false` because nothing has been read FOR this seat.
|
|
112
|
+
*
|
|
113
|
+
* Shared rather than re-spelled per seat: the panel's badge and body, the header entry's count and the
|
|
114
|
+
* dock chip all have to agree that "no reviewable session" means zero, and a second literal here is how
|
|
115
|
+
* they would drift. Frozen because it is handed to readers as a snapshot: nothing may write through it.
|
|
116
|
+
*/
|
|
117
|
+
export declare const EMPTY_PENDING_VIEW: PendingDiffSnapshot;
|
|
118
|
+
/**
|
|
119
|
+
* The view a seat DRAWS when there is nothing to review: the same emptiness as {@link EMPTY_PENDING_VIEW},
|
|
120
|
+
* but READ.
|
|
121
|
+
*
|
|
122
|
+
* The difference is what the panel draws: an unread view is a session whose list is still coming, so it
|
|
123
|
+
* shows the loading state, while this one says "no pending changes" — the truth for a session that is
|
|
124
|
+
* absent or blank. Using the unread view here would leave a docked panel spinning forever behind an entry
|
|
125
|
+
* that is disabled and can never be opened.
|
|
126
|
+
*/
|
|
127
|
+
export declare const NOTHING_TO_REVIEW_VIEW: PendingDiffSnapshot;
|
|
128
|
+
/** Function shape of {@link PendingViewHooks.pendingView}; named so a seat can call it directly. */
|
|
129
|
+
export type PendingViewReader = <T>(sessionId: SessionId | undefined, select: PendingViewSelector<T>) => T;
|
|
130
|
+
/**
|
|
131
|
+
* How a seat reads pending state for the session it is actually about.
|
|
132
|
+
*
|
|
133
|
+
* The page runs several seats at once — the sidebar footer's badge, the Session header's entry, the
|
|
134
|
+
* docked tab's chip and body — and they can belong to different sessions. Reading the ONE page-wide
|
|
135
|
+
* snapshot is what let a badge show another session's count, so a seat that knows its session reads
|
|
136
|
+
* that session's own view through {@link PendingViewHooks.pendingView} and nothing else.
|
|
137
|
+
*
|
|
138
|
+
* This lives BESIDE the face's `hooks`, not inside it: that compartment is a map of observables
|
|
139
|
+
* (`HooksSources`), and the framework binds each one onto the component props as a `use<Name>` selector
|
|
140
|
+
* hook. Folding a non-observable in there would stop the whole compartment from matching, and the panel
|
|
141
|
+
* would lose `usePending` and `useDock` with it. `pendingView` is a read, not an observable: the panel's
|
|
142
|
+
* `usePending` subscription is what re-renders it.
|
|
143
|
+
*
|
|
144
|
+
* The hook is optional because the shapes are read structurally: a host (or a test) that offers only
|
|
145
|
+
* `pending` still renders, with the panel falling back to the page-wide snapshot.
|
|
146
|
+
*/
|
|
147
|
+
export interface PendingViewHooks {
|
|
148
|
+
/**
|
|
149
|
+
* Read one session's own view, with `select` applied inside the subscription.
|
|
150
|
+
*
|
|
151
|
+
* `sessionId === undefined` means "I have no session of my own": the answer is then the page-wide
|
|
152
|
+
* view (the newest session read), which is what a whole-page caller wants. A session that has never
|
|
153
|
+
* been read answers an unread empty view — never another session's list.
|
|
154
|
+
* @param sessionId - the session whose view to read.
|
|
155
|
+
* @param select - what to take out of that view.
|
|
156
|
+
* @returns the selected value.
|
|
157
|
+
*/
|
|
158
|
+
pendingView?: PendingViewReader | undefined;
|
|
159
|
+
}
|
|
38
160
|
/** The injected face the panel component receives from the plugin body. */
|
|
39
|
-
export interface PendingPanelFace {
|
|
161
|
+
export interface PendingPanelFace extends PendingViewHooks {
|
|
40
162
|
hooks: {
|
|
41
163
|
/** Live pending-diff snapshot for the current page. */
|
|
42
164
|
pending: HostObservable<PendingDiffSnapshot>;
|
|
@@ -57,8 +179,26 @@ export interface PendingPanelFace {
|
|
|
57
179
|
/** Close the docked tab now, when one exists (the quick-summon chord's job
|
|
58
180
|
* while the panel lives in the sidebar). */
|
|
59
181
|
closeDock?: (() => void) | undefined;
|
|
182
|
+
/**
|
|
183
|
+
* Ask the host whether a newer release of this plugin is published.
|
|
184
|
+
*
|
|
185
|
+
* Optional, and deliberately so: a seat that is not given one (an older shell wiring, or a
|
|
186
|
+
* harness that does not stage it) simply draws no update chip. The panel calls it ONCE, after
|
|
187
|
+
* mount, and never waits on it — a host that cannot answer, or has no answer to give, leaves the
|
|
188
|
+
* chip's site exactly as it was. The answer is drawn in ONE place (the open file's status bar, at
|
|
189
|
+
* the panel's bottom edge) out of one piece of panel state.
|
|
190
|
+
*/
|
|
191
|
+
onCheckUpdate?: (() => Promise<DiffApprovalUpdateValue>) | undefined;
|
|
60
192
|
/** Read the pending list for the current session into the snapshot. */
|
|
61
193
|
onRefresh: (sessionId: SessionId | undefined) => void;
|
|
194
|
+
/**
|
|
195
|
+
* The badge's tick, for a seat with nothing on screen: ask how many rows the session has, with no file
|
|
196
|
+
* content on the wire. Optional so a face built against an older shape still renders — the panel then
|
|
197
|
+
* keeps the full read, which is what such a page did before the verb existed.
|
|
198
|
+
*/
|
|
199
|
+
onRefreshCount?: ((sessionId: SessionId | undefined) => void) | undefined;
|
|
200
|
+
/** The reader is looking at this file now: its row's unseen dot goes out. */
|
|
201
|
+
onMarkSeen?: ((sessionId: SessionId | undefined, id: string) => void) | undefined;
|
|
62
202
|
/** Keep one operation. `keepListed` leaves the resolved entry in the list. */
|
|
63
203
|
onKeep: (sessionId: SessionId, id: string, keepListed?: boolean) => Promise<void>;
|
|
64
204
|
/** Revert one operation (restore its prior content, or remove a created file).
|
|
@@ -75,15 +215,6 @@ export interface PendingPanelFace {
|
|
|
75
215
|
onPreviewImage: (sessionId: SessionId, path: string) => Promise<string | undefined>;
|
|
76
216
|
/** Paste a copied reference into the session's chat input and focus it. */
|
|
77
217
|
onPasteReference: (sessionId: SessionId, reference: string) => void;
|
|
78
|
-
/**
|
|
79
|
-
* Send one prompt into the session as a real turn (the discussion's answer
|
|
80
|
-
* comes back through `watchChat`).
|
|
81
|
-
* @returns whether a send verb was available; false means the caller should
|
|
82
|
-
* fall back to the composer route.
|
|
83
|
-
*/
|
|
84
|
-
onAskAgent: (sessionId: SessionId, text: string) => boolean;
|
|
85
|
-
/** Watch a session's transcript and turn state for a discussion's answer. */
|
|
86
|
-
watchChat: (sessionId: SessionId, listener: (view: ChatView) => void) => () => void;
|
|
87
218
|
/** Undo the session's last keep/revert, then refresh the list; resolves to the affected entry id when it is still pending. */
|
|
88
219
|
onUndo: (sessionId: SessionId) => Promise<string | undefined>;
|
|
89
220
|
/** Redo the session's last undone keep/revert, then refresh the list; resolves to the affected entry id when it is still pending. */
|
|
@@ -101,8 +232,49 @@ export interface PendingPanelFace {
|
|
|
101
232
|
onKeepAll: (sessionId: SessionId) => Promise<void>;
|
|
102
233
|
/** Revert every pending entry of one session in a single host call (bulk). */
|
|
103
234
|
onRevertAll: (sessionId: SessionId) => Promise<void>;
|
|
235
|
+
/**
|
|
236
|
+
* Keep a PICK of files in one host call. The pick is one decision the reader made, so it goes to the
|
|
237
|
+
* host as one request — one durable write, one read back, and one undo step — rather than a loop of
|
|
238
|
+
* per-file calls that undo would then peel apart.
|
|
239
|
+
*/
|
|
240
|
+
onKeepMany: (sessionId: SessionId, ids: readonly string[], keepListed: boolean | undefined) => Promise<void>;
|
|
241
|
+
/**
|
|
242
|
+
* Revert a PICK of files in one host call; the same one request, one write, one undo step. Unlike a
|
|
243
|
+
* keep this writes over the files, and a file the agent created is DELETED with no undo — so the panel
|
|
244
|
+
* puts a confirmation in front of a pick whose revert would delete (see the pick's menu).
|
|
245
|
+
*/
|
|
246
|
+
onRevertMany: (sessionId: SessionId, ids: readonly string[], keepListed: boolean | undefined) => Promise<void>;
|
|
247
|
+
/**
|
|
248
|
+
* Write one annotation down in the session's comment store. The host owns the
|
|
249
|
+
* record from here on, so the next read is what shows it (and every other client
|
|
250
|
+
* of this session sees it too).
|
|
251
|
+
*/
|
|
252
|
+
onCommentAdd: (sessionId: SessionId, comment: CommentDraft) => Promise<DiffApprovalCommentAddValue>;
|
|
253
|
+
/** Drop one annotation from the session's comment store. */
|
|
254
|
+
onCommentRemove: (sessionId: SessionId, id: string) => Promise<DiffApprovalCommentRemoveValue>;
|
|
255
|
+
/**
|
|
256
|
+
* Drop several annotations in one host call. Ending a pick of comments is one action the reader
|
|
257
|
+
* asked for, so it goes to the host as one request and comes back as one read — see
|
|
258
|
+
* `PendingDiffStore.commentRemoveMany`.
|
|
259
|
+
*/
|
|
260
|
+
onCommentRemoveMany: (sessionId: SessionId, ids: readonly string[]) => Promise<DiffApprovalCommentRemoveManyValue>;
|
|
261
|
+
/**
|
|
262
|
+
* Ask one stored comment as its own turn of the session; the answer arrives on the
|
|
263
|
+
* next read, derived by the host from the transcript. `prompt` is what the agent is
|
|
264
|
+
* asked and `text` is the reader's own words inside it, which the host stores on the
|
|
265
|
+
* question so the thread can draw what was written.
|
|
266
|
+
*/
|
|
267
|
+
onCommentAsk: (sessionId: SessionId, id: string, prompt: string, text: string) => Promise<DiffApprovalCommentAskValue>;
|
|
268
|
+
/**
|
|
269
|
+
* The reader has this comment's card in view: its unseen dot goes out, and the answers on it become
|
|
270
|
+
* the baseline a later rewrite is measured against. A reader action, so it is its own call rather
|
|
271
|
+
* than something a read does — a read that cleared attention would make the dot impossible to see.
|
|
272
|
+
*/
|
|
273
|
+
onCommentSeen?: ((sessionId: SessionId, id: string) => void) | undefined;
|
|
104
274
|
/** Acknowledge the redo-cleared notice so it is only surfaced once. */
|
|
105
275
|
onAckRedoCleared: () => void;
|
|
276
|
+
/** Acknowledge a refused-undo notice once the panel has said it (see `undoNotice`). */
|
|
277
|
+
onAckUndoNotice: () => void;
|
|
106
278
|
/** Collapse the DSH sidebar (no-op when already collapsed) before the floating
|
|
107
279
|
* modal opens on a narrow window, so the sidebar can't overlap it. */
|
|
108
280
|
collapseSidebar: () => void;
|
|
@@ -7,13 +7,56 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client';
|
|
9
9
|
import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots';
|
|
10
|
-
import type { DiffApprovalAddValue, DiffApprovalBlockRange, DiffApprovalBrowseValue, DiffApprovalOpenAction, DiffApprovalRefreshValue, VcsImportValue } from '../types.ts';
|
|
10
|
+
import type { DiffApprovalAddValue, DiffApprovalBlockRange, DiffApprovalBrowseValue, DiffApprovalCommentAddValue, DiffApprovalCommentAskValue, DiffApprovalCommentRemoveManyValue, DiffApprovalCommentRemoveValue, DiffApprovalOpenAction, DiffApprovalRefreshValue, VcsImportValue } from '../types.ts';
|
|
11
11
|
import type { PendingDiffSnapshot } from './slots.ts';
|
|
12
|
-
import type { DiffApprovalPort } from './port.ts';
|
|
12
|
+
import type { CommentDraft, DiffApprovalPort } from './port.ts';
|
|
13
13
|
/** The observable the panel reads and the plugin body drives. */
|
|
14
14
|
export interface PendingDiffStore extends HostObservable<PendingDiffSnapshot> {
|
|
15
|
+
/**
|
|
16
|
+
* One session's own view of the pending list.
|
|
17
|
+
*
|
|
18
|
+
* The page runs several mounts of the panel at once — the footer entry, the Session header entry and
|
|
19
|
+
* the docked tab — and they can be about DIFFERENT sessions (the reader is in one, while the docked
|
|
20
|
+
* tab was opened from another). One page-wide snapshot made every badge read whichever session's read
|
|
21
|
+
* landed last, which is the cross-talk the reader reported: a badge showing another session's count,
|
|
22
|
+
* and a session with a real list showing zero. So each session's read publishes only into its own
|
|
23
|
+
* slot, and every consumer takes its own slot, by the session it is actually about.
|
|
24
|
+
*
|
|
25
|
+
* A session that has never been read answers an unread empty view — never another session's list.
|
|
26
|
+
* `sessionId === undefined` answers the most recently published view — exactly what
|
|
27
|
+
* {@link PendingDiffStore.getSnapshot} answers — because that is what a caller with no session of its
|
|
28
|
+
* own (a whole-page concern) is asking for.
|
|
29
|
+
* @param sessionId - the session whose view to read.
|
|
30
|
+
* @returns that session's snapshot, identity-stable until it is next published.
|
|
31
|
+
*/
|
|
32
|
+
viewFor: (sessionId: SessionId | undefined) => PendingDiffSnapshot;
|
|
33
|
+
/**
|
|
34
|
+
* Every session's view this page is HOLDING, one per session it has read.
|
|
35
|
+
*
|
|
36
|
+
* This is the reader a press needs (see `produced-diff.ts`): a shell press names a file and asks whether
|
|
37
|
+
* ANY session this page has read holds it, and the press cannot wait for a poll. The slots are where the
|
|
38
|
+
* fresh paths live — the light read's `paths` and the last full read's `files` — so answering from the
|
|
39
|
+
* slots is what keeps a file edited since the last full read from being a press nothing recognises.
|
|
40
|
+
* @returns the held views; a session never read contributes nothing (it has no slot).
|
|
41
|
+
*/
|
|
42
|
+
views: () => readonly PendingDiffSnapshot[];
|
|
15
43
|
/** Re-read one session's pending list (an absent session empties the view). */
|
|
16
44
|
refresh: (sessionId: SessionId | undefined) => Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Ask how many rows this session has, THE LIGHT WAY: one number, no file content.
|
|
47
|
+
*
|
|
48
|
+
* This is what a seat polls while it is showing nothing but its badge. It publishes into the same
|
|
49
|
+
* per-session slot as `refresh` — as `count`, never as `files` — so a badge keeps reading the view it
|
|
50
|
+
* already subscribes to, and it deliberately does NOT set `read` (a count is not a list: a session
|
|
51
|
+
* whose list has never been read stays unread, and its list stays empty rather than looking loaded).
|
|
52
|
+
*
|
|
53
|
+
* Failure keeps the LAST GOOD number: a count that failed knows nothing, and showing 0 or a blank
|
|
54
|
+
* badge for a list that may well be full is worse than showing the previous figure. A host that
|
|
55
|
+
* predates the endpoint (`unknown endpoint "list-count"`) is detected here ONCE and then this call
|
|
56
|
+
* degrades to `refresh`, which is exactly the old behaviour at exactly the old cost — one full read
|
|
57
|
+
* per tick — and an absent session has no count to ask for, so it returns without a call.
|
|
58
|
+
*/
|
|
59
|
+
refreshCount: (sessionId: SessionId | undefined) => Promise<void>;
|
|
17
60
|
/** Keep one operation. `keepListed` leaves the resolved entry in the list. */
|
|
18
61
|
keep: (sessionId: SessionId, id: string, keepListed?: boolean) => Promise<void>;
|
|
19
62
|
/** Revert one operation. `keepListed` leaves the resolved entry in the list. */
|
|
@@ -35,18 +78,58 @@ export interface PendingDiffStore extends HostObservable<PendingDiffSnapshot> {
|
|
|
35
78
|
browse: (sessionId: SessionId, path?: string) => Promise<DiffApprovalBrowseValue>;
|
|
36
79
|
/** Add one named path to the list, then refresh; resolves to what the scan did. */
|
|
37
80
|
addPath: (sessionId: SessionId, path: string, includeUnchanged: boolean, exact?: boolean) => Promise<DiffApprovalAddValue>;
|
|
81
|
+
/**
|
|
82
|
+
* The reader is looking at this file now: its row's unseen dot goes out. Deliberately NOT followed by a
|
|
83
|
+
* re-read — see the note at the implementation.
|
|
84
|
+
*/
|
|
85
|
+
markSeen: (sessionId: SessionId, id: string) => Promise<void>;
|
|
38
86
|
/** Open one file with its default application or reveal it in the folder. */
|
|
39
87
|
open: (sessionId: SessionId, id: string, action: DiffApprovalOpenAction) => Promise<void>;
|
|
40
88
|
/** Keep every pending entry of one session in a single host call, then refresh. */
|
|
41
89
|
keepAll: (sessionId: SessionId) => Promise<void>;
|
|
42
90
|
/** Revert every pending entry of one session in a single host call, then refresh. */
|
|
43
91
|
revertAll: (sessionId: SessionId) => Promise<void>;
|
|
92
|
+
/**
|
|
93
|
+
* Keep a pick of entries, then refresh once. The host makes the whole pick ONE undo step, so this is
|
|
94
|
+
* one action to the reader in every sense: one request, one read back, one Ctrl+Z.
|
|
95
|
+
*/
|
|
96
|
+
keepMany: (sessionId: SessionId, ids: readonly string[], keepListed: boolean | undefined) => Promise<void>;
|
|
97
|
+
/**
|
|
98
|
+
* Revert a pick of entries, then refresh once. Its undo restores what the host could snapshot: a file
|
|
99
|
+
* the agent created is DELETED with no undo at all, so the panel asks before a pick that would do it.
|
|
100
|
+
*/
|
|
101
|
+
revertMany: (sessionId: SessionId, ids: readonly string[], keepListed: boolean | undefined) => Promise<void>;
|
|
44
102
|
/** Inline one workspace image as a base64 data URI (empty when unreadable). */
|
|
45
103
|
previewImage: (sessionId: SessionId, path: string) => Promise<string | undefined>;
|
|
104
|
+
/**
|
|
105
|
+
* Write one annotation down, then refresh so the thread appears from the host's own
|
|
106
|
+
* copy (the panel keeps no thread state of its own).
|
|
107
|
+
*/
|
|
108
|
+
commentAdd: (sessionId: SessionId, comment: CommentDraft) => Promise<DiffApprovalCommentAddValue>;
|
|
109
|
+
/** Drop one annotation, then refresh. */
|
|
110
|
+
commentRemove: (sessionId: SessionId, id: string) => Promise<DiffApprovalCommentRemoveValue>;
|
|
111
|
+
/**
|
|
112
|
+
* Drop several annotations in ONE host call, then refresh once.
|
|
113
|
+
*
|
|
114
|
+
* One refresh is the difference a batch makes here: a loop of `commentRemove` re-reads (and
|
|
115
|
+
* re-renders) the whole session between every comment, so the list visibly dismantles itself one
|
|
116
|
+
* item at a time while the reader asked for one action.
|
|
117
|
+
*/
|
|
118
|
+
commentRemoveMany: (sessionId: SessionId, ids: readonly string[]) => Promise<DiffApprovalCommentRemoveManyValue>;
|
|
119
|
+
/** Ask one stored comment as its own turn, then refresh (the answer is derived on read). */
|
|
120
|
+
commentAsk: (sessionId: SessionId, id: string, prompt: string, text: string) => Promise<DiffApprovalCommentAskValue>;
|
|
121
|
+
/** The reader is looking at a comment: its card's dot goes out, then the list is re-read. */
|
|
122
|
+
commentSeen: (sessionId: SessionId, id: string) => Promise<void>;
|
|
46
123
|
/** Drop every local fact (used on connection reset). */
|
|
47
124
|
reset: () => void;
|
|
48
125
|
/** Acknowledge a redo-cleared notice so the panel surfaces it only once. */
|
|
49
126
|
clearRedoCleared: () => void;
|
|
127
|
+
/**
|
|
128
|
+
* Acknowledge the last refused-undo notice, once the panel has said it. Without this
|
|
129
|
+
* the field would sit there for the rest of the session and the same refusal later
|
|
130
|
+
* would not be news; the panel says it and clears it in one step.
|
|
131
|
+
*/
|
|
132
|
+
clearUndoNotice: () => void;
|
|
50
133
|
}
|
|
51
134
|
/**
|
|
52
135
|
* Create the store over the review-channel port.
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asking the session's agent about one comment, and reading the answer back out of
|
|
3
|
+
* the transcript.
|
|
4
|
+
*
|
|
5
|
+
* The submission goes through the host's own session API (`ctx.sessionController`),
|
|
6
|
+
* which is the one path that already knows how to admit a prompt: it mints the
|
|
7
|
+
* message, persists it, and — the reason this module needs no identity of its own —
|
|
8
|
+
* stores the request id the caller supplies on the exact user message it accepts
|
|
9
|
+
* (`source.rpcId`). That id is the join key between a comment and its message, and it
|
|
10
|
+
* makes a retried submission the same submission rather than a second one.
|
|
11
|
+
*
|
|
12
|
+
* The answer is never stored: `answers()` derives it from the session's own derived
|
|
13
|
+
* message history on every read, so the transcript stays the single source of truth
|
|
14
|
+
* for what the agent said. The agent is asked with `mode: 'queue'`, so a comment is
|
|
15
|
+
* its own turn ("one ordinary message of its own turn") — two comments asked at the
|
|
16
|
+
* same time are two turns, not one turn two blocks have to share.
|
|
17
|
+
*
|
|
18
|
+
* @module dsh-diff-approval/src/comment-ask
|
|
19
|
+
*/
|
|
20
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
21
|
+
import type { SessionId } from '@deepseek-ai/dsh-session';
|
|
22
|
+
import type { DiffApprovalCommentAskValue, CommentRecord } from './types.ts';
|
|
23
|
+
import type { CommentScope, CommentStore } from './comments.ts';
|
|
24
|
+
/** One derived transcript message, narrowed to what answering a comment needs. */
|
|
25
|
+
interface DerivedMessage {
|
|
26
|
+
role?: unknown;
|
|
27
|
+
content?: unknown;
|
|
28
|
+
source?: {
|
|
29
|
+
kind?: unknown;
|
|
30
|
+
rpcId?: unknown;
|
|
31
|
+
} | undefined;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* One entry of the session's event log, narrowed to what answering a comment reads.
|
|
35
|
+
*
|
|
36
|
+
* The log is where the turn numbers live: `deriveMessages` hands back messages with no
|
|
37
|
+
* attribution at all, and an answer belongs to the turn that claimed the question, not
|
|
38
|
+
* to "everything up to the next message a human happened to write".
|
|
39
|
+
*/
|
|
40
|
+
interface LogEvent {
|
|
41
|
+
type?: unknown;
|
|
42
|
+
data?: LogData | undefined;
|
|
43
|
+
}
|
|
44
|
+
/** One event's payload, as the by-type reads below see it. */
|
|
45
|
+
interface LogData {
|
|
46
|
+
/** The turn a message or boundary belongs to (`turn/start`, `user/message`, …). */
|
|
47
|
+
turn?: unknown;
|
|
48
|
+
/** `assistant/message` carries its assembled message here rather than flat. */
|
|
49
|
+
message?: DerivedMessage | undefined;
|
|
50
|
+
/** `user/message` IS the message, so role/content/source sit on the payload. */
|
|
51
|
+
role?: unknown;
|
|
52
|
+
content?: unknown;
|
|
53
|
+
source?: {
|
|
54
|
+
kind?: unknown;
|
|
55
|
+
rpcId?: unknown;
|
|
56
|
+
} | undefined;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* What one question's transcript read found.
|
|
60
|
+
*
|
|
61
|
+
* `ended` is the turn's own end, not a verdict about the answer: whether an answer exists
|
|
62
|
+
* is what `answer` says. Keeping the two apart is what lets the panel show a stopped note
|
|
63
|
+
* for a turn that ended with nothing while a turn that ended AFTER writing keeps showing
|
|
64
|
+
* its answer.
|
|
65
|
+
*/
|
|
66
|
+
export interface AskRead {
|
|
67
|
+
/** The claiming turn's last assistant text, or `undefined` while it has written none. */
|
|
68
|
+
answer: string | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* The turn that claimed the question, as the transcript itself attributes it. The inbox
|
|
71
|
+
* claim records the same number (`agent/inbox/claimed`); this is the read that does not
|
|
72
|
+
* depend on having been subscribed at the time.
|
|
73
|
+
*/
|
|
74
|
+
turn: number | undefined;
|
|
75
|
+
/** The claiming turn is over (`turn/end` in the log), whatever it did or did not write. */
|
|
76
|
+
ended: boolean;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* One request's already-read transcripts, keyed by the session read.
|
|
80
|
+
*
|
|
81
|
+
* `undefined` is cached as a value (hence `has`, not a truthy check): a session whose log cannot be
|
|
82
|
+
* read must not be asked for again within the same read, or a folded-over lineage would pay for the
|
|
83
|
+
* same failure once per transcript.
|
|
84
|
+
*/
|
|
85
|
+
export type TranscriptCache = Map<SessionId, readonly LogEvent[] | undefined>;
|
|
86
|
+
/**
|
|
87
|
+
* The answer to one submitted comment, read from the session's own event log.
|
|
88
|
+
*
|
|
89
|
+
* Our prompt and the turn that ran because of it are one thing. The submission carries a
|
|
90
|
+
* request id on its `user/message` (`source.rpcId`), and `turn/start` is the bracket the
|
|
91
|
+
* log puts around a turn, so the question's own turn IS the window: the assistant
|
|
92
|
+
* messages of that turn are the answer, and `turn/end` for it is where the question is
|
|
93
|
+
* over. Nothing between the two belongs to anyone else.
|
|
94
|
+
*
|
|
95
|
+
* That is what keeps a later input from stealing the answer. A session log has
|
|
96
|
+
* user-role messages no human wrote — a subagent report, a background-job notice, an
|
|
97
|
+
* injected context — and under the old "walk to the next human message, take the last
|
|
98
|
+
* assistant text" rule any of them let the span run on into the NEXT turn's answer, so a
|
|
99
|
+
* comment that had been answered was silently replaced by whatever the agent later said
|
|
100
|
+
* about something else entirely. A tool-result message (role `user` as well) has the
|
|
101
|
+
* same problem for the opposite reason: it is a boundary trap that would end a segment
|
|
102
|
+
* mid-turn and make a question with a tool call behind it look unanswered. Bound by the
|
|
103
|
+
* turn, both read correctly: the tool result is inside it, and the later input is not.
|
|
104
|
+
*
|
|
105
|
+
* @param events - the session's event log, in sequence order.
|
|
106
|
+
* @param requestId - the request id the comment was submitted under.
|
|
107
|
+
* @returns the claiming turn, its answer text, and whether the turn is over.
|
|
108
|
+
*/
|
|
109
|
+
export declare function answerForRequest(events: readonly LogEvent[], requestId: string): AskRead;
|
|
110
|
+
/**
|
|
111
|
+
* Submits comments as turns of their session, and reads their answers back.
|
|
112
|
+
*
|
|
113
|
+
* One instance serves every session: the store is the state, and the per-agent
|
|
114
|
+
* subscription is registered on first use and lives with the agent's own context.
|
|
115
|
+
*/
|
|
116
|
+
export declare class CommentAsker {
|
|
117
|
+
private readonly ctx;
|
|
118
|
+
private readonly comments;
|
|
119
|
+
/**
|
|
120
|
+
* The agent each session's inbox events are subscribed on, keyed by session id, with
|
|
121
|
+
* the disposers that release those subscriptions.
|
|
122
|
+
*
|
|
123
|
+
* Keyed by AGENT IDENTITY, not merely "this session id was watched once": a
|
|
124
|
+
* resumed or re-created agent for the same session id is a different object
|
|
125
|
+
* whose listeners live on its OWN context, so a session-id set would consider
|
|
126
|
+
* the new agent already watched and never subscribe to it — leaving `turn`
|
|
127
|
+
* undefined forever, `agent/turn-stopping` never arriving, and the panel stuck
|
|
128
|
+
* on "replying" with no way out (the defect this map replaces). The disposers are
|
|
129
|
+
* what keeps a replacement from leaking a second set of handlers per event.
|
|
130
|
+
*/
|
|
131
|
+
private readonly watched;
|
|
132
|
+
/**
|
|
133
|
+
* @param ctx - the plugin's host context (its `sessionController` and `agents` are asked for lazily).
|
|
134
|
+
* @param comments - the comment store the ask is recorded in.
|
|
135
|
+
*/
|
|
136
|
+
constructor(ctx: Context, comments: CommentStore);
|
|
137
|
+
/**
|
|
138
|
+
* Ask one stored comment as its own turn.
|
|
139
|
+
*
|
|
140
|
+
* The request id is recorded BEFORE the submission leaves, so a claim event that
|
|
141
|
+
* arrives while the submission is still in flight already finds its comment. A
|
|
142
|
+
* submission that rejects is marked dropped rather than left looking pending.
|
|
143
|
+
*
|
|
144
|
+
* @param sessionId - the session the question is asked FROM: the prompt goes into it, so its
|
|
145
|
+
* transcript is where the answer will appear.
|
|
146
|
+
* @param commentId - the comment to ask.
|
|
147
|
+
* @param prompt - the prompt text to send verbatim.
|
|
148
|
+
* @param text - the reader's own words, which the prompt wraps: the thread keeps these.
|
|
149
|
+
* @param signal - aborts the submission.
|
|
150
|
+
* @param inScope - which comment authors the caller's lineage covers, so a thread another seat
|
|
151
|
+
* wrote can be asked from this one.
|
|
152
|
+
* @returns what the request did.
|
|
153
|
+
*/
|
|
154
|
+
ask(sessionId: SessionId, commentId: string, prompt: string, text: string, signal: AbortSignal, inScope: CommentScope): Promise<DiffApprovalCommentAskValue>;
|
|
155
|
+
/**
|
|
156
|
+
* One scope's questions, grouped by THE TRANSCRIPT each was submitted into.
|
|
157
|
+
*
|
|
158
|
+
* A lineage's threads may hold questions asked from several seats, and an answer lives in the
|
|
159
|
+
* transcript of the session that was asked — so the read is per transcript, not per thread. Grouping
|
|
160
|
+
* here (rather than reading each thread separately) is what keeps a poll to ONE log walk per
|
|
161
|
+
* transcript, however many threads asked there.
|
|
162
|
+
*
|
|
163
|
+
* The legacy case is why `askTranscript` exists: a question asked before the asking session was
|
|
164
|
+
* recorded has only the comment's own session to go on, which is where it went.
|
|
165
|
+
* @param visible - the comments this read may see (already scoped by the caller).
|
|
166
|
+
* @returns the request ids per transcript; empty when nothing was ever asked.
|
|
167
|
+
*/
|
|
168
|
+
answerGroups(visible: readonly CommentRecord[]): Map<SessionId, string[]>;
|
|
169
|
+
/**
|
|
170
|
+
* Every question submitted into ONE transcript and what it says about them, keyed by the
|
|
171
|
+
* question's request id (which is what `CommentAsk` records and what a client matches
|
|
172
|
+
* against). Derived from that session's event log on every call and never stored: the log
|
|
173
|
+
* is the one source of truth for what the agent said, so a stored copy could only ever
|
|
174
|
+
* disagree with it.
|
|
175
|
+
* @param sessionId - the transcript to read.
|
|
176
|
+
* @param requestIds - the questions that were submitted into it.
|
|
177
|
+
* @param cache - this request's already-read logs, so the answer fold and the turn-end read
|
|
178
|
+
* below share ONE walk of a log that can hold >100k events.
|
|
179
|
+
* @returns the read per question; empty when the session holds no questions.
|
|
180
|
+
*/
|
|
181
|
+
answersFor(sessionId: SessionId, requestIds: readonly string[], cache?: TranscriptCache): Record<string, AskRead>;
|
|
182
|
+
/**
|
|
183
|
+
* The turns the session's log shows as over, for the questions it holds.
|
|
184
|
+
*
|
|
185
|
+
* `agent/turn-stopping` already reports a turn's end live, and that is what usually
|
|
186
|
+
* writes `ended` down (see `markTurnEnded`). Reading it out of the log as well is what
|
|
187
|
+
* makes the flag survive a session this process did not watch end — a resumed session,
|
|
188
|
+
* a client that connected afterwards, a deployment whose agent events cannot be
|
|
189
|
+
* subscribed at all — because `turn/end` is durable and the event is not.
|
|
190
|
+
*
|
|
191
|
+
* @param sessionId - the transcript whose turns to check.
|
|
192
|
+
* @param cache - this request's already-read logs (see `answersFor`).
|
|
193
|
+
* @returns the turns the log closed, or empty when the log cannot be read.
|
|
194
|
+
*/
|
|
195
|
+
endedTurns(sessionId: SessionId, cache?: TranscriptCache): number[];
|
|
196
|
+
/**
|
|
197
|
+
* One session's event log, as the reads above take it.
|
|
198
|
+
*
|
|
199
|
+
* `snapshotEvents()` and not `deriveMessages()`: the derived history is a list of
|
|
200
|
+
* messages with no turn attribution, and the turn is the whole point of the answer read
|
|
201
|
+
* (see `answerForRequest`). The event log is the session's append-only source of truth
|
|
202
|
+
* and `snapshotEvents` hands back a frozen, sequence-ordered copy of it, so walking it
|
|
203
|
+
* is a plain read that cannot disturb the session.
|
|
204
|
+
*
|
|
205
|
+
* The cache is per REQUEST, not per session: one list read asks this twice for every transcript
|
|
206
|
+
* (the answer fold and the turn-end read), and the copy is the expensive part of a long session's
|
|
207
|
+
* log. `has` rather than a truthy check, so "cannot be read" is cached as such instead of being
|
|
208
|
+
* asked again.
|
|
209
|
+
* @param sessionId - the session to read.
|
|
210
|
+
* @param cache - this request's already-read logs, when the caller is doing several reads.
|
|
211
|
+
* @returns its events, or undefined when no live session can be read.
|
|
212
|
+
*/
|
|
213
|
+
private sessionEvents;
|
|
214
|
+
/** The live agent for one session, or `undefined` when this host drives none. */
|
|
215
|
+
private agentOf;
|
|
216
|
+
/**
|
|
217
|
+
* Subscribe to one agent's inbox, once per LIVE agent, moving to a replacement agent
|
|
218
|
+
* when the session's agent changes under us.
|
|
219
|
+
*
|
|
220
|
+
* The listeners live on the agent's own context, so they go away with the agent
|
|
221
|
+
* and never outlive it — which is exactly why the dedup has to be per agent and
|
|
222
|
+
* not per session: a session whose agent was re-created (a resume, a restart, a
|
|
223
|
+
* rebuilt agent for the same id) has nothing listening on the new one, and every
|
|
224
|
+
* event this module depends on would silently stop arriving. The remembered
|
|
225
|
+
* agent is compared by identity, so a different object re-subscribes and a
|
|
226
|
+
* disposed one's stale entry cannot block it. What the replaced agent held is
|
|
227
|
+
* released first, so re-arming is not a leak: one set of handlers per live agent,
|
|
228
|
+
* not one more per identity change.
|
|
229
|
+
*
|
|
230
|
+
* Called on every ask AND on every poll read (`answersFor`), because the agent can be
|
|
231
|
+
* replaced while a question is still pending — the case where nothing asks again.
|
|
232
|
+
* @param sessionId - the session whose agent to watch.
|
|
233
|
+
*/
|
|
234
|
+
private watch;
|
|
235
|
+
}
|
|
236
|
+
export {};
|