dsh-diff-approval 0.29.1 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +7 -4
  2. package/README.zh.md +7 -4
  3. package/assets/fonts/README.md +10 -8
  4. package/assets/fonts/manifest.json +1 -1
  5. package/lib/client.js +5161 -1972
  6. package/lib/index.js +4331 -442
  7. package/lib/types/annotate-skill.d.ts +34 -0
  8. package/lib/types/annotate-tool.d.ts +227 -0
  9. package/lib/types/annotate.d.ts +186 -0
  10. package/lib/types/atomic-write.d.ts +77 -0
  11. package/lib/types/client/PendingPanel.d.ts +92 -24
  12. package/lib/types/client/category-color.d.ts +62 -0
  13. package/lib/types/client/chords.d.ts +70 -17
  14. package/lib/types/client/discussion.d.ts +76 -16
  15. package/lib/types/client/dock.d.ts +8 -8
  16. package/lib/types/client/dsh-icons.d.ts +98 -0
  17. package/lib/types/client/header-entry.d.ts +27 -11
  18. package/lib/types/client/ids.d.ts +18 -0
  19. package/lib/types/client/locales.d.ts +88 -36
  20. package/lib/types/client/panel-memory.d.ts +118 -35
  21. package/lib/types/client/path-match.d.ts +50 -0
  22. package/lib/types/client/port.d.ts +77 -1
  23. package/lib/types/client/presentation-menu.d.ts +1 -1
  24. package/lib/types/client/produced-diff.d.ts +136 -22
  25. package/lib/types/client/reference.d.ts +0 -2
  26. package/lib/types/client/remap-sync.d.ts +18 -1
  27. package/lib/types/client/session-seat.d.ts +93 -0
  28. package/lib/types/client/settings.d.ts +40 -14
  29. package/lib/types/client/slots.d.ts +184 -12
  30. package/lib/types/client/store.d.ts +85 -2
  31. package/lib/types/comment-ask.d.ts +236 -0
  32. package/lib/types/comment-lines.d.ts +131 -0
  33. package/lib/types/comments.d.ts +465 -0
  34. package/lib/types/index.d.ts +7 -2
  35. package/lib/types/pending.d.ts +53 -4
  36. package/lib/types/types.d.ts +356 -4
  37. package/lib/types/vcs.d.ts +53 -26
  38. package/lib/types/version.d.ts +162 -0
  39. package/package.json +7 -2
  40. package/lib/types/client/chat-bridge.d.ts +0 -70
@@ -5,13 +5,46 @@
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, DiffApprovalListCountValue, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, DiffApprovalPreviewImageValue, DiffApprovalRefreshValue, DiffApprovalUpdateValue, 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. */
14
35
  list(sessionId: SessionId): Promise<DiffApprovalListValue>;
36
+ /**
37
+ * Ask HOW MANY rows a full `list` read would carry for this session, reading no file content.
38
+ *
39
+ * This is the badge's own question: the full read ships every visible entry's whole old/new text
40
+ * (megabytes on a long-lived session), while the count costs one stat per visible row on the host and
41
+ * puts a single number on the wire. It is the SAME visibility rule as `list` on the host's side.
42
+ *
43
+ * A host that predates the endpoint throws instead — its handler answers `unknown endpoint
44
+ * "list-count"` — and the caller is expected to fall back to the full read (see
45
+ * `PendingDiffStore.refreshCount`), which is exactly the behaviour a page had before this verb existed.
46
+ */
47
+ listCount(sessionId: SessionId): Promise<DiffApprovalListCountValue>;
15
48
  /** Keep one operation. `keepListed` leaves the resolved entry in the list. */
16
49
  keep(sessionId: SessionId, id: string, keepListed?: boolean): Promise<DiffApprovalActionValue>;
17
50
  /** Revert one operation. `keepListed` leaves the resolved entry in the list. */
@@ -35,14 +68,57 @@ export interface DiffApprovalPort {
35
68
  * path must be one regular file: a directory is refused instead of being scanned.
36
69
  */
37
70
  addPath(sessionId: SessionId, path: string, includeUnchanged: boolean, exact?: boolean): Promise<DiffApprovalAddValue>;
71
+ /** Tell the host the reader is looking at a file: the row's unseen dot goes out. */
72
+ markSeen(sessionId: SessionId, id: string): Promise<void>;
38
73
  /** Open one file with its default application or reveal it in the folder. */
39
74
  open(sessionId: SessionId, id: string, action: DiffApprovalOpenAction): Promise<DiffApprovalOpenValue>;
40
75
  /** Keep every pending entry of one session in a single host call (one batch). */
41
76
  keepAll(sessionId: SessionId): Promise<DiffApprovalBulkValue>;
42
77
  /** Revert every pending entry of one session in a single host call (one batch). */
43
78
  revertAll(sessionId: SessionId): Promise<DiffApprovalBulkValue>;
79
+ /**
80
+ * Keep a PICK of entries in one host call: one request, one durable write and one undo step — the
81
+ * host records the whole pick as a single batch, so one Ctrl+Z takes that one decision back instead
82
+ * of peeling it apart file by file. `keepListed` says whether the resolved files stay listed.
83
+ *
84
+ * There is a `revertMany` beside it, and the pair is NOT symmetric: a keep writes nothing, while a
85
+ * revert writes the baseline over each file — and DELETES the ones the agent created, which the host
86
+ * cannot undo. The panel asks before sending a pick whose revert would delete anything.
87
+ */
88
+ keepMany(sessionId: SessionId, ids: readonly string[], keepListed: boolean | undefined): Promise<DiffApprovalBulkValue>;
89
+ /** Revert a PICK of entries in one host call: the same one request, one write, one undo step. The undo
90
+ * restores the entries the host could snapshot — see `keepMany` for the deletes it cannot. */
91
+ revertMany(sessionId: SessionId, ids: readonly string[], keepListed: boolean | undefined): Promise<DiffApprovalBulkValue>;
44
92
  /** Read one workspace image and inline it as a base64 data URI (for the Markdown preview). */
45
93
  previewImage(sessionId: SessionId, path: string): Promise<DiffApprovalPreviewImageValue>;
94
+ /**
95
+ * Write one annotation down. The host refuses a comment whose entry is not in the
96
+ * session's list, and the refusal is reported rather than retried.
97
+ */
98
+ commentAdd(sessionId: SessionId, comment: CommentDraft): Promise<DiffApprovalCommentAddValue>;
99
+ /** Drop one annotation (what "a comment dies with its entry" does by hand). */
100
+ commentRemove(sessionId: SessionId, id: string): Promise<DiffApprovalCommentRemoveValue>;
101
+ /**
102
+ * Drop several annotations in one host call: one request, one write of the session's comment file,
103
+ * and one read afterwards (`store.commentRemoveMany`). A batch that is a loop of `commentRemove`
104
+ * calls is a different thing — N requests, N writes — which is what this endpoint exists to avoid.
105
+ */
106
+ commentRemoveMany(sessionId: SessionId, ids: readonly string[]): Promise<DiffApprovalCommentRemoveManyValue>;
107
+ /** Asks one stored comment as its own turn of the session. */
108
+ commentAsk(sessionId: SessionId, id: string, prompt: string, text: string): Promise<DiffApprovalCommentAskValue>;
109
+ /** Tell the host the reader is looking at a comment: the card's unseen dot goes out. */
110
+ commentSeen(sessionId: SessionId, id: string): Promise<void>;
111
+ /**
112
+ * Ask whether a newer release of this plugin is published, and what changed since the
113
+ * installed one.
114
+ *
115
+ * No session is named: the answer is about the PACKAGE, not about a review. The host owns
116
+ * the whole question — it reads its own `package.json`, reaches the registry (a browser
117
+ * may not: no CORS header there) and decides `newer` — so a caller only has to render the
118
+ * answer. A host that predates the endpoint answers `unknown endpoint "update-check"`, and
119
+ * the caller is expected to ignore that failure exactly as it ignores a network one.
120
+ */
121
+ checkUpdate(): Promise<DiffApprovalUpdateValue>;
46
122
  }
47
123
  /** Build the port over one generic RPC caller.
48
124
  * @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 '@deepseek-ai/dsh-client-ui-primitives';
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,141 @@
1
1
  /**
2
- * Inject a "查看差异" button beside each DSH produced-file chip.
3
- *
4
- * The harness's `ProducedFiles` component renders each produced file as a
5
- * `<button>` chip inside `[data-produced-files-row]`; the full path rides the
6
- * chip's `title`. This plugin wants a quick "view this file's diff" affordance
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.
13
- *
14
- * The injected button is intentionally self-contained (inline styles + a fixed
15
- * label) so it needs no stylesheet and no harness change.
2
+ * Route a file press's own click.
3
+ *
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:
7
+ *
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 LINK whose file this panel does not hold, a modified press (Ctrl/Cmd/Shift/
17
+ * Alt, or a 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. A file-list ROW is the one press taken over whether or not the panel holds its file (see
20
+ * `ROW_SELECTOR`); "we do not hold it" then decides what the menu offers, not whether it appears.
21
+ *
22
+ * The name of the module (and of the event it raises) dates from when the produced-file card was the
23
+ * only press of this kind; `data-presented-files-row` is that same list under 0.1.7's name for it.
16
24
  */
17
- /** Window event dispatched by the injected button; PendingPanel listens for it. */
25
+ import type { PendingDiffSnapshot } from './slots.ts';
26
+ /** Window event dispatched when a pending file's press is taken over; the panel listens for it. */
27
+ export declare const CHIP_MENU_EVENT = "diff-approval:chip-menu";
28
+ /**
29
+ * Whether the page HOLDS this path — the `held` label a press carries into the menu.
30
+ *
31
+ * It is a LABEL now, not a gate: every file-list row's press becomes the menu regardless, and `held` only
32
+ * decides whether "在审批面板中查看" opens the row directly or adds it first (see `PendingPanel`). It is
33
+ * answered from the views the page is holding, i.e. from the last FULL read of each session: while every
34
+ * surface is shut nothing re-reads the list, so a row edited since then is labelled not-held. That is
35
+ * acceptable rather than hidden: the add answers the truth (a path already listed comes back `duplicate` and
36
+ * selects the same entry), so the item still opens the file — one request later. What the label must not do
37
+ * is claim a row exists that does not: `false` errs towards adding, never towards acting on nothing.
38
+ * @param views - the views this page is holding, one per session it has read (see `PendingDiffStore.views`).
39
+ * @param path - the path a press named.
40
+ * @returns whether any held view holds it.
41
+ */
42
+ export declare function panelHolds(views: readonly PendingDiffSnapshot[], path: string): boolean;
43
+ /** Window event dispatched by the panel to open a file in the panel; PendingPanel listens for it. */
18
44
  export declare const OPEN_FILE_EVENT = "diff-approval:open-file";
45
+ /** What a press tells the panel: which file, where the press was (the menu hangs under it), and whether
46
+ * the review list already holds that file. */
47
+ export interface ProducedChipMenuDetail {
48
+ /** The path the pressed element names. */
49
+ path: string;
50
+ /** The pressed element's own box, in viewport coordinates. */
51
+ x: number;
52
+ y: number;
53
+ /**
54
+ * Whether the review list holds this path right now.
55
+ *
56
+ * It decides what the menu's items DO, never whether the menu appears: a row press always becomes the
57
+ * menu, and "not held" is what makes "在审批面板中查看" add the path first (see `PendingPanel`). It is
58
+ * also what a future item that NEEDS a listed entry has to gate on — pressing keep or revert for a file
59
+ * the host is not holding would simply fail.
60
+ */
61
+ held: boolean;
62
+ /**
63
+ * The session the press belongs to, when its own surface says so.
64
+ *
65
+ * WITHOUT THIS the panel can only ask the session IT is showing to add the path, and the host resolves that
66
+ * path against that session's workspace: a press in session A's transcript while the panel is still bound
67
+ * to session B answers `outside` ("路径不在当前工作区内") — measured, and it was the reader's bug: the first
68
+ * press refused and the second worked, because by then the panel had followed the shell's selection.
69
+ *
70
+ * Read from the ANCESTORS of the pressed element only (see `sessionOfPress`), and `undefined` when none of
71
+ * them names a session. That is the honest half: the panel then asks its own session and, if the host
72
+ * refuses, says so with the wording that names what was checked rather than blaming the path.
73
+ */
74
+ sessionId?: string | undefined;
75
+ }
76
+ /** What the bridge needs from the host to decide, and to report. */
77
+ export interface ProducedChipBridge {
78
+ /**
79
+ * Whether the review list holds this path right now.
80
+ *
81
+ * A LABEL, not a gate: it rides every press this bridge takes over (a file-list row and a message's file
82
+ * link alike) and decides only what the menu's items DO — see `held` above. It is called synchronously on
83
+ * every press because that is when the menu is raised, and there is no second chance at a press.
84
+ */
85
+ isPending: (path: string) => boolean;
86
+ /** A pending file's press was taken over: open the menu the press asked for. */
87
+ onMenu: (detail: ProducedChipMenuDetail) => void;
88
+ /**
89
+ * Whether this plugin's file menu is MOUNTED at all — the reader's setting, read at press time.
90
+ *
91
+ * `false` leaves every press exactly as DSH has it: no menu, no `preventDefault`, not even a path lookup,
92
+ * so the plugin's presence is indistinguishable from it never having loaded. It is read per press rather
93
+ * than latched, because the setting can be switched while the page is open.
94
+ *
95
+ * ABSENT means enabled: a host, or a test, that offers no setting keeps the behaviour this bridge has
96
+ * always had.
97
+ */
98
+ enabled?: (() => boolean) | undefined;
99
+ }
100
+ /**
101
+ * Press a file's own press again — the shell's open, run the only way this plugin can run it
102
+ * faithfully, with the bridge standing down for that press (see the file's own doc).
103
+ *
104
+ * The menu's first row must open the file exactly the way the shell does, and what that is is the
105
+ * shell's business, not this plugin's: pressing the same element again keeps every part of it —
106
+ * the viewer it opens, the line a `#L24` suffix asked for, whatever a future shell adds there.
107
+ *
108
+ * @param path - the file the press names.
109
+ * @returns whether an element naming it was found to press.
110
+ */
111
+ export declare function replayFilePress(path: string): boolean;
19
112
  /**
20
- * Start injecting diff buttons into the produced-files row.
21
- * @param label - localized "查看差异" text, used as the icon button's accessible
22
- * name + hover title (the button renders only an icon).
23
- * @param openPath - called with a produced-file path when its injected button is
24
- * clicked; the diff-approval panel decides whether that file is still pending.
25
- * @returns a cleanup that disconnects the observer and removes injected buttons.
113
+ * The session a press belongs to, read from the pressed element's OWN surface.
114
+ *
115
+ * Ancestors only, and the NEAREST ancestor that names a session wins — whichever spelling it uses:
116
+ *
117
+ * `data-row-key="session:<id>"` a session-LIST item, measured by this repo's e2e harness. Right for a
118
+ * press raised inside the sidebar, and NOT an ancestor of a transcript
119
+ * chip (measured on the live shell: exactly one such element in the whole
120
+ * document, and it does not contain the chip).
121
+ * `data-conversation-session` the conversation BODY, measured on the live shell — this is the one that
122
+ * covers the reader's press, a chip inside a message.
123
+ * `data-session-id` unmeasured alternative spelling, kept as a last resort.
124
+ *
125
+ * Walking the chain rather than asking three separate `closest` queries is what makes "nearest wins" true
126
+ * across spellings: a chip inside a session row that ALSO nests a conversation body takes the nearer
127
+ * body's session. A document-wide search is still deliberately NOT done — an identity hit may be a SIBLING,
128
+ * an `href` or an `aria-controls`, and guessing which session a press belongs to would be worse than not
129
+ * knowing: a wrong guess would ask a THIRD session to add the file, which this can never do.
130
+ *
131
+ * @param press - the element the press landed on.
132
+ * @returns the session id, or undefined when no ancestor names one.
133
+ */
134
+ export declare function sessionOfPress(press: Element): string | undefined;
135
+ /**
136
+ * Start routing the presses that would open a file in the shell's own viewer.
137
+ *
138
+ * @param bridge - the host's decision (is the file pending) and where to report a press that is.
139
+ * @returns a cleanup that stops routing.
26
140
  */
27
- export declare function startProducedDiffInjection(label: string, openPath: (path: string) => void): () => void;
141
+ export declare function startProducedChipMenu(bridge: ProducedChipBridge): () => void;
@@ -5,8 +5,6 @@
5
5
  * the panel.
6
6
  * @module dsh-diff-approval/client/reference
7
7
  */
8
- /** The marker that replaces a reference's line number when its lines are gone. */
9
- export declare const LINE_MISSING_LABEL = "LINE_MISSING";
10
8
  /**
11
9
  * Last path segment of a file path, any separator style.
12
10
  * @param path - the path to shorten.
@@ -6,6 +6,7 @@
6
6
  * `reference.ts`.
7
7
  * @module dsh-diff-approval/client/remap-sync
8
8
  */
9
+ import type { SessionId } from '@deepseek-ai/dsh-client-connection/client';
9
10
  import type { PendingDiffStore } from './store.ts';
10
11
  /** One queued message's minimal content block; text blocks carry `text`, and every other kind passes through verbatim. */
11
12
  interface QueueBlock {
@@ -28,6 +29,22 @@ export interface ReferenceRemapOpts {
28
29
  readQueue: () => readonly QueueMessage[];
29
30
  /** Apply an edit mutation to one queued message's full content. */
30
31
  writeQueue: (itemId: string, content: readonly QueueBlock[]) => void;
32
+ /**
33
+ * The session whose list this sync follows and whose view supplies the workspace path, read per
34
+ * observation.
35
+ *
36
+ * The pending list is per session now, and the draft and the queue this sync rewrites belong to the
37
+ * session the page is SHOWING (see `shownSessionId`): observing another session's files would remap one
38
+ * session's references inside another session's composer. It must not be a pointer to whichever mount
39
+ * refreshed last either — with a dock tab on one session and the header on another, alternating polls
40
+ * moved that pointer about once a second and dropped these baselines with it, so a file that really
41
+ * changed was only ever re-seeded and never remapped. The baselines are still dropped when this answers
42
+ * a DIFFERENT session, so a session switch seeds fresh baselines instead of reading as "file changed".
43
+ *
44
+ * Absent, the page-wide view is observed instead — the same fallback a reader with no session gets
45
+ * (`viewFor(undefined)`, which answers the newest read).
46
+ */
47
+ sessionId?: (() => SessionId | undefined) | undefined;
31
48
  }
32
49
  /**
33
50
  * Subscribe to the pending store and remap references as each file's content
@@ -39,6 +56,6 @@ export interface ReferenceRemapOpts {
39
56
  */
40
57
  export declare function attachReferenceRemap(opts: ReferenceRemapOpts): {
41
58
  unsubscribe: () => void;
42
- remapFile: (path: string, oldText: string, newText: string) => void;
59
+ remapFile: (sessionId: SessionId, path: string, oldText: string, newText: string) => void;
43
60
  };
44
61
  export {};
@@ -0,0 +1,93 @@
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. Call it from an EFFECT, never during render.
22
+ *
23
+ * Writing this during the header's render was the cross-talk's other half: the value a render read
24
+ * depended on whether another component had already rendered, so two mounts could disagree about which
25
+ * session was showing — and, in concurrent rendering, a discarded render could publish a session that
26
+ * was never committed. From an effect the value only ever describes a committed render.
27
+ * @param id - the id, or `undefined` when the shell named none.
28
+ */
29
+ export declare function publishSessionId(id: SessionId | undefined): void;
30
+ /** The session the header entry last showed, or `undefined` when it never named one. */
31
+ export declare function publishedSessionId(): SessionId | undefined;
32
+ /**
33
+ * The published session as a React value: the root-scoped mounts (the sidebar footer's badge) have no
34
+ * session prop, so this is how they follow the one the Session header is showing.
35
+ *
36
+ * It subscribes rather than reading the module global at render time, because the write now happens in
37
+ * an effect: a reader that only read the global would keep whatever the value was on its first render
38
+ * and never notice the reader switching sessions.
39
+ * @returns the session the header entry is showing, or `undefined`.
40
+ */
41
+ export declare function usePublishedSessionId(): SessionId | undefined;
42
+ /**
43
+ * The selected session a shell's session-list state names, by every field the shells have used.
44
+ * @param state - the store state handed to the selector.
45
+ * @returns the id, or `undefined` when this shape names none.
46
+ */
47
+ export declare function selectedSessionOf(state: unknown): SessionId | undefined;
48
+ /**
49
+ * Whether one session row is BLANK — a freshly created session with nothing in it yet, which has
50
+ * nothing to review and leaves this plugin's entry inert exactly like no session at all.
51
+ *
52
+ * `byId[id].blank` is the row's own flag and has kept its name across the shells; a store that
53
+ * carries `blank` on the STATE (with the id beside it) is read too. An absent flag is not blank: a
54
+ * shape this plugin does not recognise must not disable the panel.
55
+ *
56
+ * @param state - the store state handed to the selector.
57
+ * @param id - the session the answer is about; `undefined` answers false.
58
+ * @returns true only when the shell says that session is blank.
59
+ */
60
+ export declare function sessionIsBlank(state: unknown, id: SessionId | undefined): boolean;
61
+ /**
62
+ * Whether a seat has NO reviewable session: it cannot name one at all, or the shell says the one it names
63
+ * is blank (a brand-new session with nothing said in it yet).
64
+ *
65
+ * The two halves are ONE question and must be asked of the SAME id — the id the seat would read its view
66
+ * for. Splitting them is what this helper exists to stop: a seat that drew its view for the published
67
+ * session while testing `sessionIsBlank` against a different (absent) id stayed enabled on a blank session
68
+ * and drew the previous session's list — from another workspace as readily as from its own, because nothing
69
+ * in this client compares workspaces.
70
+ *
71
+ * @param state - the session-list state handed to the selector.
72
+ * @param id - the session this seat would read its view for.
73
+ * @returns true when there is nothing to review.
74
+ */
75
+ export declare function unreviewableSession(state: unknown, id: SessionId | undefined): boolean;
76
+ /**
77
+ * The session the PAGE is showing, resolved the way a whole-page reader has to resolve it: the
78
+ * session-list store's own selection first, else what the Session header entry published.
79
+ *
80
+ * `readSessionsSelection` is supplied by the plugin body (it owns the `sessions` service), and may
81
+ * return `undefined` for a shell whose state names none — 0.1.7's store carries no selected session and
82
+ * hands the id to its own session-scoped seats instead, which is why the published id is read here too
83
+ * (`publishSessionId` runs from the header entry's effect). This is a plain read for non-React code: the
84
+ * component that publishes it reads the SUBSCRIBED value ({@link usePublishedSessionId}).
85
+ *
86
+ * It is deliberately NOT every session anyone has read: the page-wide readers here (the reference remap,
87
+ * which rewrites the visible composer's draft) mean the session the reader is looking at, and taking the
88
+ * newest session some other mount polled would address a composer that is not on screen.
89
+ *
90
+ * @param readSessionsSelection - the store's own selected session, when its shape names one.
91
+ * @returns the id, or `undefined` when nothing on this page names a session.
92
+ */
93
+ export declare function shownSessionId(readSessionsSelection: () => SessionId | undefined): SessionId | undefined;
@@ -204,32 +204,58 @@ export declare function setSplitMode(value: boolean): void;
204
204
  export declare function mdPreviewEnabled(): boolean;
205
205
  /** Persist the Markdown-preview default preference. */
206
206
  export declare function setMdPreviewEnabled(value: boolean): void;
207
+ /**
208
+ * Whether this plugin MOUNTS its own file menu at all — the press bridge that takes over a file press in
209
+ * the shell's lists, a file link in a message, and a relative link in the Markdown preview.
210
+ *
211
+ * Default ON, and the default is the interesting half: only an explicit `'0'` turns it off, so a reader who
212
+ * never touches the setting keeps the menu they have always had (and a storage that cannot be read — a
213
+ * privacy mode, a cleared profile — leaves the plugin working rather than silently disabled).
214
+ *
215
+ * Off means the bridge does not take the press over AT ALL: no menu, and the shell's own behaviour runs
216
+ * exactly as if this plugin were not installed. That includes this plugin's own preview, where a relative
217
+ * link therefore goes back to the browser's own navigation — which is the honest consequence of asking this
218
+ * plugin to keep its hands off, not a bug to special-case.
219
+ *
220
+ * @returns whether the file menu is enabled.
221
+ */
222
+ export declare function chipMenuEnabled(): boolean;
223
+ /** Persist the file-menu preference (see {@link chipMenuEnabled}). */
224
+ export declare function setChipMenuEnabled(value: boolean): void;
207
225
  /**
208
226
  * Whether commenting on a diff range is offered: the selection frame's comment button,
209
- * and the chord that stands for it. Defaults to off while the mode is a preview (see
210
- * {@link COMMENT_MODE_PREVIEW_KEY}); only an explicit `'1'` enables it. This gates
211
- * STARTING a thread, not the threads themselves — one already written keeps rendering,
212
- * so turning the mode off never hides work or interrupts an answer on its way back.
227
+ * and the chord that stands for it. **Defaults to ON** — comment mode is the mode now,
228
+ * not a preview — so only an explicit `'0'` turns it off (the `chipMenuEnabled` idiom a
229
+ * few lines up), and an absent field means on for every profile that predates the flip.
230
+ * This gates STARTING a thread, not the threads themselves — one already written keeps
231
+ * rendering, so turning the mode off never hides work or interrupts an answer on its way
232
+ * back.
213
233
  * @returns whether comment mode is on.
214
234
  */
215
235
  export declare function commentModeEnabled(): boolean;
216
- /** Window event: comment mode was toggled (see {@link setCommentModeEnabled}). */
217
- export declare const COMMENT_MODE_CHANGED_EVENT = "diff-approval:comment-mode";
218
- /** Persist the comment-mode preference. */
236
+ /** Window event: comment mode was toggled (see {@link setCommentModeEnabled}). A DIFFERENT string from
237
+ * {@link COMMENT_MODE_KEY} on purpose: a key literal and an event literal that read the same make every
238
+ * grep a guess (the hazard this file already names for the file-menu key at the top). */
239
+ export declare const COMMENT_MODE_CHANGED_EVENT = "diff-approval:comment-mode-changed";
240
+ /** Persist the comment-mode preference (`'1'` on, `'0'` off — see {@link commentModeEnabled}). */
219
241
  export declare function setCommentModeEnabled(value: boolean): void;
220
242
  /**
221
243
  * Whether the diff's code uses the font this plugin bundles.
222
244
  *
223
- * Defaults to **off**: the bundled face is JetBrains Maple Mono subset and
224
- * sliced into 138 woff2 files, and a page pulls the slices its characters land
225
- * in — tens of kilobytes for a comment, up to ~120 KB for a rare character. That
226
- * is real traffic, so it is the reader's decision, not a default they discover.
227
- * Only an explicit `'1'` enables it.
245
+ * **Defaults to ON**, like comment mode: absent means on, and only an explicit `'0'` turns
246
+ * it off. The old field's answer is deliberately NOT carried over (see the constant): a
247
+ * reader who had turned the bundled face off before this change comes back with it on, which
248
+ * is the one behaviour the reader asked for. The cost is real and worth naming where the
249
+ * switch is: the face is a JetBrains Maple Mono subset sliced into 138 woff2 files, and a
250
+ * page pulls only the slices its characters land in — tens of kilobytes for a comment, up to
251
+ * ~120 KB for a rare character — downloaded on demand rather than shipped.
228
252
  * @returns whether the bundled code font is enabled.
229
253
  */
230
254
  export declare function codeFontEnabled(): boolean;
231
- /** Window event: the bundled code font was turned on or off (see {@link setCodeFontEnabled}). */
232
- export declare const CODE_FONT_CHANGED_EVENT = "diff-approval:code-font";
255
+ /** Window event: the bundled code font was turned on or off (see {@link setCodeFontEnabled}). A DIFFERENT
256
+ * string from {@link CODE_FONT_KEY} on purpose — they used to be the same literal, which made a grep for
257
+ * either one a guess (the hazard this file names for the file-menu key at the top). */
258
+ export declare const CODE_FONT_CHANGED_EVENT = "diff-approval:code-font-changed";
233
259
  /** Persist the bundled-code-font preference. */
234
260
  export declare function setCodeFontEnabled(value: boolean): void;
235
261
  /**