diff --git a/tests/web/composer-drafts.spec.ts b/tests/web/composer-drafts.spec.ts
index 76a11f27..4cbf3923 100644
--- a/tests/web/composer-drafts.spec.ts
+++ b/tests/web/composer-drafts.spec.ts
@@ -1201,3 +1201,75 @@ it("does not write over unread storage after an initial read failure, and hydrat
view.select("B");
expect(view.input.value).toBe("B from before");
});
+
+it("keeps cited review identity and excerpt in the draft across later renders and bounds long excerpts", async () => {
+ const view = setup();
+ const source = {
+ repositoryRoot: "/workspace",
+ comparison: "unstaged" as const,
+ revision: "displayed-v1",
+ };
+ view.update({
+ addSourcesRequest: {
+ sessionId: "A",
+ path: "/workspace/a.jsonl",
+ revision: 1,
+ feedback: {
+ filePath: "src/old.ts",
+ side: "old",
+ line: 8,
+ code: "old text",
+ source,
+ },
+ },
+ });
+ const cited = view.input.value;
+ expect(cited).toContain(JSON.stringify(source));
+ expect(cited).toContain("> old text");
+ view.update({ snapshot: { ...snapshot(), cursor: 2 } });
+ expect(view.input.value).toBe(cited);
+ view.update({
+ addSourcesRequest: {
+ sessionId: "A",
+ path: "/workspace/a.jsonl",
+ revision: 2,
+ feedback: {
+ kind: "plan",
+ sessionId: "A",
+ sessionPath: "/workspace/a.jsonl",
+ entryId: "result-entry",
+ toolCallId: "plan-call",
+ text: "x".repeat(9000),
+ },
+ },
+ });
+ expect(view.input.value).toContain('"entryId":"result-entry"');
+ expect(view.input.value).toContain('"toolCallId":"plan-call"');
+ expect(view.input.value).toContain("x".repeat(8000));
+ expect(view.input.value).not.toContain("x".repeat(8001));
+ expect(view.input.value).toContain(i18n.t("sourceFeedbackTruncated"));
+ expect(view.sendPrompt).not.toHaveBeenCalled();
+ const submitted = view.input.value;
+ fireEvent.click(screen.getByRole("button", { name: i18n.t("send") }));
+ await waitFor(() => expect(view.sendPrompt).toHaveBeenCalledWith(submitted));
+});
+
+it("rejects plan text whose own Session identity differs from its draft request", () => {
+ const view = setup();
+ view.update({
+ addSourcesRequest: {
+ sessionId: "A",
+ path: "/workspace/a.jsonl",
+ revision: 1,
+ feedback: {
+ kind: "plan",
+ sessionId: "B",
+ sessionPath: "/workspace/b.jsonl",
+ entryId: "result-entry",
+ text: "wrong session",
+ },
+ },
+ });
+ expect(view.input.value).toBe("");
+ expect(view.sendPrompt).not.toHaveBeenCalled();
+});
diff --git a/tests/web/plan-card.spec.ts b/tests/web/plan-card.spec.ts
index 46c98640..df043d66 100644
--- a/tests/web/plan-card.spec.ts
+++ b/tests/web/plan-card.spec.ts
@@ -15,6 +15,8 @@ import {
type WebLiveMessage,
type WebSnapshot,
} from "../../web/protocol/types.ts";
+import type { PlanTextReference } from "../../web/ui/src/features/review/review-reference.ts";
+import { PlanCard } from "../../web/ui/src/features/transcript/PlanCard.tsx";
import { Transcript } from "../../web/ui/src/features/transcript/Transcript.tsx";
import { i18n } from "../../web/ui/src/i18n.ts";
@@ -65,6 +67,7 @@ function transcript(
messages: WebLiveMessage[],
live: WebLiveMessage[] = [],
active = true,
+ onReferencePlan?: (reference: PlanTextReference) => void,
) {
const snapshot: WebSnapshot = {
protocolVersion: 1,
@@ -108,6 +111,7 @@ function transcript(
thinkingDurations: {},
scrollToBottom: 0,
onResend: async () => true,
+ onReferencePlan,
}),
);
}
@@ -370,3 +374,60 @@ it("collapses without approving and copies the actual plan with explicit copy fe
vi.unstubAllGlobals();
}
});
+
+it("references selected plan text with its result identity and clears selection for a replacement result", () => {
+ const onReference = vi.fn();
+ const plan = result("# Plan\n\nSelected paragraph.");
+ const node = (message: WebLiveMessage, sessionPath = "/tmp/session") =>
+ createElement(
+ I18nextProvider,
+ { i18n },
+ createElement(PlanCard, {
+ result: message,
+ sessionId: "session",
+ sessionPath,
+ entryId: "result-entry",
+ onReference,
+ }),
+ );
+ const view = render(node(plan));
+ const button = screen.getByRole("button", {
+ name: i18n.t("planCardReference"),
+ }) as HTMLButtonElement;
+ expect(button.disabled).toBe(true);
+ const paragraph = screen.getByText("Selected paragraph.");
+ const range = document.createRange();
+ range.selectNodeContents(paragraph);
+ window.getSelection()?.removeAllRanges();
+ window.getSelection()?.addRange(range);
+ fireEvent.mouseUp(paragraph);
+ expect(onReference).not.toHaveBeenCalled();
+ expect(button.disabled).toBe(false);
+ fireEvent.click(button);
+ expect(onReference).toHaveBeenCalledExactlyOnceWith({
+ kind: "plan",
+ sessionId: "session",
+ sessionPath: "/tmp/session",
+ entryId: "result-entry",
+ toolCallId: "plan-1",
+ text: "Selected paragraph.",
+ });
+ view.rerender(node(result("# Plan\n\nRevised paragraph.")));
+ expect(button.disabled).toBe(true);
+ expect(screen.getByText(i18n.t("planCardNotApproval"))).toBeTruthy();
+ view.rerender(node(plan, "/tmp/copied-session"));
+ expect(button.disabled).toBe(true);
+});
+
+it("offers plan references through the transcript only for the controlled Session", () => {
+ const callback = vi.fn();
+ const view = render(transcript([call, result()], [], false, callback));
+ expect(
+ screen.queryByRole("button", { name: i18n.t("planCardReference") }),
+ ).toBeNull();
+ view.rerender(transcript([call, result()], [], true, callback));
+ expect(
+ screen.getByRole("button", { name: i18n.t("planCardReference") }),
+ ).toBeTruthy();
+ expect(callback).not.toHaveBeenCalled();
+});
diff --git a/tests/web/review-panel.spec.ts b/tests/web/review-panel.spec.ts
index 6dcc18fa..0a616b74 100644
--- a/tests/web/review-panel.spec.ts
+++ b/tests/web/review-panel.spec.ts
@@ -1197,3 +1197,55 @@ it("keeps hidden diff reads and focus dormant, then cancels a read when hidden a
rerender(node(true, "new-file.ts"));
expect(readFile).toHaveBeenCalledTimes(2);
});
+
+it("references the pinned displayed review revision after a newer summary arrives", async () => {
+ const quote = vi.fn();
+ const node = (revision: string) =>
+ withI18n(
+ createElement(ReviewPanel, {
+ review: {
+ result: { ok: true, snapshot: { ...snapshot, revision } },
+ loading: false,
+ error: null,
+ refresh: async () => {},
+ },
+ initialFilePath: snapshot.files[0]!.path,
+ onClose: () => {},
+ onReferenceLine: quote,
+ }),
+ );
+ const view = render(node("displayed-v1"));
+ await act(async () => {});
+ view.rerender(node("latest-v2"));
+ fireEvent.click(
+ screen.getByRole("button", {
+ name: i18n.t("reviewSelectNewLine", { line: 1 }),
+ }),
+ );
+ fireEvent.click(
+ screen.getByRole("button", { name: i18n.t("reviewQuoteLine") }),
+ );
+ expect(quote).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ source: {
+ repositoryRoot: snapshot.repositoryRoot,
+ comparison: snapshot.comparison,
+ revision: "displayed-v1",
+ },
+ }),
+ );
+ fireEvent.click(screen.getByRole("button", { name: "Show latest diff" }));
+ fireEvent.click(
+ screen.getByRole("button", {
+ name: i18n.t("reviewSelectNewLine", { line: 1 }),
+ }),
+ );
+ fireEvent.click(
+ screen.getByRole("button", { name: i18n.t("reviewQuoteLine") }),
+ );
+ expect(quote).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ source: expect.objectContaining({ revision: "latest-v2" }),
+ }),
+ );
+});
diff --git a/web/dist/app.js b/web/dist/app.js
index 71b5d3f9..5d7d23eb 100644
--- a/web/dist/app.js
+++ b/web/dist/app.js
@@ -1,15 +1,15 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["app-Tooltip.js","app-rolldown-runtime.js","app-jsx-runtime.js","app-common.js","app-MermaidPreview.js","app-preload-helper.js","app-useTranslation.js","app-x.js","app-purify.es.js","app-chunk-Y2CYZVJY.js","app-src.js","app-chunk-O7XYJQB3.js","app-chunk-ZIGJFQKS.js","app-dist.js","app-chunk-6AEJRKK7.js","app-chunk-742MDFTN.js","app-chunk-MBY4JIJT.js","app-chunk-7PRAP22T.js","app-chunk-7INBJB4K.js","app-chunk-J5ZVWO5B.js","app-rough.esm.js","app-chunk-5DYCD2WN.js","app-chunk-Z7XXMR3K.js","app-line.js","app-path.js","app-array.js","app-chunk-UA2S7LBM.js","app-chunk-GNY47TPC.js","app-chunk-DUW6YSOI.js","app-chunk-LNGE3PJU.js","app-pdf.js","app-InteractiveTerminal.js","app-Dialog.js"])))=>i.map(i=>d[i]);
-import{i as e,n as t,t as n}from"./app-rolldown-runtime.js";import{n as r,t as i}from"./app-jsx-runtime.js";import{A as a,C as o,D as s,F as c,G as l,H as u,I as d,J as f,K as p,L as m,N as h,O as g,P as _,R as v,S as y,T as b,U as x,V as S,W as C,X as w,Y as T,Z as E,_ as D,a as O,b as k,c as A,d as j,f as M,g as N,h as P,i as F,j as I,k as ee,l as L,m as R,o as te,p as z,q as B,r as ne,s as re,t as V,u as ie,v as ae,w as oe,x as se,y as ce,z as le}from"./app-Tooltip.js";import{C as H,_ as ue,a as de,b as fe,d as pe,g as me,h as he,i as ge,m as _e,n as ve,o as ye,r as be,s as xe,t as Se,u as Ce,v as we,y as Te}from"./app-Dialog.js";import{t as Ee}from"./app-preload-helper.js";import{a as U,i as De,n as Oe,r as ke,t as W}from"./app-useTranslation.js";import{n as Ae,r as G,t as je}from"./app-x.js";import{t as Me}from"./app-purify.es.js";(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),t.credentials=e.crossOrigin===`use-credentials`?`include`:e.crossOrigin===`anonymous`?`omit`:`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var Ne=n((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0`u`?null:document;function Df(e,t,n){var r=Ef;if(r&&typeof t==`string`&&t){var i=kt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),xf.has(i)||(xf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Bd(t,`link`,e),dt(t),r.head.appendChild(t)))}}function Of(e){Cf.D(e),Df(`dns-prefetch`,e,null)}function kf(e,t){Cf.C(e,t),Df(`preconnect`,e,t)}function Af(e,t,n){Cf.L(e,t,n);var r=Ef;if(r&&e&&t){var i=`link[rel="preload"][as="`+kt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+kt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+kt(n.imageSizes)+`"]`)):i+=`[href="`+kt(e)+`"]`;var a=i;switch(t){case`style`:a=If(e);break;case`script`:a=Bf(e)}bf.has(a)||(e=p({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),bf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(Lf(a))||t===`script`&&r.querySelector(Vf(a))||(t=r.createElement(`link`),Bd(t,`link`,e),dt(t),r.head.appendChild(t)))}}function jf(e,t){Cf.m(e,t);var n=Ef;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+kt(r)+`"][href="`+kt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Bf(e)}if(!bf.has(a)&&(e=p({rel:`modulepreload`,href:e},t),bf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Vf(a)))return}r=n.createElement(`link`),Bd(r,`link`,e),dt(r),n.head.appendChild(r)}}}function Mf(e,t,n){Cf.S(e,t,n);var r=Ef;if(r&&e){var i=ut(r).hoistableStyles,a=If(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(Lf(a)))s.loading=5;else{e=p({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=bf.get(a))&&Wf(e,n);var c=o=r.createElement(`link`);dt(c),Bd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Uf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Nf(e,t){Cf.X(e,t);var n=Ef;if(n&&e){var r=ut(n).hoistableScripts,i=Bf(e),a=r.get(i);a||(a=n.querySelector(Vf(i)),a||(e=p({src:e,async:!0},t),(t=bf.get(i))&&Gf(e,t),a=n.createElement(`script`),dt(a),Bd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Pf(e,t){Cf.M(e,t);var n=Ef;if(n&&e){var r=ut(n).hoistableScripts,i=Bf(e),a=r.get(i);a||(a=n.querySelector(Vf(i)),a||(e=p({src:e,async:!0,type:`module`},t),(t=bf.get(i))&&Gf(e,t),a=n.createElement(`script`),dt(a),Bd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Ff(e,t,n,r){var i=(i=V.current)?Sf(i):null;if(!i)throw Error(a(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=If(n.href),n=ut(i).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=If(n.href);var o=ut(i).hoistableStyles,s=o.get(e);if(s||(i=i.ownerDocument||i,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=i.querySelector(Lf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),bf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},bf.set(e,n),o||zf(i,e,n,s.state))),t&&r===null)throw Error(a(528,``));return s}if(t&&r!==null)throw Error(a(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Bf(n),n=ut(i).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(a(444,e))}}function If(e){return`href="`+kt(e)+`"`}function Lf(e){return`link[rel="stylesheet"][`+e+`]`}function Rf(e){return p({},e,{"data-precedence":e.precedence,precedence:null})}function zf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Bd(t,`link`,n),dt(t),e.head.appendChild(t))}function Bf(e){return`[src="`+kt(e)+`"]`}function Vf(e){return`script[async]`+e}function Hf(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+kt(n.href)+`"]`);if(r)return t.instance=r,dt(r),r;var i=p({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),dt(r),Bd(r,`style`,i),Uf(r,n.precedence,e),t.instance=r;case`stylesheet`:i=If(n.href);var o=e.querySelector(Lf(i));if(o)return t.state.loading|=4,t.instance=o,dt(o),o;r=Rf(n),(i=bf.get(i))&&Wf(r,i),o=(e.ownerDocument||e).createElement(`link`),dt(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Bd(o,`link`,r),t.state.loading|=4,Uf(o,n.precedence,e),t.instance=o;case`script`:return o=Bf(n.src),(i=e.querySelector(Vf(o)))?(t.instance=i,dt(i),i):(r=n,(i=bf.get(o))&&(r=p({},n),Gf(r,i)),e=e.ownerDocument||e,i=e.createElement(`script`),dt(i),Bd(i,`link`,r),e.head.appendChild(i),t.instance=i);case`void`:return null;default:throw Error(a(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Uf(r,n.precedence,e));return t.instance}function Uf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o`u`?null:document;function Df(e,t,n){var r=Ef;if(r&&typeof t==`string`&&t){var i=kt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),xf.has(i)||(xf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Bd(t,`link`,e),dt(t),r.head.appendChild(t)))}}function Of(e){Cf.D(e),Df(`dns-prefetch`,e,null)}function kf(e,t){Cf.C(e,t),Df(`preconnect`,e,t)}function Af(e,t,n){Cf.L(e,t,n);var r=Ef;if(r&&e&&t){var i=`link[rel="preload"][as="`+kt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+kt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+kt(n.imageSizes)+`"]`)):i+=`[href="`+kt(e)+`"]`;var a=i;switch(t){case`style`:a=If(e);break;case`script`:a=Bf(e)}bf.has(a)||(e=p({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),bf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(Lf(a))||t===`script`&&r.querySelector(Vf(a))||(t=r.createElement(`link`),Bd(t,`link`,e),dt(t),r.head.appendChild(t)))}}function jf(e,t){Cf.m(e,t);var n=Ef;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+kt(r)+`"][href="`+kt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Bf(e)}if(!bf.has(a)&&(e=p({rel:`modulepreload`,href:e},t),bf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Vf(a)))return}r=n.createElement(`link`),Bd(r,`link`,e),dt(r),n.head.appendChild(r)}}}function Mf(e,t,n){Cf.S(e,t,n);var r=Ef;if(r&&e){var i=ut(r).hoistableStyles,a=If(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(Lf(a)))s.loading=5;else{e=p({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=bf.get(a))&&Wf(e,n);var c=o=r.createElement(`link`);dt(c),Bd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Uf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Nf(e,t){Cf.X(e,t);var n=Ef;if(n&&e){var r=ut(n).hoistableScripts,i=Bf(e),a=r.get(i);a||(a=n.querySelector(Vf(i)),a||(e=p({src:e,async:!0},t),(t=bf.get(i))&&Gf(e,t),a=n.createElement(`script`),dt(a),Bd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Pf(e,t){Cf.M(e,t);var n=Ef;if(n&&e){var r=ut(n).hoistableScripts,i=Bf(e),a=r.get(i);a||(a=n.querySelector(Vf(i)),a||(e=p({src:e,async:!0,type:`module`},t),(t=bf.get(i))&&Gf(e,t),a=n.createElement(`script`),dt(a),Bd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Ff(e,t,n,r){var i=(i=B.current)?Sf(i):null;if(!i)throw Error(a(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=If(n.href),n=ut(i).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=If(n.href);var o=ut(i).hoistableStyles,s=o.get(e);if(s||(i=i.ownerDocument||i,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=i.querySelector(Lf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),bf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},bf.set(e,n),o||zf(i,e,n,s.state))),t&&r===null)throw Error(a(528,``));return s}if(t&&r!==null)throw Error(a(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Bf(n),n=ut(i).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(a(444,e))}}function If(e){return`href="`+kt(e)+`"`}function Lf(e){return`link[rel="stylesheet"][`+e+`]`}function Rf(e){return p({},e,{"data-precedence":e.precedence,precedence:null})}function zf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Bd(t,`link`,n),dt(t),e.head.appendChild(t))}function Bf(e){return`[src="`+kt(e)+`"]`}function Vf(e){return`script[async]`+e}function Hf(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+kt(n.href)+`"]`);if(r)return t.instance=r,dt(r),r;var i=p({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),dt(r),Bd(r,`style`,i),Uf(r,n.precedence,e),t.instance=r;case`stylesheet`:i=If(n.href);var o=e.querySelector(Lf(i));if(o)return t.state.loading|=4,t.instance=o,dt(o),o;r=Rf(n),(i=bf.get(i))&&Wf(r,i),o=(e.ownerDocument||e).createElement(`link`),dt(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Bd(o,`link`,r),t.state.loading|=4,Uf(o,n.precedence,e),t.instance=o;case`script`:return o=Bf(n.src),(i=e.querySelector(Vf(o)))?(t.instance=i,dt(i),i):(r=n,(i=bf.get(o))&&(r=p({},n),Gf(r,i)),e=e.ownerDocument||e,i=e.createElement(`script`),dt(i),Bd(i,`link`,r),e.head.appendChild(i),t.instance=i);case`void`:return null;default:throw Error(a(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Uf(r,n.precedence,e));return t.instance}function Uf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o
diff --git a/web/ui/src/features/transcript/Transcript.tsx b/web/ui/src/features/transcript/Transcript.tsx
index 6f9bab06..3e76aae1 100644
--- a/web/ui/src/features/transcript/Transcript.tsx
+++ b/web/ui/src/features/transcript/Transcript.tsx
@@ -101,7 +101,10 @@ import "./provider-outcomes.css";
import "./conversation-navigation.css";
import "./message-branch.css";
+import type { PlanTextReference } from "../review/review-reference.ts";
+
interface TranscriptProps {
+ onReferencePlan?: (reference: PlanTextReference) => void;
label?: string;
notice?: ReactNode;
activityObserved?: boolean;
@@ -3181,6 +3184,7 @@ export function Transcript(props: TranscriptProps) {
sessionId={selectedId}
sessionPath={selectedPath}
entryId={persistedResultEntry?.entryId}
+ onReference={active ? props.onReferencePlan : undefined}
/>
) : isEvidenceTool(part.name) && !child?.outputMissing ? (
@@ -3627,6 +3632,7 @@ export function Transcript(props: TranscriptProps) {
props.forkAvailable,
props.forkPending,
props.onInspectSubagent,
+ props.onReferencePlan,
props.snapshot.runtime.capabilities.subagents,
props.snapshot.runtime.capabilities.workflows?.items,
props.snapshot.preferences.expandThinking,
diff --git a/web/ui/src/i18n.ts b/web/ui/src/i18n.ts
index bd55ea26..4fc975de 100644
--- a/web/ui/src/i18n.ts
+++ b/web/ui/src/i18n.ts
@@ -1679,6 +1679,11 @@ const resources = {
steeringMessagesTitle: "Pending instructions for this turn",
steerStaleTurn:
"That turn has ended or changed. Your draft is kept. Check the updated status and send again.",
+ planCardReference: "Reference selection",
+ planCardReferenceHint: "Select plan text, then add it to your draft",
+ sourceFeedbackPlan: "Plan reference:",
+ sourceFeedbackProjection: "Review version:",
+ sourceFeedbackTruncated: "[Reference excerpt truncated]",
sourceFeedbackNew: "{{path}} · new line {{line}}",
sourceFeedbackOld: "{{path}} · old line {{line}}",
forkMessage: "Fork from here",
@@ -3712,6 +3717,11 @@ const resources = {
steeringMessagesTitle: "当前轮次待处理的纠偏指示",
steerStaleTurn:
"目标轮次已结束或变化。草稿已保留,请确认更新后的状态并重新发送。",
+ planCardReference: "引用所选文字",
+ planCardReferenceHint: "选择计划文字,再添加到输入草稿",
+ sourceFeedbackPlan: "计划引用:",
+ sourceFeedbackProjection: "审阅版本:",
+ sourceFeedbackTruncated: "[引用摘录已截断]",
sourceFeedbackNew: "{{path}} · 新版本第 {{line}} 行",
sourceFeedbackOld: "{{path}} · 旧版本第 {{line}} 行",
forkMessage: "从此处分叉",
diff --git a/web/ui/src/styles.css b/web/ui/src/styles.css
index 641ecdf6..2c40391e 100644
--- a/web/ui/src/styles.css
+++ b/web/ui/src/styles.css
@@ -693,11 +693,13 @@ button.subagent-card-open:hover { background:var(--sidebar-hover); }
.plan-card-toggle { display: flex; flex: 1; min-width: 0; align-items: center; gap: 8px; padding: 6px 8px; text-align: left; font-size: 12px; font-weight: 600; }
.plan-card-toggle > span:first-of-type { color: var(--text); }
.plan-card-disclosure { margin-left: auto; font-size: 11px; font-weight: 400; }
+.plan-card-reference { display: flex; align-items: center; gap: 6px; padding: 6px 8px; font-size: 12px; }
+.plan-card-header .plan-card-reference:disabled { opacity: .5; cursor: default; background: transparent; }
.plan-card-copy { display: grid; place-items: center; width: 36px; flex-shrink: 0; }
.plan-card-body { padding: 4px 16px 16px; }
.plan-card-note { margin: 0 0 12px; color: var(--muted); font-size: 12px; line-height: 1.5; }
-.plan-card-body > .markdown,
-.plan-card-body > .message-full-text > .markdown { overflow-x: auto; }
+.plan-card-text > .markdown,
+.plan-card-text > .message-full-text > .markdown { overflow-x: auto; }
.plan-card .markdown ol { list-style-type: decimal; }
.plan-card .markdown ul { list-style-type: disc; }
.plan-card .markdown ul.contains-task-list { list-style-type: none; }