dsh-diff-approval 0.30.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 +3 -0
- package/README.zh.md +3 -0
- package/assets/fonts/README.md +10 -8
- package/assets/fonts/manifest.json +1 -1
- package/lib/client.js +2414 -848
- package/lib/index.js +1672 -265
- package/lib/types/annotate-skill.d.ts +1 -1
- package/lib/types/annotate-tool.d.ts +11 -0
- package/lib/types/annotate.d.ts +28 -0
- package/lib/types/client/PendingPanel.d.ts +27 -21
- 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 +7 -0
- package/lib/types/client/dock.d.ts +8 -3
- package/lib/types/client/dsh-icons.d.ts +39 -1
- package/lib/types/client/header-entry.d.ts +8 -7
- package/lib/types/client/locales.d.ts +42 -12
- package/lib/types/client/panel-memory.d.ts +24 -6
- package/lib/types/client/path-match.d.ts +50 -0
- package/lib/types/client/port.d.ts +29 -2
- package/lib/types/client/produced-diff.d.ts +83 -6
- 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 +47 -7
- package/lib/types/client/settings.d.ts +40 -14
- package/lib/types/client/slots.d.ts +115 -3
- package/lib/types/client/store.d.ts +50 -0
- package/lib/types/comment-ask.d.ts +47 -20
- package/lib/types/comments.d.ts +141 -33
- package/lib/types/index.d.ts +1 -1
- package/lib/types/pending.d.ts +30 -4
- package/lib/types/types.d.ts +180 -4
- package/lib/types/vcs.d.ts +2 -1
- package/lib/types/version.d.ts +162 -0
- package/package.json +4 -2
package/lib/types/pending.d.ts
CHANGED
|
@@ -51,10 +51,11 @@ export declare class PendingDiffStore {
|
|
|
51
51
|
/** Fold a captured entry into the path's single global entry. */
|
|
52
52
|
private merge;
|
|
53
53
|
/**
|
|
54
|
-
* Combine a stored entry and a new capture. `oldText` comes from the earlier
|
|
55
|
-
* capture
|
|
56
|
-
*
|
|
57
|
-
* whole pending change regardless of
|
|
54
|
+
* Combine a stored entry and a new capture. `oldText` comes from the earlier capture (the file's
|
|
55
|
+
* original basis) — except where that capture was the file's creation and this one edits it, when
|
|
56
|
+
* the creation's own content becomes the basis — and `newText` from the later (latest content).
|
|
57
|
+
* The session set is the union, so the entry always spans the whole pending change regardless of
|
|
58
|
+
* which session contributed which part.
|
|
58
59
|
*/
|
|
59
60
|
private mergeEntry;
|
|
60
61
|
/**
|
|
@@ -82,6 +83,15 @@ export declare class PendingDiffStore {
|
|
|
82
83
|
/**
|
|
83
84
|
* Advance one entry's tracked content after a block-level keep/revert. The
|
|
84
85
|
* entry keeps its path; only the given side's text and capture time move.
|
|
86
|
+
*
|
|
87
|
+
* A keep that advances `oldText` also ends the entry's life as a creation. `earlierVersion` is the fact
|
|
88
|
+
* "this file did not exist before", and once part of the file has been accepted there IS an earlier
|
|
89
|
+
* version to restore — what the reader kept. The whole-file action must therefore become 回退, not 删除:
|
|
90
|
+
* deleting would throw away the part they just accepted. (Leaving the field alone kept a partly-kept new
|
|
91
|
+
* file offering to delete itself, which is what sent the reader here.)
|
|
92
|
+
*
|
|
93
|
+
* Every caller of this is a reader action — a block keep or revert, a refresh from version control — so it
|
|
94
|
+
* also takes the unseen dot down: the reader has just handled this file.
|
|
85
95
|
* @param path - the file path.
|
|
86
96
|
* @param patch - the side to advance (`oldText` for keep, `newText` for revert).
|
|
87
97
|
* @returns whether the entry changed.
|
|
@@ -90,6 +100,22 @@ export declare class PendingDiffStore {
|
|
|
90
100
|
oldText?: string;
|
|
91
101
|
newText?: string;
|
|
92
102
|
}): boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Mark paths the reader has now seen — the file was opened, or its row was acted on. The dot is about
|
|
105
|
+
* attention rather than content, so this and the entry leaving the list are the only ways it goes out, and
|
|
106
|
+
* nothing brings it back except a new capture.
|
|
107
|
+
* @param paths - one path, or several.
|
|
108
|
+
* @returns whether anything changed.
|
|
109
|
+
*/
|
|
110
|
+
markSeen(paths: string | readonly string[]): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Raise the dot for paths whose content moved WITHOUT the reader asking — a writer outside the panel, a
|
|
113
|
+
* second process. The capture sites set the field on the entry they fold; this is for the changes the
|
|
114
|
+
* plugin notices by itself while refreshing, which do not come through a capture.
|
|
115
|
+
* @param paths - one path, or several.
|
|
116
|
+
* @returns whether anything changed.
|
|
117
|
+
*/
|
|
118
|
+
markUnseen(paths: string | readonly string[]): boolean;
|
|
93
119
|
/**
|
|
94
120
|
* Restore one entry exactly as given (an undo/redo replays a snapshot). The
|
|
95
121
|
* entry is inserted or replaced by path.
|
package/lib/types/types.d.ts
CHANGED
|
@@ -5,6 +5,26 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { SessionId } from '@deepseek-ai/dsh-session/types';
|
|
7
7
|
import type { CommentLineRange } from './comment-lines.ts';
|
|
8
|
+
/**
|
|
9
|
+
* Where a session sits in the lineage a merged view groups by.
|
|
10
|
+
*
|
|
11
|
+
* Read off the session's own header when an entry is CAPTURED (`SessionHeader.parentSession`, `.origin`,
|
|
12
|
+
* `.delegationDepth`, `.cwd`) and carried on the entry, so a client can group without a second wire
|
|
13
|
+
* field. Every part is optional and absent when the header — or that one field of it — was not
|
|
14
|
+
* available: an ordinary root session has no parent by design, and a host older than these header
|
|
15
|
+
* fields reports none of them. An unknown lineage is recorded as unknown; guessing a root would file a
|
|
16
|
+
* session under the wrong one. Nothing reads this yet: the record is written for the merged view to use.
|
|
17
|
+
*/
|
|
18
|
+
export interface SessionLineage {
|
|
19
|
+
/** The session this one was forked from, or absent for an ordinary root. */
|
|
20
|
+
parentSessionId?: SessionId | undefined;
|
|
21
|
+
/** The header's coarse product classification — `'subagent'` for a subagent child. */
|
|
22
|
+
origin?: string | undefined;
|
|
23
|
+
/** How deep below its root the session was delegated, when the shell reports it. */
|
|
24
|
+
delegationDepth?: number | undefined;
|
|
25
|
+
/** The session's own working directory, when its header names one. */
|
|
26
|
+
cwd?: string | undefined;
|
|
27
|
+
}
|
|
8
28
|
/**
|
|
9
29
|
* One file's pending entry, global and unique per `path`: the complete set of
|
|
10
30
|
* unhandled changes folded into a single cumulative span across every session
|
|
@@ -17,8 +37,12 @@ export interface PendingEntry {
|
|
|
17
37
|
id: string;
|
|
18
38
|
/** Backend-resolved display path (the tool's output `path`). */
|
|
19
39
|
path: string;
|
|
20
|
-
/**
|
|
21
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Whether there is an earlier version of the file to restore: `'file'` when
|
|
42
|
+
* the whole-file action writes the earlier text back, `'none'` when the file
|
|
43
|
+
* did not exist, so the whole-file action deletes it.
|
|
44
|
+
*/
|
|
45
|
+
earlierVersion: PendingEntryKind;
|
|
22
46
|
/** File content before the first captured operation (empty for a creation). */
|
|
23
47
|
oldText: string;
|
|
24
48
|
/** File content after the latest captured operation. */
|
|
@@ -29,14 +53,70 @@ export interface PendingEntry {
|
|
|
29
53
|
sessionId: SessionId;
|
|
30
54
|
/** Every session that touched the file (drives the per-session list filter). */
|
|
31
55
|
sessionIds: SessionId[];
|
|
56
|
+
/**
|
|
57
|
+
* Where this entry's session sits in its lineage, as the shell reported it when the entry was
|
|
58
|
+
* CAPTURED — display-only data for a merged view. The entry keeps its real `sessionId`/`sessionIds`
|
|
59
|
+
* either way: grouping by lineage never rewrites who did the work. Absent on a record written before
|
|
60
|
+
* this field existed, on a session whose header carried none of it, and on an ordinary root (which
|
|
61
|
+
* has no parent by design) — so a reader must treat absent as "not known", never as "a root".
|
|
62
|
+
*/
|
|
63
|
+
lineage?: SessionLineage | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Whether a change the reader has not looked at yet arrived on this path — the dot beside its row. Set
|
|
66
|
+
* when an agent operation (or an agent-side admission) changes the entry, cleared when the reader opens
|
|
67
|
+
* the file or acts on the row, and deliberately NOT part of an undo snapshot: a snapshot restores
|
|
68
|
+
* CONTENT, and having seen something is not content.
|
|
69
|
+
*/
|
|
70
|
+
unseen?: boolean | undefined;
|
|
32
71
|
}
|
|
33
|
-
/**
|
|
34
|
-
export type PendingEntryKind = '
|
|
72
|
+
/** Whether an entry has an earlier version of its file to restore. */
|
|
73
|
+
export type PendingEntryKind = 'file' | 'none';
|
|
74
|
+
/**
|
|
75
|
+
* Which way a row's other contributor sits relative to the session LISTING it, walked from the recorded
|
|
76
|
+
* subagent parent links (see `lineageView` on the host).
|
|
77
|
+
*
|
|
78
|
+
* `child` — the other session descends from the lister (the lister is on the other's upward chain).
|
|
79
|
+
* `parent` — the other session is ABOVE the lister (the lister descends from it). It means an ANCESTOR,
|
|
80
|
+
* possibly more than one hop up, not only the direct parent: the Chinese 上级 says exactly that without
|
|
81
|
+
* ambiguity, so the English copy and this name keep that meaning.
|
|
82
|
+
* `sibling` — same lineage root, neither is on the other's chain (two children of one parent, say).
|
|
83
|
+
* `mixed` — non-requester owners of DIFFERENT directions: never one of them, and never a guess at one.
|
|
84
|
+
*/
|
|
85
|
+
export type LineageDirection = 'child' | 'parent' | 'sibling' | 'mixed';
|
|
35
86
|
/**
|
|
36
87
|
* One listed pending entry: the stored operation plus the live file state the
|
|
37
88
|
* list endpoint computes by reading the file's current content.
|
|
38
89
|
*/
|
|
39
90
|
export interface PendingFileDiff extends PendingEntry {
|
|
91
|
+
/**
|
|
92
|
+
* Whether this row reached the list through the LINEAGE MERGE rather than because the LISTING session
|
|
93
|
+
* itself touched it. The HOST computes it per read (`listWithState`), because only the host can walk a
|
|
94
|
+
* lineage — the client cannot, and must not try: it is what lets the panel show a child session's row
|
|
95
|
+
* without re-deriving the merge in the browser. Absent on a host older than the merge and absent on a
|
|
96
|
+
* row this session touched, so a client that does not know the field behaves exactly as it did before.
|
|
97
|
+
*/
|
|
98
|
+
viaLineage?: boolean | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* Whether some session OTHER than the listing one — and inside the listing session's lineage — also
|
|
101
|
+
* contributed to this row, so a row the session touched itself still says a child had a hand in it. Kept
|
|
102
|
+
* apart from `viaLineage` on purpose: that one answers "why is this row here" (visibility), this one
|
|
103
|
+
* answers "whose change does it carry" (the mark). Only a session in the same lineage counts: entries are
|
|
104
|
+
* keyed by path globally, so an unrelated session's touch of the same file must never be called a child's
|
|
105
|
+
* work. Host-computed per read (`listWithState`), absent on a host older than the mark.
|
|
106
|
+
*/
|
|
107
|
+
hasChildContribution?: boolean | undefined;
|
|
108
|
+
/**
|
|
109
|
+
* WHICH WAY the row's other contributor stands, so the marker's sentence tells the truth from the seat it
|
|
110
|
+
* is drawn in: the same row is a child's change in its parent's panel and a parent's change in its
|
|
111
|
+
* child's. Host-computed per read from the SAME recorded parent links the merge walks, and carried to the
|
|
112
|
+
* browser like `viaLineage` — the client never infers lineage.
|
|
113
|
+
*
|
|
114
|
+
* Absent whenever no mark applies (the lister is the only contributor), and absent on a host older than
|
|
115
|
+
* this field — in which case the client says the neutral sentence rather than picking a direction.
|
|
116
|
+
* `mixed` is likewise the honest answer when the row carries non-requester owners of different
|
|
117
|
+
* directions; it is never resolved to one of them.
|
|
118
|
+
*/
|
|
119
|
+
lineageDirection?: LineageDirection | undefined;
|
|
40
120
|
/**
|
|
41
121
|
* Whether the file no longer exists (or cannot be resolved). Reverting a
|
|
42
122
|
* missing entry restores its old content, which recreates the file.
|
|
@@ -110,6 +190,47 @@ export interface DiffApprovalListValue {
|
|
|
110
190
|
*/
|
|
111
191
|
commentAnswers: Record<string, string>;
|
|
112
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Value returned by the channel's `list-count` endpoint: how many rows a full `list` read for the same
|
|
195
|
+
* session would carry.
|
|
196
|
+
*
|
|
197
|
+
* The badge needs a number once a second, and a full read is enormously more than a number: it ships
|
|
198
|
+
* every visible entry's whole `oldText`+`newText` (measured at 6.77 MB across 302 stored entries, with
|
|
199
|
+
* single rows at 1.2-1.6 MB). So this is the LIGHT read — the same visibility rule (`lineageView().sees`
|
|
200
|
+
* for the requesting session) and the same `ensureLoaded` discipline as `list`, and nothing else.
|
|
201
|
+
*
|
|
202
|
+
* WHAT IT DELIBERATELY DOES NOT DO: it reads no file content (`ctx.fs.readText` is never called), so it
|
|
203
|
+
* computes no diff, adopts no external change, creates no undo checkpoint, sweeps no comments and does not
|
|
204
|
+
* persist anything. It sends no texts, no comment records, no `commentLines`, no revision and no errors —
|
|
205
|
+
* `count` is the whole answer.
|
|
206
|
+
*
|
|
207
|
+
* WHAT THAT COSTS THE CALLER: a count is not a list. It asks `stat` about each visible row (the cheap
|
|
208
|
+
* liveness signal) and drops a row whose file is GONE, which is the one state `list` also drops without
|
|
209
|
+
* reading — but a file that is present and merely unreadable cannot be told from a readable one without
|
|
210
|
+
* reading it, so it is counted here and dropped by the next full read. The number can therefore be one
|
|
211
|
+
* poll stale, and the next full read is what corrects it; nothing else in this value is authoritative.
|
|
212
|
+
*/
|
|
213
|
+
export interface DiffApprovalListCountValue {
|
|
214
|
+
/** How many rows this session's next full `list` read would carry. */
|
|
215
|
+
count: number;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Value returned by the channel's `update-check` endpoint: whether a newer release of
|
|
219
|
+
* this package is published, and what changed since the installed one.
|
|
220
|
+
*
|
|
221
|
+
* The comparison itself is the HOST's (`src/version.ts`): it is the side that knows the
|
|
222
|
+
* installed version, and the only side that can reach the registry (the browser cannot —
|
|
223
|
+
* `registry.npmjs.org` sends no CORS header). `newer` is therefore an ANSWER, not a hint:
|
|
224
|
+
* the client never re-derives it, so exactly one rule decides "is this newer".
|
|
225
|
+
*/
|
|
226
|
+
export interface DiffApprovalUpdateValue {
|
|
227
|
+
/** The installed version as this host read it; empty when it could not be read at all. */
|
|
228
|
+
current: string;
|
|
229
|
+
/** The published version; absent when the registry was unreachable or answered nonsense. */
|
|
230
|
+
latest?: string | undefined;
|
|
231
|
+
/** Whether `latest` is STRICTLY newer than `current`; always false when either is unparsable. */
|
|
232
|
+
newer: boolean;
|
|
233
|
+
}
|
|
113
234
|
/** What the open endpoint asks the OS to do with a file. */
|
|
114
235
|
export type DiffApprovalOpenAction = 'open' | 'reveal';
|
|
115
236
|
/** Value returned by the channel's open endpoint. */
|
|
@@ -287,6 +408,20 @@ export interface CommentAsk {
|
|
|
287
408
|
* has admitted anything.
|
|
288
409
|
*/
|
|
289
410
|
requestId: string;
|
|
411
|
+
/**
|
|
412
|
+
* The SESSION THIS QUESTION WAS SUBMITTED INTO — the transcript that will carry its answer.
|
|
413
|
+
*
|
|
414
|
+
* It is not always the comment's own `sessionId`: once a lineage reads every seat's threads (`list`
|
|
415
|
+
* fans the read out across the lineage root), any seat may ask a question on a thread another seat
|
|
416
|
+
* wrote, and the prompt goes into the seat the human is using. The answer fold then has to know which
|
|
417
|
+
* transcript to read for which question, and turn numbers are only unique WITHIN one session — so this
|
|
418
|
+
* is what `markTurnEnded` matches on as well.
|
|
419
|
+
*
|
|
420
|
+
* Absent on a question asked before this field existed, when the only session a question could have
|
|
421
|
+
* gone to was the comment's own: the fold reads the comment's `sessionId` for those (see
|
|
422
|
+
* `askTranscript` in the comment store), which is exactly where their answers already are.
|
|
423
|
+
*/
|
|
424
|
+
sessionId?: SessionId | undefined;
|
|
290
425
|
/**
|
|
291
426
|
* The question's own words, as the reader typed them — without the marker, the
|
|
292
427
|
* `(path:lines)` reference and the answer rules that wrap them in the submitted
|
|
@@ -352,12 +487,53 @@ export interface CommentRecord {
|
|
|
352
487
|
* something the reader said would put words in their mouth. Absent means the reader.
|
|
353
488
|
*/
|
|
354
489
|
author?: 'agent' | undefined;
|
|
490
|
+
/**
|
|
491
|
+
* Which CLASS of annotation this one belongs to, as the calling agent named it (see
|
|
492
|
+
* `annotate-tool.ts`'s `category` argument).
|
|
493
|
+
*
|
|
494
|
+
* The panel draws it as a coloured round dot on the comment list's row, so an agent that annotates
|
|
495
|
+
* several passes or topics can be told apart at a glance. OPAQUE to every reader of this record: the
|
|
496
|
+
* panel hashes the string to pick a colour and shows the string itself in the mark's own label, and
|
|
497
|
+
* nothing parses it, orders it or validates its shape. Absent means "no class, no dot" — which is
|
|
498
|
+
* every comment the reader writes themselves, and every annotation written before this field existed.
|
|
499
|
+
* Short by construction (the tool trims it and caps it at `MAX_ANNOTATION_CATEGORY_CHARS`).
|
|
500
|
+
*/
|
|
501
|
+
category?: string | undefined;
|
|
355
502
|
/** Epoch milliseconds the comment was written. */
|
|
356
503
|
createdAt: number;
|
|
357
504
|
/** Epoch milliseconds of the last change to this record. */
|
|
358
505
|
updatedAt: number;
|
|
359
506
|
/** The questions asked in this thread, oldest first; empty until one is asked. */
|
|
360
507
|
asks?: CommentAsk[] | undefined;
|
|
508
|
+
/**
|
|
509
|
+
* Whether something the reader has not looked at yet arrived in this thread — the dot beside its
|
|
510
|
+
* card. Set when an agent-authored annotation is written or when an answer lands in the thread,
|
|
511
|
+
* cleared when the card comes into view, and deliberately NOT part of an undo snapshot: a snapshot
|
|
512
|
+
* restores CONTENT, and having seen something is not content.
|
|
513
|
+
*/
|
|
514
|
+
unseen?: boolean | undefined;
|
|
515
|
+
/**
|
|
516
|
+
* The answer text the reader has already been told about, keyed by question id.
|
|
517
|
+
*
|
|
518
|
+
* The answer's text is deliberately absent from this record (see the doc above) — the transcript is
|
|
519
|
+
* the one source
|
|
520
|
+
* of truth for what the agent said — but "is this the answer I have already seen?" cannot be
|
|
521
|
+
* answered from the transcript alone, because a read has nothing to compare against. This is the
|
|
522
|
+
* baseline the comparison takes: the host writes the current answer here when the card goes out of
|
|
523
|
+
* the reader's way, and an answer whose text differs from this is news. Persisted rather than held
|
|
524
|
+
* in memory, so a restart does not light a dot the reader had already cleared.
|
|
525
|
+
*/
|
|
526
|
+
answerSeen?: Record<string, string> | undefined;
|
|
527
|
+
/**
|
|
528
|
+
* The latest answer text the host has observed for each question, keyed by question id.
|
|
529
|
+
*
|
|
530
|
+
* The other half of the comparison above, and kept apart from it on purpose: this follows what the
|
|
531
|
+
* agent said, while `answerSeen` follows what the reader has looked at. An answer whose text differs
|
|
532
|
+
* from its stored `answerSeen` entry is what lights the dot — and an answer that disappears leaves
|
|
533
|
+
* this empty rather than looking like a change, so a transcript that cannot be read never re-lights
|
|
534
|
+
* a thread the reader already read.
|
|
535
|
+
*/
|
|
536
|
+
answerNow?: Record<string, string> | undefined;
|
|
361
537
|
}
|
|
362
538
|
/** Value returned by the channel's comment-add endpoint. */
|
|
363
539
|
export interface DiffApprovalCommentAddValue {
|
package/lib/types/vcs.d.ts
CHANGED
|
@@ -34,7 +34,8 @@ export interface VcsRoot {
|
|
|
34
34
|
export interface VcsChange {
|
|
35
35
|
/** Absolute path of the changed file (inside the workspace). */
|
|
36
36
|
path: string;
|
|
37
|
-
|
|
37
|
+
/** Whether an earlier version of the file exists: `'file'` restores it, `'none'` deletes the file. */
|
|
38
|
+
earlierVersion: 'file' | 'none';
|
|
38
39
|
/** Baseline (pre-change) content; empty for a brand-new file. */
|
|
39
40
|
oldText: string;
|
|
40
41
|
/** Working content; empty for a deleted file. */
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Is a newer release published?" — the host's half of the panel's update notice.
|
|
3
|
+
*
|
|
4
|
+
* The check lives on the host because the browser cannot make it: `registry.npmjs.org`
|
|
5
|
+
* sends no CORS header, and only the host knows which version of this package is
|
|
6
|
+
* actually installed (it reads its own `package.json`). The client asks once through
|
|
7
|
+
* this plugin's existing channel and renders whatever comes back.
|
|
8
|
+
*
|
|
9
|
+
* Everything here FAILS SOFT: a timeout, a refused connection, a registry answer that
|
|
10
|
+
* is not JSON, a package with no `version` — all of them end as `undefined`, never as a
|
|
11
|
+
* throw. A reader on a plane must not see a network error in their review panel, and the
|
|
12
|
+
* panel must never wait on this to draw.
|
|
13
|
+
*
|
|
14
|
+
* The dialog shows NOTHING but the versions and a button, so this module asks for nothing
|
|
15
|
+
* but the version: there is no changelog to slice and no URL to hand over.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-diff-approval/version
|
|
18
|
+
*/
|
|
19
|
+
import type { DiffApprovalUpdateValue } from './types.ts';
|
|
20
|
+
/** The published name this check asks the registry about. */
|
|
21
|
+
export declare const UPDATE_PACKAGE = "dsh-diff-approval";
|
|
22
|
+
/** The registry's "latest" document for this package: `{ version, … }`. */
|
|
23
|
+
export declare const UPDATE_REGISTRY = "https://registry.npmjs.org/dsh-diff-approval/latest";
|
|
24
|
+
/**
|
|
25
|
+
* How long the request may take. Short on purpose: this is a nicety in a status bar,
|
|
26
|
+
* and the client-side request is issued in the background — but the host's own worker
|
|
27
|
+
* should not be held by a hanging socket either.
|
|
28
|
+
*/
|
|
29
|
+
export declare const UPDATE_TIMEOUT_MS = 3000;
|
|
30
|
+
/**
|
|
31
|
+
* How long a registry answer may be reused before asking again: TEN MINUTES.
|
|
32
|
+
*
|
|
33
|
+
* The cache exists for the network cost of the REMOTE answer only (see {@link createUpdateCheck}), so this is
|
|
34
|
+
* the one staleness a reader can be shown. Ten minutes because a release cadence is measured in days: the
|
|
35
|
+
* figure is far shorter than any release gap, so a genuinely new release reaches an already-running host
|
|
36
|
+
* without a restart, and far longer than a page load, so reloading a page — or opening a second seat — does
|
|
37
|
+
* not hammer a public registry. It is deliberately NOT permanent: the process-lifetime memo this replaced
|
|
38
|
+
* kept serving a verdict computed before an upgrade, which is exactly how a reader saw "a new version" for a
|
|
39
|
+
* version they had already installed.
|
|
40
|
+
*/
|
|
41
|
+
export declare const UPDATE_CACHE_TTL_MS: number;
|
|
42
|
+
/** A parsed `major.minor.patch`. */
|
|
43
|
+
export type Version = readonly [number, number, number];
|
|
44
|
+
/** One HTTP answer, as much of it as this module reads. */
|
|
45
|
+
export interface FetchResponseFace {
|
|
46
|
+
readonly ok: boolean;
|
|
47
|
+
readonly status: number;
|
|
48
|
+
text(): Promise<string>;
|
|
49
|
+
}
|
|
50
|
+
/** The one call this module makes on the network, injected so tests need no socket. */
|
|
51
|
+
export type FetchFace = (url: string, init: {
|
|
52
|
+
signal: AbortSignal;
|
|
53
|
+
headers: Record<string, string>;
|
|
54
|
+
}) => Promise<FetchResponseFace>;
|
|
55
|
+
/** Where to ask, and how. Every field has a production default. */
|
|
56
|
+
export interface UpdateProbe {
|
|
57
|
+
/** Registry `latest` document. */
|
|
58
|
+
registryUrl?: string;
|
|
59
|
+
/** Per-request budget, in ms. */
|
|
60
|
+
timeoutMs?: number;
|
|
61
|
+
/** The transport; defaults to the runtime's global `fetch`. */
|
|
62
|
+
fetchImpl?: FetchFace;
|
|
63
|
+
}
|
|
64
|
+
/** How {@link createUpdateCheck} reads and asks; every field has a production default. */
|
|
65
|
+
export interface UpdateCheckOptions {
|
|
66
|
+
/** The manifest the INSTALLED version is read from; the package's own `package.json` by default. */
|
|
67
|
+
manifestUrl?: URL;
|
|
68
|
+
/** Registry `latest` document. */
|
|
69
|
+
registryUrl?: string;
|
|
70
|
+
/** Per-request budget, in ms. */
|
|
71
|
+
timeoutMs?: number;
|
|
72
|
+
/** The transport; defaults to the runtime's global `fetch`. */
|
|
73
|
+
fetchImpl?: FetchFace;
|
|
74
|
+
/** How long a fetched registry answer may be reused, in ms; {@link UPDATE_CACHE_TTL_MS} by default. */
|
|
75
|
+
ttlMs?: number;
|
|
76
|
+
/**
|
|
77
|
+
* The clock, in ms since the epoch; `Date.now` by default. Injected so a test can age the cached REMOTE
|
|
78
|
+
* answer without waiting ten real minutes — the TTL is the one thing here a test cannot otherwise reach.
|
|
79
|
+
*/
|
|
80
|
+
now?: () => number;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Parse `major.minor.patch`, and NOTHING else.
|
|
84
|
+
*
|
|
85
|
+
* Deliberately strict, because the alternative is a bogus "newer": a pre-release
|
|
86
|
+
* (`0.31.0-rc.1`), a range (`^0.31.0`), a dist-tag (`latest`) or a stray suffix all
|
|
87
|
+
* answer `undefined`, and an unparsable side of a comparison is never allowed to look
|
|
88
|
+
* newer. A leading `v` is tolerated because tags carry one.
|
|
89
|
+
*
|
|
90
|
+
* @param text - the candidate, from a registry document or a `package.json`.
|
|
91
|
+
* @returns the three numbers, or `undefined` when the text is not exactly that shape.
|
|
92
|
+
*/
|
|
93
|
+
export declare function parseVersion(text: unknown): Version | undefined;
|
|
94
|
+
/**
|
|
95
|
+
* Is `latest` strictly newer than `current`?
|
|
96
|
+
*
|
|
97
|
+
* Strictly: equal versions are NOT newer, and neither side parsing means NOT newer —
|
|
98
|
+
* the rule the code above exists to make safe. Comparison is component-wise on the
|
|
99
|
+
* three numbers, so `0.9.0` precedes `0.10.0` (string comparison would get that wrong).
|
|
100
|
+
*
|
|
101
|
+
* @param latest - the published version.
|
|
102
|
+
* @param current - the installed version.
|
|
103
|
+
* @returns whether a notice is warranted.
|
|
104
|
+
*/
|
|
105
|
+
export declare function isNewerVersion(latest: unknown, current: unknown): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Compare two already-parsed versions.
|
|
108
|
+
*
|
|
109
|
+
* Component-wise, because a string comparison gets `0.9.0` vs `0.10.0` wrong — the reason both
|
|
110
|
+
* callers go through here rather than comparing something textual.
|
|
111
|
+
*
|
|
112
|
+
* @param left - one version.
|
|
113
|
+
* @param right - the other.
|
|
114
|
+
* @returns negative, zero or positive, as a comparator should.
|
|
115
|
+
*/
|
|
116
|
+
export declare function compareVersions(left: Version, right: Version): number;
|
|
117
|
+
/**
|
|
118
|
+
* The version installed in this package, read from the `package.json` beside the built
|
|
119
|
+
* `lib/` — the only authority on what the reader is running, and the value the registry
|
|
120
|
+
* answer is compared against.
|
|
121
|
+
*
|
|
122
|
+
* Resolved relative to this module, so it holds for the bundled `lib/index.js`
|
|
123
|
+
* (`../package.json` is the package root) and for the source under test (`../package.json`
|
|
124
|
+
* is the repository root) alike.
|
|
125
|
+
*
|
|
126
|
+
* @param manifestUrl - the manifest to read; the package's own `package.json` unless a caller
|
|
127
|
+
* says otherwise. It is a parameter because a test runner can serve modules over a non-file
|
|
128
|
+
* URL, where the default cannot be resolved — the production path is the default, and the
|
|
129
|
+
* end-to-end case is what proves the default itself.
|
|
130
|
+
* @returns the version, or `undefined` when the file is missing or has none.
|
|
131
|
+
*/
|
|
132
|
+
export declare function installedVersion(manifestUrl?: URL): Promise<string | undefined>;
|
|
133
|
+
/**
|
|
134
|
+
* Ask the registry for the published version.
|
|
135
|
+
*
|
|
136
|
+
* @param probe - where to ask and how; every field defaults (see {@link UpdateProbe}).
|
|
137
|
+
* @returns the version, or `undefined` on any failure — including an answer that is not
|
|
138
|
+
* a version, which must never become a notice.
|
|
139
|
+
*/
|
|
140
|
+
export declare function fetchLatestVersion(probe?: UpdateProbe): Promise<string | undefined>;
|
|
141
|
+
/**
|
|
142
|
+
* Build the answer behind the `update-check` endpoint: one function a host calls per request.
|
|
143
|
+
*
|
|
144
|
+
* WHAT IS CACHED, AND FOR HOW LONG — the whole point of this factory:
|
|
145
|
+
*
|
|
146
|
+
* - The REMOTE answer (the registry's latest version) is cached, because that is the network cost. It is held
|
|
147
|
+
* for {@link UPDATE_CACHE_TTL_MS} and then fetched again, so a release published while the host is running
|
|
148
|
+
* still reaches the reader without a restart. A FAILED fetch is not cached at all: remembering a failure
|
|
149
|
+
* would hide the notice for the whole TTL after one offline moment, and there is no network cost to save.
|
|
150
|
+
* - The INSTALLED version is read from the manifest on EVERY call. It is a file read, not a request, and it is
|
|
151
|
+
* the side that changes when the reader upgrades — so a verdict computed before that upgrade must never be
|
|
152
|
+
* replayed. Caching it was a real defect: a host that answered while an older version was installed kept
|
|
153
|
+
* announcing "a newer release" to every page until it was restarted.
|
|
154
|
+
*
|
|
155
|
+
* `newer` is therefore decided afresh on every call, from today's manifest against the cached registry answer.
|
|
156
|
+
* NEVER THROWS: every failure — no manifest, an unreachable registry, a nonsense answer — ends as "nothing to
|
|
157
|
+
* say" (`newer: false`), so a network problem can never become an error in the reader's status bar.
|
|
158
|
+
*
|
|
159
|
+
* @param options - where to read and how; every field defaults (see {@link UpdateCheckOptions}).
|
|
160
|
+
* @returns the check itself; call it as often as you like.
|
|
161
|
+
*/
|
|
162
|
+
export declare function createUpdateCheck(options?: UpdateCheckOptions): () => Promise<DiffApprovalUpdateValue>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-diff-approval",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
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",
|
|
@@ -60,10 +60,12 @@
|
|
|
60
60
|
"test": "vitest run",
|
|
61
61
|
"e2e": "playwright test",
|
|
62
62
|
"compat": "node scripts/dsh-compat.mjs",
|
|
63
|
+
"compat:newer": "node scripts/dsh-compat.mjs --newer",
|
|
64
|
+
"verify:release": "node scripts/verify-release.mjs",
|
|
63
65
|
"fonts:check": "python -m scripts.fonts.build --check",
|
|
64
66
|
"fonts:plan": "python -m scripts.fonts.build --plan",
|
|
65
67
|
"prepare": "tsc -b tsconfig.json && tsdown --env.DSH_BUILD_FACE client",
|
|
66
|
-
"release": "release-it"
|
|
68
|
+
"release": "release-it && node scripts/verify-release.mjs"
|
|
67
69
|
},
|
|
68
70
|
"license": "MIT",
|
|
69
71
|
"dependencies": {
|