@dev.fast/whiteboard 0.3.4-preview.20261019.127 → 0.3.4
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/dist/{account-alias-B5-HYi1D.js → account-alias-CprZojXd.js} +8 -13
- package/dist/agent-cli-C6SPSII0.js +2 -0
- package/dist/{agent-cli-CohHf6Ua.js → agent-cli-CThN7Q59.js} +10 -15
- package/dist/{agent-client-Dtc-Kn_w.js → agent-client-BOOi7EUR.js} +3 -13
- package/dist/{authoring-tools-B7KU04f7.js → authoring-tools-BNwuMnz6.js} +27 -34
- package/dist/{background-server-C03RKxKS.js → background-server-CkvxA-6O.js} +5 -5
- package/dist/{background-server-CB9C4uoc.js → background-server-DLSDogkI.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/{cli-CJaEb7zC.js → cli-R4yfa5DT.js} +1 -1
- package/dist/{cli-runner-DGDIqh3V.js → cli-runner-Cuc9FSzu.js} +26 -26
- package/dist/cli.js +3 -4
- package/dist/{client-lKVF_Elo.js → client-BN_FDxFr.js} +47 -253
- package/dist/{fuzzy-match-BmVJJm9I.js → fuzzy-match-BO9bK37N.js} +4 -6
- package/dist/{headless-host-DRtjf9WA.js → headless-host-CVBOQyRS.js} +1 -1
- package/dist/{headless-host-XhL1X4vX.js → headless-host-Cog6SpS9.js} +140 -513
- package/dist/{local-data-Cpl4ENbJ.js → local-data-D_9tvdsp.js} +323 -2419
- package/dist/{mcp-ovmiUE_x.js → mcp-D1hziANa.js} +9 -5
- package/dist/{package-paths-BWxO-UEL.js → package-paths-C--__jEv.js} +1 -1
- package/dist/profile-CYPmvBoA.js +2 -0
- package/dist/{profile-BadSkCkI.js → profile-DokFw782.js} +8 -20
- package/dist/{remote-attach-Dd-YobM6.js → remote-attach-CEJ-kvDY.js} +4 -4
- package/dist/{remote-extensions-CNXwqK0D.js → remote-extensions-Blcjr9Ze.js} +3 -3
- package/dist/{remote-extensions-VP-kfq0C.js → remote-extensions-GN8Gf_qG.js} +1 -1
- package/dist/{remote-language-server-BXk87KCr.js → remote-language-server-BTKBQ7x4.js} +5 -5
- package/dist/{remote-uninstall-DuOzsvaD.js → remote-uninstall-DW7Gdryh.js} +6 -6
- package/dist/{request-origin-B9qCPiVB.js → request-origin-Zcn_CjX4.js} +12 -45
- package/dist/{review-app-launcher-C7el2Uoy.js → review-app-launcher-CkwLclzR.js} +2 -2
- package/dist/{review-home-paths-D8AoCdd2.js → review-home-paths-j2BRAaNv.js} +17 -19
- package/dist/{review-telemetry-BsoRMC2w.js → review-telemetry-BLNwYnLV.js} +5 -5
- package/dist/runtime-CKiWhGrf.js +52 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/{s3-CsG2EWED.js → s3-DtJFWC0G.js} +1 -1
- package/dist/{s3-config-CjcLyFV5.js → s3-config-BTO12tCC.js} +2 -4
- package/dist/{s3-config-zz4GWh2T.js → s3-config-CSy3fx8H.js} +1 -1
- package/dist/server/desktop-host.js +76 -29
- package/dist/{server-discovery-CKjw3-6h.js → server-discovery-Bey8IqXH.js} +3 -3
- package/dist/sharing/index.d.ts +70 -484
- package/dist/sharing/index.js +1 -1
- package/dist/{src-WPaQn75m.js → src-BMPuxWv4.js} +73 -208
- package/dist/{src-BiWNbcF_.js → src-DsM3jO8G.js} +12 -89
- package/dist/{stored-document-migration-dFbewRTG.js → stored-document-migration-CChxtFAy.js} +269 -270
- package/dist/{thread-state-Do5LX10I.js → thread-state-5pYr6_zM.js} +2 -7
- package/dist/{tool-failure-DvI-gys2.js → tool-failure-C9MpbXJJ.js} +3 -10
- package/instructions/authoring.md +2 -3
- package/instructions/scratchpad.md +23 -0
- package/package.json +4 -4
- package/src/ask/agents.ts +1 -13
- package/src/ask/checkout-files.ts +5 -2
- package/src/ask/thread-state.ts +0 -4
- package/src/ask/thread.ts +0 -16
- package/src/ask/threads.ts +3 -24
- package/src/cli-install.ts +2 -13
- package/src/cli-runner.ts +1 -4
- package/src/cli.ts +1 -4
- package/src/diff-selection-migration.ts +34 -48
- package/src/lens-selection.ts +2 -38
- package/src/review-api/README.md +13 -52
- package/src/review-api/activity.ts +4 -1
- package/src/review-api/agent-cli.ts +1 -9
- package/src/review-api/anchor-quotes.ts +7 -14
- package/src/review-api/ask-history.ts +51 -137
- package/src/review-api/authoring-tools.ts +30 -45
- package/src/review-api/blocks/definition.ts +1 -1
- package/src/review-api/blocks/markdown.ts +2 -2
- package/src/review-api/document-headings.ts +27 -58
- package/src/review-api/document-text.ts +2 -31
- package/src/review-api/document.ts +34 -281
- package/src/review-api/github-provider.ts +13 -51
- package/src/review-api/http.ts +123 -702
- package/src/review-api/instructions.ts +19 -0
- package/src/review-api/local-data.ts +46 -310
- package/src/review-api/mcp.ts +11 -3
- package/src/review-api/onboarding.ts +1 -1
- package/src/review-api/profile.ts +6 -37
- package/src/review-api/public-tools.ts +2 -24
- package/src/review-api/pull-request.ts +49 -82
- package/src/review-api/request-origin.ts +0 -10
- package/src/review-api/review-progress.ts +27 -61
- package/src/review-api/store-schema.ts +29 -38
- package/src/review-api/store.ts +478 -2142
- package/src/review-api/worktree-events.ts +5 -3
- package/src/review-api/worktree-source.ts +7 -3
- package/src/review-api/worktree-structural.integration.ts +0 -72
- package/src/review-preferences.ts +88 -0
- package/src/review-stack.ts +23 -15
- package/src/runtime.ts +30 -4
- package/src/server/desktop-host.ts +1 -6
- package/src/server/desktop-server.ts +31 -1
- package/src/server/headless-host.ts +2 -0
- package/src/server/hono-http.ts +1 -2
- package/src/server/legacy-tutorial-cleanup.ts +3 -2
- package/src/server/review-gateway-list.ts +5 -3
- package/src/server/review-gateway-memory.ts +1 -1
- package/src/server/review-lifecycle-telemetry.ts +2 -2
- package/src/server/review-server-core.ts +5 -1
- package/src/server/structural-comparisons.ts +0 -47
- package/src/sharing/export.ts +7 -36
- package/src/sharing/host.ts +14 -34
- package/src/sharing/import.ts +24 -156
- package/src/sharing/repository.ts +20 -128
- package/src/startup-trace.ts +3 -3
- package/src/stored-document-migration.ts +19 -110
- package/src/ui-telemetry-events.ts +11 -33
- package/dist/agent-cli-_YyMIMRY.js +0 -2
- package/dist/lens-selection-CyJapcsl.js +0 -214
- package/dist/profile-DJvkBJNQ.js +0 -2
- package/dist/runtime-Bj2tD33R.js +0 -27
- package/src/heading-anchors.ts +0 -42
- package/src/review-api/board-order.ts +0 -144
- package/src/review-api/checkout-groups.ts +0 -560
- package/src/review-api/foreign-references.ts +0 -328
- package/src/review-api/link-preview.ts +0 -113
- package/src/review-api/scratchpad-plan-upgrade.ts +0 -250
- package/src/review-api/store-migrations.ts +0 -357
package/src/review-api/README.md
CHANGED
|
@@ -17,7 +17,6 @@ catalog. Tests inject the native store and source-data provider.
|
|
|
17
17
|
- `repositories`: server-only local paths; clients receive an ID and display name.
|
|
18
18
|
- `resources`: immutable image, trace and software-map bytes, scoped to a repository.
|
|
19
19
|
- `review_attention`: viewed/trashed timestamps, separate from document history.
|
|
20
|
-
- `board_relations`: where a board sits in its project, outside its versions: a child's `parent` (one per board) and a board's `depends_on` edges.
|
|
21
20
|
|
|
22
21
|
One host-owned store serializes writes, including asynchronous validation.
|
|
23
22
|
Presences show who is working across database connections and never block a
|
|
@@ -52,14 +51,11 @@ All paths below are relative to `/reviews-api`.
|
|
|
52
51
|
| `GET /:id?targetId=step-3` | Full block, sequence step, flow node or flow edge |
|
|
53
52
|
| `GET /:id?full=true` | Full snapshot |
|
|
54
53
|
| `GET /:id?version=2&full=true` | Historical snapshot |
|
|
55
|
-
| `GET /:id/history` | Saved versions with titles
|
|
54
|
+
| `GET /:id/history` | Saved versions with titles and timestamps |
|
|
56
55
|
| `GET /:id/inspect` | Agent reading view: nested text outline with IDs; `targetId` reads one component completely, `full=true` includes all content, `version` selects history. `format=json` returns raw data instead. |
|
|
57
56
|
| `POST /:id/open` | Open the review in the attached Desktop; report an error when none is attached |
|
|
58
57
|
| `POST /commands` | Apply one command; return review ID, version, and for an edit the target ID, its `type`, and — after an insert or replace — `children`: its first-level children as `{id,type}` (a container's blocks; a diagram's steps, or nodes then edges), so new components are addressable without a read. A `create` with `pullRequestUrl` returns the newest existing review for that PR instead (owner/repository matched case-insensitively) unless `operation.reuseExisting` is `false`: `created:false`, a `note`, its stored `target`, `headMoved`, and `working`/`otherReviewIds` when they apply; its target is never moved. A new review reports `created:true`. Either way the result carries `review`, the review's `GET /` catalog entry (target, origin, repository name and path). An interactive `create` also opens the new review in an attached Desktop unless `operation.open` is `false`, and reports `opened` with the open result or an `openError`; the review is saved either way |
|
|
59
58
|
| `POST /commands {operation:{type:"lens_edit",reviewId,edit}}` | Write one file lens, credited to `activityId` when given: `insert {title,targets,afterId?}` (host id `lens-N`), `update {targetId,title?,targets?}` or `remove {targetId}`. Returns `{targetId, type:"lens", uncategorized}`, where `uncategorized` lists changed files and ranges no lens covers yet (at most 50 files) |
|
|
60
|
-
| `POST /:id/person-edit {baseVersion,title,document}` | Save a person's whole board as their version; return `{reviewId,version,ids}`. See Person edits |
|
|
61
|
-
| `POST /:id/focus {clientId,blockId}` | Lease the board open on a canvas, and the block a person is in (`blockId`, or null), for 15 s; `open:false` lets go. Returns `{ok:true}` |
|
|
62
|
-
| `POST /:id/discard` | Delete the board only if it has no title (it reads "Untitled") and no blocks and no child boards, no canvas holds it open and no agent is working on it; waits up to 15 s for a closing canvas to let go. Returns `{deleted}` |
|
|
63
59
|
| `GET /:id/lenses` | The current version's file lenses with each one's file count, plus the same `uncategorized` report |
|
|
64
60
|
| `POST /repositories {path}` | Register a local Git/jj repository; return ID/name |
|
|
65
61
|
| `POST /resources` | Upload an image, trace, or map; return resource ID/kind/MIME type |
|
|
@@ -93,17 +89,10 @@ The canvas continues to use the JSON snapshot routes above.
|
|
|
93
89
|
}
|
|
94
90
|
```
|
|
95
91
|
|
|
96
|
-
Commands: `create {title
|
|
97
|
-
`restore {reviewId,version}`. A
|
|
98
|
-
|
|
99
|
-
PR URLs must be canonical `https://github.com/owner/repository/pull/123` URLs. The PR number is derived from the URL; identity is metadata alongside immutable pins, not a moving source reference. A `
|
|
100
|
-
|
|
101
|
-
`set_parent {reviewId,parentId}` nests a board one level under a top-level
|
|
102
|
-
board of its project; `null` makes it top-level. `set_dependencies
|
|
103
|
-
{reviewId,dependsOn}` replaces what it builds on, and refuses a cycle.
|
|
104
|
-
`create` takes both. Neither saves a version. Summaries list `parentId`,
|
|
105
|
-
`children` (in sibling order) and `dependsOn`. Deleting a parent moves its
|
|
106
|
-
children to top level.
|
|
92
|
+
Commands: `create {title,target,pullRequestUrl?,reuseExisting?}` or `create {pullRequestUrl,title?,repositoryId?,reuseExisting?}`, `set_target {reviewId,target,pullRequestUrl?}`, `edit {reviewId,edit}`, `rename {reviewId,title}`,
|
|
93
|
+
`restore {reviewId,version}`. A review's pins, `{repositoryId,base,head}`, are its target resolved to immutable commits.
|
|
94
|
+
Agents name the checkout by `repositoryPath` in a target, or on a create from a PR alone; `/commands` registers the path and passes the store its id. Retargeting preserves content and component IDs. Restore restores title, target, PR identity, and content. Live targets still read the current checkout.
|
|
95
|
+
PR URLs must be canonical `https://github.com/owner/repository/pull/123` URLs. The PR number is derived from the URL; identity is metadata alongside immutable pins, not a moving source reference. A `create` with a target keeps that target and treats the URL as identity only. A `create` with the URL and no target resolves the PR when accepted: it reads the PR with `gh pr view` (falling back to the public GitHub API), picks the registered checkout named by `repositoryId`, else the checkout of the PR's existing review, else the first registered checkout with a remote whose configured URL is `github.com/owner/repository`, and fetches `refs/pull/N/head` and the base branch from that remote into `refs/review/github/owner/repository/pull/N/*`, never moving branches or bookmarks (a jj repository indexes the commits through a transient tag). It pins that head and its merge base with the base branch; for a PR absorbed by a merge commit it uses the merge base with GitHub's frozen base commit instead. The title defaults to the PR title. A repeat resolves the PR again, so `headMoved` compares the existing review with the PR's current head. `set_target` preserves the document and component IDs, including when source commits change. Its response reports retained source ranges to verify, those in files the new commits changed, and resources that no longer match the pins; agents repair these with `edit`. Existing versions keep their original pins and content. It preserves omitted PR identity within one repository, clears it when switching repositories, and accepts an explicit URL or null.
|
|
107
96
|
|
|
108
97
|
`attention {reviewId,action:"view"}` records viewing without changing lifecycle or document history.
|
|
109
98
|
`lifecycle {reviewId,state:"active"|"trash"}` moves a whiteboard to Trash or restores it.
|
|
@@ -121,7 +110,8 @@ the same version and side through the API, without a client-side checkout path.
|
|
|
121
110
|
|
|
122
111
|
Edits: `insert {content,parentId?,afterId?}`, `update {targetId,changes}`,
|
|
123
112
|
`move {targetId,parentId?,afterId?}`, `remove {targetId}`,
|
|
124
|
-
`replace {targetId,content}`. Omitted placement appends to the root
|
|
113
|
+
`replace {targetId,content}`. Omitted placement appends to the root; on the
|
|
114
|
+
scratchpad it prepends instead, so the pad reads newest first. Diagram
|
|
125
115
|
units (a `step` in a sequence, a `flow_node` or `flow_edge` in a flow diagram)
|
|
126
116
|
require their diagram as the parent and can move within it, but not between
|
|
127
117
|
diagrams; removing a flow node removes the edges that touched it. A new
|
|
@@ -148,35 +138,10 @@ There is no expected-version parameter. Later same-field edits win. A command
|
|
|
148
138
|
whose response was lost may or may not have applied; read the review before
|
|
149
139
|
retrying.
|
|
150
140
|
|
|
151
|
-
## Person edits
|
|
152
|
-
|
|
153
|
-
The canvas saves a person's edits with `POST /:id/person-edit`, never through
|
|
154
|
-
an agent tool. The body is the whole board: `title`, and `document` with each
|
|
155
|
-
block as stored. A new block has no `id` and may carry a `clientKey`; the host
|
|
156
|
-
gives it a fresh id and returns `ids: {clientKey: id}`. Ids must be unique; an id
|
|
157
|
-
from an earlier version may come back. The save is checked like an agent edit.
|
|
158
|
-
If `baseVersion` is not the current version, nothing is saved and the request
|
|
159
|
-
fails with 409: "The board changed since version N; it is now at version M.
|
|
160
|
-
Merge the latest version and save again." A save that changes nothing returns
|
|
161
|
-
the current version and saves none. The version carries `personEdit: {changed}`,
|
|
162
|
-
the ids of blocks added, removed or changed, and no `lastEdit`, so no courier
|
|
163
|
-
draws it.
|
|
164
|
-
|
|
165
|
-
While a person edits a block, the canvas posts `POST /:id/focus` about every
|
|
166
|
-
5 s. Each lease expires 15 s after its last post. An agent `edit` that adds,
|
|
167
|
-
removes or changes a leased block fails with 409: "The user is editing this
|
|
168
|
-
block (ID). Edit another block, or try again after they move on." Nothing is
|
|
169
|
-
saved. A section or callout changes only when its own fields do, and a block
|
|
170
|
-
moved unchanged does not change. A lease never refuses a person's edit or
|
|
171
|
-
restore, only an agent's. A board in Trash refuses every change, a person's
|
|
172
|
-
included, until it is restored.
|
|
173
|
-
|
|
174
141
|
## Validation and remaining work
|
|
175
142
|
|
|
176
|
-
Markdown source links use `[label](
|
|
177
|
-
(or `base`, or `#L10` for one line)
|
|
178
|
-
to quote any commit of the board's own repository; a short commit is saved in full,
|
|
179
|
-
and one the repository lacks rejects the edit. Paths are repository-relative and URL-encoded
|
|
143
|
+
Markdown source links use `[label](review-source:head/src/save.ts#L10-L24)`
|
|
144
|
+
(or `base`, or `#L10` for one line). Paths are repository-relative and URL-encoded
|
|
180
145
|
where needed. Inline and reference-style links open the existing native side peek.
|
|
181
146
|
The same Markdown parser feeds the host's source checks and the renderer, so code
|
|
182
147
|
examples and unused definitions do not become source requests. Invalid paths or
|
|
@@ -198,10 +163,9 @@ working-copy files. It checks code ranges and resource ownership before saving.
|
|
|
198
163
|
Images are fully decoded to PNG; traces retain supplied text with an explicit
|
|
199
164
|
client-supplied provenance label. Map uploads use the existing nested map format,
|
|
200
165
|
with a JSON shape check followed by the existing relationship/coverage validator
|
|
201
|
-
and pinned source-range checks. Uploads take `{id,
|
|
166
|
+
and pinned source-range checks. Uploads take `{id,repositoryId,kind,...}` with
|
|
202
167
|
`base64` for images, `trace:{label,events:[{id,role,text}]}` for traces, or
|
|
203
|
-
`pins,side,model` for maps
|
|
204
|
-
(no agent tool uploads maps). Reusing an upload ID requires identical content.
|
|
168
|
+
`pins,side,model` for maps. Reusing an upload ID requires identical content.
|
|
205
169
|
|
|
206
170
|
The canvas preserves React identities during updates. The stream coalesces
|
|
207
171
|
updates when a reader falls behind; reconnecting starts with the current saved
|
|
@@ -313,11 +277,8 @@ commands are local and are never authored into review documents.
|
|
|
313
277
|
|
|
314
278
|
## Review targets
|
|
315
279
|
|
|
316
|
-
`target`
|
|
317
|
-
|
|
318
|
-
`{kind:"commits",repositoryPath,head,base?}` or
|
|
319
|
-
`{kind:"pullRequest",repositoryPath,pullRequest}` (stored as commits plus the PR
|
|
320
|
-
origin). Commit revisions resolve on acceptance.
|
|
280
|
+
`target` is either `{kind:"worktree",repositoryId,base?}` or
|
|
281
|
+
`{kind:"commits",repositoryId,head,base?}`. Commit revisions resolve on acceptance.
|
|
321
282
|
Omitted commit base means source at head with no diff, exactly as base=head;
|
|
322
283
|
supply its parent to review the changes introduced by a single commit.
|
|
323
284
|
|
|
@@ -68,9 +68,12 @@ export class ReviewActivity {
|
|
|
68
68
|
private readonly db: DatabaseSync,
|
|
69
69
|
private readonly assertReview?: (reviewId: string) => void,
|
|
70
70
|
) {
|
|
71
|
+
// Presences replace the per-scope leases; both last minutes, so the old
|
|
72
|
+
// table is dropped rather than migrated.
|
|
71
73
|
db.exec(`CREATE TABLE IF NOT EXISTS authoring_presences(
|
|
72
74
|
activity_id TEXT PRIMARY KEY, review_id TEXT NOT NULL, slot INTEGER NOT NULL,
|
|
73
|
-
started_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, focus TEXT, surface TEXT)
|
|
75
|
+
started_at INTEGER NOT NULL, expires_at INTEGER NOT NULL, focus TEXT, surface TEXT);
|
|
76
|
+
DROP TABLE IF EXISTS authoring_sessions;`);
|
|
74
77
|
|
|
75
78
|
for (const row of db
|
|
76
79
|
.prepare(
|
|
@@ -20,12 +20,7 @@ import { type ReviewApiClient, ReviewApiError } from "./client.js";
|
|
|
20
20
|
import { exitOnInstallUpdate } from "./install-update.js";
|
|
21
21
|
import { callPublicTool, publicTool } from "./public-tools.js";
|
|
22
22
|
import { RECOVERY } from "./recovery.js";
|
|
23
|
-
import {
|
|
24
|
-
ASK_THREAD_ENV,
|
|
25
|
-
REVIEW_AGENT_HEADER,
|
|
26
|
-
REVIEW_THREAD_HEADER,
|
|
27
|
-
REVIEW_VIA_HEADER,
|
|
28
|
-
} from "./request-origin.js";
|
|
23
|
+
import { REVIEW_AGENT_HEADER, REVIEW_VIA_HEADER } from "./request-origin.js";
|
|
29
24
|
import { toolFailure } from "./tool-failure.js";
|
|
30
25
|
|
|
31
26
|
interface AgentCliInput {
|
|
@@ -69,12 +64,9 @@ export async function runReviewAgentCli(input: AgentCliInput): Promise<number> {
|
|
|
69
64
|
if (extra.length || (mode === "mcp" && name))
|
|
70
65
|
throw new Error("Unexpected arguments. Use whiteboard api --help.");
|
|
71
66
|
|
|
72
|
-
const thread = env[ASK_THREAD_ENV]?.trim();
|
|
73
|
-
|
|
74
67
|
const headers = Object.fromEntries([
|
|
75
68
|
[REVIEW_VIA_HEADER, mode === "mcp" ? "mcp" : "api"],
|
|
76
69
|
[REVIEW_AGENT_HEADER, reviewSessionAgent(env)],
|
|
77
|
-
[REVIEW_THREAD_HEADER, thread ?? ""],
|
|
78
70
|
]);
|
|
79
71
|
|
|
80
72
|
if (mode === "mcp") {
|
|
@@ -4,12 +4,7 @@ import {
|
|
|
4
4
|
selectionKey,
|
|
5
5
|
} from "@review/lens-selection.js";
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
type Pins,
|
|
9
|
-
anchorPins,
|
|
10
|
-
boardRepositoryId,
|
|
11
|
-
selectionReferences,
|
|
12
|
-
} from "./document.js";
|
|
7
|
+
import { type Pins, anchorPins, selectionReferences } from "./document.js";
|
|
13
8
|
import type { Snapshot } from "./store.js";
|
|
14
9
|
|
|
15
10
|
export interface AnchorQuote {
|
|
@@ -45,17 +40,15 @@ export async function anchorQuotes(
|
|
|
45
40
|
|
|
46
41
|
const known = new Set(
|
|
47
42
|
before
|
|
48
|
-
? selectionReferences(before.document, {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}).map(({ id, source }) => key(id, source))
|
|
43
|
+
? selectionReferences(before.document, { tolerant: true }).map(
|
|
44
|
+
({ id, source }) => key(id, source),
|
|
45
|
+
)
|
|
52
46
|
: [],
|
|
53
47
|
);
|
|
54
48
|
|
|
55
|
-
const added = selectionReferences(after.document, {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}).filter(({ id, source }) => !known.has(key(id, source)));
|
|
49
|
+
const added = selectionReferences(after.document, { tolerant: true }).filter(
|
|
50
|
+
({ id, source }) => !known.has(key(id, source)),
|
|
51
|
+
);
|
|
59
52
|
|
|
60
53
|
const quotes = await Promise.all(
|
|
61
54
|
added
|
|
@@ -17,11 +17,7 @@ import { ReviewInputError } from "./document.js";
|
|
|
17
17
|
|
|
18
18
|
/** A saved Ask conversation: its history entry, and how to reopen it. */
|
|
19
19
|
export const askRecordSchema = askHistoryEntrySchema.extend({
|
|
20
|
-
|
|
21
|
-
anchor: z.object({ board: z.string(), block: z.string().optional() }),
|
|
22
|
-
/** The project of the board it started on, read from that board, or its
|
|
23
|
-
* tombstone once deleted; absent for a shared board. */
|
|
24
|
-
projectId: z.string().optional(),
|
|
20
|
+
reviewId: z.string(),
|
|
25
21
|
/** The ACP session the agent keeps the transcript under. */
|
|
26
22
|
sessionId: z.string(),
|
|
27
23
|
/** The review version asked about; its pins recreate the checkout. */
|
|
@@ -38,9 +34,7 @@ export type AskRecord = z.infer<typeof askRecordSchema>;
|
|
|
38
34
|
const rowSchema = z
|
|
39
35
|
.object({
|
|
40
36
|
id: z.string(),
|
|
41
|
-
|
|
42
|
-
anchor_block: z.string().nullable(),
|
|
43
|
-
project_id: z.string().nullable(),
|
|
37
|
+
review_id: z.string(),
|
|
44
38
|
agent: z.string(),
|
|
45
39
|
session_id: z.string(),
|
|
46
40
|
version: z.number(),
|
|
@@ -57,11 +51,7 @@ const rowSchema = z
|
|
|
57
51
|
.transform((row) =>
|
|
58
52
|
askRecordSchema.parse({
|
|
59
53
|
id: row.id,
|
|
60
|
-
|
|
61
|
-
row.anchor_block === null
|
|
62
|
-
? { board: row.anchor_board }
|
|
63
|
-
: { board: row.anchor_board, block: row.anchor_block },
|
|
64
|
-
projectId: row.project_id ?? undefined,
|
|
54
|
+
reviewId: row.review_id,
|
|
65
55
|
agent: row.agent,
|
|
66
56
|
sessionId: row.session_id,
|
|
67
57
|
version: row.version,
|
|
@@ -78,36 +68,17 @@ const rowSchema = z
|
|
|
78
68
|
}),
|
|
79
69
|
);
|
|
80
70
|
|
|
81
|
-
/**
|
|
82
|
-
*
|
|
83
|
-
* disagree with its board's. */
|
|
84
|
-
const THREAD_PROJECT_SQL = `COALESCE(
|
|
85
|
-
(SELECT project_id FROM reviews WHERE reviews.id=ask_conversations.anchor_board),
|
|
86
|
-
(SELECT project_id FROM deleted_boards WHERE deleted_boards.id=ask_conversations.anchor_board))`;
|
|
87
|
-
|
|
88
|
-
/** Every column of a thread, with its project. */
|
|
89
|
-
const THREAD_COLUMNS = `ask_conversations.*, ${THREAD_PROJECT_SQL} AS project_id`;
|
|
90
|
-
|
|
91
|
-
/** Saved Ask conversations by project, each anchored on the board it started
|
|
92
|
-
* on. No foreign key on the anchor: shared reviews are not in `reviews`.
|
|
93
|
-
* So deleting a board removes its threads by hand (removeReview). */
|
|
71
|
+
/** Saved Ask conversations, per review. Shared reviews are not in
|
|
72
|
+
* `reviews`, so there is no foreign key; deleting a review deletes its rows. */
|
|
94
73
|
export class AskHistory {
|
|
95
|
-
private readonly listeners = new Set<() => void>();
|
|
96
|
-
|
|
97
74
|
private externalWriteGuard?: (reviewId: string) => void;
|
|
98
75
|
setExternalWriteGuard(guard: (reviewId: string) => void) {
|
|
99
76
|
this.externalWriteGuard = guard;
|
|
100
77
|
}
|
|
101
|
-
constructor(
|
|
102
|
-
private readonly db: DatabaseSync,
|
|
103
|
-
/** The store's rule for writing about a board: one in Trash is
|
|
104
|
-
* read-only. */
|
|
105
|
-
private readonly writable: (reviewId: string) => void = () => {},
|
|
106
|
-
) {
|
|
78
|
+
constructor(private readonly db: DatabaseSync) {
|
|
107
79
|
db.exec(`CREATE TABLE IF NOT EXISTS ask_conversations(
|
|
108
80
|
id TEXT PRIMARY KEY,
|
|
109
|
-
|
|
110
|
-
anchor_block TEXT,
|
|
81
|
+
review_id TEXT NOT NULL,
|
|
111
82
|
agent TEXT NOT NULL,
|
|
112
83
|
session_id TEXT NOT NULL,
|
|
113
84
|
version INTEGER NOT NULL,
|
|
@@ -120,13 +91,26 @@ export class AskHistory {
|
|
|
120
91
|
entries TEXT,
|
|
121
92
|
bypass INTEGER NOT NULL DEFAULT 0,
|
|
122
93
|
permissions TEXT,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
94
|
+
UNIQUE(agent, session_id));
|
|
95
|
+
CREATE INDEX IF NOT EXISTS ask_conversations_review ON ask_conversations(review_id, updated_at);`);
|
|
96
|
+
|
|
97
|
+
// Conversations saved before bypassing permissions, or before auto mode,
|
|
98
|
+
// lack its column.
|
|
99
|
+
const columns = new Set(
|
|
100
|
+
db
|
|
101
|
+
.prepare("PRAGMA table_info(ask_conversations)")
|
|
102
|
+
.all()
|
|
103
|
+
.map((column) => String(column.name)),
|
|
128
104
|
);
|
|
129
105
|
|
|
106
|
+
if (!columns.has("bypass"))
|
|
107
|
+
db.exec(
|
|
108
|
+
"ALTER TABLE ask_conversations ADD COLUMN bypass INTEGER NOT NULL DEFAULT 0",
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (!columns.has("permissions"))
|
|
112
|
+
db.exec("ALTER TABLE ask_conversations ADD COLUMN permissions TEXT");
|
|
113
|
+
|
|
130
114
|
// What each agent offered last, so a new question can pick a model and
|
|
131
115
|
// effort, and a command, before its agent starts.
|
|
132
116
|
db.exec(
|
|
@@ -138,46 +122,34 @@ export class AskHistory {
|
|
|
138
122
|
);
|
|
139
123
|
}
|
|
140
124
|
|
|
141
|
-
/** Calls back when a conversation is saved, renamed, used or deleted. */
|
|
142
|
-
subscribe(listener: () => void) {
|
|
143
|
-
this.listeners.add(listener);
|
|
144
|
-
|
|
145
|
-
return () => {
|
|
146
|
-
this.listeners.delete(listener);
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
private changed() {
|
|
151
|
-
for (const listener of this.listeners) listener();
|
|
152
|
-
}
|
|
153
|
-
|
|
154
125
|
private assertWritable(reviewId: string) {
|
|
155
126
|
if (reviewId.startsWith("shared-")) this.externalWriteGuard?.(reviewId);
|
|
156
|
-
else this.writable(reviewId);
|
|
157
|
-
}
|
|
158
127
|
|
|
128
|
+
const row = this.db
|
|
129
|
+
.prepare("SELECT deleted_at FROM review_attention WHERE review_id=?")
|
|
130
|
+
.get(reviewId);
|
|
131
|
+
|
|
132
|
+
if (row?.deleted_at)
|
|
133
|
+
throw new ReviewInputError(
|
|
134
|
+
`This whiteboard was moved to Trash on ${new Date(String(row.deleted_at)).toLocaleDateString("en-US", { month: "short", day: "numeric" })}. Restore it with session_lifecycle({"sessionId":"${reviewId}","state":"active"}) to continue.`,
|
|
135
|
+
409,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
159
138
|
private assertThreadWritable(id: string) {
|
|
160
139
|
const record = this.get(id);
|
|
161
140
|
|
|
162
|
-
if (record) this.assertWritable(record.
|
|
141
|
+
if (record) this.assertWritable(record.reviewId);
|
|
163
142
|
}
|
|
164
143
|
save(record: AskRecord) {
|
|
165
|
-
this.assertWritable(record.
|
|
144
|
+
this.assertWritable(record.reviewId);
|
|
166
145
|
this.db
|
|
167
146
|
.prepare(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
`INSERT INTO ask_conversations(id,anchor_board,anchor_block,agent,session_id,version,head,cwd,selection,title,created_at,updated_at,bypass,permissions)
|
|
171
|
-
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
172
|
-
ON CONFLICT(id) DO UPDATE SET anchor_board=excluded.anchor_board, anchor_block=excluded.anchor_block,
|
|
173
|
-
agent=excluded.agent, session_id=excluded.session_id, version=excluded.version, head=excluded.head,
|
|
174
|
-
cwd=excluded.cwd, selection=excluded.selection, title=excluded.title, created_at=excluded.created_at,
|
|
175
|
-
updated_at=excluded.updated_at, bypass=excluded.bypass, permissions=excluded.permissions`,
|
|
147
|
+
`INSERT OR REPLACE INTO ask_conversations(id,review_id,agent,session_id,version,head,cwd,selection,title,created_at,updated_at,bypass,permissions)
|
|
148
|
+
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
176
149
|
)
|
|
177
150
|
.run(
|
|
178
151
|
record.id,
|
|
179
|
-
record.
|
|
180
|
-
record.anchor.block ?? null,
|
|
152
|
+
record.reviewId,
|
|
181
153
|
record.agent,
|
|
182
154
|
record.sessionId,
|
|
183
155
|
record.version,
|
|
@@ -190,7 +162,6 @@ export class AskHistory {
|
|
|
190
162
|
record.permissions === "bypass" ? 1 : 0,
|
|
191
163
|
record.permissions ?? null,
|
|
192
164
|
);
|
|
193
|
-
this.changed();
|
|
194
165
|
}
|
|
195
166
|
|
|
196
167
|
/** What the agent may do without asking when the conversation reopens. */
|
|
@@ -218,7 +189,6 @@ export class AskHistory {
|
|
|
218
189
|
this.db
|
|
219
190
|
.prepare("UPDATE ask_conversations SET entries=? WHERE id=?")
|
|
220
191
|
.run(JSON.stringify(entries), id);
|
|
221
|
-
this.changed();
|
|
222
192
|
}
|
|
223
193
|
|
|
224
194
|
/** The agent's name for the conversation, which the list shows. */
|
|
@@ -227,7 +197,6 @@ export class AskHistory {
|
|
|
227
197
|
this.db
|
|
228
198
|
.prepare("UPDATE ask_conversations SET title=? WHERE id=?")
|
|
229
199
|
.run(title.slice(0, 200), id);
|
|
230
|
-
this.changed();
|
|
231
200
|
}
|
|
232
201
|
|
|
233
202
|
saveOffer(agent: AskAgentId, offer: AskOffer) {
|
|
@@ -276,105 +245,50 @@ export class AskHistory {
|
|
|
276
245
|
this.db
|
|
277
246
|
.prepare("UPDATE ask_conversations SET updated_at=? WHERE id=?")
|
|
278
247
|
.run(at, id);
|
|
279
|
-
this.changed();
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/** Conversations in a board's project, or started on the board when it
|
|
283
|
-
* has none. Newest first. */
|
|
284
|
-
list(board: string, projectId?: string) {
|
|
285
|
-
// The project's boards by their index on project_id, then their
|
|
286
|
-
// threads by ask_conversations_review.
|
|
287
|
-
return this.entries(
|
|
288
|
-
"anchor_board=? OR anchor_board IN (SELECT id FROM reviews WHERE project_id=? UNION ALL SELECT id FROM deleted_boards WHERE project_id=?)",
|
|
289
|
-
board,
|
|
290
|
-
projectId ?? null,
|
|
291
|
-
projectId ?? null,
|
|
292
|
-
);
|
|
293
248
|
}
|
|
294
249
|
|
|
295
|
-
/**
|
|
296
|
-
|
|
297
|
-
return this.entries("1");
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
private entries(
|
|
301
|
-
where: string,
|
|
302
|
-
...params: (string | null)[]
|
|
303
|
-
): (AskHistoryEntry & { projectId?: string })[] {
|
|
304
|
-
// A transcript can be long: SQLite finds its first question, and the
|
|
305
|
-
// rest of it is left unread.
|
|
250
|
+
/** Newest first. */
|
|
251
|
+
list(reviewId: string): AskHistoryEntry[] {
|
|
306
252
|
return this.db
|
|
307
253
|
.prepare(
|
|
308
|
-
|
|
309
|
-
created_at, updated_at, NULL AS entries, bypass, permissions,
|
|
310
|
-
(SELECT json_extract(value,'$.text') FROM json_each(entries)
|
|
311
|
-
WHERE json_extract(value,'$.kind')='user' LIMIT 1) AS question
|
|
312
|
-
FROM ask_conversations WHERE ${where} ORDER BY updated_at DESC`,
|
|
254
|
+
"SELECT * FROM ask_conversations WHERE review_id=? ORDER BY updated_at DESC",
|
|
313
255
|
)
|
|
314
|
-
.all(
|
|
256
|
+
.all(reviewId)
|
|
315
257
|
.map((row) => {
|
|
316
258
|
const {
|
|
317
|
-
|
|
259
|
+
reviewId: _reviewId,
|
|
318
260
|
sessionId: _sessionId,
|
|
319
261
|
version: _version,
|
|
320
262
|
cwd: _cwd,
|
|
321
|
-
entries
|
|
263
|
+
entries,
|
|
322
264
|
permissions: _permissions,
|
|
323
265
|
...entry
|
|
324
266
|
} = rowSchema.parse(row);
|
|
325
267
|
|
|
326
|
-
const question =
|
|
268
|
+
const question = entries?.find((entry) => entry.kind === "user")?.text;
|
|
327
269
|
|
|
328
|
-
if (question
|
|
270
|
+
if (question?.trim()) entry.question = question;
|
|
329
271
|
|
|
330
|
-
return
|
|
272
|
+
return entry;
|
|
331
273
|
});
|
|
332
274
|
}
|
|
333
275
|
|
|
334
276
|
get(id: string): AskRecord | undefined {
|
|
335
277
|
const row = this.db
|
|
336
|
-
.prepare(
|
|
278
|
+
.prepare("SELECT * FROM ask_conversations WHERE id=?")
|
|
337
279
|
.get(id);
|
|
338
280
|
|
|
339
281
|
return row ? rowSchema.parse(row) : undefined;
|
|
340
282
|
}
|
|
341
283
|
|
|
342
|
-
/** The reviewer saw the thread, on screen with the app focused. Only
|
|
343
|
-
* when it was seen changes: not its order, nor when it was used. */
|
|
344
|
-
markViewed(id: string, at = new Date().toISOString()) {
|
|
345
|
-
this.db
|
|
346
|
-
.prepare("UPDATE ask_conversations SET viewed_at=? WHERE id=?")
|
|
347
|
-
.run(at, id);
|
|
348
|
-
this.changed();
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/** When each of these threads was last viewed, where it ever was.
|
|
352
|
-
* Threads from before views were kept have none, and none is unread. */
|
|
353
|
-
viewedAt(ids: readonly string[]): Map<string, string> {
|
|
354
|
-
const select = this.db.prepare(
|
|
355
|
-
"SELECT viewed_at FROM ask_conversations WHERE id=? AND viewed_at IS NOT NULL",
|
|
356
|
-
);
|
|
357
|
-
|
|
358
|
-
return new Map(
|
|
359
|
-
ids.flatMap((id) => {
|
|
360
|
-
const row = select.get(id);
|
|
361
|
-
|
|
362
|
-
return row ? [[id, String(row.viewed_at)] as const] : [];
|
|
363
|
-
}),
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/** Forgets the threads that started on a board gone for good. */
|
|
368
284
|
removeReview(reviewId: string) {
|
|
369
285
|
this.db
|
|
370
|
-
.prepare("DELETE FROM ask_conversations WHERE
|
|
286
|
+
.prepare("DELETE FROM ask_conversations WHERE review_id=?")
|
|
371
287
|
.run(reviewId);
|
|
372
|
-
this.changed();
|
|
373
288
|
}
|
|
374
289
|
|
|
375
290
|
delete(id: string) {
|
|
376
291
|
this.assertThreadWritable(id);
|
|
377
292
|
this.db.prepare("DELETE FROM ask_conversations WHERE id=?").run(id);
|
|
378
|
-
this.changed();
|
|
379
293
|
}
|
|
380
294
|
}
|