cli-jaw 2.2.12 → 2.2.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/src/agent/error-classifier.js +11 -2
  2. package/dist/src/agent/error-classifier.js.map +1 -1
  3. package/dist/src/agent/events/claude.js +7 -1
  4. package/dist/src/agent/events/claude.js.map +1 -1
  5. package/dist/src/agent/events/fulltext-bound.js +32 -0
  6. package/dist/src/agent/events/fulltext-bound.js.map +1 -0
  7. package/dist/src/agent/events/grok.js +7 -1
  8. package/dist/src/agent/events/grok.js.map +1 -1
  9. package/dist/src/agent/events/helpers.js +17 -4
  10. package/dist/src/agent/events/helpers.js.map +1 -1
  11. package/dist/src/agent/lifecycle-handler.js +56 -5
  12. package/dist/src/agent/lifecycle-handler.js.map +1 -1
  13. package/dist/src/agent/pi-runtime.js +30 -1
  14. package/dist/src/agent/pi-runtime.js.map +1 -1
  15. package/dist/src/agent/spawn/exit-drain.js +96 -0
  16. package/dist/src/agent/spawn/exit-drain.js.map +1 -0
  17. package/dist/src/agent/spawn/line-buffer.js +26 -0
  18. package/dist/src/agent/spawn/line-buffer.js.map +1 -0
  19. package/dist/src/agent/spawn/process-kill.js +31 -0
  20. package/dist/src/agent/spawn/process-kill.js.map +1 -1
  21. package/dist/src/agent/spawn.js +80 -23
  22. package/dist/src/agent/spawn.js.map +1 -1
  23. package/dist/src/agent/watchdog.js +21 -5
  24. package/dist/src/agent/watchdog.js.map +1 -1
  25. package/dist/src/routes/link-preview.js +14 -0
  26. package/dist/src/routes/link-preview.js.map +1 -1
  27. package/package.json +3 -1
  28. package/public/dist/assets/CodeCanvas-CbfPP7Ad.js +3 -0
  29. package/public/dist/assets/{DocPanel-DtYwv3n3.js → DocPanel-Dz-J_l5M.js} +1 -1
  30. package/public/dist/assets/MarkdownRenderer-CXrilOsG.js +15 -0
  31. package/public/dist/assets/MarkdownRenderer-DZVh_xX2.js +1 -0
  32. package/public/dist/assets/{MilkdownWysiwygEditor-BNoLGbFC.js → MilkdownWysiwygEditor-Em7iC6y9.js} +8 -8
  33. package/public/dist/assets/{react-dom-AWKMTIe7.js → bounded-set-CFlZiEF4.js} +1 -1
  34. package/public/dist/assets/manager-DaN201mN.js +13 -0
  35. package/public/dist/assets/{wiki-link-resolver-BfNXN9px.js → wiki-link-resolver-DyFzBTIQ.js} +1 -1
  36. package/public/dist/manager/index.html +1 -1
  37. package/public/manager/src/SidebarRailRouter.tsx +9 -3
  38. package/public/manager/src/browser-panel/use-embedded-target-sync.ts +19 -1
  39. package/public/manager/src/code/CodeCanvas.tsx +14 -2
  40. package/public/manager/src/code/CodeTranscript.tsx +33 -9
  41. package/public/manager/src/code/CodeWorkbench.tsx +1 -1
  42. package/public/manager/src/code/code-event-dedupe.ts +27 -3
  43. package/public/manager/src/code/use-code-transcript-scroll.ts +28 -3
  44. package/public/manager/src/code/use-throttled-markdown.ts +75 -0
  45. package/public/manager/src/code/useCodeTranscriptVirtualRows.ts +19 -0
  46. package/public/manager/src/electron-metrics.tsx +7 -0
  47. package/public/manager/src/jaw-ceo/useJawCeo.ts +6 -1
  48. package/public/manager/src/jaw-ceo/useJawCeoVirtualTimeline.ts +5 -0
  49. package/public/manager/src/lib/bounded-set.ts +15 -0
  50. package/public/manager/src/lib/fnv1a.ts +14 -0
  51. package/public/manager/src/notes/rendering/CodeBlock.tsx +13 -4
  52. package/public/manager/src/notes/rendering/MarkdownRenderer.tsx +14 -2
  53. package/public/manager/src/notes/rendering/highlight-cache.ts +111 -0
  54. package/public/manager/src/notes/useNotesExternalSync.ts +6 -1
  55. package/scripts/bundle-sidecar.sh +8 -0
  56. package/scripts/check-electron-no-native.cjs +90 -4
  57. package/scripts/check-native-load.cjs +157 -0
  58. package/scripts/check-sidecar-smoke.mjs +151 -0
  59. package/scripts/ensure-native-modules.cjs +222 -18
  60. package/scripts/release-gates.mjs +29 -0
  61. package/scripts/run-electron-builder.mjs +60 -0
  62. package/public/dist/assets/CodeCanvas-BZo3dohZ.js +0 -3
  63. package/public/dist/assets/MarkdownRenderer-CHacL2M_.js +0 -15
  64. package/public/dist/assets/MarkdownRenderer-DyrB9FbZ.js +0 -1
  65. package/public/dist/assets/manager-DL1GygQL.js +0 -13
@@ -19,4 +19,4 @@ import{n as e,r as t,t as n}from"./rolldown-runtime-XQCOJYun.js";import{_ as r,a
19
19
  `,inConstruct:`mathFlowMeta`},{character:`$`,after:t?void 0:`\\$`,inConstruct:`phrasing`},{character:`$`,inConstruct:`mathFlowMeta`},{atBreak:!0,character:`$`,after:`\\$`}],handlers:{math:n,inlineMath:r}};function n(e,t,n,r){let i=e.value||``,a=n.createTracker(r),o=`$`.repeat(Math.max(Oi(i,`$`)+1,2)),s=n.enter(`mathFlow`),c=a.move(o);if(e.meta){let t=n.enter(`mathFlowMeta`);c+=a.move(n.safe(e.meta,{after:`
20
20
  `,before:c,encode:[`$`],...a.current()})),t()}return c+=a.move(`
21
21
  `),i&&(c+=a.move(i+`
22
- `)),c+=a.move(o),s(),c}function r(e,n,r){let i=e.value||``,a=1;for(t||a++;RegExp(`(^|[^$])`+`\\$`.repeat(a)+`([^$]|$)`).test(i);)a++;let o=`$`.repeat(a);/[^ \r\n]/.test(i)&&(/^[ \r\n]/.test(i)&&/[ \r\n]$/.test(i)||/^\$|\$$/.test(i))&&(i=` `+i+` `);let s=-1;for(;++s<r.unsafe.length;){let e=r.unsafe[s];if(!e.atBreak)continue;let t=r.compilePattern(e),n;for(;n=t.exec(i);){let e=n.index;i.codePointAt(e)===10&&i.codePointAt(e-1)===13&&e--,i=i.slice(0,e)+` `+i.slice(n.index+1)}}return o+i+o}function i(){return`$`}}var Ao={tokenize:Mo,concrete:!0,name:`mathFlow`},jo={tokenize:No,partial:!0};function Mo(e,t,n){let r=this,i=r.events[r.events.length-1],a=i&&i[1].type===`linePrefix`?i[2].sliceSerialize(i[1],!0).length:0,o=0;return s;function s(t){return e.enter(`mathFlow`),e.enter(`mathFlowFence`),e.enter(`mathFlowFenceSequence`),c(t)}function c(t){return t===36?(e.consume(t),o++,c):o<2?n(t):(e.exit(`mathFlowFenceSequence`),G(e,l,`whitespace`)(t))}function l(t){return t===null||B(t)?d(t):(e.enter(`mathFlowFenceMeta`),e.enter(`chunkString`,{contentType:`string`}),u(t))}function u(t){return t===null||B(t)?(e.exit(`chunkString`),e.exit(`mathFlowFenceMeta`),d(t)):t===36?n(t):(e.consume(t),u)}function d(n){return e.exit(`mathFlowFence`),r.interrupt?t(n):e.attempt(jo,f,g)(n)}function f(t){return e.attempt({tokenize:_,partial:!0},g,p)(t)}function p(t){return(a?G(e,m,`linePrefix`,a+1):m)(t)}function m(t){return t===null?g(t):B(t)?e.attempt(jo,f,g)(t):(e.enter(`mathFlowValue`),h(t))}function h(t){return t===null||B(t)?(e.exit(`mathFlowValue`),m(t)):(e.consume(t),h)}function g(n){return e.exit(`mathFlow`),t(n)}function _(e,t,n){let i=0;return G(e,a,`linePrefix`,r.parser.constructs.disable.null.includes(`codeIndented`)?void 0:4);function a(t){return e.enter(`mathFlowFence`),e.enter(`mathFlowFenceSequence`),s(t)}function s(t){return t===36?(i++,e.consume(t),s):i<o?n(t):(e.exit(`mathFlowFenceSequence`),G(e,c,`whitespace`)(t))}function c(r){return r===null||B(r)?(e.exit(`mathFlowFence`),t(r)):n(r)}}}function No(e,t,n){let r=this;return i;function i(n){return n===null?t(n):(e.enter(`lineEnding`),e.consume(n),e.exit(`lineEnding`),a)}function a(e){return r.parser.lazy[r.now().line]?n(e):t(e)}}function Po(e){let t=(e||{}).singleDollarTextMath;return t??=!0,{tokenize:n,resolve:Fo,previous:Io,name:`mathText`};function n(e,n,r){let i=0,a,o;return s;function s(t){return e.enter(`mathText`),e.enter(`mathTextSequence`),c(t)}function c(n){return n===36?(e.consume(n),i++,c):i<2&&!t?r(n):(e.exit(`mathTextSequence`),l(n))}function l(t){return t===null?r(t):t===36?(o=e.enter(`mathTextSequence`),a=0,d(t)):t===32?(e.enter(`space`),e.consume(t),e.exit(`space`),l):B(t)?(e.enter(`lineEnding`),e.consume(t),e.exit(`lineEnding`),l):(e.enter(`mathTextData`),u(t))}function u(t){return t===null||t===32||t===36||B(t)?(e.exit(`mathTextData`),l(t)):(e.consume(t),u)}function d(t){return t===36?(e.consume(t),a++,d):a===i?(e.exit(`mathTextSequence`),e.exit(`mathText`),n(t)):(o.type=`mathTextData`,u(t))}}}function Fo(e){let t=e.length-4,n=3,r,i;if((e[n][1].type===`lineEnding`||e[n][1].type===`space`)&&(e[t][1].type===`lineEnding`||e[t][1].type===`space`)){for(r=n;++r<t;)if(e[r][1].type===`mathTextData`){e[t][1].type=`mathTextPadding`,e[n][1].type=`mathTextPadding`,n+=2,t-=2;break}}for(r=n-1,t++;++r<=t;)i===void 0?r!==t&&e[r][1].type!==`lineEnding`&&(i=r):(r===t||e[r][1].type===`lineEnding`)&&(e[i][1].type=`mathTextData`,r!==i+2&&(e[i][1].end=e[r-1][1].end,e.splice(i+2,r-i-2),t-=r-i-2,r=i+2),i=void 0);return e}function Io(e){return e!==36||this.events[this.events.length-1][1].type===`characterEscape`}function Lo(e){return{flow:{36:Ao},text:{36:Po(e)}}}var Ro={};function zo(e){let t=this,n=e||Ro,r=t.data(),i=r.micromarkExtensions||=[],a=r.fromMarkdownExtensions||=[],o=r.toMarkdownExtensions||=[];i.push(Lo(n)),a.push(Oo()),o.push(ko(n))}var Bo=[[`javascript`,a],[`js`,a],[`typescript`,b],[`ts`,b],[`python`,v],[`py`,v],[`bash`,r],[`shell`,l],[`sh`,l],[`json`,u],[`css`,p],[`xml`,h],[`html`,h],[`markdown`,c],[`md`,c],[`yaml`,s],[`yml`,s],[`sql`,_],[`rust`,f],[`rs`,f],[`go`,o],[`java`,g],[`cpp`,m],[`c`,m],[`diff`,i],[`plaintext`,d],[`text`,d]],Vo=!1;function Ho(){Vo||=(Bo.forEach(([e,t])=>{y.registerLanguage(e,t)}),!0)}function Uo(e){return e?.trim().toLowerCase().replace(/^language-/,``)||`text`}function Wo(e,t){Ho();let n=Uo(t);if(!y.getLanguage(n))return{html:e.replace(/[&<>"']/g,e=>({"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`})[e]??e),language:n,highlighted:!1};try{return{html:y.highlight(e,{language:n,ignoreIllegals:!0}).value,language:n,highlighted:!0}}catch{return{html:e.replace(/[&<>"']/g,e=>({"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`})[e]??e),language:n,highlighted:!1}}}var Go=new Set([`http:`,`https:`,`mailto:`]);function Ko(e){try{let t=new URL(e,window.location.origin);return t.origin===window.location.origin&&!e.startsWith(`file:`)?!0:Go.has(t.protocol)}catch{return!1}}function qo(e){return Ko(e)?e:``}function Jo(e){let t=e.trim();if(!t||/^(?:javascript|data|file):/i.test(t)||/^\/(?:Users|home|var|tmp|private|etc)\//i.test(t))return``;let n=t.startsWith(`./.assets/`)?t.slice(2):t.startsWith(`.assets/`)?t:null;return n?n.includes(`\\`)||n.split(`/`).some(e=>!e||e===`.`||e===`..`)?``:`/api/dashboard/notes/asset?path=${encodeURIComponent(n)}`:Ko(t)?t:``}var Yo={...kr,attributes:{...kr.attributes,code:[...kr.attributes?.code??[],[`className`,/^language-./,`math-inline`,`math-display`]]}},Xo=/\[\[([^\[\]\n]+)\]\]/g,Zo=/^\[\[([^\[\]\n]+)\]\]$/,Qo=`.md`,$o=new Set([`.git`,`.assets`,`_templates`,`_snippets`,`_plugins`]);function es(e,t){let n=0;for(let r=t-1;r>=0&&e[r]===`\\`;r--)n++;return n%2==1}function ts(e,t){for(let n=0;n<e.length;n++)if(e[n]===t&&!es(e,n))return n;return-1}function ns(e){let t=e.match(Zo);if(!t)return null;let n=t[1]?.trim()??``;if(!n)return null;let r=ts(n,`|`),i=(r===-1?n:n.slice(0,r)).trim(),a=r===-1?``:n.slice(r+1).trim(),o=ts(i,`#`),s=(o===-1?i:i.slice(0,o)).trim(),c=o===-1?``:i.slice(o+1).trim();return s?{raw:e,inner:n,target:s,heading:c||null,displayText:a||null}:null}function rs(e){let t=new Map;if(!e)return t;for(let n of e)t.has(n.raw)||t.set(n.raw,{...n,source:`outgoing`});return t}function is(e,t,n){let r=e.get(t)??[];r.push(n),e.set(t,r)}function as(e){return e.endsWith(Qo)?e.slice(0,-3):e}function os(e){return as(e.split(`/`).at(-1)||e)}function ss(e){let t={byPath:new Map,byPathNoExt:new Map,byAlias:new Map,byStem:new Map};for(let n of e??[]){t.byPath.set(n.path,n),t.byPathNoExt.set(as(n.path),n);for(let e of n.aliases)is(t.byAlias,e,n);is(t.byStem,os(n.path),n)}return t}function cs(e){return e.map(e=>e.path).sort((e,t)=>e.localeCompare(t))}function ls(e){return e.split(`/`).some(e=>$o.has(e))}function us(e){return!e||e.includes(`\0`)||e.includes(`\\`)||e.startsWith(`/`)||e.split(`/`).some(e=>!e||e===`.`||e===`..`)?!0:ls(e)}function ds(e,t){return{source:`notes-fallback`,sourcePath:``,raw:e.raw,target:e.target,...e.displayText?{displayText:e.displayText}:{},...e.heading?{heading:e.heading}:{},line:0,column:0,startOffset:t,endOffset:t+e.raw.length,status:`missing`,reason:`not_found`}}function fs(e,t){let{candidatePaths:n,reason:r,...i}=e;return{...i,status:`resolved`,resolvedPath:t}}function ps(e,t){let{candidatePaths:n,resolvedPath:r,...i}=e;return{...i,status:`missing`,reason:t}}function ms(e,t){let{resolvedPath:n,...r}=e;return{...r,status:`ambiguous`,reason:`ambiguous`,candidatePaths:cs(t)}}function hs(e,t){if(t.length===0)return null;let[n]=t;return t.length===1&&n?fs(e,n.path):ms(e,t)}function gs(e,t,n,r=0){let i=rs(t).get(e);if(i)return i;let a=ns(e);if(!a)return null;let o=ds(a,r),s=a.target.trim();if(us(s))return ps(o,`invalid_target`);let c=ss(n),l=c.byPath.get(s);if(l)return fs(o,l.path);if(!s.endsWith(Qo)){let e=c.byPath.get(`${s}${Qo}`);if(e)return fs(o,e.path);let t=c.byPathNoExt.get(s);if(t)return fs(o,t.path)}let u=hs(o,c.byAlias.get(s)??[]);if(u)return u;if(!s.includes(`/`)){let e=hs(o,c.byStem.get(s)??[]);if(e)return e}return o}function _s(e,t){let n=ns(t);return e.displayText||n?.displayText||n?.inner||e.target||t}function vs(e){return e.reason===`ambiguous`?`Ambiguous link target`:e.reason===`invalid_target`?`Invalid link target`:`No matching note`}export{D as A,qn as C,Pn as D,Fn as E,S as M,En as O,fr as S,Gn as T,ki as _,_s as a,kr as b,Yo as c,Wo as d,zo as f,Ii as g,Bi as h,gs as i,x as j,z as k,Jo as l,_a as m,rs as n,vs as o,Do as p,es as r,Ko as s,Xo as t,qo as u,Ti as v,Wn as w,yr as x,jr as y};
22
+ `)),c+=a.move(o),s(),c}function r(e,n,r){let i=e.value||``,a=1;for(t||a++;RegExp(`(^|[^$])`+`\\$`.repeat(a)+`([^$]|$)`).test(i);)a++;let o=`$`.repeat(a);/[^ \r\n]/.test(i)&&(/^[ \r\n]/.test(i)&&/[ \r\n]$/.test(i)||/^\$|\$$/.test(i))&&(i=` `+i+` `);let s=-1;for(;++s<r.unsafe.length;){let e=r.unsafe[s];if(!e.atBreak)continue;let t=r.compilePattern(e),n;for(;n=t.exec(i);){let e=n.index;i.codePointAt(e)===10&&i.codePointAt(e-1)===13&&e--,i=i.slice(0,e)+` `+i.slice(n.index+1)}}return o+i+o}function i(){return`$`}}var Ao={tokenize:Mo,concrete:!0,name:`mathFlow`},jo={tokenize:No,partial:!0};function Mo(e,t,n){let r=this,i=r.events[r.events.length-1],a=i&&i[1].type===`linePrefix`?i[2].sliceSerialize(i[1],!0).length:0,o=0;return s;function s(t){return e.enter(`mathFlow`),e.enter(`mathFlowFence`),e.enter(`mathFlowFenceSequence`),c(t)}function c(t){return t===36?(e.consume(t),o++,c):o<2?n(t):(e.exit(`mathFlowFenceSequence`),G(e,l,`whitespace`)(t))}function l(t){return t===null||B(t)?d(t):(e.enter(`mathFlowFenceMeta`),e.enter(`chunkString`,{contentType:`string`}),u(t))}function u(t){return t===null||B(t)?(e.exit(`chunkString`),e.exit(`mathFlowFenceMeta`),d(t)):t===36?n(t):(e.consume(t),u)}function d(n){return e.exit(`mathFlowFence`),r.interrupt?t(n):e.attempt(jo,f,g)(n)}function f(t){return e.attempt({tokenize:_,partial:!0},g,p)(t)}function p(t){return(a?G(e,m,`linePrefix`,a+1):m)(t)}function m(t){return t===null?g(t):B(t)?e.attempt(jo,f,g)(t):(e.enter(`mathFlowValue`),h(t))}function h(t){return t===null||B(t)?(e.exit(`mathFlowValue`),m(t)):(e.consume(t),h)}function g(n){return e.exit(`mathFlow`),t(n)}function _(e,t,n){let i=0;return G(e,a,`linePrefix`,r.parser.constructs.disable.null.includes(`codeIndented`)?void 0:4);function a(t){return e.enter(`mathFlowFence`),e.enter(`mathFlowFenceSequence`),s(t)}function s(t){return t===36?(i++,e.consume(t),s):i<o?n(t):(e.exit(`mathFlowFenceSequence`),G(e,c,`whitespace`)(t))}function c(r){return r===null||B(r)?(e.exit(`mathFlowFence`),t(r)):n(r)}}}function No(e,t,n){let r=this;return i;function i(n){return n===null?t(n):(e.enter(`lineEnding`),e.consume(n),e.exit(`lineEnding`),a)}function a(e){return r.parser.lazy[r.now().line]?n(e):t(e)}}function Po(e){let t=(e||{}).singleDollarTextMath;return t??=!0,{tokenize:n,resolve:Fo,previous:Io,name:`mathText`};function n(e,n,r){let i=0,a,o;return s;function s(t){return e.enter(`mathText`),e.enter(`mathTextSequence`),c(t)}function c(n){return n===36?(e.consume(n),i++,c):i<2&&!t?r(n):(e.exit(`mathTextSequence`),l(n))}function l(t){return t===null?r(t):t===36?(o=e.enter(`mathTextSequence`),a=0,d(t)):t===32?(e.enter(`space`),e.consume(t),e.exit(`space`),l):B(t)?(e.enter(`lineEnding`),e.consume(t),e.exit(`lineEnding`),l):(e.enter(`mathTextData`),u(t))}function u(t){return t===null||t===32||t===36||B(t)?(e.exit(`mathTextData`),l(t)):(e.consume(t),u)}function d(t){return t===36?(e.consume(t),a++,d):a===i?(e.exit(`mathTextSequence`),e.exit(`mathText`),n(t)):(o.type=`mathTextData`,u(t))}}}function Fo(e){let t=e.length-4,n=3,r,i;if((e[n][1].type===`lineEnding`||e[n][1].type===`space`)&&(e[t][1].type===`lineEnding`||e[t][1].type===`space`)){for(r=n;++r<t;)if(e[r][1].type===`mathTextData`){e[t][1].type=`mathTextPadding`,e[n][1].type=`mathTextPadding`,n+=2,t-=2;break}}for(r=n-1,t++;++r<=t;)i===void 0?r!==t&&e[r][1].type!==`lineEnding`&&(i=r):(r===t||e[r][1].type===`lineEnding`)&&(e[i][1].type=`mathTextData`,r!==i+2&&(e[i][1].end=e[r-1][1].end,e.splice(i+2,r-i-2),t-=r-i-2,r=i+2),i=void 0);return e}function Io(e){return e!==36||this.events[this.events.length-1][1].type===`characterEscape`}function Lo(e){return{flow:{36:Ao},text:{36:Po(e)}}}var Ro={};function zo(e){let t=this,n=e||Ro,r=t.data(),i=r.micromarkExtensions||=[],a=r.fromMarkdownExtensions||=[],o=r.toMarkdownExtensions||=[];i.push(Lo(n)),a.push(Oo()),o.push(ko(n))}var Bo=[[`javascript`,a],[`js`,a],[`typescript`,b],[`ts`,b],[`python`,v],[`py`,v],[`bash`,r],[`shell`,l],[`sh`,l],[`json`,u],[`css`,p],[`xml`,h],[`html`,h],[`markdown`,c],[`md`,c],[`yaml`,s],[`yml`,s],[`sql`,_],[`rust`,f],[`rs`,f],[`go`,o],[`java`,g],[`cpp`,m],[`c`,m],[`diff`,i],[`plaintext`,d],[`text`,d]],Vo=!1;function Ho(){Vo||=(Bo.forEach(([e,t])=>{y.registerLanguage(e,t)}),!0)}function Uo(e){return e?.trim().toLowerCase().replace(/^language-/,``)||`text`}function Wo(e,t){Ho();let n=Uo(t);if(!y.getLanguage(n))return{html:e.replace(/[&<>"']/g,e=>({"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`})[e]??e),language:n,highlighted:!1};try{return{html:y.highlight(e,{language:n,ignoreIllegals:!0}).value,language:n,highlighted:!0}}catch{return{html:e.replace(/[&<>"']/g,e=>({"&":`&amp;`,"<":`&lt;`,">":`&gt;`,'"':`&quot;`,"'":`&#39;`})[e]??e),language:n,highlighted:!1}}}var Go=new Set([`http:`,`https:`,`mailto:`]);function Ko(e){try{let t=new URL(e,window.location.origin);return t.origin===window.location.origin&&!e.startsWith(`file:`)?!0:Go.has(t.protocol)}catch{return!1}}function qo(e){return Ko(e)?e:``}function Jo(e){let t=e.trim();if(!t||/^(?:javascript|data|file):/i.test(t)||/^\/(?:Users|home|var|tmp|private|etc)\//i.test(t))return``;let n=t.startsWith(`./.assets/`)?t.slice(2):t.startsWith(`.assets/`)?t:null;return n?n.includes(`\\`)||n.split(`/`).some(e=>!e||e===`.`||e===`..`)?``:`/api/dashboard/notes/asset?path=${encodeURIComponent(n)}`:Ko(t)?t:``}var Yo={...kr,attributes:{...kr.attributes,code:[...kr.attributes?.code??[],[`className`,/^language-./,`math-inline`,`math-display`]]}},Xo=/\[\[([^\[\]\n]+)\]\]/g,Zo=/^\[\[([^\[\]\n]+)\]\]$/,Qo=`.md`,$o=new Set([`.git`,`.assets`,`_templates`,`_snippets`,`_plugins`]);function es(e,t){let n=0;for(let r=t-1;r>=0&&e[r]===`\\`;r--)n++;return n%2==1}function ts(e,t){for(let n=0;n<e.length;n++)if(e[n]===t&&!es(e,n))return n;return-1}function ns(e){let t=e.match(Zo);if(!t)return null;let n=t[1]?.trim()??``;if(!n)return null;let r=ts(n,`|`),i=(r===-1?n:n.slice(0,r)).trim(),a=r===-1?``:n.slice(r+1).trim(),o=ts(i,`#`),s=(o===-1?i:i.slice(0,o)).trim(),c=o===-1?``:i.slice(o+1).trim();return s?{raw:e,inner:n,target:s,heading:c||null,displayText:a||null}:null}function rs(e){let t=new Map;if(!e)return t;for(let n of e)t.has(n.raw)||t.set(n.raw,{...n,source:`outgoing`});return t}function is(e,t,n){let r=e.get(t)??[];r.push(n),e.set(t,r)}function as(e){return e.endsWith(Qo)?e.slice(0,-3):e}function os(e){return as(e.split(`/`).at(-1)||e)}function ss(e){let t={byPath:new Map,byPathNoExt:new Map,byAlias:new Map,byStem:new Map};for(let n of e??[]){t.byPath.set(n.path,n),t.byPathNoExt.set(as(n.path),n);for(let e of n.aliases)is(t.byAlias,e,n);is(t.byStem,os(n.path),n)}return t}function cs(e){return e.map(e=>e.path).sort((e,t)=>e.localeCompare(t))}function ls(e){return e.split(`/`).some(e=>$o.has(e))}function us(e){return!e||e.includes(`\0`)||e.includes(`\\`)||e.startsWith(`/`)||e.split(`/`).some(e=>!e||e===`.`||e===`..`)?!0:ls(e)}function ds(e,t){return{source:`notes-fallback`,sourcePath:``,raw:e.raw,target:e.target,...e.displayText?{displayText:e.displayText}:{},...e.heading?{heading:e.heading}:{},line:0,column:0,startOffset:t,endOffset:t+e.raw.length,status:`missing`,reason:`not_found`}}function fs(e,t){let{candidatePaths:n,reason:r,...i}=e;return{...i,status:`resolved`,resolvedPath:t}}function ps(e,t){let{candidatePaths:n,resolvedPath:r,...i}=e;return{...i,status:`missing`,reason:t}}function ms(e,t){let{resolvedPath:n,...r}=e;return{...r,status:`ambiguous`,reason:`ambiguous`,candidatePaths:cs(t)}}function hs(e,t){if(t.length===0)return null;let[n]=t;return t.length===1&&n?fs(e,n.path):ms(e,t)}function gs(e,t,n,r=0){let i=rs(t).get(e);if(i)return i;let a=ns(e);if(!a)return null;let o=ds(a,r),s=a.target.trim();if(us(s))return ps(o,`invalid_target`);let c=ss(n),l=c.byPath.get(s);if(l)return fs(o,l.path);if(!s.endsWith(Qo)){let e=c.byPath.get(`${s}${Qo}`);if(e)return fs(o,e.path);let t=c.byPathNoExt.get(s);if(t)return fs(o,t.path)}let u=hs(o,c.byAlias.get(s)??[]);if(u)return u;if(!s.includes(`/`)){let e=hs(o,c.byStem.get(s)??[]);if(e)return e}return o}function _s(e,t){let n=ns(t);return e.displayText||n?.displayText||n?.inner||e.target||t}function vs(e){return e.reason===`ambiguous`?`Ambiguous link target`:e.reason===`invalid_target`?`Invalid link target`:`No matching note`}export{z as A,fr as C,Fn as D,Gn as E,x as M,S as N,Pn as O,yr as S,Wn as T,Ii as _,_s as a,jr as b,Yo as c,Wo as d,Uo as f,Bi as g,_a as h,gs as i,D as j,En as k,Jo as l,Do as m,rs as n,vs as o,zo as p,es as r,Ko as s,Xo as t,qo as u,ki as v,qn as w,kr as x,Ti as y};
@@ -6,7 +6,7 @@
6
6
  <link rel="icon" type="image/png" href="/dist/assets/icon-192-CwXPIYF5.png">
7
7
  <title>Jaw Manager</title>
8
8
  <script src="/manager/theme-boot.js"></script>
9
- <script type="module" crossorigin src="/dist/assets/manager-DL1GygQL.js"></script>
9
+ <script type="module" crossorigin src="/dist/assets/manager-DaN201mN.js"></script>
10
10
  <link rel="stylesheet" crossorigin href="/dist/assets/vendor-render-Bjnw0wQ6.css">
11
11
  <link rel="stylesheet" crossorigin href="/dist/assets/manager-C4eNDNFt.css">
12
12
  </head>
@@ -347,11 +347,17 @@ export function SidebarRailRouter(props: Props) {
347
347
  && !props.notesSelectedNote.tags?.includes(props.notesModel.tagFilter),
348
348
  );
349
349
  // External file open: focus/create a Files tab and assign the file to it.
350
- function handleRightPreviewFile(path: string): void {
350
+ // Stable identity matters: this is passed as `onLocalFileOpen` into
351
+ // MarkdownRenderer (via CodeCanvas/DocPanel), and a fresh function each
352
+ // render defeats its React.memo and the memoized components object.
353
+ // Depend on `dispatch`, not `panelLayout`: the context value is rebuilt on
354
+ // every panel state change, which would churn this identity right back.
355
+ const panelDispatch = panelLayout.dispatch;
356
+ const handleRightPreviewFile = useCallback((path: string): void => {
351
357
  const previewPath = expandDesktopHomePath(path.trim());
352
358
  if (!previewPath) return;
353
- panelLayout.dispatch({ type: 'OPEN_FILE_IN_FILES_TAB', path: previewPath });
354
- }
359
+ panelDispatch({ type: 'OPEN_FILE_IN_FILES_TAB', path: previewPath });
360
+ }, [panelDispatch]);
355
361
 
356
362
  // Per-tab callbacks bound to a specific Files module tab.
357
363
  const handleTabPreviewFile = useCallback((tabId: string, path: string): void => {
@@ -153,7 +153,25 @@ export function useEmbeddedBrowserTargetSync(active: boolean, selectedPort: numb
153
153
  }
154
154
 
155
155
  const unsubscribe = bridge.onWebviewState(() => schedulePush());
156
- const interval = window.setInterval(() => { void push(); }, REFRESH_INTERVAL_MS);
156
+ // D4: throttle the refresh while hidden rather than skipping it. This
157
+ // push is what keeps `lastPushAt` fresh, and the server drops a target
158
+ // from the registry after STALE_AFTER_MS (5 min,
159
+ // src/manager/routes/embedded-browser.ts:54). Skipping outright would
160
+ // 404 the agent's browser targets after five hidden minutes, while the
161
+ // command long-poll kept delivering commands that then fail. Pushing at
162
+ // a slower cadence keeps registration alive at a fraction of the cost.
163
+ const HIDDEN_PUSH_INTERVAL_MS = 4 * 60 * 1000;
164
+ let lastHiddenPush = 0;
165
+ const interval = window.setInterval(() => {
166
+ if (typeof document !== 'undefined' && document.hidden) {
167
+ if (Date.now() - lastHiddenPush < HIDDEN_PUSH_INTERVAL_MS) return;
168
+ lastHiddenPush = Date.now();
169
+ void push();
170
+ return;
171
+ }
172
+ lastHiddenPush = 0;
173
+ void push();
174
+ }, REFRESH_INTERVAL_MS);
157
175
  void push();
158
176
  void pollCommands();
159
177
 
@@ -285,7 +285,13 @@ export function CodeCanvas({ port, workingDir, onWorkingDirChange, onOpenLocalFi
285
285
  const pending: PendingPermission = { permissionId, toolCall, options };
286
286
  handleIncomingPermissionRequest(client, permissionMode, pending, {
287
287
  appendMessage: entry => setMessages(prev => [...prev, entry]),
288
- enqueuePermission: permission => setPermissions(prev => [...prev, permission]),
288
+ enqueuePermission: permission => {
289
+ setPermissions(prev => [...prev, permission]);
290
+ // The permission queue renders outside the transcript, so its
291
+ // appearance never shows up in the transcript footprint. Scroll
292
+ // explicitly or the prompt can land below the fold.
293
+ scrollTranscriptToBottom('smooth');
294
+ },
289
295
  });
290
296
  }
291
297
  } else if (kind === 'code_session_info_update') {
@@ -300,6 +306,9 @@ export function CodeCanvas({ port, workingDir, onWorkingDirChange, onOpenLocalFi
300
306
  } else if (kind === 'code_plan') {
301
307
  const entries = (update['entries'] ?? []) as Array<{ title?: string; status?: string }>;
302
308
  setPlanEntries(entries.filter(e => e.title).map(e => ({ title: String(e.title), status: String(e.status ?? 'pending') })));
309
+ // Plan entries render in the header, not in `messages`, so the
310
+ // footprint effect cannot see this change.
311
+ scrollTranscriptToBottom('smooth');
303
312
  } else if (kind === 'code_available_commands_update') {
304
313
  setAvailableCommands(mergeCodeCommands(normalizeCodeCommands(update['availableCommands'])));
305
314
  } else if (kind === 'code_child_exit') {
@@ -318,7 +327,10 @@ export function CodeCanvas({ port, workingDir, onWorkingDirChange, onOpenLocalFi
318
327
  setSending(false);
319
328
  }
320
329
 
321
- setTimeout(() => scrollTranscriptToBottom('smooth'), 50);
330
+ // D4: no unconditional scroll per event. This fired for every SSE event
331
+ // including ones that touch no message (code_usage_update, code_plan),
332
+ // and the footprint effect in useCodeTranscriptScroll already covers
333
+ // every case where the transcript actually grew.
322
334
  }, [client, permissionMode, scrollTranscriptToBottom]);
323
335
 
324
336
  useCodeEvents({ port, sessionId: activeSessionId, sessionIdRef: activeSessionIdRef, onEvent: handleCodeEvent, onTransport: setTransportState });
@@ -1,15 +1,33 @@
1
- import { lazy, Suspense, useCallback, type KeyboardEvent, type RefObject } from 'react';
1
+ import { lazy, Suspense, useCallback, useRef, type KeyboardEvent, type RefObject } from 'react';
2
2
  import type { ToolContent, TranscriptEntry } from './code-types';
3
3
  import { useCodeTranscriptVirtualRows } from './useCodeTranscriptVirtualRows';
4
+ import { useThrottledMarkdown } from './use-throttled-markdown';
4
5
 
5
6
  const MarkdownRenderer = lazy(() => import('../notes/rendering/MarkdownRenderer').then(m => ({ default: m.MarkdownRenderer })));
6
7
 
8
+ /**
9
+ * D1: assistant markdown is re-parsed in full on every update, so a per-token
10
+ * render is O(L²) in message length. Throttle the text adaptively (80ms under
11
+ * 2k chars up to 400ms past ~50k) before it reaches the markdown pipeline.
12
+ * Trailing-edge, so the final text always lands without a turn-done signal.
13
+ */
14
+ function AssistantMarkdown({ text, onOpenLocalFile }: { text: string; onOpenLocalFile?: ((path: string) => void) | undefined }) {
15
+ const throttled = useThrottledMarkdown(text);
16
+ return (
17
+ <Suspense fallback={<span>{throttled}</span>}>
18
+ <MarkdownRenderer markdown={throttled} tableMode="linear" onLocalFileOpen={onOpenLocalFile} />
19
+ </Suspense>
20
+ );
21
+ }
22
+
7
23
  type CodeTranscriptProps = {
8
24
  messages: TranscriptEntry[];
9
25
  sending: boolean;
10
26
  workingDir: string;
11
27
  transcriptRef: RefObject<HTMLDivElement | null>;
12
28
  onOpenLocalFile?: ((path: string) => void) | undefined;
29
+ /** Switching sessions must discard index-keyed row measurements (050 D2). */
30
+ sessionId?: string | null | undefined;
13
31
  };
14
32
 
15
33
  function renderToolContent(content: ToolContent, index: number) {
@@ -165,9 +183,7 @@ function renderCodeMessage(msg: TranscriptEntry, i: number, onOpenLocalFile?: ((
165
183
  <span className="code-message-role">{msg.role === 'user' ? 'You' : 'JWC'}</span>
166
184
  <div className="code-message-text">
167
185
  {msg.role === 'assistant' ? (
168
- <Suspense fallback={<span>{msg.text}</span>}>
169
- <MarkdownRenderer markdown={msg.text} tableMode="linear" onLocalFileOpen={onOpenLocalFile} />
170
- </Suspense>
186
+ <AssistantMarkdown text={msg.text} onOpenLocalFile={onOpenLocalFile} />
171
187
  ) : msg.text}
172
188
  </div>
173
189
  </>
@@ -185,18 +201,26 @@ function renderSendingMessage() {
185
201
  );
186
202
  }
187
203
 
188
- export function CodeTranscript({ messages, sending, workingDir, transcriptRef, onOpenLocalFile }: CodeTranscriptProps) {
204
+ export function CodeTranscript({ messages, sending, workingDir, transcriptRef, onOpenLocalFile, sessionId }: CodeTranscriptProps) {
189
205
  const showSending = sending && messages[messages.length - 1]?.role !== 'assistant';
190
206
  const rowCount = messages.length + (showSending ? 1 : 0);
207
+ // D3: these two callbacks used to depend on `messages`. Every streaming
208
+ // token makes a new array identity, so both were re-created, which
209
+ // invalidated the virtualizer's layout effect and forced a full
210
+ // setOptions + _willUpdate + setSnapshot — a second render pass per token.
211
+ // Read through a ref instead so only `count` actually changes.
212
+ const messagesRef = useRef(messages);
213
+ messagesRef.current = messages;
191
214
  const getItemKey = useCallback((index: number): string | number => {
192
- const msg = messages[index];
215
+ const msg = messagesRef.current[index];
193
216
  return msg ? transcriptMessageKey(msg, index) : `sending-${index}`;
194
- }, [messages]);
217
+ }, []);
195
218
  const estimateSize = useCallback((index: number): number => (
196
- estimateTranscriptRowSize(messages[index])
197
- ), [messages]);
219
+ estimateTranscriptRowSize(messagesRef.current[index])
220
+ ), []);
198
221
  const virtual = useCodeTranscriptVirtualRows({
199
222
  count: rowCount,
223
+ resetKey: sessionId ?? null,
200
224
  scrollElementRef: transcriptRef,
201
225
  getItemKey,
202
226
  estimateSize,
@@ -95,7 +95,7 @@ export function CodeWorkbench(props: CodeWorkbenchProps) {
95
95
  {props.transportState === 'reconnecting' ? 'Live updates reconnecting…' : 'Live updates disconnected — retrying.'}
96
96
  </section>
97
97
  )}
98
- <CodeTranscript messages={props.messages} sending={props.sending} workingDir={props.codeWorkingDir} transcriptRef={props.transcriptRef} onOpenLocalFile={props.onOpenLocalFile} />
98
+ <CodeTranscript messages={props.messages} sending={props.sending} workingDir={props.codeWorkingDir} transcriptRef={props.transcriptRef} onOpenLocalFile={props.onOpenLocalFile} sessionId={props.activeSessionId} />
99
99
  <CodePermissionQueue permissions={props.permissions} onAnswer={props.onPermissionAnswer} />
100
100
  <div className="code-composer-dock">
101
101
  <div className="code-composer-surface" aria-label="Code composer controls">
@@ -1,5 +1,25 @@
1
1
  import type { CodeSessionReplayEvent } from './code-session-client';
2
2
  import type { CodeEvent } from './useCodeEvents';
3
+ import { fnv1a32 } from '../lib/fnv1a';
4
+ import { addBounded } from '../lib/bounded-set';
5
+
6
+ /**
7
+ * D3 (260803 unit, 050 phase): the dedupe key used to embed the FULL chunk
8
+ * text, so the set retained roughly a second copy of the whole transcript and
9
+ * grew for the session's lifetime.
10
+ *
11
+ * Collision domain, stated precisely: `stableId` is a message id or SSE event
12
+ * id, and a message id is minted once per TURN — every delta of one assistant
13
+ * turn shares it. So within a turn the text really was the discriminator, and
14
+ * hashing it does concentrate risk there. What bounds it is that the key also
15
+ * carries the text length, so only same-length chunks of the same turn can
16
+ * collide: roughly 10^3 same-length items against 2^32, i.e. ~10^-4.
17
+ *
18
+ * The consequence of a collision is a hard drop — `shouldDropDuplicateCodeChunk`
19
+ * returns before the append — so it is worth keeping that number honest rather
20
+ * than calling stableId "unique".
21
+ */
22
+ const MAX_SEEN_KEYS = 2000;
3
23
 
4
24
  type ChunkLikeEvent = Pick<CodeEvent, 'event' | 'sessionId' | 'update' | 'sseEventId'>;
5
25
  export type AssistantChunkMergeAction = 'append' | 'drop' | 'replace';
@@ -29,14 +49,18 @@ export function codeChunkEventKey(event: ChunkLikeEvent, text: string): string |
29
49
  : '';
30
50
  const stableId = messageId ? `msg:${messageId}` : sseEventId ? `sse:${sseEventId}` : '';
31
51
  if (!stableId) return null;
32
- return `${event.sessionId ?? ''}:${event.event}:${stableId}:${text}`;
52
+ return `${event.sessionId ?? ''}:${event.event}:${stableId}:${text.length}:${fnv1a32(text)}`;
33
53
  }
34
54
 
35
55
  export function rememberCodeChunkEvents(seen: Set<string>, events: CodeSessionReplayEvent[]): void {
36
56
  for (const event of events) {
37
57
  const text = textFromCodeChunk(event.update);
38
58
  const key = codeChunkEventKey(event, text);
39
- if (key) seen.add(key);
59
+ // Seeding from a session replay must NOT evict its own earlier entries:
60
+ // a replay longer than the bound would drop keys for content already on
61
+ // screen, and the next matching live chunk would be appended again as
62
+ // "new". Size the bound to the replay, then let the live path trim.
63
+ if (key) addBounded(seen, key, Math.max(MAX_SEEN_KEYS, events.length));
40
64
  }
41
65
  }
42
66
 
@@ -44,7 +68,7 @@ export function shouldDropDuplicateCodeChunk(seen: Set<string>, event: ChunkLike
44
68
  const key = codeChunkEventKey(event, text);
45
69
  if (!key) return false;
46
70
  if (seen.has(key)) return true;
47
- seen.add(key);
71
+ addBounded(seen, key, MAX_SEEN_KEYS);
48
72
  return false;
49
73
  }
50
74
 
@@ -12,16 +12,41 @@ export function useCodeTranscriptScroll(messages: TranscriptEntry[], sending: bo
12
12
  const latestTranscriptFootprint = useMemo(() => {
13
13
  const last = messages[messages.length - 1];
14
14
  if (!last) return 'empty';
15
- const toolContentSize = last.toolContent?.reduce((total, content) => total + JSON.stringify(content).length, 0) ?? 0;
15
+ // D5: this used to JSON.stringify the whole toolContent on every
16
+ // `messages` identity change — i.e. every streaming token — which
17
+ // serializes hundreds of KB just to build a change-detection string.
18
+ // Length fields discriminate the same transitions at O(items) cost.
19
+ const toolContentSize = last.toolContent?.reduce((total, content) => {
20
+ const c = content as { text?: string; diff?: string; output?: string };
21
+ return total + (c.text?.length ?? 0) + (c.diff?.length ?? 0) + (c.output?.length ?? 0);
22
+ }, 0) ?? 0;
16
23
  return `${messages.length}:${last.role}:${last.text.length}:${last.toolOutput?.length ?? 0}:${toolContentSize}:${last.toolStatus ?? ''}`;
17
24
  }, [messages]);
18
25
 
26
+ // D4: coalesce every scroll request onto a single in-flight frame. The old
27
+ // shape queued a rAF *plus* a nested 80ms timeout per call, and callers fire
28
+ // per SSE event, so ~40 tok/s produced ~120 layout-forcing callbacks per
29
+ // second with two smooth-scroll animations fighting each other.
30
+ const pendingFrameRef = useRef<number | null>(null);
31
+ const settleTimerRef = useRef<number | null>(null);
32
+
33
+ useEffect(() => () => {
34
+ if (pendingFrameRef.current !== null) cancelAnimationFrame(pendingFrameRef.current);
35
+ if (settleTimerRef.current !== null) window.clearTimeout(settleTimerRef.current);
36
+ }, []);
37
+
19
38
  const scrollTranscriptToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => {
20
- window.requestAnimationFrame(() => {
39
+ if (pendingFrameRef.current !== null) return;
40
+ pendingFrameRef.current = window.requestAnimationFrame(() => {
41
+ pendingFrameRef.current = null;
21
42
  const node = transcriptRef.current;
22
43
  if (!node) return;
23
44
  node.scrollTo({ top: node.scrollHeight, behavior });
24
- window.setTimeout(() => {
45
+ // One trailing correction, not one per call: content that lands
46
+ // after this frame still needs a final snap.
47
+ if (settleTimerRef.current !== null) window.clearTimeout(settleTimerRef.current);
48
+ settleTimerRef.current = window.setTimeout(() => {
49
+ settleTimerRef.current = null;
25
50
  const latest = transcriptRef.current;
26
51
  if (!latest) return;
27
52
  latest.scrollTo({ top: latest.scrollHeight, behavior: 'auto' });
@@ -0,0 +1,75 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+
3
+ /**
4
+ * D1 (260803 unit, 020 phase) — adaptive streaming throttle.
5
+ *
6
+ * Rendering assistant markdown re-parses the FULL accumulated text on every
7
+ * update (remark → rehype → sanitize → KaTeX), so per-render cost grows with
8
+ * message length and a naive per-token render is O(L²) overall.
9
+ *
10
+ * The legacy UI reached this conclusion first; these constants are lifted
11
+ * verbatim from public/js/streaming-render.ts:18-30 rather than re-derived,
12
+ * so both renderers behave the same under load.
13
+ */
14
+ const FULL_RENDER_THRESHOLD = 2000;
15
+ const THROTTLE_MS = 80;
16
+ const THROTTLE_MAX_MS = 400;
17
+
18
+ export function throttleMsFor(textLength: number): number {
19
+ if (textLength <= FULL_RENDER_THRESHOLD) return THROTTLE_MS;
20
+ const scaled = THROTTLE_MS * (textLength / FULL_RENDER_THRESHOLD);
21
+ return Math.min(THROTTLE_MAX_MS, Math.round(scaled));
22
+ }
23
+
24
+ /**
25
+ * Returns `text` throttled by the adaptive interval above. Growth is rate
26
+ * limited; a shrink or replacement (new message, session switch) applies
27
+ * immediately so the view never shows stale content from another turn.
28
+ *
29
+ * Always trailing-edge: the final value lands even if the last change arrives
30
+ * inside a throttle window, so no explicit "turn done" signal is needed.
31
+ */
32
+ export function useThrottledMarkdown(text: string): string {
33
+ const [visible, setVisible] = useState(text);
34
+ const lastEmitRef = useRef(0);
35
+ const timerRef = useRef<number | null>(null);
36
+
37
+ useEffect(() => {
38
+ // Only a pure append is streaming growth. A prefix-only check would
39
+ // treat an in-place edit past the compared window as growth and hold
40
+ // stale text for up to one interval; comparing the full visible string
41
+ // is exact, and only runs on the fast path.
42
+ const isAppend = text.length >= visible.length && text.startsWith(visible);
43
+ if (!isAppend) {
44
+ if (timerRef.current !== null) {
45
+ window.clearTimeout(timerRef.current);
46
+ timerRef.current = null;
47
+ }
48
+ lastEmitRef.current = Date.now();
49
+ setVisible(text);
50
+ return;
51
+ }
52
+ if (text === visible) return;
53
+
54
+ const wait = throttleMsFor(text.length);
55
+ const since = Date.now() - lastEmitRef.current;
56
+ if (since >= wait) {
57
+ lastEmitRef.current = Date.now();
58
+ setVisible(text);
59
+ return;
60
+ }
61
+
62
+ if (timerRef.current !== null) window.clearTimeout(timerRef.current);
63
+ timerRef.current = window.setTimeout(() => {
64
+ timerRef.current = null;
65
+ lastEmitRef.current = Date.now();
66
+ setVisible(text);
67
+ }, wait - since);
68
+ }, [text, visible]);
69
+
70
+ useEffect(() => () => {
71
+ if (timerRef.current !== null) window.clearTimeout(timerRef.current);
72
+ }, []);
73
+
74
+ return visible;
75
+ }
@@ -24,6 +24,12 @@ export type CodeTranscriptVirtualRows = {
24
24
 
25
25
  export function useCodeTranscriptVirtualRows(args: {
26
26
  count: number;
27
+ /**
28
+ * Changing this discards measurements. Normally the session id: row heights
29
+ * are keyed by index, so carrying them into a different transcript retains
30
+ * memory AND mis-measures the new rows.
31
+ */
32
+ resetKey?: string | null;
27
33
  scrollElementRef: RefObject<HTMLDivElement | null>;
28
34
  getItemKey: (index: number) => string | number;
29
35
  estimateSize?: (index: number) => number;
@@ -57,6 +63,19 @@ export function useCodeTranscriptVirtualRows(args: {
57
63
 
58
64
  useEffect(() => virtualizer._didMount(), [virtualizer]);
59
65
 
66
+ // D2 (260803 unit, 050 phase): the instance lives in a ref that was never
67
+ // cleared, so measured row sizes survived session switches.
68
+ //
69
+ // Use the public `measure()` rather than emptying `measurementsCache`.
70
+ // The durable store is `itemSizeCache` (private, cleared only by
71
+ // `measure()`); `measurementsCache` is reassigned to a fresh lazy view on
72
+ // every recompute, so clearing it releases nothing.
73
+ useEffect(() => {
74
+ virtualizer.measure();
75
+ }, [virtualizer, args.resetKey]);
76
+
77
+ useEffect(() => () => { virtualizerRef.current = null; }, []);
78
+
60
79
  useLayoutEffect(() => {
61
80
  virtualizer.setOptions({
62
81
  ...virtualizer.options,
@@ -77,6 +77,13 @@ export function ElectronMetricsPanel(props: ElectronMetricsPanelProps = {}) {
77
77
  let timer: ReturnType<typeof setTimeout> | null = null;
78
78
 
79
79
  const tick = async () => {
80
+ // D4: skip the work while the window is hidden, but keep the timer
81
+ // chain alive so it resumes instantly on return. A poller that
82
+ // exists to REPORT memory pressure should not be creating idle cost.
83
+ if (typeof document !== 'undefined' && document.hidden) {
84
+ if (!cancelled) timer = setTimeout(tick, POLL_INTERVAL_MS);
85
+ return;
86
+ }
80
87
  try {
81
88
  const result = await fetchElectronMetrics();
82
89
  if (cancelled) return;
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { addBounded } from '../lib/bounded-set';
2
3
  import type { ManagerEvent } from '../types';
3
4
  import {
4
5
  continueJawCeoCompletion,
@@ -90,6 +91,9 @@ function toJawCeoEvent(event: ManagerEvent): JawCeoManagerEvent | null {
90
91
  };
91
92
  }
92
93
 
94
+ /** Bound for the CEO console's event dedupe set (050 D3). */
95
+ const MAX_SEEN_EVENT_KEYS = 2000;
96
+
93
97
  export type UseJawCeoArgs = {
94
98
  selectedPort: number | null;
95
99
  documentVisible: boolean;
@@ -162,7 +166,8 @@ export function useJawCeo(args: UseJawCeoArgs): JawCeoController {
162
166
  if (!event) continue;
163
167
  const key = eventKey(event);
164
168
  if (seenEventKeysRef.current.has(key)) continue;
165
- seenEventKeysRef.current.add(key);
169
+ // Same unbounded-growth pattern as the code transcript (050 D3).
170
+ addBounded(seenEventKeysRef.current, key, MAX_SEEN_EVENT_KEYS);
166
171
  freshEvents.push(event);
167
172
  }
168
173
  if (freshEvents.length === 0) return;
@@ -81,6 +81,11 @@ export function useJawCeoVirtualTimeline(args: {
81
81
 
82
82
  useEffect(() => virtualizer._didMount(), [virtualizer]);
83
83
 
84
+ // D2 sibling (050 phase): release the instance on unmount. The CEO console
85
+ // has no per-session key to reset against — it renders one continuous
86
+ // timeline — so only the retention half applies here.
87
+ useEffect(() => () => { virtualizerRef.current = null; }, []);
88
+
84
89
  useLayoutEffect(() => {
85
90
  virtualizer.setOptions({
86
91
  ...virtualizer.options,
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Insertion-ordered Set with a FIFO bound. `Set` already preserves insertion
3
+ * order, so the oldest entry is simply the first key — no side queue needed.
4
+ *
5
+ * Used for streaming dedupe sets, which otherwise grow for the lifetime of a
6
+ * session (260803 unit, 050 phase D3).
7
+ */
8
+ export function addBounded(set: Set<string>, key: string, max: number): void {
9
+ set.add(key);
10
+ while (set.size > max) {
11
+ const oldest = set.values().next();
12
+ if (oldest.done) return;
13
+ set.delete(oldest.value);
14
+ }
15
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * FNV-1a 32-bit. Cheap, dependency-free, and good enough for cache/dedupe keys
3
+ * where the alternative is embedding a full copy of the hashed text.
4
+ * t3code uses the same function for its highlight cache keys
5
+ * (apps/web/src/components/ChatMarkdown.tsx:307).
6
+ */
7
+ export function fnv1a32(input: string): string {
8
+ let hash = 0x811c9dc5;
9
+ for (let i = 0; i < input.length; i += 1) {
10
+ hash ^= input.charCodeAt(i);
11
+ hash = Math.imul(hash, 0x01000193) >>> 0;
12
+ }
13
+ return hash.toString(36);
14
+ }
@@ -1,15 +1,22 @@
1
- import { useState } from 'react';
1
+ import { memo, useMemo, useState } from 'react';
2
2
  import { copyText } from '../../clipboard/copy-text';
3
- import { highlightCode } from './highlight-languages';
3
+ import { highlightCodeCached } from './highlight-cache';
4
4
 
5
5
  type CodeBlockProps = {
6
6
  code: string;
7
7
  language?: string;
8
8
  };
9
9
 
10
- export function CodeBlock(props: CodeBlockProps) {
10
+ function CodeBlockImpl(props: CodeBlockProps) {
11
11
  const [copied, setCopied] = useState(false);
12
- const result = highlightCode(props.code, props.language);
12
+ // D2: highlighting used to run synchronously in the render body on every
13
+ // parent render — i.e. on every streaming token — and again on each
14
+ // virtualized remount. useMemo covers re-renders, the module cache covers
15
+ // remounts, and the cache applies the oversize cutoff.
16
+ const result = useMemo(
17
+ () => highlightCodeCached(props.code, props.language),
18
+ [props.code, props.language],
19
+ );
13
20
  const label = result.language || 'text';
14
21
 
15
22
  async function copyCode(): Promise<void> {
@@ -39,3 +46,5 @@ export function CodeBlock(props: CodeBlockProps) {
39
46
  </div>
40
47
  );
41
48
  }
49
+
50
+ export const CodeBlock = memo(CodeBlockImpl);
@@ -1,4 +1,4 @@
1
- import { Children, createElement, isValidElement, useMemo } from 'react';
1
+ import { Children, createElement, isValidElement, memo, useMemo } from 'react';
2
2
  import type { MouseEvent } from 'react';
3
3
  import type { ComponentProps, CSSProperties, ReactNode } from 'react';
4
4
  import ReactMarkdown from 'react-markdown';
@@ -188,7 +188,7 @@ function splitChildrenWithLocalFileLinks(
188
188
  return out;
189
189
  }
190
190
 
191
- export function MarkdownRenderer(props: MarkdownRendererProps) {
191
+ function MarkdownRendererImpl(props: MarkdownRendererProps) {
192
192
  const renderedMarkdown = useMemo(
193
193
  () => splitPreviewFrontmatter(props.markdown).body,
194
194
  [props.markdown],
@@ -201,6 +201,11 @@ export function MarkdownRenderer(props: MarkdownRendererProps) {
201
201
  onNavigate: props.onWikiLinkNavigate ?? NOOP_NAVIGATE,
202
202
  }), [props.outgoing, props.notes, props.onWikiLinkNavigate]);
203
203
 
204
+ // D1: this whole block used to run in the render body, minting ~20 fresh
205
+ // component identities per render. React keys reconciliation off element
206
+ // type identity, so new types mean unmount + remount of the entire rendered
207
+ // subtree — on every streaming token, for the full accumulated text.
208
+ const components = useMemo<Components>(() => {
204
209
  const inlineTransform = (children: ReactNode, keyPrefix: string): ReactNode => {
205
210
  const withWikiLinks = splitChildrenWithWikiLinks(children, wikiCtx, keyPrefix);
206
211
  return splitChildrenWithLocalFileLinks(withWikiLinks, props.onLocalFileOpen, keyPrefix);
@@ -304,6 +309,8 @@ export function MarkdownRenderer(props: MarkdownRendererProps) {
304
309
  components.th = linearCellTransform('th');
305
310
  components.td = linearCellTransform('td');
306
311
  }
312
+ return components;
313
+ }, [wikiCtx, props.onLocalFileOpen, props.tableMode]);
307
314
 
308
315
  return (
309
316
  <ReactMarkdown
@@ -320,3 +327,8 @@ export function MarkdownRenderer(props: MarkdownRendererProps) {
320
327
  </ReactMarkdown>
321
328
  );
322
329
  }
330
+
331
+ // D1: memo so an unchanged past message is not re-rendered when a sibling
332
+ // updates. Default shallow compare is correct here — every prop is either a
333
+ // primitive or a reference the caller already keeps stable.
334
+ export const MarkdownRenderer = memo(MarkdownRendererImpl);