@north-light/crouter 0.3.190 → 0.3.192

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.
@@ -141,10 +141,15 @@ export type BrokerGeneratedNameRequest = {
141
141
  kind: string;
142
142
  };
143
143
  };
144
- /** A daemon-selected Pi editor-label directive. No handler calls a broker. */
144
+ /** A daemon-selected Pi editor-label directive. No handler calls a broker.
145
+ * `description`/`icon` are the values as STORED (an empty icon is left unset),
146
+ * so the broker announces what the node now carries rather than what it asked
147
+ * for. All three are present exactly when `applied`. */
145
148
  export interface BrokerGeneratedNameResultDTO {
146
149
  applied: boolean;
147
150
  editorLabel?: string;
151
+ description?: string;
152
+ icon?: string;
148
153
  }
149
154
  export interface BrokerPersonaAckRequest {
150
155
  from: {
@@ -30,12 +30,34 @@ export interface BrokerWelcomeFrame<M = unknown> {
30
30
  snapshot?: BrokerWelcomeSnapshot<M>;
31
31
  }
32
32
  /**
33
- * The broker-control frames a relay consumer reads: `welcome` (catch-up snapshot)
34
- * and `error`. The broker interleaves others (display_*, ack, …)
33
+ * The crtrd broker `node_named` control frame — the node's generated name at the
34
+ * instant crtrd committed it.
35
+ *
36
+ * A node names itself off its first real message: the naming extension asks a
37
+ * headless model for a handle, commits it through crtrd's guarded compare-and-set,
38
+ * and pushes this frame down the attach stream in the same callback. It is sent
39
+ * ONLY when that guarded commit APPLIED, so a name a human already set is never
40
+ * announced as generated.
41
+ *
42
+ * It exists because the name lands SECONDS AFTER the turn that triggered it — a
43
+ * consumer that settles its own display state on turn-end reads an unnamed node and
44
+ * has nothing left to re-read on. This frame is the push: `description` is the
45
+ * 3-8 word kebab-case handle, `icon` the Nerd Font glyph the namer chose (empty
46
+ * when it chose none), and `editorLabel` crtrd's own rendered label for the node.
47
+ */
48
+ export interface BrokerNodeNamedFrame {
49
+ type: 'node_named';
50
+ description: string;
51
+ icon: string;
52
+ editorLabel: string;
53
+ }
54
+ /**
55
+ * The broker-control frames a relay consumer reads: `welcome` (catch-up snapshot),
56
+ * `error`, and `node_named`. The broker interleaves others (display_*, ack, …)
35
57
  * under non-colliding `type` discriminants; a relay mapper drops those through its
36
58
  * `default` arm untyped, so they are not enumerated here.
37
59
  */
38
- export type BrokerControlFrame<M = unknown> = BrokerWelcomeFrame<M> | BrokerErrorFrame;
60
+ export type BrokerControlFrame<M = unknown> = BrokerWelcomeFrame<M> | BrokerErrorFrame | BrokerNodeNamedFrame;
39
61
  /**
40
62
  * What the crtrd broker attach delivers to a relay consumer: the live engine
41
63
  * event stream (`E` — pi's `AgentSessionEvent`, relayed verbatim) unioned with the
@@ -151,6 +151,12 @@ export function createFrameHandler(ctx, fx) {
151
151
  case 'error':
152
152
  fx.setNotice(frame.code === 'read_only' ? 'read-only' : `error: ${frame.message}`);
153
153
  break;
154
+ case 'node_named':
155
+ // Nothing to do here: the naming extension shares this process, so it
156
+ // set the live session name in the same callback and pi's own
157
+ // `session_info_changed` already moved this viewer's label chip. The
158
+ // frame exists for clients that are NOT in the broker's process.
159
+ break;
154
160
  case 'ack':
155
161
  handleAck(frame);
156
162
  break;
@@ -1534,7 +1534,7 @@ ${m}
1534
1534
  Follow the instructions below exactly as written \u2014 every part, including the presence or absence of arguments above, is a load-bearing requirement.
1535
1535
 
1536
1536
  ${n}`}function Qfr(e){try{let t=JSON.parse(e);return Array.isArray(t)?t.flatMap(r=>{if(typeof r!="object"||r===null||Array.isArray(r))return[];let n=r;return typeof n.name!="string"||typeof n.description!="string"||typeof n.source!="string"?[]:n.source!=="builtin"&&n.source!=="command"&&n.source!=="template"?[]:n.template!==void 0&&typeof n.template!="string"?[]:n.expansion!==void 0&&!ise(n.expansion)?[]:[{name:n.name,description:n.description,source:n.source,...typeof n.template=="string"?{template:n.template}:{},...ise(n.expansion)?{expansion:n.expansion}:{}}]}):[]}catch{return[]}}function U9n(e){let t=[],r="",n=null;for(let o of e)n!==null?o===n?n=null:r+=o:o==='"'||o==="'"?n=o:/\s/.test(o)?r!==""&&(t.push(r),r=""):r+=o;return r!==""&&t.push(r),t}function F9n(e,t){let r=t.join(" ");return e.replace(/\$\{(\d+):-([^}]*)\}|\$\{@:(\d+)(?::(\d+))?\}|\$(ARGUMENTS|@|\d+)/g,(n,o,i,s,a,l)=>{if(o!==void 0)return t[Number.parseInt(o,10)-1]||i||"";if(s!==void 0){let u=Math.max(0,Number.parseInt(s,10)-1);return a!==void 0?t.slice(u,u+Number.parseInt(a,10)).join(" "):t.slice(u).join(" ")}return l==="ARGUMENTS"||l==="@"?r:t[Number.parseInt(l||"0",10)-1]??""})}function qfr(e,t){let r=e.match(/^\/([^\s]+)(?:\s+([\s\S]*))?$/);if(r===null)return;let n=r[1];if(t.some(l=>l.source==="builtin"&&l.name===n))return;let o=e.indexOf(" "),i=o===-1?e.slice(1):e.slice(1,o),s=t.find(l=>l.source==="command"&&l.name===i);if(s!==void 0){if(s.expansion===void 0||!ise(s.expansion))return;let l=o===-1?"":e.slice(o+1);return{name:s.name,invocation:e,content:Gfr(s.expansion,l)}}let a=t.find(l=>l.source==="template"&&l.name===n);if(typeof a?.template=="string")return{name:a.name,invocation:e,content:F9n(a.template,U9n(r[2]??""))}}var B9n="CRTR_TIMING",Vfr="CRTR_TIMING_T0";function $9n(){return performance.timeOrigin+performance.now()}var Wfr=(()=>{let e=process.env[B9n];return e!==void 0&&e!==""&&e!=="0"&&e.toLowerCase()!=="false"})(),z9n=(()=>{if(!Wfr)return 0;let e=Number(process.env[Vfr]);if(Number.isFinite(e)&&e>0)return e;let t=performance.timeOrigin;return process.env[Vfr]=String(t),t})(),G9n=(()=>{let e=process.env.CRTR_TIMING_PROC;return e!==void 0&&e!==""?e:process.argv[2]==="surface"&&process.argv[3]==="attach"?"viewer":"cli"})(),W$e=0;function Cw(e,t){if(!Wfr)return;let r=$9n(),n=r-z9n,o=W$e===0?n:r-W$e;W$e=r;let i=performance.now(),s=t===void 0?"":" "+Object.entries(t).map(([u,c])=>`${u}=${String(c)}`).join(" "),a=process.env.CRTR_TIMING_PROC??G9n,l=u=>u.toFixed(1).padStart(8);process.stderr.write(`[timing] +${l(n)}ms \u0394${l(o)}ms up=${l(i)}ms t=${r.toFixed(1)} ${a}/${process.pid} ${e}${s}
1537
- `)}function jfr(e,t){let{socket:r}=e,n=()=>{r.send({type:"get_commands"}),t.resetMemoryRefs()},o=a=>{if(a.for==="get_commands"&&a.ok&&a.detail){try{t.setCommands(Qfr(a.detail))}catch{}return}if(a.for==="navigate_tree"&&a.ok){a.detail&&t.setEditorText(a.detail);return}if(a.for==="share"){a.ok&&a.detail?(t.setNotice(`Shared: ${a.detail}`),b2e(a.detail)):a.ok||t.setNotice(`share failed: ${a.detail??"unknown error"}`);return}if(a.for==="clone"&&!a.ok){t.setNotice(`clone failed: ${a.detail??"unknown error"}`);return}if(!a.ok){t.setNotice(`command failed: ${a.for}${a.detail?` \u2014 ${a.detail}`:""}`);return}a.for==="reload"&&(t.reloadBindings(),t.setCommands(),n(),t.setNotice("Reloaded keybindings, extensions, skills, prompts, and themes"))},i=a=>{t.chat.handleEvent(a),a.type==="session_info_changed"?t.refreshNodeIdentity():a.type==="thinking_level_changed"?t.setNotice(t.modelThinkingLabel()??`Thinking level: ${a.level}`):(a.type==="tool_execution_start"||a.type==="tool_execution_end")&&t.renderFooter()},s=!1;return a=>{switch(t.foldState(a),a.type){case"welcome":{s||(s=!0,Cw("viewer.welcome")),t.chat.applySnapshot(a.snapshot),t.refreshNodeIdentity(),n(),a.pending_dialog!=null&&t.attachDialog(a.pending_dialog);break}case"tool_group_summary":t.chat.applyToolGroupSummary(a.key,a.summary);break;case"working_activity":t.chat.setWorkingActivity(a.activity);break;case"model_changed":{let l=t.modelThinkingLabel();l&&t.setNotice(l);break}case"error":t.setNotice(a.code==="read_only"?"read-only":`error: ${a.message}`);break;case"ack":o(a);break;case"extension_ui_request":a.method!=="setStatus"&&a.method!=="setWidget"&&a.method!=="setTitle"&&t.attachDialog(a);break;case"extension_ui_dismiss":t.dismissDialog(a.id);break;case"bash_start":t.chat.bashStart(a.command,a.excludeFromContext);break;case"bash_output":t.chat.bashOutput(a.chunk);break;case"bash_end":t.chat.bashEnd(a);break;default:i(a);break}}}var Q9n=["crtr.attach.scroll.up","crtr.attach.scroll.down","crtr.attach.scroll.page-up","crtr.attach.scroll.page-down","crtr.attach.scroll.live","crtr.attach.mouse.toggle","crtr.attach.copy.message"];function Hfr(e,t,r){return lI(e,t,cI(r),(n,o)=>ln(n,o))}function j$e(e,t){t[e]()}function Yfr(e,t,r){for(let{actionId:n}of w_)if(Hfr(e,n,t))return j$e(n,r),!0;return!1}function Kfr(e,t,r){for(let n of Q9n)if(Hfr(e,n,t))return r[n](),!0;return!1}function Jfr(e){for(let t of["crtr.attach.clear-or-detach","crtr.attach.detach"])if(e.gestures(t).length>0)return Zh(e,t)}function H$e(e,t,r,n){let o=e();if(t(o.bindings),r())try{n(o.bindings,o.tmuxPassthrough)}catch{}return o}var q9n=1e3;function Xfr(e,t){let{tui:r,editor:n}=e,o=0,i={"crtr.attach.detach":t.detach,"crtr.attach.clear-or-detach":()=>{let s=Date.now();if(s-o<q9n){o=0,t.detach();return}o=s,n.setText(""),t.setNotice(`Editor cleared \u2014 press ${t.detachHint()} again to detach`),r.requestRender()},"crtr.attach.graph.toggle":t.toggleGraph,"crtr.attach.help.toggle":t.toggleHelp,"crtr.attach.inbox.toggle":t.toggleInbox,"crtr.attach.model-ladder.next":()=>t.cycleModelLadder("forward"),"crtr.attach.model-ladder.previous":()=>t.cycleModelLadder("backward"),"crtr.attach.command.inspect":t.inspectLoadedCommand,"crtr.attach.file-review":t.openFileReview,"crtr.attach.profile-files":t.openProfileFiles,"crtr.attach.whip":t.whip};return r.addInputListener(s=>{if(s.includes("\x1B[I")||s.includes("\x1B[O")){t.setPaneFocused(s.lastIndexOf("\x1B[I")>s.lastIndexOf("\x1B[O")),r.requestRender();let l=s.replaceAll("\x1B[I","").replaceAll("\x1B[O","");return l.length>0?{data:l}:{consume:!0}}let a=Ece(s);if(a!==void 0)return j$e(a.actionId,i),a.remaining.length>0?{data:a.remaining}:{consume:!0};if(!t.hasExclusiveSurface())return n.focused&&Kfr(t.bindings(),s,t.viewportActions())?{consume:!0}:t.rosterHandleKey(s)?{consume:!0}:Yfr(t.bindings(),s,i)?{consume:!0}:void 0})}import{spawnSync as V9n}from"node:child_process";var W9n="\x1B[?1002h\x1B[?1006h",Zfr="\x1B[?1002l\x1B[?1006l",ehr=3;function j9n(){if(!Ll())return!1;let e=V9n("tmux",["show-options","-gv","mouse"],{encoding:"utf8"});return e.status===0&&e.stdout?.trim()==="on"}function thr(e,t){let r=t==="on"||t==="tmux"&&j9n(),n=()=>{e(r?W9n:Zfr)},o=()=>{r=!1;try{e(Zfr)}catch{}};return process.once("exit",o),{enabled:()=>r,toggle:()=>(r=!r,n(),r),reassert:n,disable:o,install:(i,s)=>i.addInputListener(a=>{let l=!1,c=a.replace(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/g,(p,d,f,m,h)=>{l=!0;let g=Number(d),y=g&-29;return y===64?s.onWheel("up",ehr,Number(f)):y===65?s.onWheel("down",ehr,Number(f)):y===32&&h==="M"?s.onPrimaryDrag(Number(f),Number(m)):y===0&&h==="m"?s.onPrimaryRelease(Number(f),Number(m)):g===0&&h==="M"&&s.onPrimaryPress(Number(f),Number(m)),""}).replace(/\x1b\[M[\s\S]{3}/g,()=>(l=!0,""));if(l)return c.length>0?{data:c}:{consume:!0}})}}var H9n=new Intl.Segmenter(void 0,{granularity:"grapheme"}),J$e="\x1B[7m",rhr="\x1B[27m";function Y9n(e,t){let r=e[t+1];if(r!==void 0){if(r==="]"||r==="_"||r==="P"||r==="^"){let n=e.indexOf("\x07",t+2),o=e.indexOf("\x1B\\",t+2),i=n===-1?o:o===-1?n:Math.min(n,o);return i===-1?void 0:{text:e.slice(t,i+(i===n?1:2)),end:i+(i===n?1:2)}}if(r==="["){let n=t+2;for(;n<e.length&&(e.charCodeAt(n)<64||e.charCodeAt(n)>126);)n++;return n>=e.length?void 0:{text:e.slice(t,n+1),end:n+1}}return{text:e.slice(t,t+2),end:t+2}}}function sse(e,t){let r=0,n=0;for(;n<e.length;){if(e[n]==="\x1B"){let s=Y9n(e,n);if(s===void 0)break;t.onEscape?.(s.text),n=s.end;continue}let o=e.indexOf("\x1B",n),i=e.slice(n,o===-1?void 0:o);for(let s of H9n.segment(i)){let a=$a(s.segment);t.onGlyph?.({text:s.segment,cell:r,width:a}),r+=a}n=o===-1?e.length:o}return r}function nhr(e,t,r){let n="";return sse(e,{onGlyph:o=>{o.cell>=t&&o.cell<r&&(n+=o.text)}}),n}function ohr(e,t,r,n){let o=Math.min(r,n);if(o<=t||t<0)return e;let i="",s=!1,a=()=>{s||(i+=J$e,s=!0)},l=()=>{s&&(i+=rhr,s=!1)},u=sse(e,{onEscape:c=>{i+=c,s&&c.endsWith("m")&&(i+=J$e)},onGlyph:c=>{c.cell>=t&&c.cell<o?a():l(),i+=c.text}});if(l(),u<o){let c=Math.max(0,t-u),p=o-Math.max(t,u);i+=" ".repeat(c)+J$e+" ".repeat(p)+rhr}return i}function K9n(e,t){return e.row<t.row||e.row===t.row&&e.column<=t.column}function ihr(e,t,r){let[n,o]=K9n(e,t)?[e,t]:[t,e],i=[];for(let s=n.row;s<=o.row;s++){let a=s===n.row?n.column-1:0,l=s===o.row?o.column:r;l>a&&i.push({row:s,from:a,to:l})}return i}function shr(e){let t,r,n=()=>t!==void 0&&r!==void 0?{anchor:t,head:r}:void 0,o=()=>{let i=n();if(!i)return"";let s=e.frame(),a=ihr(i.anchor,i.head,e.width()).map(l=>nhr(s[l.row-1]??"",l.from,l.to).trimEnd());for(;a.length>0&&a[a.length-1]==="";)a.pop();return a.join(`
1537
+ `)}function jfr(e,t){let{socket:r}=e,n=()=>{r.send({type:"get_commands"}),t.resetMemoryRefs()},o=a=>{if(a.for==="get_commands"&&a.ok&&a.detail){try{t.setCommands(Qfr(a.detail))}catch{}return}if(a.for==="navigate_tree"&&a.ok){a.detail&&t.setEditorText(a.detail);return}if(a.for==="share"){a.ok&&a.detail?(t.setNotice(`Shared: ${a.detail}`),b2e(a.detail)):a.ok||t.setNotice(`share failed: ${a.detail??"unknown error"}`);return}if(a.for==="clone"&&!a.ok){t.setNotice(`clone failed: ${a.detail??"unknown error"}`);return}if(!a.ok){t.setNotice(`command failed: ${a.for}${a.detail?` \u2014 ${a.detail}`:""}`);return}a.for==="reload"&&(t.reloadBindings(),t.setCommands(),n(),t.setNotice("Reloaded keybindings, extensions, skills, prompts, and themes"))},i=a=>{t.chat.handleEvent(a),a.type==="session_info_changed"?t.refreshNodeIdentity():a.type==="thinking_level_changed"?t.setNotice(t.modelThinkingLabel()??`Thinking level: ${a.level}`):(a.type==="tool_execution_start"||a.type==="tool_execution_end")&&t.renderFooter()},s=!1;return a=>{switch(t.foldState(a),a.type){case"welcome":{s||(s=!0,Cw("viewer.welcome")),t.chat.applySnapshot(a.snapshot),t.refreshNodeIdentity(),n(),a.pending_dialog!=null&&t.attachDialog(a.pending_dialog);break}case"tool_group_summary":t.chat.applyToolGroupSummary(a.key,a.summary);break;case"working_activity":t.chat.setWorkingActivity(a.activity);break;case"model_changed":{let l=t.modelThinkingLabel();l&&t.setNotice(l);break}case"error":t.setNotice(a.code==="read_only"?"read-only":`error: ${a.message}`);break;case"node_named":break;case"ack":o(a);break;case"extension_ui_request":a.method!=="setStatus"&&a.method!=="setWidget"&&a.method!=="setTitle"&&t.attachDialog(a);break;case"extension_ui_dismiss":t.dismissDialog(a.id);break;case"bash_start":t.chat.bashStart(a.command,a.excludeFromContext);break;case"bash_output":t.chat.bashOutput(a.chunk);break;case"bash_end":t.chat.bashEnd(a);break;default:i(a);break}}}var Q9n=["crtr.attach.scroll.up","crtr.attach.scroll.down","crtr.attach.scroll.page-up","crtr.attach.scroll.page-down","crtr.attach.scroll.live","crtr.attach.mouse.toggle","crtr.attach.copy.message"];function Hfr(e,t,r){return lI(e,t,cI(r),(n,o)=>ln(n,o))}function j$e(e,t){t[e]()}function Yfr(e,t,r){for(let{actionId:n}of w_)if(Hfr(e,n,t))return j$e(n,r),!0;return!1}function Kfr(e,t,r){for(let n of Q9n)if(Hfr(e,n,t))return r[n](),!0;return!1}function Jfr(e){for(let t of["crtr.attach.clear-or-detach","crtr.attach.detach"])if(e.gestures(t).length>0)return Zh(e,t)}function H$e(e,t,r,n){let o=e();if(t(o.bindings),r())try{n(o.bindings,o.tmuxPassthrough)}catch{}return o}var q9n=1e3;function Xfr(e,t){let{tui:r,editor:n}=e,o=0,i={"crtr.attach.detach":t.detach,"crtr.attach.clear-or-detach":()=>{let s=Date.now();if(s-o<q9n){o=0,t.detach();return}o=s,n.setText(""),t.setNotice(`Editor cleared \u2014 press ${t.detachHint()} again to detach`),r.requestRender()},"crtr.attach.graph.toggle":t.toggleGraph,"crtr.attach.help.toggle":t.toggleHelp,"crtr.attach.inbox.toggle":t.toggleInbox,"crtr.attach.model-ladder.next":()=>t.cycleModelLadder("forward"),"crtr.attach.model-ladder.previous":()=>t.cycleModelLadder("backward"),"crtr.attach.command.inspect":t.inspectLoadedCommand,"crtr.attach.file-review":t.openFileReview,"crtr.attach.profile-files":t.openProfileFiles,"crtr.attach.whip":t.whip};return r.addInputListener(s=>{if(s.includes("\x1B[I")||s.includes("\x1B[O")){t.setPaneFocused(s.lastIndexOf("\x1B[I")>s.lastIndexOf("\x1B[O")),r.requestRender();let l=s.replaceAll("\x1B[I","").replaceAll("\x1B[O","");return l.length>0?{data:l}:{consume:!0}}let a=Ece(s);if(a!==void 0)return j$e(a.actionId,i),a.remaining.length>0?{data:a.remaining}:{consume:!0};if(!t.hasExclusiveSurface())return n.focused&&Kfr(t.bindings(),s,t.viewportActions())?{consume:!0}:t.rosterHandleKey(s)?{consume:!0}:Yfr(t.bindings(),s,i)?{consume:!0}:void 0})}import{spawnSync as V9n}from"node:child_process";var W9n="\x1B[?1002h\x1B[?1006h",Zfr="\x1B[?1002l\x1B[?1006l",ehr=3;function j9n(){if(!Ll())return!1;let e=V9n("tmux",["show-options","-gv","mouse"],{encoding:"utf8"});return e.status===0&&e.stdout?.trim()==="on"}function thr(e,t){let r=t==="on"||t==="tmux"&&j9n(),n=()=>{e(r?W9n:Zfr)},o=()=>{r=!1;try{e(Zfr)}catch{}};return process.once("exit",o),{enabled:()=>r,toggle:()=>(r=!r,n(),r),reassert:n,disable:o,install:(i,s)=>i.addInputListener(a=>{let l=!1,c=a.replace(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/g,(p,d,f,m,h)=>{l=!0;let g=Number(d),y=g&-29;return y===64?s.onWheel("up",ehr,Number(f)):y===65?s.onWheel("down",ehr,Number(f)):y===32&&h==="M"?s.onPrimaryDrag(Number(f),Number(m)):y===0&&h==="m"?s.onPrimaryRelease(Number(f),Number(m)):g===0&&h==="M"&&s.onPrimaryPress(Number(f),Number(m)),""}).replace(/\x1b\[M[\s\S]{3}/g,()=>(l=!0,""));if(l)return c.length>0?{data:c}:{consume:!0}})}}var H9n=new Intl.Segmenter(void 0,{granularity:"grapheme"}),J$e="\x1B[7m",rhr="\x1B[27m";function Y9n(e,t){let r=e[t+1];if(r!==void 0){if(r==="]"||r==="_"||r==="P"||r==="^"){let n=e.indexOf("\x07",t+2),o=e.indexOf("\x1B\\",t+2),i=n===-1?o:o===-1?n:Math.min(n,o);return i===-1?void 0:{text:e.slice(t,i+(i===n?1:2)),end:i+(i===n?1:2)}}if(r==="["){let n=t+2;for(;n<e.length&&(e.charCodeAt(n)<64||e.charCodeAt(n)>126);)n++;return n>=e.length?void 0:{text:e.slice(t,n+1),end:n+1}}return{text:e.slice(t,t+2),end:t+2}}}function sse(e,t){let r=0,n=0;for(;n<e.length;){if(e[n]==="\x1B"){let s=Y9n(e,n);if(s===void 0)break;t.onEscape?.(s.text),n=s.end;continue}let o=e.indexOf("\x1B",n),i=e.slice(n,o===-1?void 0:o);for(let s of H9n.segment(i)){let a=$a(s.segment);t.onGlyph?.({text:s.segment,cell:r,width:a}),r+=a}n=o===-1?e.length:o}return r}function nhr(e,t,r){let n="";return sse(e,{onGlyph:o=>{o.cell>=t&&o.cell<r&&(n+=o.text)}}),n}function ohr(e,t,r,n){let o=Math.min(r,n);if(o<=t||t<0)return e;let i="",s=!1,a=()=>{s||(i+=J$e,s=!0)},l=()=>{s&&(i+=rhr,s=!1)},u=sse(e,{onEscape:c=>{i+=c,s&&c.endsWith("m")&&(i+=J$e)},onGlyph:c=>{c.cell>=t&&c.cell<o?a():l(),i+=c.text}});if(l(),u<o){let c=Math.max(0,t-u),p=o-Math.max(t,u);i+=" ".repeat(c)+J$e+" ".repeat(p)+rhr}return i}function K9n(e,t){return e.row<t.row||e.row===t.row&&e.column<=t.column}function ihr(e,t,r){let[n,o]=K9n(e,t)?[e,t]:[t,e],i=[];for(let s=n.row;s<=o.row;s++){let a=s===n.row?n.column-1:0,l=s===o.row?o.column:r;l>a&&i.push({row:s,from:a,to:l})}return i}function shr(e){let t,r,n=()=>t!==void 0&&r!==void 0?{anchor:t,head:r}:void 0,o=()=>{let i=n();if(!i)return"";let s=e.frame(),a=ihr(i.anchor,i.head,e.width()).map(l=>nhr(s[l.row-1]??"",l.from,l.to).trimEnd());for(;a.length>0&&a[a.length-1]==="";)a.pop();return a.join(`
1538
1538
  `)};return{begin(i,s){t={row:s,column:i},r=void 0},extend(i,s){return t===void 0||r!==void 0&&r.row===s&&r.column===i?!1:(r={row:s,column:i},!0)},release(){let i=t;if(i===void 0)return;if(r===void 0)return t=void 0,{kind:"click",column:i.column,row:i.row};let s=o();if(s==="")return t=void 0,r=void 0,{kind:"selection",notice:"Nothing to copy \u2014 selection is empty"};let{system:a,tmux:l}=Joe(s),u=a&&l?"clipboard + tmux buffer":a?"clipboard":l?"tmux buffer":void 0,c=s.split(`
1539
1539
  `).length;return{kind:"selection",notice:u===void 0?"Copy failed \u2014 no clipboard tool found":`Copied selection (${c} ${c===1?"line":"lines"}) \u2014 ${u}`}},clear(){let i=r!==void 0;return t=void 0,r=void 0,i},active(){return r!==void 0},apply(i){let s=n();if(!s)return i.slice();let a=e.width(),l=i.slice();for(let u of ihr(s.anchor,s.head,a)){let c=u.row-1;c<0||c>=l.length||(l[c]=ohr(l[c],u.from,u.to,a))}return l}}}import{existsSync as J9n}from"node:fs";import{fileURLToPath as X9n}from"node:url";function ahr(e,t,r){if(!Number.isInteger(t)||!Number.isInteger(r)||t<1||r<1)return;let n;for(let o=0;o<Math.min(t,e.length);o++){let i=o===t-1?r:void 0,s=Z9n(e[o]??"",n,i);if(n=s.activeUri,s.uri!==void 0)return s.uri}}function Z9n(e,t,r){let n=t,o;return sse(e,{onEscape:i=>{let s=e7n(i);s!==null&&(n=s===""?void 0:s)},onGlyph:i=>{if(o!==void 0||r===void 0||n===void 0||i.width===0)return;let s=r-1;s>=i.cell&&s<i.cell+i.width&&(o=n)}}),{activeUri:n,uri:o}}function e7n(e){if(!e.startsWith("\x1B]8;"))return null;let t=e.endsWith("\x1B\\")?2:1,r=e.slice(4,-t),n=r.indexOf(";");return n===-1?null:r.slice(n+1)}function chr(e){let t;try{let o=new URL(e.uri);if(o.protocol!=="file:")return"invalid";t=X9n(o)}catch{return"invalid"}if(!J9n(t))return"missing";let r=`crtr surface attach to ${ps(e.nodeId)}${e.observer?" --observer":""}`,n=`nvim ${ps(t)} || :; exec ${r}`;return e.beforeRespawn(),Rce({pane:e.pane,cwd:e.cwd,env:kce(),command:n})?"opened":"failed"}import{execFile as t7n}from"node:child_process";function lhr(e){switch(e){case"active":return 0;case"idle":return 1;case"done":return 2;case"canceled":return 3;case"dead":return 4;default:return 5}}var r7n=.72,n7n={anchor:"center",width:"72%",minWidth:48,maxHeight:"72%",margin:1},o7n=[["crtr.graph.dismiss","dismiss"],["crtr.graph.focus","focus"],["crtr.graph.revive","revive"],["crtr.graph.close-subtree","close"],["crtr.graph.focus-manager","manager"]],i7n=[["crtr.graph.dismiss","dismiss"]],s7n=[["crtr.graph.down","down"],["crtr.graph.up","up"],["crtr.graph.first","first"],["crtr.graph.last","last"],["crtr.graph.collapse-or-manager","fold/manager"],["crtr.graph.expand-or-child","expand/child"]],ase=class{constructor(t,r,n,o,i,s,a="controller",l=()=>{},u=()=>{}){this.tui=t;this.self=r;this.getAsks=n;this.palette=o;this.bindings=i;this.source=s;this.role=a;this.onRefreshFailure=l;this.onNotice=u;this.remote=s instanceof Tb}tui;self;getAsks;palette;bindings;source;role;onRefreshFailure;onNotice;handle;userExpanded=new Set;userCollapsed=new Set;folds={userExpanded:this.userExpanded,userCollapsed:this.userCollapsed};cursorId;scrollTop=0;pendingConfirm;snapshot;snapshotSeq=0;remote;async fetchFocused(){if(this.remote)return new Set;try{return new Set((await Rs().listFocuses()).map(t=>t.node_id))}catch{return new Set}}setBindings(t){this.bindings=t,this.handle!==void 0&&this.tui.requestRender()}matches(t,r){return lI(this.bindings,t,r,(n,o)=>ln(n,o))}hint(t){return t.flatMap(([r,n])=>this.bindings.gestures(r).length===0?[]:[`${Zh(this.bindings,r)} ${n}`])}fitHints(t,r){let o=[],i=0;for(let s of this.hint(t)){let a=(o.length===0?0:3)+vb(s);if(i+a>r)break;o.push(s),i+=a}return o.join(" \xB7 ")}isOpen(){return this.handle!==void 0}open(){this.handle===void 0&&(this.scrollTop=0,this.pendingConfirm=void 0,this.cursorId=this.self,this.handle=this.tui.showOverlay(this,n7n),this.refresh())}close(){this.handle!==void 0&&(this.pendingConfirm=void 0,this.handle.hide(),this.handle=void 0,this.tui.requestRender())}toggle(){this.handle===void 0?this.open():this.close()}refresh(){if(this.handle===void 0)return;let t=++this.snapshotSeq;this.buildSnapshot().then(r=>{t===this.snapshotSeq&&(this.snapshot=r,this.tui.requestRender())}).catch(r=>{this.onRefreshFailure(r),this.tui.requestRender()})}invalidate(){this.refresh()}async buildSnapshot(){SV();let t=new Map,r=new Map,n=new Map,o=new Map,i=new Map,s=async _=>{if(t.has(_))return t.get(_)??null;let b;try{b=await this.source.getNode(_)}catch{b=null}return t.set(_,b),b},a=async _=>{let b=r.get(_);if(b!==void 0)return b;let S;try{S=await this.source.subscriptionsOf(_)}catch{S=[]}let v=[];for(let I of S)(await s(I.node_id))?.kind!=="human"&&v.push(I.node_id);return r.set(_,v),v},l=async _=>{if(o.has(_))return o.get(_);let b;try{b=(await this.source.subscribersOf(_))[0]?.node_id}catch{b=void 0}return o.set(_,b),b},u=async _=>{let b=_,S=new Set([b]);for(;;){let v=await l(b);if(v===void 0||S.has(v))break;S.add(v),b=v}return b},c=async _=>lhr((await s(_))?.status),p=async _=>{let b=n.get(_);if(b!==void 0)return b;let S=[...await a(_)],v=new Map;return await Promise.all(S.map(async I=>{v.set(I,await c(I))})),S.sort((I,R)=>(v.get(I)??99)-(v.get(R)??99)),n.set(_,S),S},d=new Set,f=new Set,m=async _=>{let b=(await s(_))?.status==="active";if(f.has(_))return{active:b?1:0,reveal:b||_===this.self};f.add(_);let S=0,v=!1;for(let I of await a(_)){let R=await m(I);S+=R.active,R.reveal&&(v=!0)}return i.set(_,S),v&&d.add(_),{active:S+(b?1:0),reveal:v||b||_===this.self}};await m(await u(this.self));let h=[],g=new Set,y=async(_,b,S,v)=>{if(g.has(_)){let F=S?"":v?"\u2514\u2500 ":"\u251C\u2500 ";h.push({id:_,hasKids:!1,isSelf:_===this.self,branch:b+F,cycle:!0,collapsed:!1});return}g.add(_);let I=await p(_),R=this.folds.userExpanded.has(_)?!1:this.folds.userCollapsed.has(_)?!0:!d.has(_),N=S?"":v?"\u2514\u2500 ":"\u251C\u2500 ";if(h.push({id:_,hasKids:I.length>0,isSelf:_===this.self,branch:b+N,cycle:!1,collapsed:R}),R)return;let O=S?"":b+(v?" ":"\u2502 ");for(let F=0;F<I.length;F++)await y(I[F],O,!1,F===I.length-1)};return await y(await u(this.self),"",!0,!0),{rows:h,activeBelow:i,nodes:t,childIds:r,sortedChildIds:n,managerOf:o,focused:await this.fetchFocused()}}renderSnapshotRow(t,r,n,o){let i=(v,I)=>n?bV(v,Rb(),Yie):I?bV(v,Rb(),Efr):yV(v);if(r.cycle){let v=`${r.branch} ${Cp}\u21BA ${tse(r.id)}${Bs}`;return i(v,!1)}let s=t.nodes.get(r.id)??null,a=Jie(s,this.remote),l=TV(s,r.id),u=r.isSelf?`${AV}${l}${Bs}`:l,c=`${Cp}${s?.kind??""}${Bs}`,p=`${Cp}${Xie(r.id,this.remote)}${Bs}`,d=r.hasKids&&r.collapsed,f=!n&&d?`${Cp}\u25B8${Bs} `:" ",m=ese(s),h=t.childIds.get(r.id)?.length??0,g=s!==null&&h>0?` ${Cp}\u2933${h}${Bs}`:"",y=s!==null&&s.status!=="active"&&(t.activeBelow.get(s.node_id)??0)>0?` ${Kie}\u21E3${t.activeBelow.get(s.node_id)??0}${Bs}`:"",_=s!==null&&!this.remote?Hie(s,_V(r.id)):null,b=_!==null?` \xB7 ${Q$e(_)}`:"",S=`${r.branch}${f}${a} ${u} ${c} ${p}${m}${g}${y}${rse(r.id,o)}${Zie(r.id,s,this.remote)}${b}`;return i(S,Tfr(r.id,s,t.focused,this.remote))}render(t){SV();let r=this.getAsks(),n=this.snapshot?.rows??[],o=n.findIndex(y=>y.id===this.cursorId);o<0&&(o=n.findIndex(y=>y.id===this.self),o<0&&(o=0)),this.cursorId=n[o]?.id??this.self;let i=process.stdout.rows??Afr,s=Math.max(3,Math.min(Math.floor(i*r7n),i-2)),a=Math.max(2,Math.min(n.length,s-2)),l=a;for(let y=0;y<4;y++){o<this.scrollTop&&(this.scrollTop=o),o>=this.scrollTop+l&&(this.scrollTop=o-l+1),this.scrollTop=Math.max(0,Math.min(this.scrollTop,Math.max(0,n.length-l)));let _=a-(this.scrollTop>0?1:0)-(this.scrollTop+l<n.length?1:0);if(_===l)break;l=Math.max(1,_)}let u=Math.min(n.length,this.scrollTop+l),c=[];this.scrollTop>0&&c.push(`${Cp} \u2191 ${this.scrollTop} more${Bs}`);for(let y=this.scrollTop;y<u;y++){let _=this.snapshot;c.push(_!==void 0?this.renderSnapshotRow(_,n[y],y===o,r):"")}for(u<n.length&&c.push(`${Cp} \u2193 ${n.length-u} more${Bs}`);c.length<a;)c.push("");let p=n.length>0?`${AV}\u2317 canvas graph${Bs} ${Cp}(${o+1}/${n.length})${Bs}`:`${AV}\u2317 canvas graph${Bs} ${Cp}(loading)${Bs}`,d=this.pendingConfirm!==void 0?`${EV}${this.pendingConfirm.label} ${AV}${this.hint([["crtr.graph.confirm.accept","accept"],["crtr.graph.confirm.cancel","cancel"]]).join(" \xB7 ")}${Bs}`:`${Cp}${this.fitHints([...this.remote||this.role!=="controller"?i7n:o7n,...s7n],Math.max(1,t-5))}${Bs}`,f=this.palette.border,m=Math.max(1,t-4),h=c.map(y=>`${f("\u2502")} ${bn(y,m,"",!0)} ${f("\u2502")}`),g=this.palette.surface;return[this.borderRow("\u256D","\u256E",p,t),...h,this.borderRow("\u2570","\u256F",d,t)].map(g)}borderRow(t,r,n,o){let i=this.palette.border,s=Math.max(1,o-5),a=bn(n,s,"\u2026"),l=Math.max(0,o-5-vb(a));return`${i(`${t}\u2500`)} ${a} ${i("\u2500".repeat(l)+r)}`}handleInput(t){try{this.dispatch(t)}catch{}}dispatch(t){if(this.pendingConfirm!==void 0){if(this.matches("crtr.graph.confirm.accept",t)){let u=this.pendingConfirm.action;this.pendingConfirm=void 0,u()}else this.pendingConfirm=void 0;this.tui.requestRender();return}if(this.matches("crtr.graph.dismiss",t)){this.close();return}let r=this.snapshot,n=r?.rows??[],o=n.findIndex(u=>u.id===this.cursorId);o<0&&(o=Math.max(0,n.findIndex(u=>u.id===this.self)));let i=n[o],s=u=>u!==void 0&&!u.cycle,a=(u,c)=>{for(let p=u+c;p>=0&&p<n.length;p+=c)if(s(n[p]))return p;return u},l=u=>{let c=u===1?0:n.length-1;for(let p=c;p>=0&&p<n.length;p+=u)if(s(n[p]))return p;return o};if(this.matches("crtr.graph.down",t)){o=a(o,1),this.cursorId=n[o]?.id??this.cursorId,this.tui.requestRender();return}if(this.matches("crtr.graph.up",t)){o=a(o,-1),this.cursorId=n[o]?.id??this.cursorId,this.tui.requestRender();return}if(this.matches("crtr.graph.first",t)){this.cursorId=n[l(1)]?.id??this.cursorId,this.tui.requestRender();return}if(this.matches("crtr.graph.last",t)){this.cursorId=n[l(-1)]?.id??this.cursorId,this.tui.requestRender();return}if(this.matches("crtr.graph.collapse-or-manager",t)){if(i!==void 0&&i.hasKids&&!i.collapsed)this.userCollapsed.add(i.id),this.userExpanded.delete(i.id),this.refresh();else{let u=r?.managerOf.get(this.cursorId??this.self);u!==void 0&&n.some(c=>c.id===u)&&(this.cursorId=u),this.tui.requestRender()}return}if(this.matches("crtr.graph.expand-or-child",t)){if(i!==void 0&&i.collapsed&&i.hasKids)this.userExpanded.add(i.id),this.userCollapsed.delete(i.id),this.refresh();else if(i!==void 0&&i.hasKids){let u=r?.sortedChildIds.get(i.id)?.[0];u!==void 0&&(this.cursorId=u),this.tui.requestRender()}return}if(this.matches("crtr.graph.focus",t)){if(this.remote)return;if(this.role!=="controller"){this.onNotice("Read-only observer attach \u2014 cannot focus nodes");return}this.cursorId!==void 0&&this.focusTarget(this.cursorId),this.close();return}if(this.matches("crtr.graph.revive",t)){if(this.remote)return;if(this.role!=="controller"){this.onNotice("Read-only observer attach \u2014 cannot revive nodes");return}let u=this.cursorId,c=u===void 0?null:r?.nodes.get(u)??null;u!==void 0&&c!==null&&c.status!=="active"&&c.status!=="idle"&&(this.reviveTarget(u),this.close());return}if(this.matches("crtr.graph.focus-manager",t)){if(this.remote)return;if(this.role!=="controller"){this.onNotice("Read-only observer attach \u2014 cannot focus nodes");return}let u=r?.managerOf.get(this.self);u!==void 0&&(this.focusTarget(u),this.close());return}if(this.matches("crtr.graph.close-subtree",t)){if(this.remote)return;if(this.role!=="controller"){this.onNotice("Read-only observer attach \u2014 cannot close nodes");return}let u=this.cursorId??this.self,c=r?.nodes.get(u)??null,p=c!==null?z$e(c):tse(u);this.pendingConfirm={label:`kill ${p}?`,action:()=>this.shellCrtr(["node","lifecycle","close","--node",u])},this.tui.requestRender();return}}focusTarget(t){let r=["surface","node","focus",t],n=process.env.TMUX_PANE;n!==void 0&&n!==""&&r.push("--pane",n),this.shellCrtr(r)}reviveTarget(t){this.shellCrtr(["node","lifecycle","revive",t],()=>this.focusTarget(t))}shellCrtr(t,r){try{t7n("crtr",t,o=>{o==null&&r?.()}).stdin?.end()}catch{}}};var a7n=.72,c7n={anchor:"center",width:"72%",minWidth:48,maxHeight:"72%",margin:1},cse=class{constructor(t,r){this.tui=t;this.pal=r}tui;pal;handle;bindings;scrollTop=0;lastContentRows=1;setBindings(t){this.bindings=t,this.handle!==void 0&&this.tui.requestRender()}toggle(){this.handle===void 0?this.open():this.close()}close(){this.handle!==void 0&&(this.handle.hide(),this.handle=void 0,this.tui.requestRender())}isOpen(){return this.handle!==void 0}invalidate(){this.handle!==void 0&&this.tui.requestRender()}render(t){let r=Math.max(1,t);if(r<5)return this.renderNarrow(r);let n=r-4,o=this.body(n),i=Math.max(1,this.tui.terminal.rows),s=Math.max(3,Math.min(Math.floor(i*a7n),Math.max(3,i-2))),a=Math.max(1,s-2),l=o.length>a,u=l&&a>=3?a-2:1;this.lastContentRows=u,this.scrollTop=Math.max(0,Math.min(this.scrollTop,Math.max(0,o.length-u)));let c=Math.min(o.length,this.scrollTop+u),p=[];for(l&&a>=3&&p.push(this.scrollTop>0?this.pal.muted(`\u2191 ${this.scrollTop} more`):""),p.push(...o.slice(this.scrollTop,c)),l&&a>=3?p.push(c<o.length?this.pal.muted(`\u2193 ${o.length-c} more`):""):l&&a===2&&p.push(c<o.length?this.pal.muted(`\u2193 ${o.length-c} more`):"");p.length<a;)p.push("");let d=this.pal.bold(this.pal.accent("\u2328 attach help")),f=this.pal.muted("esc dismiss \xB7 \u2191\u2193 scroll");return[this.borderRow("\u256D","\u256E",d,r),...p.map(m=>this.boxRow(m,r)),this.borderRow("\u2570","\u256F",f,r)].map(this.pal.surface)}handleInput(t){return ln(t,"escape")||ln(t,"q")?(this.close(),!0):ln(t,"up")||this.matchesBinding("crtr.attach.scroll.up",t)?this.scrollBy(-1):ln(t,"down")||this.matchesBinding("crtr.attach.scroll.down",t)?this.scrollBy(1):ln(t,"pageUp")||this.matchesBinding("crtr.attach.scroll.page-up",t)?this.scrollBy(-this.pageSize()):ln(t,"pageDown")||this.matchesBinding("crtr.attach.scroll.page-down",t)?this.scrollBy(this.pageSize()):!0}open(){this.scrollTop=0,this.handle=this.tui.showOverlay(this,c7n)}matchesBinding(t,r){return this.bindings!==void 0&&lI(this.bindings,t,r,(n,o)=>ln(n,o))}scrollBy(t){let r=this.body(Math.max(1,this.tui.terminal.columns-4)).length,n=Math.max(0,r-this.lastContentRows),o=Math.max(0,Math.min(n,this.scrollTop+t));return o!==this.scrollTop&&(this.scrollTop=o,this.tui.requestRender()),!0}pageSize(){return Math.max(1,this.lastContentRows-1)}body(t){return this.lines().flatMap(r=>this.wrap(r.text,t).map(n=>this.style(n,r.style)))}lines(){let t=(s,a)=>({text:`${this.binding(s)} ${a}`,style:"plain"}),r=s=>({text:s,style:"heading"}),n=s=>({text:s,style:"muted"}),o={text:"",style:"plain"},i=w_.map(({actionId:s})=>{let a=mm.find(l=>l.id===s)?.name??s;return t(s,a)});return[r("Scrolling"),t("crtr.attach.scroll.up","scroll one line up"),t("crtr.attach.scroll.down","scroll one line down"),t("crtr.attach.scroll.page-up","scroll one page up"),t("crtr.attach.scroll.page-down","scroll one page down"),t("crtr.attach.scroll.live","return to the live bottom"),n("mouse wheel (when reporting is on)"),o,r("Selection and copy"),{text:"Drag with the mouse to select; releasing copies the selection to the clipboard (and the tmux buffer). Any key clears the highlight.",style:"plain"},{text:"prefix + [ tmux copy-mode selects and copies the visible screenful; it works even while mouse reporting is on.",style:"plain"},{text:"Option-drag (iTerm2, Terminal.app) or Shift-drag (Ghostty, Alacritty, xterm) makes a true terminal selection that bypasses mouse reporting.",style:"plain"},{text:"Block-copy commands copy transcript content and are not capped at one screenful.",style:"plain"},o,r("Copy commands"),{text:"/copy last assistant message.",style:"plain"},{text:`/copy-message (${this.binding("crtr.attach.copy.message")}) newest copyable block entirely visible in the viewport; otherwise the newest one that intersects it.`,style:"plain"},{text:"/copy-tool newest tool call's result text.",style:"plain"},{text:"/copy-transcript every copyable block's text, oldest to newest, joined by blank lines.",style:"plain"},o,r("Mouse"),{text:`/mouse or ${this.binding("crtr.attach.mouse.toggle")} toggle mouse reporting \u2014 wheel scrolling and viewer drag-select. Turning it off restores tmux drag-select and copy-mode mouse.`,style:"plain"},o,r("Other attach keys"),...i]}binding(t){return this.bindings===void 0?"Loading\u2026":Zh(this.bindings,t)}wrap(t,r){if(t==="")return[""];let n=Math.max(1,r),o=t.split(/\s+/),i=[],s="";for(let a of o){let l=s===""?a:`${s} ${a}`;s!==""&&on(l)>n?(i.push(bn(s,n,"",!0)),s=` ${a}`):s=l}return s!==""&&i.push(bn(s,n,"",!0)),i}style(t,r){switch(r){case"heading":return this.pal.bold(this.pal.accent(t));case"muted":return this.pal.muted(t);case"plain":return t}}boxRow(t,r){let n=r-4,o=bn(t,n,"",!0),i=" ".repeat(Math.max(0,n-on(o)));return`${this.pal.border("\u2502")} ${o}${i} ${this.pal.border("\u2502")}`}borderRow(t,r,n,o){if(o<6)return bn(`${this.pal.border(t)}${n}${this.pal.border(r)}`,o,"",!0);let i=o-5,s=bn(n,i,"\u2026"),a=Math.max(0,o-5-on(s));return`${this.pal.border(`${t}\u2500`)} ${s} ${this.pal.border("\u2500".repeat(a)+r)}`}renderNarrow(t){let r=bn(this.pal.bold(this.pal.accent("help")),t,"",!0);return[this.pal.surface(r)]}};import{basename as l7n}from"node:path";function lse(e){return l7n(e)}async function uhr(e,t){await Rs().resolveHumanTicket(lse(e),{responses:t})}async function phr(e,t){await Rs().cancelHumanTicket(lse(e),t)}function dhr(e){if(e instanceof Es)return e.status===503||e.code==="daemon_unavailable";let t=e?.code;return t==="ECONNREFUSED"||t==="ENOENT"}function X1(e){return dhr(e)?"could not reach crtrd \u2014 nothing was submitted":e instanceof Es||e instanceof Error?e.message:String(e)}function mhr(e){return dhr(e)?"unreachable":e instanceof Es&&e.status===409&&e.code==="ticket_already_resolved"?"already_resolved":e instanceof Es&&e.status===500&&e.code==="delivery_failed"?"delivery_failed":"other"}import{join as u7n}from"node:path";function Z1(e,t){let r=`${new Date().toISOString()} ${e} ${t}`.replaceAll(/[\r\n]/g," ");BW(u7n(ku(),"attach-invariants.log"),r)}var use=class{constructor(t,r,n){this.surface=t;this.focusTarget=r;this.getRows=n}surface;focusTarget;getRows;_focused=!1;get focused(){return this._focused}set focused(t){this._focused=t,bU(this.focusTarget)&&(this.focusTarget.focused=t)}get wantsKeyRelease(){return this.focusTarget.wantsKeyRelease??this.surface.wantsKeyRelease??!1}render(t){let r=Math.max(0,Math.floor(this.getRows())),n=Math.max(0,Math.floor(t));this.surface.setRows(r);let o=this.surface.render(n);o.length!==r&&X$e(`surface rendered ${o.length} rows; expected ${r}`);let i=o.slice(0,r).map(a=>on(a)>n?(X$e(`surface row is ${on(a)} cells wide; expected at most ${n}`),fhr(pm(a,0,n,!0),n)):fhr(a,n));for(;i.length<r;)i.push(" ".repeat(n));let s=p7n(i);return s>1?(X$e(`surface frame contains ${s} CURSOR_MARKER sequences; expected at most one`),i.map(d7n)):i}handleInput(t){(this.focusTarget.handleInput??this.surface.handleInput)?.call(this.focusTarget.handleInput===void 0?this.surface:this.focusTarget,t)}invalidate(){this.surface.invalidate()}};function fhr(e,t){return bn(e,t,"",!0)}function p7n(e){return e.reduce((t,r)=>t+r.split(fa).length-1,0)}function d7n(e){return e.split(fa).join("")}function X$e(e){Z1("surface-frame",e)}var m7n={anchor:"top-left",width:"100%",maxHeight:"100%",margin:0},f7n={anchor:"top-left",width:"100%",margin:0,nonCapturing:!0},dse=class{capabilities;tui;mouse;getRows;statusNotice;formatNotice;noticeMs;selfPane;stack=[];noticeOverlay;noticeTimer;constructor(t){this.tui=t.tui,this.mouse=t.mouse,this.getRows=t.rows,this.statusNotice=t.statusNotice,this.formatNotice=t.formatNotice,this.noticeMs=t.noticeMs,this.selfPane=t.selfPane,this.capabilities={...t.capabilities,selfPane:t.selfPane}}mount(t,r){this.disposeNotice();let n=new use(t,r?.focus??t,this.getRows),o=this.tui.showOverlay(n,m7n),i={component:t,frame:n,overlay:o,cleanup:[],closed:!1,closing:!1},s=this.stack[this.stack.length-1];return s&&s.overlay.setHidden(!0),this.stack.push(i),{close:()=>this.closeEntry(i,!0),invalidate:()=>{!i.closed&&this.stack[this.stack.length-1]===i&&this.tui.requestRender()},onClose:a=>{if(i.closed||i.closing){pse("onClose registered after surface close");return}i.cleanup.push(a)}}}notice(t,r){if(this.disposeNotice(),this.stack.length===0){this.statusNotice(t,r);return}if(t==="")return;let n=new eze(()=>this.formatNotice(t));this.noticeOverlay=this.tui.showOverlay(n,f7n),r?.sticky!==!0&&this.noticeMs>0&&(this.noticeTimer=setTimeout(()=>{this.noticeTimer=void 0,this.disposeNoticeOverlay()},this.noticeMs))}suspendForChild(){return this.tui.stop(),()=>{this.tui.start(),this.mouse.reassert(),this.tui.invalidate(),this.tui.requestRender(!0)}}async focusNode(t){if(!this.capabilities.canFocusNode||this.selfPane===void 0||this.selfPane==="")return this.notice("Focusing a node needs a local viewer in tmux"),!1;try{let r=await Rs().getNode(t),n=await tdr(t,{pane:this.selfPane,cwd:r.cwd,name:r.name});return n.focused||this.notice("Could not open that node\u2019s viewer"),n.focused}catch(r){return this.notice(X1(r)),!1}}pickReviewFile(t){return new Promise(r=>{let n,o=!1,i=a=>{o||(o=!0,r(a),n.close())},s=Tie(t.candidates,t.cwd,a=>i(a),()=>i(null),a=>this.notice(a));n=this.mount(h7n(s.component,t.title),{focus:s.focus}),n.onClose(()=>i(null))})}handleWheel(t,r){return this.stack[this.stack.length-1]?.component.handleWheel?.(t,r)??!1}hasFullPaneSurface(){return this.stack.length>0}closeAll(){let t=this.stack[0];t&&this.closeEntry(t,!1),this.disposeNotice()}closeEntry(t,r){if(t.closed||t.closing)return;if(this.stack.indexOf(t)===-1){t.closed=!0;return}for(t.closing=!0;this.stack.indexOf(t)!==-1;){let i=this.stack.indexOf(t),s=this.stack[i+1];if(!s)break;this.closeEntry(s,!1)}try{t.overlay.hide()}catch(i){pse(`surface overlay hide failed: ${Z$e(i)}`)}for(let i of t.cleanup)try{let s=i();s&&typeof s.then=="function"&&s.catch(a=>pse(`surface cleanup failed: ${Z$e(a)}`))}catch(s){pse(`surface cleanup failed: ${Z$e(s)}`)}t.cleanup.length=0,t.closed=!0,t.closing=!1;let o=this.stack.indexOf(t);if(o!==-1&&this.stack.splice(o,1),r){this.disposeNotice();let i=this.stack[this.stack.length-1];i&&i.overlay.setHidden(!1)}}disposeNotice(){this.disposeNoticeOverlay(),this.noticeTimer!==void 0&&(clearTimeout(this.noticeTimer),this.noticeTimer=void 0)}disposeNoticeOverlay(){this.noticeOverlay?.hide(),this.noticeOverlay=void 0}},eze=class{constructor(t){this.text=t}text;render(t){return[bn(this.text(),Math.max(0,t),"",!0)]}invalidate(){}};function h7n(e,t){let r=0;return{setRows:n=>{r=Math.max(0,Math.floor(n))},render:n=>{let o=e.render(n),s=(t===void 0?o:[bn(t,Math.max(0,n),"",!0),...o.slice(1)]).slice(0,r);for(;s.length<r;)s.push("");return s},invalidate:()=>e.invalidate()}}function pse(e){Z1("surface-host",e)}function Z$e(e){return e instanceof Error?e.message:String(e)}import{watch as DXn}from"node:fs";function g7n(e,t){switch(t){case"gray":return`${oie}${e}${Cr}`;case"cyan":return`${cl}${e}${Cr}`;case"red":return`${Npr}${e}${Cr}`;case"yellow":return`${Su}${e}${Cr}`;default:return e}}var _7n={notify:"\u2709",decision:"\u25C6",context:"\u270E",error:"\u26A0",review:"\u25A4"},A7n={notify:"gray",decision:"cyan",context:"cyan",error:"red",review:"yellow"};function E7n(e){let t=Math.floor((Date.now()-new Date(e).getTime())/6e4);return t>=60?`${Math.floor(t/60)}h ago`:t>0?`${t}m ago`:"just now"}function hhr(e,t,r,n=Number.MAX_SAFE_INTEGER){if(e.length===0)return[` ${Xr}${nie}No pending interactions${Cr}`];let o=t-4,i=e.map((l,u)=>y7n(l,u,r,o)),s=b7n(i,r,Math.max(1,n-2)),a=[` ${zh}${e.length} pending${Cr}`,""];s.start>0&&a.push(` ${Xr}\u2191 ${s.start} above${Cr}`);for(let l=s.start;l<s.end;l++)a.push(...i[l]);return s.end<e.length&&a.push(` ${Xr}\u2193 ${e.length-s.end} below${Cr}`),a}function y7n(e,t,r,n){let o=e.kind==="deck"?e.interactionKind??"decision":"review",i=_7n[o]??"\xB7",s=E7n(e.blockedSince),a=t===r?`${cl}\u25B8${Cr} `:" ",l=Math.max(1,n-s.length-6),u=`${a}${g7n(i,A7n[o]??"cyan")} ${zh}${z2e(e.title||`(${e.id.slice(0,8)})`,l)}${Cr} ${Xr}${s}${Cr}`,c=e.claim?`claimed by ${e.claim.owner} \xB7 ${e.subtitle}`:e.subtitle;return[u,` ${Xr}${z2e(c,n-6)}${Cr}`]}function b7n(e,t,r){let n=Math.max(0,Math.min(t,e.length-1)),o=n+1,i=!0;for(;;){let s=o<e.length?{start:n,end:o+1}:void 0,a=n>0?{start:n-1,end:o}:void 0,u=(i?[s,a]:[a,s]).find(c=>c!==void 0&&S7n(e,c.start,c.end)<=r);if(u===void 0)break;n=u.start,o=u.end,i=!i}return{start:n,end:o}}function S7n(e,t,r){let n=t>0?1:0;for(let o=t;o<r;o++)n+=e[o].length;return r<e.length&&n++,n}function tze(e,t,r="list"){if(e<60||t<18)return{mode:"minimum",listWidth:e,detailWidth:0,height:t};if(e<96)return{mode:r,listWidth:e,detailWidth:e,height:t};let n=Math.max(30,Math.min(44,Math.floor(e/3)));return{mode:"two-column",listWidth:n,detailWidth:e-n-1,height:t}}import{readdirSync as U7n,statSync as Thr}from"node:fs";import{basename as Shr,join as F7n}from"node:path";import{existsSync as C7n,linkSync as IAc,mkdirSync as vAc,realpathSync as RAc,renameSync as wAc,unlinkSync as x7n,writeFileSync as NAc}from"node:fs";function wb(e){return`${e}/deck.json`}function eU(e){return`${e}/review.json`}function tU(e){return`${e}/response.json`}function xw(e){return`${e}/progress.json`}function gE(e){return`${e}/claim.json`}function ghr(e){return C7n(tU(e))}function rze(e){try{x7n(xw(e))}catch(t){if(t.code!=="ENOENT")throw t}}import{basename as FAc,dirname as BAc,isAbsolute as Ahr,resolve as $Ac,sep as zAc}from"node:path";D2();D2();var _hr=["notify","decision","context","error","review"];var I7n=yn.object({id:yn.string().min(1),label:yn.string().min(1),description:yn.string().optional()}),v7n=yn.object({selectedOptionId:yn.string().optional(),selectedOptionIds:yn.array(yn.string()).optional(),freetext:yn.string().optional(),label:yn.string().optional()}),R7n=yn.object({id:yn.string().regex(/^[A-Za-z0-9_-]+$/,{error:"interaction id must match /^[A-Za-z0-9_-]+$/"}).min(1).max(64),title:yn.string().trim().min(1),subtitle:yn.string().trim().min(1),body:yn.string().optional(),bodyPath:yn.string().optional(),options:yn.array(I7n),multiSelect:yn.boolean().optional(),allowFreetext:yn.boolean().optional(),freetextLabel:yn.string().optional(),kind:yn.enum(_hr).optional(),preAnswered:v7n.optional()}),Ehr=yn.object({sessionName:yn.string().optional(),askedBy:yn.string().optional(),blockedSince:yn.string().optional(),nodeId:yn.string().optional()}),w7n=yn.object({title:yn.string().trim().min(1),source:Ehr.optional(),interactions:yn.array(R7n).min(1)}).superRefine((e,t)=>{let r=new Set;e.interactions.forEach((n,o)=>{n.body!==void 0&&n.bodyPath!==void 0&&t.addIssue({code:"custom",message:"body and bodyPath are mutually exclusive",path:["interactions",o]}),r.has(n.id)&&t.addIssue({code:"custom",message:`duplicate interaction id "${n.id}"`,path:["interactions",o,"id"]}),r.add(n.id)})}),N7n=Ehr,O7n=yn.object({schema:yn.literal("humanloop.review/v1"),file:yn.string().min(1).refine(Ahr,"file must be absolute"),output:yn.string().min(1).refine(Ahr,"output must be absolute"),title:yn.string().trim().min(1),subtitle:yn.string().trim().min(1),source:N7n,blockedSince:yn.string().datetime({offset:!0})}).strict();function xV(e){return w7n.parse(e)}function nze(e){return O7n.parse(e)}import{mkdirSync as k7n,watch as P7n,writeFileSync as YAc}from"node:fs";import{basename as D7n,dirname as yhr,join as M7n}from"node:path";function IV(){return tce()}function L7n(){return M7n(ku(),"signals","inbox-activity")}function bhr(e){let t=L7n();try{k7n(yhr(t),{recursive:!0,mode:448});let r=P7n(yhr(t),(n,o)=>{String(o)===D7n(t)&&e()});return()=>r.close()}catch{return()=>{}}}function Chr(e){let t=fd(gE(e));if(!(t===null||typeof t.host!="string"||typeof t.claimedAt!="string"||typeof t.heartbeatAt!="string"))return{owner:t.host,claimedAt:t.claimedAt,heartbeatAt:t.heartbeatAt}}function B7n(e,t){let r=fd(wb(e));if(r===null)return null;let n;try{n=xV(r)}catch{return null}let o=n.interactions[0];if(o===void 0)return null;let i=n.source?.blockedSince;if(i===void 0)try{i=Thr(wb(e)).mtime.toISOString()}catch{return null}return{dir:e,id:t,kind:"deck",title:n.title,subtitle:o.subtitle,interactionKind:o.kind,interactionCount:n.interactions.length,source:n.source??{},blockedSince:i,claim:Chr(e)}}function $7n(e,t){let r=fd(eU(e));if(r===null)return null;let n;try{n=nze(r)}catch{return null}return{dir:e,id:t,kind:"review",title:n.title,subtitle:n.subtitle,file:n.file,output:n.output,source:n.source,blockedSince:n.blockedSince,claim:Chr(e)}}function mse(){let e;try{e=U7n(IV())}catch{return[]}let t=[];for(let r of e){let n=F7n(IV(),r);try{if(!Thr(n).isDirectory())continue}catch{continue}if(ghr(n))continue;let o=B7n(n,Shr(n))??$7n(n,Shr(n));o!==null&&t.push(o)}return t.sort((r,n)=>n.blockedSince.localeCompare(r.blockedSince)||r.id.localeCompare(n.id))}import{existsSync as fse,renameSync as z7n,unlinkSync as xhr,writeFileSync as Ihr}from"node:fs";import{hostname as oze}from"node:os";import{randomUUID as G7n}from"node:crypto";var Q7n=3e4;function q7n(e){if(typeof e!="object"||e===null)return null;let t=e;return typeof t.token!="string"||typeof t.host!="string"||!Number.isInteger(t.pid)||typeof t.claimedAt!="string"||typeof t.heartbeatAt!="string"?null:{token:t.token,host:t.host,pid:t.pid,claimedAt:t.claimedAt,heartbeatAt:t.heartbeatAt,...typeof t.tmuxClient=="string"?{tmuxClient:t.tmuxClient}:{}}}function hse(e){return q7n(fd(gE(e)))}function V7n(e){try{return process.kill(e,0),!0}catch{return!1}}function W7n(e,t=Date.now(),r=oze()){if(e.host===r)return!V7n(e.pid);let n=Date.parse(e.heartbeatAt);return!Number.isFinite(n)||t-n>Q7n}function vhr(e,t={}){return fm(`${e}/.ticket-lock`,()=>{if(fse(tU(e))||!fse(wb(e))&&!fse(eU(e)))return null;let r=gE(e),n=hse(e);if(n!==null&&!W7n(n,Date.now(),t.host??oze()))return null;fse(r)&&xhr(r);let o=t.host??oze(),i=t.pid??process.pid,s=(t.now??new Date).toISOString(),a={token:G7n(),host:o,pid:i,claimedAt:s,heartbeatAt:s,...t.tmuxClient?{tmuxClient:t.tmuxClient}:{}};return Ihr(r,`${JSON.stringify(a)}
1540
1540
  `,{flag:"wx",mode:384}),a})}function Rhr(e,t,r=new Date){return fm(`${e}/.ticket-lock`,()=>{let n=hse(e);if(n===null||n.token!==t)return!1;let o=`${gE(e)}.${t}.heartbeat`;return Ihr(o,`${JSON.stringify({...n,heartbeatAt:r.toISOString()})}
@@ -0,0 +1,14 @@
1
+ /** What crtrd stored, as it stored it — the payload of a `node_named` frame. */
2
+ export interface NodeNamedNotice {
3
+ description: string;
4
+ icon: string;
5
+ editorLabel: string;
6
+ }
7
+ /** Register the broker's fan-out. One broker hosts one pi process, so the last
8
+ * registration wins; a second broker in one process is not a thing that
9
+ * exists. */
10
+ export declare function onNodeNamed(fn: (notice: NodeNamedNotice) => void): void;
11
+ /** Announce a generated name crtrd's guarded commit APPLIED. A no-op when no
12
+ * broker is listening — the same extension code also runs under test harnesses
13
+ * that host no client registry. */
14
+ export declare function publishNodeNamed(notice: NodeNamedNotice): void;
@@ -0,0 +1,27 @@
1
+ // node-named.ts — the in-process hop from the naming extension to the broker's
2
+ // client fan-out.
3
+ //
4
+ // A node's generated name is committed by the naming extension
5
+ // (`pi-extensions/broker-local.ts`), which runs INSIDE the broker's own pi
6
+ // process but holds no reference to the broker's client registry: pi loads
7
+ // extensions by path, so there is no constructor seam to pass one through. This
8
+ // module is that seam — one module-level slot, both ends resolving the same
9
+ // file in the same process.
10
+ //
11
+ // The frame it carries (`node_named`) exists because the name lands seconds
12
+ // AFTER the turn that triggered it: a consumer settling its display state on
13
+ // turn-end reads an unnamed node and has nothing left to re-read on. The
14
+ // committing side pushes instead.
15
+ let listener;
16
+ /** Register the broker's fan-out. One broker hosts one pi process, so the last
17
+ * registration wins; a second broker in one process is not a thing that
18
+ * exists. */
19
+ export function onNodeNamed(fn) {
20
+ listener = fn;
21
+ }
22
+ /** Announce a generated name crtrd's guarded commit APPLIED. A no-op when no
23
+ * broker is listening — the same extension code also runs under test harnesses
24
+ * that host no client registry. */
25
+ export function publishNodeNamed(notice) {
26
+ listener?.(notice);
27
+ }
@@ -1,6 +1,6 @@
1
1
  import type { AgentSession, AgentSessionEvent, RpcExtensionUIRequest, RpcExtensionUIResponse, RpcSessionState, SessionEntry, SessionInfo, SettingsConfig, SessionStats } from '@earendil-works/pi-coding-agent';
2
2
  import type { Api, ImageContent, Model } from '@earendil-works/pi-ai';
3
- import type { BrokerErrorFrame } from '../../api/dto/broker.js';
3
+ import type { BrokerErrorFrame, BrokerNodeNamedFrame } from '../../api/dto/broker.js';
4
4
  import type { ToolGroupSummary } from '../../shared/tool-groups.js';
5
5
  /** pi's own RPC types, re-exported unchanged. `broker-protocol` stays the one
6
6
  * import site for the wire union; the shapes themselves are pi's, versioned by
@@ -386,6 +386,15 @@ export interface ModelChangedFrame {
386
386
  * promise instead of hanging it — absent on uncorrelated errors (engine drive
387
387
  * errors, command-op failures, frame_overflow). There is NO `retryable` field. */
388
388
  export type ErrorFrame = BrokerErrorFrame;
389
+ /** The `node_named` control frame. SOURCED from the shared
390
+ * `@north-light/crouter-api` contract for the same reason `error` is: Northlight
391
+ * Core reads this frame off its own attach, so the wire shape here and the shape
392
+ * it consumes are one compiler-checked type.
393
+ *
394
+ * Broadcast when the naming extension's GUARDED commit applied — a name a human
395
+ * already set never produces one. The in-process hop from the extension to this
396
+ * fan-out is `broker/node-named.ts`. */
397
+ export type NodeNamedFrame = BrokerNodeNamedFrame;
389
398
  /** Result of a command op (§1.3): `for` echoes the op name, `ok` the
390
399
  * outcome, `detail` an optional human-readable note. */
391
400
  export interface AckFrame {
@@ -589,7 +598,7 @@ export interface ToolGroupSummaryFrame {
589
598
  /** Everything the broker can send. Live `AgentSessionEvent`s are relayed
590
599
  * verbatim (the broker adds nothing); the broker's own control frames carry
591
600
  * non-colliding `type` discriminants. */
592
- export type BrokerToClient = WelcomeFrame | WorkingActivityFrame | ModelChangedFrame | ErrorFrame | AckFrame | BrokerDataFrame | BashStartFrame | BashOutputFrame | BashEndFrame | ExtensionUIRequestFrame | ExtensionUIDismissFrame | ToolGroupSummaryFrame | AgentSessionEvent;
601
+ export type BrokerToClient = WelcomeFrame | WorkingActivityFrame | ModelChangedFrame | ErrorFrame | NodeNamedFrame | AckFrame | BrokerDataFrame | BashStartFrame | BashOutputFrame | BashEndFrame | ExtensionUIRequestFrame | ExtensionUIDismissFrame | ToolGroupSummaryFrame | AgentSessionEvent;
593
602
  /** Encode one frame as a single newline-terminated JSON line. */
594
603
  export declare function encodeFrame(frame: ClientToBroker | BrokerToClient): string;
595
604
  /** Byte bounds for a {@link FrameDecoder} (C5). */
@@ -42,6 +42,7 @@ import { visibleMessages } from './session-visibility.js';
42
42
  import { isUnknownModel } from './model-selection.js';
43
43
  import { BrokerClientRegistry } from './broker/client-registry.js';
44
44
  import { ToolGroupTracker } from './broker/tool-groups.js';
45
+ import { onNodeNamed } from './broker/node-named.js';
45
46
  import { FaultRetry } from './broker/fault-retry.js';
46
47
  import { EventProjection } from './broker/event-projection.js';
47
48
  import { createFrameDispatchContext, handleFrame } from './broker/frame-dispatch.js';
@@ -275,6 +276,11 @@ export async function runBroker(nodeId, startupAt = process.hrtime.bigint(), sta
275
276
  const registry = new BrokerClientRegistry(nodeId);
276
277
  const pendingDialogs = new Map();
277
278
  const toolGroups = new ToolGroupTracker(nodeId, (frame) => registry.broadcast(frame));
279
+ // The naming extension commits the node's generated name a few seconds into
280
+ // its first turn and hands it here (broker/node-named.ts). Every attached
281
+ // client learns it on the pipe it already holds instead of re-reading the node
282
+ // on some later, unrelated trigger.
283
+ onNodeNamed((notice) => registry.broadcast({ type: 'node_named', ...notice }));
278
284
  let disposed = false;
279
285
  let disposePending = false;
280
286
  let server;
@@ -34,9 +34,9 @@ export declare function sanitizeIcon(raw: string): string;
34
34
  * to a sanitized SessionName, or `{name:'',icon:''}` on timeout, a missing tool
35
35
  * call, or a malformed emission so the caller can fall back to a local slug.
36
36
  *
37
- * Exported (not just for tests): the canvas-coupled persistence wrapper
38
- * generateAndPersistName lives in naming-persist.ts to keep this module
39
- * canvas-free, and imports this pure helper. */
37
+ * This module stays canvas-free so CLI leaves can reach it. The canvas-coupled
38
+ * commit path lives in pi-extensions/broker-local.ts, which imports this pure
39
+ * helper and writes through crtrd's guarded generated-name operation. */
40
40
  export declare function headlessName(body: string, sourceCap?: number): Promise<SessionName>;
41
41
  /** Generate a name from a bounded conversation source rather than a single
42
42
  * kickoff message. The caller owns selecting representative conversation text;
@@ -185,9 +185,9 @@ export function sanitizeIcon(raw) {
185
185
  * to a sanitized SessionName, or `{name:'',icon:''}` on timeout, a missing tool
186
186
  * call, or a malformed emission so the caller can fall back to a local slug.
187
187
  *
188
- * Exported (not just for tests): the canvas-coupled persistence wrapper
189
- * generateAndPersistName lives in naming-persist.ts to keep this module
190
- * canvas-free, and imports this pure helper. */
188
+ * This module stays canvas-free so CLI leaves can reach it. The canvas-coupled
189
+ * commit path lives in pi-extensions/broker-local.ts, which imports this pure
190
+ * helper and writes through crtrd's guarded generated-name operation. */
191
191
  export async function headlessName(body, sourceCap = PROMPT_CAP) {
192
192
  const empty = { name: '', icon: '' };
193
193
  const route = nameModelRequest();
@@ -350,7 +350,12 @@ function handleGeneratedName(ctx) {
350
350
  description: request.description,
351
351
  ...(request.icon.trim() === '' ? {} : { icon: request.icon }),
352
352
  });
353
- return { applied: true, editorLabel: editorLabel(updated) };
353
+ return {
354
+ applied: true,
355
+ editorLabel: editorLabel(updated),
356
+ description: updated.description ?? '',
357
+ icon: updated.icon ?? '',
358
+ };
354
359
  }
355
360
  const expected = request.expected;
356
361
  if (node.name !== expected.name || (node.description ?? '') !== expected.description
@@ -360,7 +365,12 @@ function handleGeneratedName(ctx) {
360
365
  description: request.description,
361
366
  icon: request.icon.trim() === '' ? undefined : request.icon,
362
367
  });
363
- return { applied: true, editorLabel: editorLabel(updated) };
368
+ return {
369
+ applied: true,
370
+ editorLabel: editorLabel(updated),
371
+ description: updated.description ?? '',
372
+ icon: updated.icon ?? '',
373
+ };
364
374
  });
365
375
  return { status: 200, body: result };
366
376
  }
@@ -1,6 +1,6 @@
1
1
  // Run with: node --import tsx/esm --test src/pi-extensions/__tests__/canvas-structured-output.test.ts
2
2
  //
3
- // Regression: Codex strict function schemas require every property in `required`.
3
+ // Regression: strict function schemas require all properties and closed object shapes.
4
4
  import { after, test } from 'node:test';
5
5
  import assert from 'node:assert/strict';
6
6
  import { mkdtempSync, rmSync } from 'node:fs';
@@ -37,9 +37,15 @@ test('optional submit fields remain optional through a strict provider tool sche
37
37
  commit: { type: 'string' },
38
38
  verification: { type: 'string' },
39
39
  blocker: { type: 'string' },
40
+ details: {
41
+ type: 'object',
42
+ properties: {
43
+ summary: { type: 'string' },
44
+ },
45
+ required: ['summary'],
46
+ },
40
47
  },
41
- required: ['status', 'verification'],
42
- additionalProperties: false,
48
+ required: ['status', 'verification', 'details'],
43
49
  };
44
50
  writeOutputSchema('structured-output-test', 'oneoff', schema);
45
51
  let definition;
@@ -51,10 +57,12 @@ test('optional submit fields remain optional through a strict provider tool sche
51
57
  });
52
58
  assert.ok(definition, 'the pending request registers submit');
53
59
  const parameters = definition['parameters'];
54
- assert.deepEqual(parameters.required, ['status', 'commit', 'verification', 'blocker']);
60
+ assert.deepEqual(parameters.required, ['status', 'commit', 'verification', 'blocker', 'details']);
55
61
  assert.deepEqual(parameters.properties.commit, { anyOf: [{ type: 'string' }, { type: 'null' }] });
56
62
  assert.deepEqual(parameters.properties.blocker, { anyOf: [{ type: 'string' }, { type: 'null' }] });
57
- assert.deepEqual(schema.required, ['status', 'verification'], 'the stored request keeps its original optionality');
63
+ assert.equal(parameters.additionalProperties, false);
64
+ assert.equal(parameters.properties.details.additionalProperties, false);
65
+ assert.deepEqual(schema.required, ['status', 'verification', 'details'], 'the stored request keeps its original optionality');
58
66
  const [providerTool] = convertResponsesTools([definition], {
59
67
  strict: false,
60
68
  supportsStrictMode: true,
@@ -9,6 +9,7 @@ import { join } from 'node:path';
9
9
  import { CRTR_DIR_NAME } from '../types.js';
10
10
  import { REVIVE_KICKOFF_SENTINEL } from '../shared/generated-context.js';
11
11
  import { brokerExtensionState, commitBrokerGeneratedName, } from '../core/runtime/broker/daemon-ops.js';
12
+ import { publishNodeNamed } from '../core/runtime/broker/node-named.js';
12
13
  import { headlessName, headlessNameFromConversation, sanitizeSessionName, slugFromPrompt, } from '../core/runtime/naming.js';
13
14
  export { REVIVE_KICKOFF_SENTINEL };
14
15
  function contextDir(nodeId) {
@@ -58,8 +59,20 @@ export function generateAndCommitInitialName(nodeId, prompt, onNamed) {
58
59
  }));
59
60
  })
60
61
  .then((result) => {
61
- if (result?.applied && result.editorLabel !== undefined)
62
- onNamed?.(result.editorLabel);
62
+ if (!result?.applied || result.editorLabel === undefined)
63
+ return;
64
+ // Push the committed name down the attach stream BEFORE updating the live
65
+ // session label: both are the same delivery — the label reaches this
66
+ // process's own editor chip, the frame reaches every attached client
67
+ // (Northlight Core titles the conversation from it). The name lands
68
+ // seconds after the turn that triggered it, so a consumer with no frame
69
+ // has nothing left to re-read on.
70
+ publishNodeNamed({
71
+ description: result.description ?? '',
72
+ icon: result.icon ?? '',
73
+ editorLabel: result.editorLabel,
74
+ });
75
+ onNamed?.(result.editorLabel);
63
76
  })
64
77
  .catch(() => { });
65
78
  }
@@ -47,6 +47,10 @@ function strictToolSchema(schema) {
47
47
  }
48
48
  }
49
49
  const properties = transformed['properties'];
50
+ const isObjectSchema = transformed['type'] === 'object'
51
+ || (properties !== null && typeof properties === 'object' && !Array.isArray(properties));
52
+ if (isObjectSchema)
53
+ transformed['additionalProperties'] = false;
50
54
  if (properties === null || typeof properties !== 'object' || Array.isArray(properties))
51
55
  return transformed;
52
56
  const required = new Set(Array.isArray(source['required']) ? source['required'].filter((name) => typeof name === 'string') : []);
@@ -125,9 +129,9 @@ export function registerCanvasStructuredOutput(pi) {
125
129
  'Use submit to answer the pending one-off structured-output request before stopping or going dormant.',
126
130
  'After one-off submit succeeds, continue with any remaining work normally.',
127
131
  ],
128
- // OpenAI strict tools require every property to be required. Optional fields
129
- // become nullable in the provider-only schema and null is removed before the
130
- // original result is persisted.
132
+ // Strict provider tools require every property to be required and every
133
+ // object to reject undeclared properties. Optional fields become nullable
134
+ // in the provider-only schema and null is removed before persistence.
131
135
  parameters: strictToolSchema(request.schema),
132
136
  constrainedSampling: { type: 'json_schema', strict: 'prefer' },
133
137
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.190",
3
+ "version": "0.3.192",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.190",
3
+ "version": "0.3.192",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.190",
9
+ "version": "0.3.192",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
@@ -1,16 +0,0 @@
1
- import type { NodeMeta } from '../canvas/index.js';
2
- /** Asynchronously generate a name for `prompt` and persist it to the node's
3
- * meta as `description` + `icon` — only if the node has none yet (so a later
4
- * message never clobbers it). Non-blocking: safe to call from inside a live pi
5
- * event loop. Best-effort; swallows all errors. The slug fallback carries no
6
- * icon: a locally derived name has no judgment behind it to pick a glyph from.
7
- *
8
- * `onNamed` (optional) fires with the freshly-persisted meta the moment the
9
- * name lands — the canvas-goal-capture naming hook passes a callback that calls
10
- * pi.setSessionName(editorLabel(meta)) so the LIVE editor label updates in the
11
- * same session, instead of waiting for the next revive/cycle. */
12
- export declare function generateAndPersistName(nodeId: string, prompt: string, onNamed?: (meta: NodeMeta) => void): void;
13
- /** Re-name an automatically titled node from its current conversation. The caller
14
- * supplies the guard that distinguishes the original generated editor title from
15
- * a title a person changed while this headless request was in flight. */
16
- export declare function regenerateAndPersistName(nodeId: string, conversation: string, isStillAutomatic: () => boolean, onNamed?: (meta: NodeMeta) => void): void;
@@ -1,68 +0,0 @@
1
- // Session naming — canvas-coupled persistence seam.
2
- //
3
- // The pure naming machinery (headless pi call + slug fallbacks) lives in
4
- // naming.ts and stays canvas-free so CLI leaves can reach it without dragging
5
- // in canvas state. This one function is the only naming path that writes to a
6
- // node's meta, so it lives here — broker-side (canvas-goal-capture), never on a
7
- // CLI leaf's import graph.
8
- import { headlessName, headlessNameFromConversation, sanitizeSessionName, slugFromPrompt } from './naming.js';
9
- import { getNode, updateNode } from '../canvas/index.js';
10
- /** Asynchronously generate a name for `prompt` and persist it to the node's
11
- * meta as `description` + `icon` — only if the node has none yet (so a later
12
- * message never clobbers it). Non-blocking: safe to call from inside a live pi
13
- * event loop. Best-effort; swallows all errors. The slug fallback carries no
14
- * icon: a locally derived name has no judgment behind it to pick a glyph from.
15
- *
16
- * `onNamed` (optional) fires with the freshly-persisted meta the moment the
17
- * name lands — the canvas-goal-capture naming hook passes a callback that calls
18
- * pi.setSessionName(editorLabel(meta)) so the LIVE editor label updates in the
19
- * same session, instead of waiting for the next revive/cycle. */
20
- export function generateAndPersistName(nodeId, prompt, onNamed) {
21
- const body = (prompt ?? '').trim();
22
- if (body === '')
23
- return;
24
- const persist = (generated) => {
25
- try {
26
- const meta = getNode(nodeId);
27
- if (meta === null)
28
- return;
29
- if ((meta.description ?? '').trim() !== '')
30
- return; // already named
31
- const clean = sanitizeSessionName(generated.name);
32
- const description = clean !== '' ? clean : slugFromPrompt(body);
33
- const icon = generated.icon.trim();
34
- const updated = updateNode(nodeId, { description, ...(icon !== '' ? { icon } : {}) });
35
- onNamed?.(updated);
36
- }
37
- catch {
38
- // best-effort
39
- }
40
- };
41
- void headlessName(body).then(persist);
42
- }
43
- /** Re-name an automatically titled node from its current conversation. The caller
44
- * supplies the guard that distinguishes the original generated editor title from
45
- * a title a person changed while this headless request was in flight. */
46
- export function regenerateAndPersistName(nodeId, conversation, isStillAutomatic, onNamed) {
47
- const body = (conversation ?? '').trim();
48
- if (body === '')
49
- return;
50
- const persist = (generated) => {
51
- try {
52
- if (!isStillAutomatic())
53
- return;
54
- const meta = getNode(nodeId);
55
- if (meta === null)
56
- return;
57
- const clean = sanitizeSessionName(generated.name);
58
- const description = clean !== '' ? clean : slugFromPrompt(body);
59
- const icon = generated.icon.trim();
60
- const updated = updateNode(nodeId, { description, icon: icon !== '' ? icon : undefined });
61
- onNamed?.(updated);
62
- }
63
- catch {
64
- // best-effort
65
- }
66
- };
67
- void headlessNameFromConversation(body).then(persist);
68
- }