wiki-viewer 2.16.0 → 2.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +4 -4
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/api/agent/activity/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/events/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/fs/move/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/fs/search/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/sidecar/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/assets/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/pdf/save/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/share/[token]/asset/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/share/[token]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/share/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/browse/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/workspaces/[id]/branch/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/workspaces/[id]/open/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/workspaces/[id]/refresh/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/workspaces/[id]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/workspaces/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/upload/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/app/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/backlinks/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/content/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/download/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/folder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/git-branches/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/git-checkout/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/git-diff/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/git-file-info/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/git-history/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/git-pull/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/move/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/new-file/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/outlinks/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/page/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/presence/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/scratch/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/search/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/slugs/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/upload/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/watch/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page/react-loadable-manifest.json +4 -5
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/s/[token]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/signin/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0ltsov-._.js +1 -1
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_system_workspaces_[id]_open_route_actions_1087xu7.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/0.u4_next_0kf.7hj._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/0.u4_next_dist_0xugq-k._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_chunks_mermaid_core_chunk-5ZQYHXKU_mjs_0z1.978._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_chunks_mermaid_core_chunk-KSCS5N6A_mjs_0v4oeem._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_01eqklo._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0858xdh._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0k6g8yo._.js +3 -3
- package/.next/standalone/.next/server/chunks/ssr/_0sr4wj.._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_06613~i._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_0o~d81.._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_0szp2v0._.js +1 -1
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/0.5ywu7_j899v.js +12 -0
- package/.next/standalone/.next/static/chunks/{0tjcc1wg-57l1.js → 065s1_n.1k11t.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0y~5s_skz1l9a.js → 0ccw-uc_3ao.w.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0ii.akeq7k4pf.js → 0f8kfd9d6kq0b.js} +1 -1
- package/.next/standalone/.next/static/chunks/0lsw6lsaj7lby.js +1 -0
- package/.next/standalone/.next/static/chunks/0rpxacw15~o7k.js +12 -0
- package/.next/standalone/.next/static/chunks/{0r7d-1tq61fa1.js → 0s2h7kvkrtq~p.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0enm-3xy21jcx.js → 0tzi1~wpigp2y.js} +2 -2
- package/.next/standalone/.next/static/chunks/117dkzyg4c~us.js +1 -0
- package/.next/standalone/.next/static/chunks/{0j7kp7w2oum68.js → 11pa_9ucyrzh9.js} +1 -1
- package/.next/standalone/.scratch/tweak-running-app/craft-checklist.md +52 -0
- package/.next/standalone/.scratch/tweak-running-app/issues/01-isolated-origin.md +108 -0
- package/.next/standalone/.scratch/tweak-running-app/issues/01-runtime-containment.md +18 -0
- package/.next/standalone/.scratch/tweak-running-app/issues/02-injection-transform.md +18 -0
- package/.next/standalone/.scratch/tweak-running-app/issues/02-tracer-bullet.md +48 -0
- package/.next/standalone/.scratch/tweak-running-app/issues/03-accept-carbonize-mutator.md +19 -0
- package/.next/standalone/.scratch/tweak-running-app/issues/03-run-robustness.md +37 -0
- package/.next/standalone/.scratch/tweak-running-app/issues/04-multifile-safety.md +37 -0
- package/.next/standalone/.scratch/tweak-running-app/issues/04-static-tracer-bullet.md +27 -0
- package/.next/standalone/.scratch/tweak-running-app/issues/05-batch-parity.md +43 -0
- package/.next/standalone/.scratch/tweak-running-app/issues/05-dynamic-node-app.md +22 -0
- package/.next/standalone/.scratch/tweak-running-app/issues/06-remote-transport-bridge.md +20 -0
- package/.next/standalone/.scratch/tweak-running-app/issues/07-retire-bespoke-web-tweak.md +17 -0
- package/.next/standalone/.scratch/tweak-running-app/keep-ticket.md +74 -0
- package/.next/standalone/.scratch/tweak-running-app/mockups.html +305 -0
- package/.next/standalone/.scratch/tweak-running-app/ops1-updated.md +215 -0
- package/.next/standalone/.scratch/tweak-running-app/ops12-continuation.md +44 -0
- package/.next/standalone/.scratch/tweak-running-app/ops22-context.md +84 -0
- package/.next/standalone/.scratch/tweak-running-app/pivot-note.md +25 -0
- package/.next/standalone/.scratch/tweak-running-app/qa-live-engine.ts +104 -0
- package/.next/standalone/.scratch/tweak-running-app/spike.md +256 -0
- package/.next/standalone/.scratch/tweak-running-app/suggest-current-assessment.md +39 -0
- package/.next/standalone/.scratch/tweak-running-app/suggest-inline-interaction-spec.md +320 -0
- package/.next/standalone/.scratch/tweak-running-app/tweak-interaction-spec.md +161 -0
- package/.next/standalone/.scratch/tweak-running-app/two-actions-interaction-spec.md +234 -0
- package/.next/standalone/.scratch/tweak-running-app/unified-live-spec.md +261 -0
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/server.js +1 -1
- package/package.json +1 -1
- package/.next/standalone/.next/static/chunks/04jvwlttwa_sw.js +0 -12
- package/.next/standalone/.next/static/chunks/161kgz.8h.3iq.js +0 -1
- package/.next/standalone/.next/static/chunks/16kzqur8~yg6b.js +0 -12
- /package/.next/standalone/.next/static/{7EbGXTo8ObPhr7UobKARQ → zwUX5EOT_44n1RoB_W9Bo}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{7EbGXTo8ObPhr7UobKARQ → zwUX5EOT_44n1RoB_W9Bo}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{7EbGXTo8ObPhr7UobKARQ → zwUX5EOT_44n1RoB_W9Bo}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# OPS-22 re-home context (ground truth, main @ 23a5129)
|
|
2
|
+
|
|
3
|
+
Goal: rebuild the (removed) "Tweak" interaction on top of the EXISTING
|
|
4
|
+
comments/suggestions sidecar ("proof") engine, so there is one collaboration
|
|
5
|
+
engine underneath. Tweak = "point at a target, say what you want, the agent
|
|
6
|
+
rewrites it." No live engine, no polling, no variants-in-iframe.
|
|
7
|
+
|
|
8
|
+
## What the proof engine already gives us (do not rebuild)
|
|
9
|
+
|
|
10
|
+
Store: `.proof/<path>.json` sidecar per file, opaque JSON, event log, revisioned.
|
|
11
|
+
Write API: `POST /api/agent/files/[...path]` `{baseRevision, by, ops[]}`, bearer
|
|
12
|
+
+ Idempotency-Key, `STALE_REVISION` 409 + fresh snapshot on drift.
|
|
13
|
+
|
|
14
|
+
Comment kinds (src/lib/proof/types.ts):
|
|
15
|
+
- `comment` (legacy): discussion/annotation. Anchor = md `ref` (block) OR
|
|
16
|
+
`lineAnchor{lineStart,lineEnd,textHash}` for text/code files.
|
|
17
|
+
- `instruction` (`kind:"instruction"`): ALREADY EXISTS. Fields
|
|
18
|
+
`instructionState: draft|queued|sent|answered`, `runId?`, `fromCommentId?`.
|
|
19
|
+
Created today only via "Turn into an instruction" (comment-thread.tsx). It has
|
|
20
|
+
NO client dispatcher and the queued/sent/answered lifecycle is dormant
|
|
21
|
+
(driven only in tests). This is the durable home for a tweak.
|
|
22
|
+
|
|
23
|
+
Suggestions (`suggestion.add`, kinds replace/insertAfter/insertBefore/delete,
|
|
24
|
+
status pending|accepted|rejected): reviewed in `suggestion-card.tsx`; Accept
|
|
25
|
+
pushes the equivalent block op into the same batch and writes the doc, gated on
|
|
26
|
+
base fingerprint. THIS IS MARKDOWN-ONLY (route + applier both gate non-md to
|
|
27
|
+
comment ops only).
|
|
28
|
+
|
|
29
|
+
Surface capability asymmetry (hard constraint):
|
|
30
|
+
- Markdown (.md): full — block-ref comments, instruction comments, suggestions,
|
|
31
|
+
accept-writes-doc. Reviewed inline via suggestion-card + comment-pip.
|
|
32
|
+
- Text/code/HTML/any other file (source viewer): line-anchored COMMENTS ONLY.
|
|
33
|
+
No suggestions, no edits, no instruction dispatcher UI, no in-place accept.
|
|
34
|
+
(source-viewer.tsx already builds the selection→lineAnchor detection UI.)
|
|
35
|
+
|
|
36
|
+
## Current entry points (post-removal)
|
|
37
|
+
|
|
38
|
+
- Markdown editor bubble menu: **Comment** (discuss/annotate) + **Suggest**
|
|
39
|
+
(propose human edit for review). Tweak was removed.
|
|
40
|
+
- Source viewer (text/code): select text → **Comment** pip only.
|
|
41
|
+
- HTML files render as a sandboxed preview iframe (website-viewer.tsx); no
|
|
42
|
+
collaboration affordance on it at all right now.
|
|
43
|
+
|
|
44
|
+
## Pre-removal Tweak UX we are preserving (recovered from tag pre-tweak-removal)
|
|
45
|
+
|
|
46
|
+
Shared chrome:
|
|
47
|
+
- Bottom-center fixed **queue bar**: "{n} tweak(s) ready" · Cancel (clears all) ·
|
|
48
|
+
[Copy as prompt | Show] extras · dispatch button (label surface-specific).
|
|
49
|
+
- **Targeting panel** anchored to the target: header "Target", textarea
|
|
50
|
+
placeholder "What should change?", Cmd/Enter = add, Cancel, Add tweak.
|
|
51
|
+
Outside-click on empty input = cancel.
|
|
52
|
+
- **Dedup in place**: re-picking the same target updates its instruction/snippet,
|
|
53
|
+
count does not grow.
|
|
54
|
+
- **Copy as prompt** (works with no agent): emits
|
|
55
|
+
`Edit the file \`X\` (a Markdown document). Apply these changes:\n\n1. \`<snippet>\`: <instruction>` (html variant lists `Element \`selector\` (<tag>): ...`).
|
|
56
|
+
Clipboard gate + "Show" textarea fallback for non-secure contexts.
|
|
57
|
+
- **Connect an agent**: when no agent attached, dispatch label becomes "Connect
|
|
58
|
+
an agent" and opens the AI panel; queue preserved.
|
|
59
|
+
|
|
60
|
+
Markdown surface: click rendered block → targeting panel → queue → "Rewrite".
|
|
61
|
+
HTML surface: element picker in iframe → note editor anchored to element → queue
|
|
62
|
+
→ "Apply" (confirm modal) → variants. (The whole speculative-preview/variant/
|
|
63
|
+
Accept-writes machinery is the part being deleted.)
|
|
64
|
+
|
|
65
|
+
## HARD FRAMING (user correction, authoritative)
|
|
66
|
+
|
|
67
|
+
There is NO standalone "Tweak" action and none will be created. There stay
|
|
68
|
+
exactly TWO editor actions: **Comment** and **Suggest**. The pleasant tweak UX
|
|
69
|
+
is PORTED INTO those two, not added beside them. Concretely, the intended split
|
|
70
|
+
(confirm/refine): Comment absorbs "tell the agent what to change" via the
|
|
71
|
+
existing `instruction` comment kind + the targeting affordance ("What should
|
|
72
|
+
change?") + copy-as-prompt + connect-an-agent; the agent's rewrite answer lands
|
|
73
|
+
as a **Suggestion** reviewed in the existing suggestion-card. Reject any design
|
|
74
|
+
that adds a third button.
|
|
75
|
+
|
|
76
|
+
## The open decision to resolve
|
|
77
|
+
|
|
78
|
+
Default dispatch action for a tweak:
|
|
79
|
+
- (a) instruction-comment default: dispatch persists each queued item as
|
|
80
|
+
`comment.add {kind:"instruction"}` on the target block/line; the user's agent
|
|
81
|
+
picks it up through its normal snapshot/activity flow and answers with a
|
|
82
|
+
suggestion (md) reviewed in suggestion-card. Copy-as-prompt = escape hatch.
|
|
83
|
+
- (b) copy-as-prompt only: no durable artifact; user pastes into their agent.
|
|
84
|
+
PM leaned (b) for simplicity; implementer leaned (a) durable with (b) as hatch.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## Direction change: unify on the Impeccable live model (supersedes the node-app-scoped plan)
|
|
2
|
+
|
|
3
|
+
After reviewing what the Impeccable `live` skill actually does (inject an overlay
|
|
4
|
+
into the app's own running page; hot-swap agent-written source variants via the
|
|
5
|
+
dev server's HMR; carbonize the chosen variant into permanent source on accept;
|
|
6
|
+
2–5 variants + tunable per-variant knobs), we are replacing the embed-in-iframe
|
|
7
|
+
node-app approach with a single unified mechanism.
|
|
8
|
+
|
|
9
|
+
Decisions:
|
|
10
|
+
- Scope = web/visual surfaces only: static HTML, dynamic HTML, and node apps.
|
|
11
|
+
Markdown prose stays on its current variants-in-place flow (no HMR / rendered
|
|
12
|
+
UI to hot-swap).
|
|
13
|
+
- Engine = reuse the existing Impeccable live engine (helper server + injection
|
|
14
|
+
adapters + HMR hot-swap + carbonize) rather than reimplement it or drop the
|
|
15
|
+
in-product experience. Requires a feasibility spike: confirm the engine can be
|
|
16
|
+
driven by the wiki-viewer product, not only by a chat agent.
|
|
17
|
+
|
|
18
|
+
Consequence:
|
|
19
|
+
- This spec (OPS-1) and its tickets (OPS-2 isolated origin, OPS-3 tracer bullet,
|
|
20
|
+
OPS-4 robustness, OPS-5 multi-file safety, OPS-6 batch parity) are SUPERSEDED.
|
|
21
|
+
The isolated-origin / grant / same-site-hardening architecture is no longer
|
|
22
|
+
needed because the Impeccable model injects into the app's own page (no parent
|
|
23
|
+
to protect) instead of embedding it in the wiki editor's iframe.
|
|
24
|
+
- Next steps: feasibility spike on driving the Impeccable engine from the product,
|
|
25
|
+
then a fresh unified spec (to-spec) and tickets (to-tickets).
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { LiveEngineClient } from "/home/sil/wiki-viewer/src/lib/live-engine/client";
|
|
5
|
+
import { startEngine, stopEngine } from "/home/sil/wiki-viewer/src/lib/live-engine/supervisor";
|
|
6
|
+
|
|
7
|
+
async function main() {
|
|
8
|
+
|
|
9
|
+
const results: string[] = [];
|
|
10
|
+
function check(name: string, cond: boolean, extra = ""): void {
|
|
11
|
+
results.push(`${cond ? "PASS" : "FAIL"} ${name}${extra ? " :: " + extra : ""}`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const appRoot = fs.mkdtempSync(path.join(os.tmpdir(), "qa-live-"));
|
|
15
|
+
fs.writeFileSync(
|
|
16
|
+
path.join(appRoot, "index.html"),
|
|
17
|
+
'<!doctype html><html><head><title>qa</title></head><body><button id="b">Hi</button></body></html>',
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
let failed = false;
|
|
21
|
+
try {
|
|
22
|
+
const info = await startEngine({ workspaceId: "qa", relPath: "index.html", appRoot });
|
|
23
|
+
check("engine boots via product supervisor + running", info.state === "running", `port=${info.port} tokenLen=${info.token.length}`);
|
|
24
|
+
|
|
25
|
+
const client = new LiveEngineClient(info);
|
|
26
|
+
const h = await client.health();
|
|
27
|
+
check("health -> ok", h.status === "ok");
|
|
28
|
+
|
|
29
|
+
const base = `http://127.0.0.1:${info.port}`;
|
|
30
|
+
|
|
31
|
+
const noTok = await fetch(`${base}/live.js`);
|
|
32
|
+
await noTok.text();
|
|
33
|
+
check("live.js without token -> 401", noTok.status === 401, `status=${noTok.status}`);
|
|
34
|
+
|
|
35
|
+
const js = await fetch(`${base}/live.js?token=${info.token}`);
|
|
36
|
+
const jsBody = await js.text();
|
|
37
|
+
check("live.js with token -> 200 real bundle", js.status === 200 && jsBody.length > 1000, `status=${js.status} len=${jsBody.length}`);
|
|
38
|
+
check("live.js self-embeds the token", jsBody.includes(info.token));
|
|
39
|
+
check("bundle references __IMPECCABLE_ globals we inject", /__IMPECCABLE_(PORT|TOKEN|VOCAB)__/.test(jsBody), (jsBody.match(/__IMPECCABLE_[A-Z_]+__/g) || []).slice(0, 6).join(","));
|
|
40
|
+
|
|
41
|
+
// Opaque-origin (sandboxed srcDoc iframe = Origin: null) CORS preflight
|
|
42
|
+
const opt = await fetch(`${base}/events?token=${info.token}`, { method: "OPTIONS", headers: { Origin: "null" } });
|
|
43
|
+
await opt.text();
|
|
44
|
+
check("OPTIONS opaque-origin preflight -> 204", opt.status === 204, `status=${opt.status}`);
|
|
45
|
+
check("CORS reflects opaque origin (null)", opt.headers.get("access-control-allow-origin") === "null", `acao=${opt.headers.get("access-control-allow-origin")}`);
|
|
46
|
+
|
|
47
|
+
// Opaque-origin with WRONG token gets no CORS grant
|
|
48
|
+
const optBad = await fetch(`${base}/events?token=nope`, { method: "OPTIONS", headers: { Origin: "null" } });
|
|
49
|
+
await optBad.text();
|
|
50
|
+
check("opaque origin + bad token -> no CORS grant", optBad.headers.get("access-control-allow-origin") == null, `acao=${optBad.headers.get("access-control-allow-origin")}`);
|
|
51
|
+
|
|
52
|
+
// Browser uplink: POST /events generate (session-creating), Origin: null
|
|
53
|
+
const genId = "a1b2c3d4";
|
|
54
|
+
const gen = await fetch(`${base}/events`, {
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: { "Content-Type": "application/json", Origin: "null" },
|
|
57
|
+
body: JSON.stringify({
|
|
58
|
+
token: info.token,
|
|
59
|
+
type: "generate",
|
|
60
|
+
id: genId,
|
|
61
|
+
action: "bolder",
|
|
62
|
+
count: 3,
|
|
63
|
+
pageUrl: "/index.html",
|
|
64
|
+
element: { outerHTML: '<button id="b">Hi</button>', tagName: "BUTTON" },
|
|
65
|
+
}),
|
|
66
|
+
});
|
|
67
|
+
const genBody = await gen.text();
|
|
68
|
+
check("POST /events generate (opaque origin, token) accepted", gen.ok, `status=${gen.status} body=${genBody.slice(0, 140)}`);
|
|
69
|
+
|
|
70
|
+
// POST /events with bad token rejected
|
|
71
|
+
const genBad = await fetch(`${base}/events`, {
|
|
72
|
+
method: "POST",
|
|
73
|
+
headers: { "Content-Type": "application/json" },
|
|
74
|
+
body: JSON.stringify({ token: "nope", type: "generate", id: "b2c3d4e5", action: "bolder", count: 3, pageUrl: "/index.html", element: { outerHTML: "<x/>" } }),
|
|
75
|
+
});
|
|
76
|
+
await genBad.text();
|
|
77
|
+
check("POST /events bad token -> 401", genBad.status === 401, `status=${genBad.status}`);
|
|
78
|
+
|
|
79
|
+
// Product-side poll receives the browser's generate
|
|
80
|
+
const poll = await client.poll({ timeoutMs: 10000, leaseMs: 30000 });
|
|
81
|
+
check("product /poll receives the generate event", poll?.type === "generate" && poll?.id === genId, `type=${String(poll?.type)} id=${String((poll as { id?: string })?.id)}`);
|
|
82
|
+
|
|
83
|
+
await stopEngine({ workspaceId: "qa", relPath: "index.html" });
|
|
84
|
+
let unreachable = false;
|
|
85
|
+
try {
|
|
86
|
+
await new LiveEngineClient(info).health();
|
|
87
|
+
} catch {
|
|
88
|
+
unreachable = true;
|
|
89
|
+
}
|
|
90
|
+
check("stopEngine tears down helper (health unreachable)", unreachable);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
failed = true;
|
|
93
|
+
results.push(`FAIL harness threw :: ${(err as Error).message}`);
|
|
94
|
+
} finally {
|
|
95
|
+
try {
|
|
96
|
+
await stopEngine({ workspaceId: "qa", relPath: "index.html" });
|
|
97
|
+
} catch {}
|
|
98
|
+
fs.rmSync(appRoot, { recursive: true, force: true });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(results.join("\n"));
|
|
102
|
+
if (failed || results.some((r) => r.startsWith("FAIL"))) process.exitCode = 1;
|
|
103
|
+
}
|
|
104
|
+
main();
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# B1 feasibility spike: driving the Impeccable live engine from wiki-viewer
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-08-18
|
|
4
|
+
**Question:** Can the wiki-viewer *product* (a server, not a chat agent) drive the
|
|
5
|
+
Impeccable `live` engine end to end (helper server, `live.js` injection, HMR
|
|
6
|
+
hot-swap, carbonize-on-accept), and how does the engine's poll loop reconcile with
|
|
7
|
+
wiki-viewer's existing attached-agent channel?
|
|
8
|
+
|
|
9
|
+
**Verdict: feasible, with a defined bridge.** The engine has no hard dependency on
|
|
10
|
+
a chat-agent CLI. Its contract is plain HTTP plus file-based state, so a product
|
|
11
|
+
server can drive it. Two integration points need real work and carry the risk:
|
|
12
|
+
(1) the browser-to-helper transport when wiki-viewer is served remotely, and
|
|
13
|
+
(2) reconciling two source-write/lock systems on the same files. One item needs a
|
|
14
|
+
quick confirmation before spec: WebSocket-upgrade passthrough for HMR in the
|
|
15
|
+
app-proxy. Everything else maps cleanly.
|
|
16
|
+
|
|
17
|
+
This memo is concrete enough to write a spec from.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What the engine actually is (verified)
|
|
22
|
+
|
|
23
|
+
Not a monolith and not chat-agent-bound. Three cooperating pieces:
|
|
24
|
+
|
|
25
|
+
1. **A long-lived helper HTTP server** (`live-server.mjs`), bound to `127.0.0.1`,
|
|
26
|
+
auto-port from `8400`, token-gated. It writes `.impeccable/live/server.json`
|
|
27
|
+
(`{pid, port, token}`). Endpoints that matter:
|
|
28
|
+
- `GET /live.js?token=` — the assembled browser overlay bundle.
|
|
29
|
+
- `GET /events?token=` — **SSE downlink**, server to browser (variant mounts,
|
|
30
|
+
`connected`, `exit`).
|
|
31
|
+
- `POST /events?token=` — browser to server uplink (`generate`, `steer`,
|
|
32
|
+
`accept`, `discard`, annotations).
|
|
33
|
+
- `GET /poll?token=&timeout=&leaseMs=` — **agent long-poll**, default 600 s.
|
|
34
|
+
- `POST /poll` — **agent reply** (`done`/`steer_done`/`error`/`complete`).
|
|
35
|
+
- `GET /source?token=&path=`, `GET /design-system.json`, `POST /annotation`,
|
|
36
|
+
`GET /health` (unauthenticated liveness), `GET /stop`.
|
|
37
|
+
- CORS allows loopback origin **or** a valid token.
|
|
38
|
+
|
|
39
|
+
2. **A set of stateless per-call CLI mutators** that read/write project source and
|
|
40
|
+
`.impeccable/live/` state: `live-wrap.mjs` (write the variant wrapper into
|
|
41
|
+
source), `live-accept.mjs` (stitch the chosen variant into source / carbonize),
|
|
42
|
+
`live-complete.mjs` (final gate), `live-status.mjs` / `live-resume.mjs`
|
|
43
|
+
(recovery), `live-inject.mjs` (inject the `<script>` tag).
|
|
44
|
+
|
|
45
|
+
3. **File-based canonical state** under `<appRoot>/.impeccable/live/`: an
|
|
46
|
+
append-only per-session JSONL journal (`sessions/<id>.jsonl`) is the source of
|
|
47
|
+
truth; plus `server.json`, `config.json`, `roots.json`, accept receipts, and
|
|
48
|
+
per-file locks. Sessions survive helper restart via journal replay.
|
|
49
|
+
|
|
50
|
+
The pi-impeccable harness extension is just sugar over this: it spawns the poll,
|
|
51
|
+
maps events to prompts, and wraps `--reply`. **The engine underneath is HTTP plus
|
|
52
|
+
files.** That is the whole reason a product can drive it.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Answering the five spike questions
|
|
57
|
+
|
|
58
|
+
### (a) Can wiki-viewer run/embed the helper server? — YES
|
|
59
|
+
|
|
60
|
+
`live-server.mjs --background` spawns a detached child, prints the connection JSON,
|
|
61
|
+
and exits. wiki-viewer's server can spawn it exactly as `app-runner.ts` already
|
|
62
|
+
spawns child processes (module-level singleton Map, keyed per workspace). Boot via
|
|
63
|
+
`live.mjs --target <appRoot>` resolves and persists `roots.json`, so every helper
|
|
64
|
+
call is cwd-independent afterward.
|
|
65
|
+
|
|
66
|
+
- Auth is a single token from `server.json`. The product reads it server-side and
|
|
67
|
+
never exposes it in logs (matches wiki-viewer's existing token discipline).
|
|
68
|
+
- `GET /health` gives a clean readiness probe, same pattern as `app-runner`'s port
|
|
69
|
+
probe.
|
|
70
|
+
- Lifecycle keys naturally onto wiki-viewer's `{workspaceId, relPath}` app model:
|
|
71
|
+
one helper per served surface, torn down with the app.
|
|
72
|
+
|
|
73
|
+
**No blocker.** This is the same shape of work wiki-viewer already does for node
|
|
74
|
+
apps.
|
|
75
|
+
|
|
76
|
+
### (b) Can wiki-viewer inject `live.js` for both served apps and static HTML? — YES, and the seam already exists
|
|
77
|
+
|
|
78
|
+
- **Served (node) apps:** `src/app/api/app-proxy/[...path]/route.ts` already
|
|
79
|
+
rewrites upstream HTML at serve time (injects `<base href>`, `fetch`/`XHR`
|
|
80
|
+
shims, a `<script>` block before `</head>`). Injecting the Impeccable prelude
|
|
81
|
+
globals plus a `<script src=".../live.js">` at that exact point is a natural
|
|
82
|
+
extension. This is **better** than `live-inject.mjs`'s source-file editing: it is
|
|
83
|
+
serve-time only, touches no source, and needs no `--remove` cleanup.
|
|
84
|
+
- **Static HTML files:** `website-viewer.tsx` renders the file (today via an
|
|
85
|
+
opaque-origin `srcDoc` sandbox). The same injection happens at render.
|
|
86
|
+
|
|
87
|
+
**Caveat that shapes the spec:** the stock `live.js` expects to reach the helper
|
|
88
|
+
directly at `http://localhost:8400` and reads prelude globals
|
|
89
|
+
`__IMPECCABLE_PORT__` / `__IMPECCABLE_TOKEN__`. That is fine only when the browser
|
|
90
|
+
is on the same machine as the helper. See (c)/transport below — for a remote
|
|
91
|
+
wiki-viewer the product must point those globals at proxied paths on its own
|
|
92
|
+
origin. Injection itself is not the hard part; the transport target is.
|
|
93
|
+
|
|
94
|
+
### (c) Can HMR hot-swap through the proxy, with fetch-source fallback for static? — NO for HMR (confirmed), but not a blocker
|
|
95
|
+
|
|
96
|
+
The engine's hot-swap model: `live-wrap`/variant writes edit **real source files**;
|
|
97
|
+
the app's own dev server HMR recompiles and pushes the swap; the browser overlay
|
|
98
|
+
re-mounts. For no-HMR surfaces (static HTML), the overlay fetches source directly
|
|
99
|
+
via the `--file` reply and swaps client-side.
|
|
100
|
+
|
|
101
|
+
- **Static HTML fetch-source fallback:** clean fit. wiki-viewer serves the file and
|
|
102
|
+
can serve `/source`-equivalent reads; no dev server needed.
|
|
103
|
+
- **Dynamic apps / HMR: CONFIRMED does not pass through the proxy today.** Verified
|
|
104
|
+
by inspection (convergent, not by a browser run):
|
|
105
|
+
1. No `upgrade`/WebSocket handler exists anywhere in `bin/` or `src/` (grep clean).
|
|
106
|
+
2. The proxy **strips** the `upgrade` header as hop-by-hop (`route.ts:20`) and the
|
|
107
|
+
route is an HTTP GET/POST handler; a Next App Router route handler cannot
|
|
108
|
+
service a WebSocket upgrade at all.
|
|
109
|
+
3. The optional HTTPS front-proxy (`bin/cli/serve.js:402`) only pipes `req -> res`;
|
|
110
|
+
no `server.on('upgrade')`.
|
|
111
|
+
4. Apps mostly run without HMR regardless: `defaultScript` prefers
|
|
112
|
+
`start > preview(built) > dev` (`app-runner.ts:139-144`), so the HMR `dev` mode
|
|
113
|
+
is the last-resort default.
|
|
114
|
+
|
|
115
|
+
**Why this is not a blocker.** Impeccable's variant swap is fundamentally a
|
|
116
|
+
browser-side operation the overlay performs; the app framework's HMR is only the
|
|
117
|
+
"smooth" way to get the freshly written source rendered. Two live options for
|
|
118
|
+
dynamic apps without WS work:
|
|
119
|
+
- **Full-reload-through-proxy:** after a variant write, force a page reload. The
|
|
120
|
+
page reloads over plain HTTP (already handled by the proxy's SW/bootstrap
|
|
121
|
+
machinery), the new markup renders, the overlay re-attaches via its durable
|
|
122
|
+
per-origin session. Functionally correct; loses only the flicker-free feel.
|
|
123
|
+
- **fetch-source fallback:** works directly for raw/static-HTML-shaped surfaces
|
|
124
|
+
(no dev server needed). It does **not** help compiled-framework apps, whose
|
|
125
|
+
rendered DOM is not their source, which is exactly why those need HMR or reload.
|
|
126
|
+
|
|
127
|
+
If the smooth HMR experience is wanted for compiled apps, the real work is adding
|
|
128
|
+
a WebSocket-upgrade passthrough: attach `server.on('upgrade')` on wiki-viewer's
|
|
129
|
+
standalone server and forward the socket to the child dev-server port (bounded,
|
|
130
|
+
but genuinely new proxy code). That is a deliberate scope decision, not a
|
|
131
|
+
prerequisite for shipping.
|
|
132
|
+
|
|
133
|
+
**Net:** static-HTML surface is unaffected and is the correct tracer bullet.
|
|
134
|
+
Dynamic-app hot-swap works via full-reload today, or gets the smooth path later via
|
|
135
|
+
WS-upgrade passthrough.
|
|
136
|
+
|
|
137
|
+
### (d) Can carbonize-on-accept map onto wiki-viewer write safety? — YES, but this is the real integration work
|
|
138
|
+
|
|
139
|
+
`live-accept.mjs` writes directly to source with its own safety layer: per-file
|
|
140
|
+
locks (`.impeccable/live/locks/<sha>.lock`, pid-liveness sweep), accept receipts
|
|
141
|
+
for idempotency (`accept-receipts/<id>.json`, conflicting op returns
|
|
142
|
+
`accept_receipt_conflict`), a generated-file guard, and a carbonize stitch-in that
|
|
143
|
+
a later step rewrites to permanent form. `live-complete.mjs` gates on a
|
|
144
|
+
`source_dirty` scan for leftover markers.
|
|
145
|
+
|
|
146
|
+
wiki-viewer has its **own** parallel safety spine: `resolveWorkspacePath()`
|
|
147
|
+
containment, `proper-lockfile` write locks, collab-state `409 COLLAB_ACTIVE`
|
|
148
|
+
enforcement, and the web-tweak accept path's base-hash drift guard ("Accept is the
|
|
149
|
+
only thing that writes the file, iff hashes still match").
|
|
150
|
+
|
|
151
|
+
Two write/lock systems over the same files is the core hazard. Resolution options
|
|
152
|
+
(a spec decision, not a blocker):
|
|
153
|
+
|
|
154
|
+
- **Preferred:** route Impeccable's source writes through wiki-viewer's own write
|
|
155
|
+
path so containment, collab-state, and locking stay authoritative. Concretely,
|
|
156
|
+
the product performs the accept mutation using wiki-viewer's writer, reusing
|
|
157
|
+
Impeccable's *logic* (marker find, variant extract, carbonize) but not its raw
|
|
158
|
+
`fs.writeFile` + its own lockfile. Impeccable's accept logic is mechanical and
|
|
159
|
+
self-contained enough to host.
|
|
160
|
+
- **Alternative:** let `live-accept.mjs` write, but fence it: ensure the target is
|
|
161
|
+
never a collab-`active` markdown file, confirm `.impeccable/` is path-denied and
|
|
162
|
+
gitignored (it is designed to be), and accept two lock systems that only ever
|
|
163
|
+
contend under concurrent edits (rare, and each is pid/owner-scoped).
|
|
164
|
+
|
|
165
|
+
Either way, `.impeccable/live/` is runtime data: must be added to `resolveWorkspacePath`'s
|
|
166
|
+
denied segments and gitignore, same treatment as `.proof`/`.git`.
|
|
167
|
+
|
|
168
|
+
### (e) Can the two poll loops reconcile? — YES, clean 1:1 mapping
|
|
169
|
+
|
|
170
|
+
wiki-viewer's attached-agent channel (`src/lib/proof/live/store.ts`) is already a
|
|
171
|
+
poll/reply state machine strikingly close to Impeccable's:
|
|
172
|
+
|
|
173
|
+
| Impeccable engine | wiki-viewer live/store |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `GET /poll` long-poll (600 s) | agent held long-poll (25 s / 400 ms tick) |
|
|
176
|
+
| event `type`: generate/steer/accept/discard/exit | request `kind`: generate/steer/accept/discard/exit (+ web.* kinds) |
|
|
177
|
+
| session journal `sessions/<id>.jsonl` | `live.db` sessions + requests, monotonic `seq` |
|
|
178
|
+
| pending → leased → replied | pending → delivered → working → resolved |
|
|
179
|
+
| one pending event per lease | one-outstanding-per-session (`409 OUTSTANDING_REQUEST`) |
|
|
180
|
+
| idempotency via accept receipts | idempotency key `live:<requestId>` |
|
|
181
|
+
| accept = only write, receipt-guarded | Accept = only write, base-hash-guarded |
|
|
182
|
+
|
|
183
|
+
The natural topology (**"product hosts engine, existing agent generates"**):
|
|
184
|
+
|
|
185
|
+
1. wiki-viewer spawns the helper and injects `live.js` (served surface or static).
|
|
186
|
+
2. The browser overlay talks to the helper (directly on localhost, or via proxied
|
|
187
|
+
endpoints when remote — see transport risk).
|
|
188
|
+
3. wiki-viewer's **server** runs the Impeccable `/poll` loop (not a chat agent).
|
|
189
|
+
4. On a `generate` event, wiki-viewer translates it into a request on its **own**
|
|
190
|
+
attached-agent channel (`live/store`), so the user's existing chat agent (AI
|
|
191
|
+
panel / MCP) produces the variants it already knows how to produce.
|
|
192
|
+
5. wiki-viewer applies the variant writes (via `live-wrap` logic) and replies
|
|
193
|
+
`done --file` to the Impeccable `/poll`; HMR/fetch-source mounts them.
|
|
194
|
+
6. On `accept`, wiki-viewer runs the accept/carbonize mutation through its write
|
|
195
|
+
path and completes.
|
|
196
|
+
|
|
197
|
+
This preserves wiki-viewer's "live is speculative until Accept" invariant and reuses
|
|
198
|
+
the agent channel that already exists, rather than duplicating it.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Risks, ranked
|
|
203
|
+
|
|
204
|
+
1. **Browser-to-helper transport when wiki-viewer is remote (highest).** `live.js`
|
|
205
|
+
assumes it can reach `localhost:8400` from the browser and read
|
|
206
|
+
`__IMPECCABLE_PORT__/__IMPECCABLE_TOKEN__`. wiki-viewer serves the app page from
|
|
207
|
+
*its* origin, and the browser may be on a different machine than the helper. The
|
|
208
|
+
product must proxy the helper's `/live.js`, `/events` (SSE), `/poll`-uplink,
|
|
209
|
+
`/source`, `/design-system.json`, `/annotation` through wiki-viewer's own origin
|
|
210
|
+
and set the injected globals to those proxied paths. This is a defined, bounded
|
|
211
|
+
bridge, but it is real code and it is the make-or-break for non-localhost use.
|
|
212
|
+
2. **HMR WebSocket passthrough in app-proxy (CONFIRMED absent).** See (c). Not a
|
|
213
|
+
shipping blocker: static HTML is unaffected; dynamic apps work via
|
|
214
|
+
full-reload-through-proxy. The smooth HMR path for compiled apps is optional
|
|
215
|
+
later work (add `server.on('upgrade')` forwarding to the child dev port).
|
|
216
|
+
3. **Dual source-write/lock systems (defined, needs a decision).** See (d). Prefer
|
|
217
|
+
routing writes through wiki-viewer's writer.
|
|
218
|
+
4. **Runtime-dir hygiene.** `.impeccable/live/` must be path-denied
|
|
219
|
+
(`resolveWorkspacePath`) and gitignored so it is never served, traversed, or
|
|
220
|
+
committed. Low effort, must not be forgotten.
|
|
221
|
+
5. **Auth/CSRF surface.** The helper's token is loopback-scoped; once proxied
|
|
222
|
+
through wiki-viewer, the proxied endpoints inherit wiki-viewer's session/agent
|
|
223
|
+
auth and CSRF Origin checks. Straightforward but must be explicit in the spec.
|
|
224
|
+
|
|
225
|
+
None of these is a "does not work." They are the shape of the bridge.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Recommendation
|
|
230
|
+
|
|
231
|
+
**Proceed to `to-spec` for the unified Impeccable-live tweak, topology "product
|
|
232
|
+
hosts engine, existing agent generates."** Risk #2 is now settled: HMR does not
|
|
233
|
+
pass through the proxy today, but it is not required to ship. Scope the spec so the
|
|
234
|
+
first slice is the static-HTML surface (no HMR, no proxy transport), dynamic apps
|
|
235
|
+
land next via full-reload-through-proxy, and smooth compiled-app HMR is an explicit
|
|
236
|
+
later ticket (WS-upgrade passthrough) rather than a gate.
|
|
237
|
+
|
|
238
|
+
Do **not** fall back to the canceled isolated-origin/iframe architecture. The
|
|
239
|
+
pivot rationale holds: Impeccable injects into the app's own page, so there is no
|
|
240
|
+
parent to protect, which is exactly what deletes the old security problem.
|
|
241
|
+
|
|
242
|
+
### Suggested first vertical slice (tracer bullet)
|
|
243
|
+
Static HTML surface only (no HMR, no proxy transport complexity): spawn helper,
|
|
244
|
+
inject `live.js` into the static preview, run the product-side `/poll` loop, wire
|
|
245
|
+
one `generate` through the existing agent channel, land one `accept` through
|
|
246
|
+
wiki-viewer's writer. That proves the whole spine end to end on the simplest
|
|
247
|
+
surface, then dynamic apps (proxy transport + HMR) layer on top.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Evidence base
|
|
252
|
+
- Impeccable engine: `/home/sil/.pi/agent/skills/impeccable/scripts/{live,live-server,live-poll,live-inject,live-wrap,live-accept,live-complete,live-status,live-resume}.mjs`; `reference/live.md`, `reference/live-setup.md`.
|
|
253
|
+
- wiki-viewer proxy injection point: `src/app/api/app-proxy/[...path]/route.ts` (serve-time HTML rewrite, `<script>` before `</head>`).
|
|
254
|
+
- Dev/HMR launchable: `src/lib/app-runner.ts` (`defaultScript` start>preview>dev, `detectCmd`, port probe).
|
|
255
|
+
- Existing agent channel: `src/lib/proof/live/store.ts` (session/request state machine); companion write-up `docs/web-tweak-live-channel.md`.
|
|
256
|
+
- Static-HTML tweak today: `src/components/editor/website-viewer.tsx`; `src/lib/web-tweak/*`.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Current Suggest feature — assessment (main @ 23a5129)
|
|
2
|
+
|
|
3
|
+
Verdict: REAL propose→pending→accept-writes/reject-discards pipeline with a
|
|
4
|
+
proper "Suggesting mode", but COARSE and side-mounted, which is why it reads like
|
|
5
|
+
comments.
|
|
6
|
+
|
|
7
|
+
## Current flow (file:line)
|
|
8
|
+
- Entry: bubble menu "Suggest edit" (bubble-menu.tsx:236-247) -> openSuggestForSelection (editor.tsx:973-975).
|
|
9
|
+
- Block resolution: resolveSelectionBlock picks the WHOLE top-level block; selection offsets computed but UNUSED by Suggest (editor.tsx:267-361, 320, 352-358).
|
|
10
|
+
- Popover: SuggestEditPopover, kind chips Replace/Insert after/Insert before/Delete, whole-block markdown textarea + optional reason (suggest-edit-popover.tsx:47-111). Posts {type:"suggestion.add", ref, kind, markdown?, basisDetail?} with 409 retry once.
|
|
11
|
+
- Suggesting mode (typing capture): status-bar radio Editing/Suggesting (editor.tsx:1047-1068); banner (852-857). flush() diffs EACH top-level block vs snapshot: changed->replace, gone->delete, appended->insertAfter (use-suggestion-capture.ts:70-124), then REVERTS editor to snapshot. No autosave in suggesting (editor-store.ts:324-328,353-356).
|
|
12
|
+
- Render: pending suggestions absolutely positioned OUTSIDE ProseMirror DOM (suggestion-card.tsx:44-48; editor.tsx:901-933). Two static panes "current / proposed" (suggestion-card.tsx:116-137).
|
|
13
|
+
- Accept: suggestion.accept -> archive + supersede other pending same-ref (ops-applier.ts:872-891) + apply block.replace/insertAfter/insertBefore/delete (893-900) -> writes .md (972). Guard: 409 retry once.
|
|
14
|
+
- Reject: archive, file unchanged (902-909).
|
|
15
|
+
|
|
16
|
+
## Capability vs Google Docs suggesting
|
|
17
|
+
- Suggesting mode toggle: PRESENT.
|
|
18
|
+
- Edits captured, never written: PRESENT.
|
|
19
|
+
- Inline text-range diffs (word-level): ABSENT (whole-block string equality).
|
|
20
|
+
- In-context redline in prose: ABSENT (overlay card beside block, not in editor DOM).
|
|
21
|
+
- Review diff clarity: PARTIAL (two static panes, no word-level diff).
|
|
22
|
+
- Per-change accept/reject in place: PARTIAL (buttons on overlay card).
|
|
23
|
+
- Accept writes doc: PRESENT. Reject discards: PRESENT.
|
|
24
|
+
- Overlapping suggestions same range: DEGENERATE (N cards pile on same pixel, no switcher/offset).
|
|
25
|
+
- Multi-author: PARTIAL (by-label; coarse whole-sidecar reload on fs events, no targeted push).
|
|
26
|
+
- View-mode Suggest button: ABSENT despite ux-contracts §6.1 promising it (contract drift; only Comment has a view-mode button).
|
|
27
|
+
|
|
28
|
+
## Gaps to make it real (T1 = perception, T2 = granularity)
|
|
29
|
+
T1:
|
|
30
|
+
1. In-doc redline rendering: TipTap mark extension (inserted=green underline, deleted=strikethrough) mounted INSIDE ProseMirror, replacing the absolute overlay card (editor.tsx:901-933, suggestion-card.tsx:44-48).
|
|
31
|
+
2. Word-level diff in review affordance (suggestion-card.tsx:116-137) even before/if kept as a card.
|
|
32
|
+
3. In-place accept/reject at the caret + overlapping-suggestion switcher ("1/3").
|
|
33
|
+
4. View-mode Suggest button (parity with ViewModeCommentButton; view-mode-comment-button.tsx:26, editor.tsx:987).
|
|
34
|
+
T2:
|
|
35
|
+
5. Text-range suggestion model: from/to offsets within block markdown instead of whole-block; touches capture (use-suggestion-capture.ts:70-124), proposal (suggest-edit-popover.tsx:85-95), model (types.ts:167-200), applier (ops-applier.ts:830-900).
|
|
36
|
+
6. Accept-merge: real text-range merge on accept instead of whole-block block.replace clobber (ops-applier.ts:893-900), for edits made while the block changed concurrently.
|
|
37
|
+
|
|
38
|
+
## Cut impact (why we are NOT cutting)
|
|
39
|
+
suggestion.accept is the ONLY human path that applies an edit (ux-contracts §6.2 :562). It is the review surface the tweak re-home routes the agent's rewrite into. Cutting kills the tweak review loop, orphans agent suggestion.add ops, breaks suggesting mode into silent data-loss, and breaks ux-contracts §6.1-6.3 + activity feed + tests.
|