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.
- package/dist/src/agent/error-classifier.js +11 -2
- package/dist/src/agent/error-classifier.js.map +1 -1
- package/dist/src/agent/events/claude.js +7 -1
- package/dist/src/agent/events/claude.js.map +1 -1
- package/dist/src/agent/events/fulltext-bound.js +32 -0
- package/dist/src/agent/events/fulltext-bound.js.map +1 -0
- package/dist/src/agent/events/grok.js +7 -1
- package/dist/src/agent/events/grok.js.map +1 -1
- package/dist/src/agent/events/helpers.js +17 -4
- package/dist/src/agent/events/helpers.js.map +1 -1
- package/dist/src/agent/lifecycle-handler.js +56 -5
- package/dist/src/agent/lifecycle-handler.js.map +1 -1
- package/dist/src/agent/pi-runtime.js +30 -1
- package/dist/src/agent/pi-runtime.js.map +1 -1
- package/dist/src/agent/spawn/exit-drain.js +96 -0
- package/dist/src/agent/spawn/exit-drain.js.map +1 -0
- package/dist/src/agent/spawn/line-buffer.js +26 -0
- package/dist/src/agent/spawn/line-buffer.js.map +1 -0
- package/dist/src/agent/spawn/process-kill.js +31 -0
- package/dist/src/agent/spawn/process-kill.js.map +1 -1
- package/dist/src/agent/spawn.js +80 -23
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/agent/watchdog.js +21 -5
- package/dist/src/agent/watchdog.js.map +1 -1
- package/dist/src/routes/link-preview.js +14 -0
- package/dist/src/routes/link-preview.js.map +1 -1
- package/package.json +3 -1
- package/public/dist/assets/CodeCanvas-CbfPP7Ad.js +3 -0
- package/public/dist/assets/{DocPanel-DtYwv3n3.js → DocPanel-Dz-J_l5M.js} +1 -1
- package/public/dist/assets/MarkdownRenderer-CXrilOsG.js +15 -0
- package/public/dist/assets/MarkdownRenderer-DZVh_xX2.js +1 -0
- package/public/dist/assets/{MilkdownWysiwygEditor-BNoLGbFC.js → MilkdownWysiwygEditor-Em7iC6y9.js} +8 -8
- package/public/dist/assets/{react-dom-AWKMTIe7.js → bounded-set-CFlZiEF4.js} +1 -1
- package/public/dist/assets/manager-DaN201mN.js +13 -0
- package/public/dist/assets/{wiki-link-resolver-BfNXN9px.js → wiki-link-resolver-DyFzBTIQ.js} +1 -1
- package/public/dist/manager/index.html +1 -1
- package/public/manager/src/SidebarRailRouter.tsx +9 -3
- package/public/manager/src/browser-panel/use-embedded-target-sync.ts +19 -1
- package/public/manager/src/code/CodeCanvas.tsx +14 -2
- package/public/manager/src/code/CodeTranscript.tsx +33 -9
- package/public/manager/src/code/CodeWorkbench.tsx +1 -1
- package/public/manager/src/code/code-event-dedupe.ts +27 -3
- package/public/manager/src/code/use-code-transcript-scroll.ts +28 -3
- package/public/manager/src/code/use-throttled-markdown.ts +75 -0
- package/public/manager/src/code/useCodeTranscriptVirtualRows.ts +19 -0
- package/public/manager/src/electron-metrics.tsx +7 -0
- package/public/manager/src/jaw-ceo/useJawCeo.ts +6 -1
- package/public/manager/src/jaw-ceo/useJawCeoVirtualTimeline.ts +5 -0
- package/public/manager/src/lib/bounded-set.ts +15 -0
- package/public/manager/src/lib/fnv1a.ts +14 -0
- package/public/manager/src/notes/rendering/CodeBlock.tsx +13 -4
- package/public/manager/src/notes/rendering/MarkdownRenderer.tsx +14 -2
- package/public/manager/src/notes/rendering/highlight-cache.ts +111 -0
- package/public/manager/src/notes/useNotesExternalSync.ts +6 -1
- package/scripts/bundle-sidecar.sh +8 -0
- package/scripts/check-electron-no-native.cjs +90 -4
- package/scripts/check-native-load.cjs +157 -0
- package/scripts/check-sidecar-smoke.mjs +151 -0
- package/scripts/ensure-native-modules.cjs +222 -18
- package/scripts/release-gates.mjs +29 -0
- package/scripts/run-electron-builder.mjs +60 -0
- package/public/dist/assets/CodeCanvas-BZo3dohZ.js +0 -3
- package/public/dist/assets/MarkdownRenderer-CHacL2M_.js +0 -15
- package/public/dist/assets/MarkdownRenderer-DyrB9FbZ.js +0 -1
- package/public/dist/assets/manager-DL1GygQL.js +0 -13
package/public/dist/assets/{wiki-link-resolver-BfNXN9px.js → wiki-link-resolver-DyFzBTIQ.js}
RENAMED
|
@@ -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=>({"&":`&`,"<":`<`,">":`>`,'"':`"`,"'":`'`})[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=>({"&":`&`,"<":`<`,">":`>`,'"':`"`,"'":`'`})[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{
|
|
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=>({"&":`&`,"<":`<`,">":`>`,'"':`"`,"'":`'`})[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=>({"&":`&`,"<":`<`,">":`>`,'"':`"`,"'":`'`})[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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =>
|
|
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
|
-
|
|
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
|
-
<
|
|
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 =
|
|
215
|
+
const msg = messagesRef.current[index];
|
|
193
216
|
return msg ? transcriptMessageKey(msg, index) : `sending-${index}`;
|
|
194
|
-
}, [
|
|
217
|
+
}, []);
|
|
195
218
|
const estimateSize = useCallback((index: number): number => (
|
|
196
|
-
estimateTranscriptRowSize(
|
|
197
|
-
), [
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
10
|
+
function CodeBlockImpl(props: CodeBlockProps) {
|
|
11
11
|
const [copied, setCopied] = useState(false);
|
|
12
|
-
|
|
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
|
-
|
|
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);
|