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
|
@@ -5,9 +5,30 @@
|
|
|
5
5
|
* @module dsh-diff-approval/client/port
|
|
6
6
|
*/
|
|
7
7
|
import type { ClientConnectionRpc, SessionId } from '@deepseek-ai/dsh-client-connection/client';
|
|
8
|
-
import type { DiffApprovalActionValue, DiffApprovalAddValue, DiffApprovalBlockRange, DiffApprovalBrowseValue, DiffApprovalBulkValue, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, DiffApprovalPreviewImageValue, DiffApprovalRefreshValue, VcsImportValue } from '../types.ts';
|
|
8
|
+
import type { CommentAnchor, CommentQuoteLine, DiffApprovalActionValue, DiffApprovalAddValue, DiffApprovalBlockRange, DiffApprovalBrowseValue, DiffApprovalBulkValue, DiffApprovalCommentAddValue, DiffApprovalCommentAskValue, DiffApprovalCommentRemoveManyValue, DiffApprovalCommentRemoveValue, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, DiffApprovalPreviewImageValue, DiffApprovalRefreshValue, VcsImportValue } from '../types.ts';
|
|
9
9
|
/** The channel the host half registers and this port calls. */
|
|
10
10
|
export declare const DIFF_APPROVAL_CHANNEL = "/diff-approval";
|
|
11
|
+
/**
|
|
12
|
+
* One annotation as the panel hands it over: what the reader wrote and where they
|
|
13
|
+
* wrote it. The display path is not part of it — the entry a comment hangs off is the
|
|
14
|
+
* host's authority on what the file is called.
|
|
15
|
+
*/
|
|
16
|
+
export interface CommentDraft {
|
|
17
|
+
/** Caller-minted id, so a retried write lands on the same comment. */
|
|
18
|
+
id: string;
|
|
19
|
+
/** The listed entry the annotation hangs off. */
|
|
20
|
+
entryId: string;
|
|
21
|
+
/** The lines it was made on, as new-file line numbers. */
|
|
22
|
+
anchor: CommentAnchor;
|
|
23
|
+
/** The anchored lines as they read then (the re-anchor fingerprint). */
|
|
24
|
+
quote: string;
|
|
25
|
+
/** `quote` with one line of context on each side, as it read then. */
|
|
26
|
+
quoteContext?: string | undefined;
|
|
27
|
+
/** The gutter numbers of `quote`'s lines, in the same order. */
|
|
28
|
+
quoteLines?: CommentQuoteLine[] | undefined;
|
|
29
|
+
/** What the reader wrote. */
|
|
30
|
+
text: string;
|
|
31
|
+
}
|
|
11
32
|
/** This package's business verbs over the review channel. */
|
|
12
33
|
export interface DiffApprovalPort {
|
|
13
34
|
/** Read one session's pending entries (plus its workspace root), oldest capture first. */
|
|
@@ -41,8 +62,36 @@ export interface DiffApprovalPort {
|
|
|
41
62
|
keepAll(sessionId: SessionId): Promise<DiffApprovalBulkValue>;
|
|
42
63
|
/** Revert every pending entry of one session in a single host call (one batch). */
|
|
43
64
|
revertAll(sessionId: SessionId): Promise<DiffApprovalBulkValue>;
|
|
65
|
+
/**
|
|
66
|
+
* Keep a PICK of entries in one host call: one request, one durable write and one undo step — the
|
|
67
|
+
* host records the whole pick as a single batch, so one Ctrl+Z takes that one decision back instead
|
|
68
|
+
* of peeling it apart file by file. `keepListed` says whether the resolved files stay listed.
|
|
69
|
+
*
|
|
70
|
+
* There is a `revertMany` beside it, and the pair is NOT symmetric: a keep writes nothing, while a
|
|
71
|
+
* revert writes the baseline over each file — and DELETES the ones the agent created, which the host
|
|
72
|
+
* cannot undo. The panel asks before sending a pick whose revert would delete anything.
|
|
73
|
+
*/
|
|
74
|
+
keepMany(sessionId: SessionId, ids: readonly string[], keepListed: boolean | undefined): Promise<DiffApprovalBulkValue>;
|
|
75
|
+
/** Revert a PICK of entries in one host call: the same one request, one write, one undo step. The undo
|
|
76
|
+
* restores the entries the host could snapshot — see `keepMany` for the deletes it cannot. */
|
|
77
|
+
revertMany(sessionId: SessionId, ids: readonly string[], keepListed: boolean | undefined): Promise<DiffApprovalBulkValue>;
|
|
44
78
|
/** Read one workspace image and inline it as a base64 data URI (for the Markdown preview). */
|
|
45
79
|
previewImage(sessionId: SessionId, path: string): Promise<DiffApprovalPreviewImageValue>;
|
|
80
|
+
/**
|
|
81
|
+
* Write one annotation down. The host refuses a comment whose entry is not in the
|
|
82
|
+
* session's list, and the refusal is reported rather than retried.
|
|
83
|
+
*/
|
|
84
|
+
commentAdd(sessionId: SessionId, comment: CommentDraft): Promise<DiffApprovalCommentAddValue>;
|
|
85
|
+
/** Drop one annotation (what "a comment dies with its entry" does by hand). */
|
|
86
|
+
commentRemove(sessionId: SessionId, id: string): Promise<DiffApprovalCommentRemoveValue>;
|
|
87
|
+
/**
|
|
88
|
+
* Drop several annotations in one host call: one request, one write of the session's comment file,
|
|
89
|
+
* and one read afterwards (`store.commentRemoveMany`). A batch that is a loop of `commentRemove`
|
|
90
|
+
* calls is a different thing — N requests, N writes — which is what this endpoint exists to avoid.
|
|
91
|
+
*/
|
|
92
|
+
commentRemoveMany(sessionId: SessionId, ids: readonly string[]): Promise<DiffApprovalCommentRemoveManyValue>;
|
|
93
|
+
/** Ask one stored comment as its own turn of the session. */
|
|
94
|
+
commentAsk(sessionId: SessionId, id: string, prompt: string, text: string): Promise<DiffApprovalCommentAskValue>;
|
|
46
95
|
}
|
|
47
96
|
/** Build the port over one generic RPC caller.
|
|
48
97
|
* @param rpc - the connection's channel caller.
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* @module dsh-diff-approval/client/presentation-menu
|
|
14
14
|
*/
|
|
15
15
|
import type { ReactNode } from 'react';
|
|
16
|
-
import type { MenuEntry } from '
|
|
16
|
+
import type { MenuEntry } from './dsh-icons.ts';
|
|
17
17
|
import type { DiffApprovalPresentation } from './settings.ts';
|
|
18
18
|
import type { Translator } from './locales.ts';
|
|
19
19
|
/**
|
|
@@ -1,27 +1,64 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Route a file press's own click.
|
|
3
3
|
*
|
|
4
|
-
* The harness
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* there, but the plugin should not have to touch the harness component. So this
|
|
8
|
-
* module watches the DOM for those chips (via a MutationObserver, the same
|
|
9
|
-
* bridge the dsh-pocket mobile fork uses to inject its 复制 buttons) and injects
|
|
10
|
-
* a small button after each one. Clicking it dispatches a window event that the
|
|
11
|
-
* diff-approval panel listens for (see PendingPanel), which opens the panel and
|
|
12
|
-
* selects the file when it is still pending, or toasts otherwise.
|
|
4
|
+
* The harness draws several presses that answer by opening a file in a viewer of its OWN, and this
|
|
5
|
+
* module takes over exactly those — and only for a file the review panel is already holding — so the
|
|
6
|
+
* reader gets both ways of opening it instead of one fixed behaviour:
|
|
13
7
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
8
|
+
* `[data-produced-files-row] button` 0.1.5's produced-file card `title` = the path
|
|
9
|
+
* `[data-presented-files-row] button` 0.1.7's presented-file card `title` = the path
|
|
10
|
+
* `[data-changed-files] button` 0.1.7's changed-files row `aria-describedby` = the path
|
|
11
|
+
* `button[title]` a message's file LINK (0.1.7) `title` = the path
|
|
12
|
+
* `button[data-ref-chip]` a `@file` chip in a message `title` = the token
|
|
13
|
+
* `a[href]` any other link naming a file `href` = the path
|
|
14
|
+
*
|
|
15
|
+
* Everything else is left entirely alone: a link to a URL, an in-page anchor, a chip naming something
|
|
16
|
+
* that is not a file, a file this panel does not hold, a modified press (Ctrl/Cmd/Shift/Alt, or a
|
|
17
|
+
* middle click — gestures the harness may give its own meaning later), and any press inside this
|
|
18
|
+
* plugin's own surface, whose buttons carry paths too and belong to the panel that is already showing
|
|
19
|
+
* the file.
|
|
20
|
+
*
|
|
21
|
+
* The name of the module (and of the event it raises) dates from when the produced-file card was the
|
|
22
|
+
* only press of this kind; `data-presented-files-row` is that same list under 0.1.7's name for it.
|
|
16
23
|
*/
|
|
17
|
-
/** Window event dispatched
|
|
24
|
+
/** Window event dispatched when a pending file's press is taken over; the panel listens for it. */
|
|
25
|
+
export declare const CHIP_MENU_EVENT = "diff-approval:chip-menu";
|
|
26
|
+
/** Window event dispatched by the panel to open a file in the panel; PendingPanel listens for it. */
|
|
18
27
|
export declare const OPEN_FILE_EVENT = "diff-approval:open-file";
|
|
28
|
+
/** What a press tells the panel: which file, and where the press was (the menu hangs under it). */
|
|
29
|
+
export interface ProducedChipMenuDetail {
|
|
30
|
+
/** The path the pressed element names. */
|
|
31
|
+
path: string;
|
|
32
|
+
/** The pressed element's own box, in viewport coordinates. */
|
|
33
|
+
x: number;
|
|
34
|
+
y: number;
|
|
35
|
+
}
|
|
36
|
+
/** What the bridge needs from the host to decide, and to report. */
|
|
37
|
+
export interface ProducedChipBridge {
|
|
38
|
+
/**
|
|
39
|
+
* Whether the panel holds this file right now. Called synchronously on every press, because
|
|
40
|
+
* the decision it answers is whether the press is prevented — there is no second chance at it.
|
|
41
|
+
*/
|
|
42
|
+
isPending: (path: string) => boolean;
|
|
43
|
+
/** A pending file's press was taken over: open the menu the press asked for. */
|
|
44
|
+
onMenu: (detail: ProducedChipMenuDetail) => void;
|
|
45
|
+
}
|
|
19
46
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
47
|
+
* Press a file's own press again — the shell's open, run the only way this plugin can run it
|
|
48
|
+
* faithfully, with the bridge standing down for that press (see the file's own doc).
|
|
49
|
+
*
|
|
50
|
+
* The menu's first row must open the file exactly the way the shell does, and what that is is the
|
|
51
|
+
* shell's business, not this plugin's: pressing the same element again keeps every part of it —
|
|
52
|
+
* the viewer it opens, the line a `#L24` suffix asked for, whatever a future shell adds there.
|
|
53
|
+
*
|
|
54
|
+
* @param path - the file the press names.
|
|
55
|
+
* @returns whether an element naming it was found to press.
|
|
56
|
+
*/
|
|
57
|
+
export declare function replayFilePress(path: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Start routing the presses that would open a file in the shell's own viewer.
|
|
60
|
+
*
|
|
61
|
+
* @param bridge - the host's decision (is the file pending) and where to report a press that is.
|
|
62
|
+
* @returns a cleanup that stops routing.
|
|
26
63
|
*/
|
|
27
|
-
export declare function
|
|
64
|
+
export declare function startProducedChipMenu(bridge: ProducedChipBridge): () => void;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which session the review is about, as every shell of this plugin has offered it.
|
|
3
|
+
*
|
|
4
|
+
* 0.1.7 changed how a client learns the selected session. Its session-list store carries
|
|
5
|
+
* `{ ids, byId, phase, projectionsBySession }` — there is no `current` any more — and the shell's own
|
|
6
|
+
* components take the id from their composed SLOT PROPS instead (`const { sessionId, useSessions } =
|
|
7
|
+
* props`, then `useSessions(s => s.byId[sessionId]?.blank)`; measured in `dsh-client-ui-conversation`
|
|
8
|
+
* and `dsh-client-ui-agent-preset`). Older shells put it in the store as `state.current`.
|
|
9
|
+
*
|
|
10
|
+
* The seats differ, and that is the whole reason this module exists: the Session HEADER seat is
|
|
11
|
+
* session-scoped and is handed `sessionId`; the sidebar FOOTER seat — where this panel's own badge
|
|
12
|
+
* lives — is rendered `renderSlot('sidebar.footer.action', { wide })` and is handed nothing else. So
|
|
13
|
+
* the header publishes what it is showing, the footer reads it, and the store is the third fallback
|
|
14
|
+
* (`current` on the shells that have it). Every reader here is total: an unknown shape answers
|
|
15
|
+
* "nothing known" rather than throwing, and no field that is absent is taken for a value.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-diff-approval/client/session-seat
|
|
18
|
+
*/
|
|
19
|
+
import type { SessionId } from '@deepseek-ai/dsh-client-connection/client';
|
|
20
|
+
/**
|
|
21
|
+
* Publish the session the header entry is showing.
|
|
22
|
+
* @param id - the id, or `undefined` when the shell named none.
|
|
23
|
+
*/
|
|
24
|
+
export declare function publishSessionId(id: SessionId | undefined): void;
|
|
25
|
+
/** The session the header entry last showed, or `undefined` when it never named one. */
|
|
26
|
+
export declare function publishedSessionId(): SessionId | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* The selected session a shell's session-list state names, by every field the shells have used.
|
|
29
|
+
* @param state - the store state handed to the selector.
|
|
30
|
+
* @returns the id, or `undefined` when this shape names none.
|
|
31
|
+
*/
|
|
32
|
+
export declare function selectedSessionOf(state: unknown): SessionId | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Whether one session row is BLANK — a freshly created session with nothing in it yet, which has
|
|
35
|
+
* nothing to review and leaves this plugin's entry inert exactly like no session at all.
|
|
36
|
+
*
|
|
37
|
+
* `byId[id].blank` is the row's own flag and has kept its name across the shells; a store that
|
|
38
|
+
* carries `blank` on the STATE (with the id beside it) is read too. An absent flag is not blank: a
|
|
39
|
+
* shape this plugin does not recognise must not disable the panel.
|
|
40
|
+
*
|
|
41
|
+
* @param state - the store state handed to the selector.
|
|
42
|
+
* @param id - the session the answer is about; `undefined` answers false.
|
|
43
|
+
* @returns true only when the shell says that session is blank.
|
|
44
|
+
*/
|
|
45
|
+
export declare function sessionIsBlank(state: unknown, id: SessionId | undefined): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* The session this mount is about: the slot prop the shell composed, else the store's own selected
|
|
48
|
+
* session, else what the header entry published (see the module doc).
|
|
49
|
+
* @param propSessionId - `sessionId` from the composed slot props, when the shell sends one.
|
|
50
|
+
* @param storeSelected - the store's selected session, when its shape names one.
|
|
51
|
+
* @returns the id, or `undefined` when nothing here knows one.
|
|
52
|
+
*/
|
|
53
|
+
export declare function sessionIdFor(propSessionId: SessionId | undefined, storeSelected: SessionId | undefined): SessionId | undefined;
|
|
@@ -2,14 +2,34 @@
|
|
|
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, 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;
|
|
15
35
|
/** Entry ids whose keep/revert is in flight; their controls are disabled. */
|
|
@@ -30,6 +50,20 @@ export interface PendingDiffSnapshot {
|
|
|
30
50
|
* told finds the list gone later with nothing to explain it (see the wire type).
|
|
31
51
|
*/
|
|
32
52
|
persistError?: string | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* The last failure to write the session's COMMENT files, or absent while those writes are
|
|
55
|
+
* working. Their own field (and their own copy) because they are their own files: a comment
|
|
56
|
+
* that only exists in the host's memory is erased by a restart exactly like an unpersisted
|
|
57
|
+
* list, and the panel says so once — the same way, and for the same reason, as `persistError`.
|
|
58
|
+
*/
|
|
59
|
+
commentPersistError?: string | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Why the last undo/redo was refused by the host, or absent when there is nothing to say.
|
|
62
|
+
* The host answers a refused undo with its reason (the divergence guard's "the file changed
|
|
63
|
+
* outside the review"), and the reader who pressed the key has to be told: the panel surfaces
|
|
64
|
+
* this once as a toast and clears it, so the next refusal is news again.
|
|
65
|
+
*/
|
|
66
|
+
undoNotice?: string | undefined;
|
|
33
67
|
/** Latched when an external change created a fresh undo checkpoint that
|
|
34
68
|
* superseded the redo history; the panel surfaces it once (deferred if the
|
|
35
69
|
* panel is closed) via a bottom-right notice. */
|
|
@@ -75,15 +109,6 @@ export interface PendingPanelFace {
|
|
|
75
109
|
onPreviewImage: (sessionId: SessionId, path: string) => Promise<string | undefined>;
|
|
76
110
|
/** Paste a copied reference into the session's chat input and focus it. */
|
|
77
111
|
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
112
|
/** Undo the session's last keep/revert, then refresh the list; resolves to the affected entry id when it is still pending. */
|
|
88
113
|
onUndo: (sessionId: SessionId) => Promise<string | undefined>;
|
|
89
114
|
/** 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 +126,43 @@ export interface PendingPanelFace {
|
|
|
101
126
|
onKeepAll: (sessionId: SessionId) => Promise<void>;
|
|
102
127
|
/** Revert every pending entry of one session in a single host call (bulk). */
|
|
103
128
|
onRevertAll: (sessionId: SessionId) => Promise<void>;
|
|
129
|
+
/**
|
|
130
|
+
* Keep a PICK of files in one host call. The pick is one decision the reader made, so it goes to the
|
|
131
|
+
* host as one request — one durable write, one read back, and one undo step — rather than a loop of
|
|
132
|
+
* per-file calls that undo would then peel apart.
|
|
133
|
+
*/
|
|
134
|
+
onKeepMany: (sessionId: SessionId, ids: readonly string[], keepListed: boolean | undefined) => Promise<void>;
|
|
135
|
+
/**
|
|
136
|
+
* Revert a PICK of files in one host call; the same one request, one write, one undo step. Unlike a
|
|
137
|
+
* keep this writes over the files, and a file the agent created is DELETED with no undo — so the panel
|
|
138
|
+
* puts a confirmation in front of a pick whose revert would delete (see the pick's menu).
|
|
139
|
+
*/
|
|
140
|
+
onRevertMany: (sessionId: SessionId, ids: readonly string[], keepListed: boolean | undefined) => Promise<void>;
|
|
141
|
+
/**
|
|
142
|
+
* Write one annotation down in the session's comment store. The host owns the
|
|
143
|
+
* record from here on, so the next read is what shows it (and every other client
|
|
144
|
+
* of this session sees it too).
|
|
145
|
+
*/
|
|
146
|
+
onCommentAdd: (sessionId: SessionId, comment: CommentDraft) => Promise<DiffApprovalCommentAddValue>;
|
|
147
|
+
/** Drop one annotation from the session's comment store. */
|
|
148
|
+
onCommentRemove: (sessionId: SessionId, id: string) => Promise<DiffApprovalCommentRemoveValue>;
|
|
149
|
+
/**
|
|
150
|
+
* Drop several annotations in one host call. Ending a pick of comments is one action the reader
|
|
151
|
+
* asked for, so it goes to the host as one request and comes back as one read — see
|
|
152
|
+
* `DiffApprovalStore.commentRemoveMany`.
|
|
153
|
+
*/
|
|
154
|
+
onCommentRemoveMany: (sessionId: SessionId, ids: readonly string[]) => Promise<DiffApprovalCommentRemoveManyValue>;
|
|
155
|
+
/**
|
|
156
|
+
* Ask one stored comment as its own turn of the session; the answer arrives on the
|
|
157
|
+
* next read, derived by the host from the transcript. `prompt` is what the agent is
|
|
158
|
+
* asked and `text` is the reader's own words inside it, which the host stores on the
|
|
159
|
+
* question so the thread can draw what was written.
|
|
160
|
+
*/
|
|
161
|
+
onCommentAsk: (sessionId: SessionId, id: string, prompt: string, text: string) => Promise<DiffApprovalCommentAskValue>;
|
|
104
162
|
/** Acknowledge the redo-cleared notice so it is only surfaced once. */
|
|
105
163
|
onAckRedoCleared: () => void;
|
|
164
|
+
/** Acknowledge a refused-undo notice once the panel has said it (see `undoNotice`). */
|
|
165
|
+
onAckUndoNotice: () => void;
|
|
106
166
|
/** Collapse the DSH sidebar (no-op when already collapsed) before the floating
|
|
107
167
|
* modal opens on a narrow window, so the sidebar can't overlap it. */
|
|
108
168
|
collapseSidebar: () => void;
|
|
@@ -7,9 +7,9 @@
|
|
|
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
15
|
/** Re-read one session's pending list (an absent session empties the view). */
|
|
@@ -41,12 +41,45 @@ export interface PendingDiffStore extends HostObservable<PendingDiffSnapshot> {
|
|
|
41
41
|
keepAll: (sessionId: SessionId) => Promise<void>;
|
|
42
42
|
/** Revert every pending entry of one session in a single host call, then refresh. */
|
|
43
43
|
revertAll: (sessionId: SessionId) => Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Keep a pick of entries, then refresh once. The host makes the whole pick ONE undo step, so this is
|
|
46
|
+
* one action to the reader in every sense: one request, one read back, one Ctrl+Z.
|
|
47
|
+
*/
|
|
48
|
+
keepMany: (sessionId: SessionId, ids: readonly string[], keepListed: boolean | undefined) => Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Revert a pick of entries, then refresh once. Its undo restores what the host could snapshot: a file
|
|
51
|
+
* the agent created is DELETED with no undo at all, so the panel asks before a pick that would do it.
|
|
52
|
+
*/
|
|
53
|
+
revertMany: (sessionId: SessionId, ids: readonly string[], keepListed: boolean | undefined) => Promise<void>;
|
|
44
54
|
/** Inline one workspace image as a base64 data URI (empty when unreadable). */
|
|
45
55
|
previewImage: (sessionId: SessionId, path: string) => Promise<string | undefined>;
|
|
56
|
+
/**
|
|
57
|
+
* Write one annotation down, then refresh so the thread appears from the host's own
|
|
58
|
+
* copy (the panel keeps no thread state of its own).
|
|
59
|
+
*/
|
|
60
|
+
commentAdd: (sessionId: SessionId, comment: CommentDraft) => Promise<DiffApprovalCommentAddValue>;
|
|
61
|
+
/** Drop one annotation, then refresh. */
|
|
62
|
+
commentRemove: (sessionId: SessionId, id: string) => Promise<DiffApprovalCommentRemoveValue>;
|
|
63
|
+
/**
|
|
64
|
+
* Drop several annotations in ONE host call, then refresh once.
|
|
65
|
+
*
|
|
66
|
+
* One refresh is the difference a batch makes here: a loop of `commentRemove` re-reads (and
|
|
67
|
+
* re-renders) the whole session between every comment, so the list visibly dismantles itself one
|
|
68
|
+
* item at a time while the reader asked for one action.
|
|
69
|
+
*/
|
|
70
|
+
commentRemoveMany: (sessionId: SessionId, ids: readonly string[]) => Promise<DiffApprovalCommentRemoveManyValue>;
|
|
71
|
+
/** Ask one stored comment as its own turn, then refresh (the answer is derived on read). */
|
|
72
|
+
commentAsk: (sessionId: SessionId, id: string, prompt: string, text: string) => Promise<DiffApprovalCommentAskValue>;
|
|
46
73
|
/** Drop every local fact (used on connection reset). */
|
|
47
74
|
reset: () => void;
|
|
48
75
|
/** Acknowledge a redo-cleared notice so the panel surfaces it only once. */
|
|
49
76
|
clearRedoCleared: () => void;
|
|
77
|
+
/**
|
|
78
|
+
* Acknowledge the last refused-undo notice, once the panel has said it. Without this
|
|
79
|
+
* the field would sit there for the rest of the session and the same refusal later
|
|
80
|
+
* would not be news; the panel says it and clears it in one step.
|
|
81
|
+
*/
|
|
82
|
+
clearUndoNotice: () => void;
|
|
50
83
|
}
|
|
51
84
|
/**
|
|
52
85
|
* Create the store over the review-channel port.
|
|
@@ -0,0 +1,209 @@
|
|
|
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 } from './types.ts';
|
|
23
|
+
import type { 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
|
+
* The answer to one submitted comment, read from the session's own event log.
|
|
80
|
+
*
|
|
81
|
+
* Our prompt and the turn that ran because of it are one thing. The submission carries a
|
|
82
|
+
* request id on its `user/message` (`source.rpcId`), and `turn/start` is the bracket the
|
|
83
|
+
* log puts around a turn, so the question's own turn IS the window: the assistant
|
|
84
|
+
* messages of that turn are the answer, and `turn/end` for it is where the question is
|
|
85
|
+
* over. Nothing between the two belongs to anyone else.
|
|
86
|
+
*
|
|
87
|
+
* That is what keeps a later input from stealing the answer. A session log has
|
|
88
|
+
* user-role messages no human wrote — a subagent report, a background-job notice, an
|
|
89
|
+
* injected context — and under the old "walk to the next human message, take the last
|
|
90
|
+
* assistant text" rule any of them let the span run on into the NEXT turn's answer, so a
|
|
91
|
+
* comment that had been answered was silently replaced by whatever the agent later said
|
|
92
|
+
* about something else entirely. A tool-result message (role `user` as well) has the
|
|
93
|
+
* same problem for the opposite reason: it is a boundary trap that would end a segment
|
|
94
|
+
* mid-turn and make a question with a tool call behind it look unanswered. Bound by the
|
|
95
|
+
* turn, both read correctly: the tool result is inside it, and the later input is not.
|
|
96
|
+
*
|
|
97
|
+
* @param events - the session's event log, in sequence order.
|
|
98
|
+
* @param requestId - the request id the comment was submitted under.
|
|
99
|
+
* @returns the claiming turn, its answer text, and whether the turn is over.
|
|
100
|
+
*/
|
|
101
|
+
export declare function answerForRequest(events: readonly LogEvent[], requestId: string): AskRead;
|
|
102
|
+
/**
|
|
103
|
+
* Read every question one session has asked, keyed by its request id.
|
|
104
|
+
* @param events - the session's event log, in sequence order.
|
|
105
|
+
* @param requestIds - the questions' request ids.
|
|
106
|
+
* @returns the read per request id; a question the log holds nothing for reads as nothing.
|
|
107
|
+
*/
|
|
108
|
+
export declare function answersIn(events: readonly LogEvent[], requestIds: readonly string[]): Record<string, AskRead>;
|
|
109
|
+
/**
|
|
110
|
+
* Submits comments as turns of their session, and reads their answers back.
|
|
111
|
+
*
|
|
112
|
+
* One instance serves every session: the store is the state, and the per-agent
|
|
113
|
+
* subscription is registered on first use and lives with the agent's own context.
|
|
114
|
+
*/
|
|
115
|
+
export declare class CommentAsker {
|
|
116
|
+
private readonly ctx;
|
|
117
|
+
private readonly comments;
|
|
118
|
+
/**
|
|
119
|
+
* The agent each session's inbox events are subscribed on, keyed by session id, with
|
|
120
|
+
* the disposers that release those subscriptions.
|
|
121
|
+
*
|
|
122
|
+
* Keyed by AGENT IDENTITY, not merely "this session id was watched once": a
|
|
123
|
+
* resumed or re-created agent for the same session id is a different object
|
|
124
|
+
* whose listeners live on its OWN context, so a session-id set would consider
|
|
125
|
+
* the new agent already watched and never subscribe to it — leaving `turn`
|
|
126
|
+
* undefined forever, `agent/turn-stopping` never arriving, and the panel stuck
|
|
127
|
+
* on "replying" with no way out (the defect this map replaces). The disposers are
|
|
128
|
+
* what keeps a replacement from leaking a second set of handlers per event.
|
|
129
|
+
*/
|
|
130
|
+
private readonly watched;
|
|
131
|
+
/**
|
|
132
|
+
* @param ctx - the plugin's host context (its `sessionController` and `agents` are asked for lazily).
|
|
133
|
+
* @param comments - the comment store the ask is recorded in.
|
|
134
|
+
*/
|
|
135
|
+
constructor(ctx: Context, comments: CommentStore);
|
|
136
|
+
/**
|
|
137
|
+
* Ask one stored comment as its own turn.
|
|
138
|
+
*
|
|
139
|
+
* The request id is recorded BEFORE the submission leaves, so a claim event that
|
|
140
|
+
* arrives while the submission is still in flight already finds its comment. A
|
|
141
|
+
* submission that rejects is marked dropped rather than left looking pending.
|
|
142
|
+
*
|
|
143
|
+
* @param sessionId - the session the comment belongs to.
|
|
144
|
+
* @param commentId - the comment to ask.
|
|
145
|
+
* @param prompt - the prompt text to send verbatim.
|
|
146
|
+
* @param text - the reader's own words, which the prompt wraps: the thread keeps these.
|
|
147
|
+
* @param signal - aborts the submission.
|
|
148
|
+
* @returns what the request did.
|
|
149
|
+
*/
|
|
150
|
+
ask(sessionId: SessionId, commentId: string, prompt: string, text: string, signal: AbortSignal): Promise<DiffApprovalCommentAskValue>;
|
|
151
|
+
/**
|
|
152
|
+
* Every question of one session and what the transcript says about it, keyed by the
|
|
153
|
+
* question's request id (which is what `CommentAsk` records and what a client matches
|
|
154
|
+
* against). Derived from the session's own event log on every call and never stored:
|
|
155
|
+
* the log is the one source of truth for what the agent said, so a stored copy could
|
|
156
|
+
* only ever disagree with it.
|
|
157
|
+
* @param sessionId - the session whose comments to read.
|
|
158
|
+
* @returns the read per question; empty when the session holds no questions.
|
|
159
|
+
*/
|
|
160
|
+
answers(sessionId: SessionId): Record<string, AskRead>;
|
|
161
|
+
/**
|
|
162
|
+
* The turns the session's log shows as over, for the questions it holds.
|
|
163
|
+
*
|
|
164
|
+
* `agent/turn-stopping` already reports a turn's end live, and that is what usually
|
|
165
|
+
* writes `ended` down (see `markTurnEnded`). Reading it out of the log as well is what
|
|
166
|
+
* makes the flag survive a session this process did not watch end — a resumed session,
|
|
167
|
+
* a client that connected afterwards, a deployment whose agent events cannot be
|
|
168
|
+
* subscribed at all — because `turn/end` is durable and the event is not.
|
|
169
|
+
*
|
|
170
|
+
* @param sessionId - the session whose turns to check.
|
|
171
|
+
* @returns the turns the log closed, or empty when the log cannot be read.
|
|
172
|
+
*/
|
|
173
|
+
endedTurns(sessionId: SessionId): number[];
|
|
174
|
+
/**
|
|
175
|
+
* One session's event log, as the reads above take it.
|
|
176
|
+
*
|
|
177
|
+
* `snapshotEvents()` and not `deriveMessages()`: the derived history is a list of
|
|
178
|
+
* messages with no turn attribution, and the turn is the whole point of the answer read
|
|
179
|
+
* (see `answerForRequest`). The event log is the session's append-only source of truth
|
|
180
|
+
* and `snapshotEvents` hands back a frozen, sequence-ordered copy of it, so walking it
|
|
181
|
+
* is a plain read that cannot disturb the session.
|
|
182
|
+
*
|
|
183
|
+
* @param sessionId - the session to read.
|
|
184
|
+
* @returns its events, or undefined when no live session can be read.
|
|
185
|
+
*/
|
|
186
|
+
private sessionEvents;
|
|
187
|
+
/** The live agent for one session, or `undefined` when this host drives none. */
|
|
188
|
+
private agentOf;
|
|
189
|
+
/**
|
|
190
|
+
* Subscribe to one agent's inbox, once per LIVE agent, moving to a replacement agent
|
|
191
|
+
* when the session's agent changes under us.
|
|
192
|
+
*
|
|
193
|
+
* The listeners live on the agent's own context, so they go away with the agent
|
|
194
|
+
* and never outlive it — which is exactly why the dedup has to be per agent and
|
|
195
|
+
* not per session: a session whose agent was re-created (a resume, a restart, a
|
|
196
|
+
* rebuilt agent for the same id) has nothing listening on the new one, and every
|
|
197
|
+
* event this module depends on would silently stop arriving. The remembered
|
|
198
|
+
* agent is compared by identity, so a different object re-subscribes and a
|
|
199
|
+
* disposed one's stale entry cannot block it. What the replaced agent held is
|
|
200
|
+
* released first, so re-arming is not a leak: one set of handlers per live agent,
|
|
201
|
+
* not one more per identity change.
|
|
202
|
+
*
|
|
203
|
+
* Called on every ask AND on every poll read (`answers`), because the agent can be
|
|
204
|
+
* replaced while a question is still pending — the case where nothing asks again.
|
|
205
|
+
* @param sessionId - the session whose agent to watch.
|
|
206
|
+
*/
|
|
207
|
+
private watch;
|
|
208
|
+
}
|
|
209
|
+
export {};
|