dsh-diff-approval 0.29.0 → 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.
@@ -4,6 +4,7 @@
4
4
  * @module dsh-diff-approval/types
5
5
  */
6
6
  import type { SessionId } from '@deepseek-ai/dsh-session/types';
7
+ import type { CommentLineRange } from './comment-lines.ts';
7
8
  /**
8
9
  * One file's pending entry, global and unique per `path`: the complete set of
9
10
  * unhandled changes folded into a single cumulative span across every session
@@ -72,6 +73,42 @@ export interface DiffApprovalListValue {
72
73
  * told about it simply finds the list gone later, with nothing to explain it (issue #6).
73
74
  */
74
75
  persistError?: string | undefined;
76
+ /**
77
+ * The last failure to write a COMMENT file, or absent while those writes are working.
78
+ * Separate from `persistError` because the two live in different files and one may be
79
+ * writable while the other is not; a comment that only exists in memory is erased by a
80
+ * restart exactly like an unpersisted list, and the panel says so once.
81
+ */
82
+ commentPersistError?: string | undefined;
83
+ /**
84
+ * The requested session's comment threads, oldest first. They ride the list read
85
+ * so a poll sees the entries and the comments that hang off them in one
86
+ * consistent snapshot — a comment list that arrived on its own channel could
87
+ * name an entry the same read had already dropped.
88
+ */
89
+ comments: CommentRecord[];
90
+ /**
91
+ * The new-file lines each comment sits on NOW, keyed by comment id — where its quote was found in
92
+ * the entry's current content (see `resolveCommentLines`, which both halves import).
93
+ *
94
+ * DERIVED on every read and never persisted: a resolved line written to the comment file would
95
+ * outlive the content it was true of, and a stale stored figure is worse than none. A comment the
96
+ * host cannot place — its quote is gone from the content — is simply ABSENT here, and a caller
97
+ * then falls back to `record.anchor`: the line the comment was WRITTEN on, which is what an
98
+ * outdated thread shows anyway. Both panes draw this one map, so the list and the open card
99
+ * cannot name two lines for one comment (issue: one comment read `[382]` in the list while its
100
+ * card read 378, because only the open file's pane had resolved it).
101
+ */
102
+ commentLines: Record<string, CommentLineRange>;
103
+ /** Bumped on every comment change, so a poller can tell "nothing new" from "re-read". */
104
+ commentsRevision: number;
105
+ /**
106
+ * The answer text for the questions the session has answered, keyed by the
107
+ * question's `requestId` (see `CommentAsk`). DERIVED on every read from the
108
+ * session's own transcript and never stored: the transcript is the one source of
109
+ * truth for what the agent said, so a stored copy could only ever disagree with it.
110
+ */
111
+ commentAnswers: Record<string, string>;
75
112
  }
76
113
  /** What the open endpoint asks the OS to do with a file. */
77
114
  export type DiffApprovalOpenAction = 'open' | 'reveal';
@@ -223,3 +260,142 @@ export interface DiffApprovalAddValue {
223
260
  /** Failure detail for `outcome: 'failed'`. */
224
261
  message?: string | undefined;
225
262
  }
263
+ /** The rows a comment was written about, as new-file line numbers. */
264
+ export interface CommentAnchor {
265
+ /** First new-file line of the annotation. */
266
+ startLine: number;
267
+ /** Last new-file line of the annotation. */
268
+ endLine: number;
269
+ }
270
+ /** The gutter pair of one quoted line, kept so an outdated thread can lay its quote out. */
271
+ export interface CommentQuoteLine {
272
+ /** Old-file number, absent on a row the old side does not have (an added line). */
273
+ old?: number | undefined;
274
+ /** New-file number — the second gutter — absent on a removed line. */
275
+ new?: number | undefined;
276
+ /** Which side of the change the row was on when the thread quoted it. */
277
+ kind?: 'add' | 'del' | 'context' | undefined;
278
+ }
279
+ /** One question asked inside a comment thread, and what became of it. */
280
+ export interface CommentAsk {
281
+ /**
282
+ * The identity of the prompt request this question was submitted as. The session
283
+ * persists it on the exact user message it accepted (`source.rpcId`), so it is the
284
+ * join key between the question and its message in the transcript — and it is the
285
+ * idempotency key: a request carrying it again is the same submission, not a second
286
+ * one. Minted by this plugin, so a question knows its own message before the session
287
+ * has admitted anything.
288
+ */
289
+ requestId: string;
290
+ /**
291
+ * The question's own words, as the reader typed them — without the marker, the
292
+ * `(path:lines)` reference and the answer rules that wrap them in the submitted
293
+ * prompt. The prompt is what the agent is asked; this is what the thread shows, so a
294
+ * follow-up is drawn as the words that were written. Set for every question asked
295
+ * now; absent only on a record written before this field existed, whose words are
296
+ * not recoverable from anywhere.
297
+ */
298
+ text?: string | undefined;
299
+ /** The turn that claimed the message, when the session reported one. */
300
+ turn?: number | undefined;
301
+ /**
302
+ * The session discarded the message before any turn claimed it (a stopped turn, a
303
+ * cancelled queue). Nothing is coming, and the reader is told so rather than left
304
+ * waiting — this is what the client used to infer from its own queue snapshot.
305
+ */
306
+ dropped?: boolean | undefined;
307
+ /**
308
+ * The turn that claimed this question has stopped. A question whose turn is over and
309
+ * that the transcript shows no answer for was cut off: the reader is told that and
310
+ * offered another try, instead of a block that waits forever on a turn that ended.
311
+ * Only ever set when true.
312
+ */
313
+ ended?: boolean | undefined;
314
+ }
315
+ /**
316
+ * One comment thread's durable record: the annotation, where it was written, and the
317
+ * questions asked inside it.
318
+ *
319
+ * The ANSWER's text is deliberately absent: `asks[].requestId` names each question's
320
+ * message, and the session transcript stays the one source of truth for what the agent
321
+ * said — a client renders an answer by reading the transcript at that id, and the list
322
+ * read carries the derived text keyed by the same id. Row indices are absent too: they
323
+ * are model-relative and meaningless after a reload, so the record keeps new-file LINE
324
+ * numbers plus the quote the reader was looking at, which is what a re-anchor needs.
325
+ */
326
+ export interface CommentRecord {
327
+ /** Stable comment id, minted by the host when the annotation is written. */
328
+ id: string;
329
+ /** The session the comment belongs to: comments never cross sessions. */
330
+ sessionId: SessionId;
331
+ /** The pending entry the comment hangs off (its id, which is its path). */
332
+ entryId: string;
333
+ /** The entry's display path when the comment was written. */
334
+ path: string;
335
+ /** The lines the annotation was made on, as NEW-file line numbers: the numbers every label,
336
+ * jump and reference shows, and the ones the host resolves the quote against. */
337
+ anchor: CommentAnchor;
338
+ /** The anchored lines as they read then (the re-anchor fingerprint). */
339
+ quote: string;
340
+ /** `quote` with one row of context on each side, as it read then. */
341
+ quoteContext?: string | undefined;
342
+ /** The gutter numbers of `quote`'s lines, in the same order. */
343
+ quoteLines?: CommentQuoteLine[] | undefined;
344
+ /** The annotation itself: what the reader wrote. */
345
+ text: string;
346
+ /**
347
+ * Who wrote `text`, when it was not the reader.
348
+ *
349
+ * The panel draws a thread's first turn as the reader's own words, which is what it is for every
350
+ * comment the reader makes. An AGENT-authored annotation is the other direction (see
351
+ * `annotate-tool.ts`): the panel is showing the agent's explanation on those lines, and drawing it as
352
+ * something the reader said would put words in their mouth. Absent means the reader.
353
+ */
354
+ author?: 'agent' | undefined;
355
+ /** Epoch milliseconds the comment was written. */
356
+ createdAt: number;
357
+ /** Epoch milliseconds of the last change to this record. */
358
+ updatedAt: number;
359
+ /** The questions asked in this thread, oldest first; empty until one is asked. */
360
+ asks?: CommentAsk[] | undefined;
361
+ }
362
+ /** Value returned by the channel's comment-add endpoint. */
363
+ export interface DiffApprovalCommentAddValue {
364
+ /**
365
+ * What the request did. `missing` means the named entry is not in the session's
366
+ * list — a comment on a file that has left it is refused rather than stored,
367
+ * which is the first of the two guards against a comment outliving its entry.
368
+ */
369
+ outcome: 'added' | 'missing';
370
+ /** The stored comment; present only when `outcome` is `added`. */
371
+ comment?: CommentRecord | undefined;
372
+ }
373
+ /** Value returned by the channel's comment-remove endpoint. */
374
+ export interface DiffApprovalCommentRemoveValue {
375
+ /** What the request did; `missing` means no such comment in that session. */
376
+ outcome: 'removed' | 'missing';
377
+ }
378
+ /** Value returned by the channel's comment-remove-many endpoint. */
379
+ export interface DiffApprovalCommentRemoveManyValue {
380
+ /**
381
+ * How many of the named comments the request dropped.
382
+ *
383
+ * The ids that were not there are not named back: the reader asked for those comments to be
384
+ * gone, and one that another client already ended is gone. What is left to say is how many
385
+ * the request itself did, which is what a test and a log can hold on to.
386
+ */
387
+ removed: number;
388
+ }
389
+ /** Value returned by the channel's comment-ask endpoint. */
390
+ export interface DiffApprovalCommentAskValue {
391
+ /**
392
+ * What the request did. `missing` means no such comment in that session;
393
+ * `no-agent` means the session has no live agent to ask (it ended, or this host
394
+ * drives no agent for it), and `failed` carries `message`.
395
+ */
396
+ outcome: 'asked' | 'missing' | 'no-agent' | 'failed';
397
+ /** The request identity the comment now carries (present when `outcome` is `asked`). */
398
+ requestId?: string | undefined;
399
+ /** Failure detail for `outcome: 'failed'`. */
400
+ message?: string | undefined;
401
+ }
@@ -9,13 +9,17 @@
9
9
  * directory.
10
10
  *
11
11
  * Scope of one import (mirrors the panel's preferences):
12
- * - modified files (git: working tree vs index — "仅未暂存"; svn: vs BASE;
13
- * p4: opened edits), imported as `edit` (old = baseline, new = working);
12
+ * - modified files, imported as `edit` (old = baseline, new = working). The GIT baseline is the
13
+ * last commit, not the index: this list answers "what is not committed yet", so a change the
14
+ * reader already `git add`ed is as much a review item as one they have not — staging is a step
15
+ * towards a commit, not a decision this panel records (`git diff HEAD` semantics). svn compares
16
+ * against BASE and p4 against `#have`, which are their "last commit" already;
14
17
  * - deleted files, imported as `edit` with an empty new side (revert restores);
15
- * - new/untracked files (git `??`, svn `?`, p4 not-yet-opened files), imported
16
- * as `create` ONLY when the untracked preference is on. For p4 that means a
17
- * full workspace scan (`p4 status`, which can be slow); with the preference
18
- * off only already-opened files are read.
18
+ * - new/untracked files, imported as `create` ONLY when the untracked preference is on: git `??`
19
+ * (never added), svn unversioned, p4 not-yet-opened. For p4 that means a full workspace scan
20
+ * (`p4 status`, which can be slow); with the preference off only already-opened files are read.
21
+ * A file that is new but already under version control (git `A`, svn `added`) needs no
22
+ * preference: it is an uncommitted change like any other.
19
23
  * @module dsh-diff-approval/vcs
20
24
  */
21
25
  /** The version-control systems this integration knows. */
@@ -36,26 +40,47 @@ export interface VcsChange {
36
40
  /** Working content; empty for a deleted file. */
37
41
  newText: string;
38
42
  }
39
- /** The subset of `ctx.shell`'s executor this module calls (kept structural so
40
- * the module stays dependency-light and testable with a fake). */
43
+ /**
44
+ * The subset of `ctx.shell`'s executor this module calls — for BOTH eras of that seam.
45
+ *
46
+ * Kept structural so the module stays dependency-light and testable with a fake, and both eras are here
47
+ * because the seam changed under this plugin. Up to 0.1.6 the executor was `resolve(request)` +
48
+ * `run(spec)`, and 0.1.7 replaced `run` with `execute(spec)`, which resolves to a PROCESS HANDLE whose
49
+ * foreground projection is `result()` (measured in `@deepseek-ai/dsh-shell`: `abstract resolve`,
50
+ * `abstract execute`, `ShellExecution.result(): Promise<ShellRunResult>`). On 0.1.7 the old shape failed
51
+ * with `shell.run is not a function` — the whole text the import button could show — so both are
52
+ * accepted, and an executor offering neither is reported by the methods it does have.
53
+ */
41
54
  export interface ShellExecutorLike {
42
- resolve(request: {
43
- command: string;
44
- workdir?: string | undefined;
45
- timeoutMs?: number | undefined;
46
- signal?: AbortSignal | undefined;
47
- /** Foreground stdout capture budget in bytes; absent uses the executor's cap. */
48
- stdoutMaxBytes?: number | undefined;
49
- }): unknown;
50
- run(spec: unknown): Promise<{
51
- exitCode: number | null;
52
- stdout: {
53
- text: string;
54
- };
55
- stderr: {
56
- text: string;
57
- };
58
- }>;
55
+ /** 0.1.7: apply the implementation's defaults and caps before execution. */
56
+ resolve?(request: ShellExecRequestLike): unknown;
57
+ /** 0.1.7: prepare and spawn; the returned handle carries `result()`. */
58
+ execute?(spec: unknown): Promise<ShellExecutionLike>;
59
+ /** Up to 0.1.6: resolve and run in one call. */
60
+ run?(spec: unknown): Promise<ShellRunResultLike>;
61
+ }
62
+ /** One command, as the seam's request takes it (`workdir`/`timeoutMs` are required only on the spec). */
63
+ interface ShellExecRequestLike {
64
+ command: string;
65
+ workdir: string;
66
+ timeoutMs: number;
67
+ signal?: AbortSignal | undefined;
68
+ /** Foreground stdout capture budget in bytes; absent uses the executor's cap. */
69
+ stdoutMaxBytes?: number | undefined;
70
+ }
71
+ /** 0.1.7's execution handle, reduced to the part this module reads. */
72
+ interface ShellExecutionLike {
73
+ result?: (() => Promise<ShellRunResultLike>) | undefined;
74
+ }
75
+ /** The outcome this module reads, in the shape both eras report it. */
76
+ interface ShellRunResultLike {
77
+ exitCode: number | null;
78
+ stdout: {
79
+ text: string;
80
+ };
81
+ stderr: {
82
+ text: string;
83
+ };
59
84
  }
60
85
  /** Reads one file's working content; undefined when the file is absent. */
61
86
  export type VcsFileReader = (absolutePath: string) => Promise<string | undefined>;
@@ -95,3 +120,4 @@ export declare function detectVcsRoot(start: string): VcsRoot | undefined;
95
120
  * @returns the changes; an absent/unusable VCS surfaces as a thrown error.
96
121
  */
97
122
  export declare function listVcsChanges(input: VcsImportInput): Promise<VcsChange[]>;
123
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-diff-approval",
3
- "version": "0.29.0",
3
+ "version": "0.30.1",
4
4
  "description": "DeepSeek Harness plugin: pending-edit review with whole-file diff and Keep/Revert",
5
5
  "packageManager": "pnpm@11.21.0",
6
6
  "author": "Wu Zhiwei",
@@ -48,6 +48,7 @@
48
48
  "@deepseek-ai/dsh-client-runtime",
49
49
  "@deepseek-ai/dsh-client-connection",
50
50
  "@deepseek-ai/dsh-client-locale",
51
+ "@deepseek-ai/dsh-client-ui-settings",
51
52
  "@deepseek-ai/dsh-client-ui-sidebar"
52
53
  ],
53
54
  "platform": "web"
@@ -57,6 +58,7 @@
57
58
  "typecheck": "tsc -b tsconfig.json",
58
59
  "build": "tsc -b tsconfig.json && tsdown --env.DSH_BUILD_FACE client",
59
60
  "test": "vitest run",
61
+ "e2e": "playwright test",
60
62
  "compat": "node scripts/dsh-compat.mjs",
61
63
  "fonts:check": "python -m scripts.fonts.build --check",
62
64
  "fonts:plan": "python -m scripts.fonts.build --plan",
@@ -90,6 +92,7 @@
90
92
  "@deepseek-ai/dsh-session": "^0.1.0-rc.5",
91
93
  "@deepseek-ai/dsh-tools": "^0.1.0-rc.5",
92
94
  "@deepseek-ai/dsh-workspace": "^0.1.0-rc.5",
95
+ "@playwright/test": "^1.63.0",
93
96
  "@release-it/conventional-changelog": "^12.0.0",
94
97
  "@testing-library/react": "^16.1.0",
95
98
  "@types/node": "^22.0.0",
@@ -1,70 +0,0 @@
1
- /**
2
- * The panel's one connection to the session's chat: send one prompt, and watch
3
- * the transcript so a discussion can show its answer where it was asked.
4
- *
5
- * Everything here is deliberately defensive. The surfaces it uses (the scoped
6
- * `conversation` input face, the `sessions` binding, the `chat` view target) are
7
- * public types but not plugin-documented API, so a build that lacks one reports
8
- * "unavailable" and the caller falls back to the composer route it already had.
9
- *
10
- * @module dsh-diff-approval/client/chat-bridge
11
- */
12
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
13
- import type { SessionId } from '@deepseek-ai/dsh-client-connection/client';
14
- /** One human-visible transcript node, reduced to what a discussion shows. */
15
- export interface ChatNodeView {
16
- /** `user`, `assistant`, or the node's own kind for anything else. */
17
- kind: string;
18
- /** The node's text blocks joined; empty for nodes that carry no prose. */
19
- text: string;
20
- /**
21
- * The runtime froze this assistant node when the turn was stopped (`interrupted` on
22
- * the durable node). A stopped turn leaves one of these, often with no text, which is
23
- * how a caller can tell "the answer was cut off" from "the answer is still coming".
24
- * Only ever set when true.
25
- */
26
- interrupted?: boolean;
27
- }
28
- /** What a discussion needs to know about the session's chat right now. */
29
- export interface ChatView {
30
- /** A turn is running for this session. */
31
- running: boolean;
32
- /** The durable transcript, in order. */
33
- nodes: readonly ChatNodeView[];
34
- /** The in-flight assistant text, or '' when nothing is streaming. */
35
- partial: string;
36
- /** The session's recorded prompt error, when there is one. */
37
- error: string | undefined;
38
- /**
39
- * The prompts the session is still holding — its inbox queue plus the local
40
- * submission echoes that have not become durable yet — or `undefined` on a build
41
- * that does not publish them. A prompt that is not in the transcript is either one
42
- * of these (still waiting) or gone (dropped with a stopped turn); without the
43
- * evidence a caller can only keep waiting.
44
- */
45
- queued: readonly string[] | undefined;
46
- }
47
- /** The session chat as the panel uses it. */
48
- export interface ChatBridge {
49
- /**
50
- * Send one prompt into the session as a real turn.
51
- * @param sessionId - the session to prompt.
52
- * @param text - the prompt text, sent verbatim.
53
- * @returns whether the send verb was available (false means "not sent").
54
- */
55
- ask: (sessionId: SessionId, text: string) => boolean;
56
- /**
57
- * Watch the session's transcript and turn state.
58
- * @param sessionId - the session to watch.
59
- * @param listener - called with the current view, then on every change.
60
- * @returns the unsubscribe function.
61
- */
62
- watch: (sessionId: SessionId, listener: (view: ChatView) => void) => () => void;
63
- }
64
- /**
65
- * Build the bridge over the client context. Every verb takes the session it
66
- * addresses, so one bridge serves whichever session the panel is showing.
67
- * @param ctx - the plugin's client context.
68
- * @returns the bridge (its verbs report unavailability instead of throwing).
69
- */
70
- export declare function createChatBridge(ctx: ClientContext): ChatBridge;