typebulb 0.44.2 → 0.45.0

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.
@@ -2440,7 +2440,7 @@ var Patcher = class {
2440
2440
 
2441
2441
  // cli/agents/pi/server/piPatcherExtension.ts
2442
2442
  var DESCRIPTION = true ? "Apply unified diffs to files \u2014 tolerant of form, strict about intent. Repairs sloppy AI-generated diffs (mangled headers, whitespace drift, missing prefixes; line numbers can be wrong or absent \u2014 hunks anchor by fuzzy-matched context lines) and applies all hunks atomically when the intent is unambiguous; fails with a precise, typed error when it isn't. Use for multi-hunk edits in one step, or when exact string-replacement editing fails on whitespace or invisible characters." : "Apply unified diffs to files.";
2443
- var BUILD_TAG = true ? "matchu-patchu-pi 0.3.5, built 2026-07-30 15:13:29" : "matchu-patchu-pi dev";
2443
+ var BUILD_TAG = true ? "matchu-patchu-pi 0.3.5, built 2026-07-30 17:52:54" : "matchu-patchu-pi dev";
2444
2444
  var errorBlocks = (errors) => {
2445
2445
  const parts = [`Patch failed with ${errors.length} error(s):`];
2446
2446
  for (const e of errors) parts.push("", e.toString());
@@ -229,25 +229,55 @@ body {
229
229
  border-radius: 10px;
230
230
  box-shadow: 0 8px 30px rgba(0, 0, 0, .45);
231
231
  }
232
- /* Owned scrollbars: no arrow buttons, transparent track, a floating thumb only when the surface
233
- actually overflows. On the menu lists the channel is ALWAYS reserved (overflow-y: scroll on the
234
- three lists); the diff doc joins them so its thumb track matches the overview ruler (.diff-ruler).
235
- Native auto scrollbars made a row band's right inset depend on list length: a
232
+ /* Menu-list scrollbars: the channel is ALWAYS reserved (overflow-y: scroll on all three lists) and
233
+ the scrollbar is owned — no arrow buttons, transparent track, a floating thumb only when the list
234
+ actually overflows. Native auto scrollbars made a row band's right inset depend on list length: a
236
235
  project with no bulbs of its own (one collapsed fold row) ran its band 15px nearer the popover
237
236
  edge than the filter box under it, and bare overflow:scroll parked a vestigial arrow-stub beside
238
237
  the short list. Width stays 15px, the native width .model-head's alignment padding hardcodes.
239
238
  Chromium pseudos on purpose: setting the standard scrollbar-color would disable them. */
240
- :is(.picker-list, .bulb-list, .gitdiff-list, .diff-doc-body)::-webkit-scrollbar { width: 15px; }
241
- :is(.picker-list, .bulb-list, .gitdiff-list, .diff-doc-body)::-webkit-scrollbar-button { display: none; }
242
- :is(.picker-list, .bulb-list, .gitdiff-list, .diff-doc-body)::-webkit-scrollbar-track { background: transparent; }
243
- :is(.picker-list, .bulb-list, .gitdiff-list, .diff-doc-body)::-webkit-scrollbar-thumb {
239
+ :is(.picker-list, .bulb-list, .gitdiff-list)::-webkit-scrollbar { width: 15px; }
240
+ :is(.picker-list, .bulb-list, .gitdiff-list)::-webkit-scrollbar-button { display: none; }
241
+ :is(.picker-list, .bulb-list, .gitdiff-list)::-webkit-scrollbar-track { background: transparent; }
242
+ :is(.picker-list, .bulb-list, .gitdiff-list)::-webkit-scrollbar-thumb {
244
243
  background-color: color-mix(in srgb, var(--fg) 28%, transparent);
245
244
  border-radius: 8px;
246
245
  /* Transparent border + padding-box clip floats a slim pill in the 15px channel. */
247
246
  border: 4px solid transparent;
248
247
  background-clip: padding-box;
249
248
  }
250
- :is(.picker-list, .bulb-list, .gitdiff-list, .diff-doc-body)::-webkit-scrollbar-thumb:hover { background-color: color-mix(in srgb, var(--fg) 45%, transparent); }
249
+ :is(.picker-list, .bulb-list, .gitdiff-list)::-webkit-scrollbar-thumb:hover { background-color: color-mix(in srgb, var(--fg) 45%, transparent); }
250
+
251
+ /* The overview ruler (overviewRuler.ts): a scroll container's bar, replaced. .oruler-host is applied
252
+ by the widget at mount — never statically — so the native bar only disappears once its replacement
253
+ is painted. BOTH hiding routes: the standard property covers Gecko, the pseudo covers WebKit, and
254
+ Blink honours either, so no engine is left showing two scrollbars. Window and marks are % of a track
255
+ that spans the pane, so they share the container's content space. Marks are pointer-inert; the track
256
+ takes the press, the drag and the wheel. Colour and the track's inset are the host's (.diff-ruler).
257
+ Cursor stays the plain arrow — that's what a scrollbar shows, and `grab` would claim otherwise. */
258
+ .oruler-host { scrollbar-width: none; }
259
+ .oruler-host::-webkit-scrollbar { width: 0; height: 0; }
260
+ .oruler { position: absolute; inset-block: 0; width: 6px; z-index: 5; cursor: default; touch-action: none; }
261
+ .oruler-mark { position: absolute; left: 0; right: 0; min-height: 3px; border-radius: 1px; pointer-events: none; }
262
+ /* Wider than the track and above the marks, so what's on screen reads as a thumb laid over the map. */
263
+ .oruler-win {
264
+ position: absolute; left: -3px; right: -3px; z-index: 1; min-height: 6px; border-radius: 3px;
265
+ pointer-events: none; background: color-mix(in srgb, var(--fg) 28%, transparent);
266
+ }
267
+ /* The menu thumbs' own two steps (28/45) — same surface, same weights. Still a wash rather than a
268
+ solid fill: the marks it covers are the ones you're about to read. */
269
+ .oruler:hover .oruler-win { background: color-mix(in srgb, var(--fg) 45%, transparent); }
270
+ /* A finger needs a target a 6px track can't give; the map is worth more widened than dropped. */
271
+ @media (pointer: coarse) {
272
+ .oruler { width: 14px; }
273
+ .oruler-win { left: -2px; right: -2px; }
274
+ }
275
+ /* Forced colours drop our tints — the window would vanish outright, and the marks' red/green IS the
276
+ information, so it keeps its own palette rather than collapsing to one system colour. */
277
+ @media (forced-colors: active) {
278
+ .oruler-win { background: transparent; border: 1px solid Highlight; }
279
+ .oruler-mark { forced-color-adjust: none; }
280
+ }
251
281
  /* Popover alignment — which edge of the pill the menu aligns to (floating-UI's start/center/end
252
282
  vocabulary; "end", right edges flush and growing left, is the unnamed default above). Tagged on
253
283
  the pill's WRAP, not each popover root: a menu with several drilled-in views (the model pill's
@@ -1082,11 +1112,17 @@ a.server-port:hover { text-decoration: underline; }
1082
1112
  .diff-doc { flex: 1; min-height: 0; display: flex; flex-direction: column; outline: none; }
1083
1113
  .diff-doc-body {
1084
1114
  flex: 1; min-height: 0; overflow: auto;
1085
- scrollbar-gutter: stable both-edges;
1086
- padding: .75rem 1.75rem 2.5rem;
1115
+ /* No scrollbar-gutter: the native bar is hidden (.oruler-host), so the 15px it used to reserve on
1116
+ each edge rides in the padding instead — same metrics, and the right one is the ruler's channel. */
1117
+ padding: .75rem calc(1.75rem + 15px) 2.5rem;
1087
1118
  font: .78rem/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
1088
1119
  white-space: pre-wrap; word-break: break-word;
1089
1120
  }
1121
+ /* The scroller is the focusable element (it owns the paging keys), but a ring round a full-bleed pane
1122
+ is a box round the whole screen. Keyboard focus shows on the ruler's window instead — the thing the
1123
+ paging keys actually move, and a real indicator rather than none at all. */
1124
+ .diff-doc-body:focus { outline: none; }
1125
+ .diff-doc-body:focus-visible ~ .diff-ruler .oruler-win { background: color-mix(in srgb, var(--accent) 55%, transparent); }
1090
1126
  .diff-doc-empty, .diff-doc-trunc { color: var(--muted); padding: .4rem 0; }
1091
1127
  /* The wrong-cwd empty state (Root.#cwdHint): centered over the empty transcript. */
1092
1128
  .cwd-hint { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; gap: .5rem; padding: 2rem; color: var(--muted); font-size: .9rem; max-width: 40rem; margin-inline: auto; }
@@ -1099,19 +1135,13 @@ a.server-port:hover { text-decoration: underline; }
1099
1135
  camouflage against red tint more than anything does against green. */
1100
1136
  .diff-doc-body .udiff-add { background: color-mix(in srgb, var(--diff-add) 15%, transparent); }
1101
1137
  .diff-doc-body .udiff-del { background: color-mix(in srgb, var(--err) 18%, transparent); }
1102
- /* The overview ruler — VS Code's scrollbar change-marks, owned since the native scrollbar can't be
1103
- painted: a thin track beside the scrollbar (15px = the owned channel's width — plain px on a
1104
- desktop-only surface, the menus' own stance), red/green marks at each change run's fractional
1105
- position, min-height so a one-line change stays visible. It spans the pane edge to edge, which is
1106
- the thumb's track exactly once the arrow buttons are gone — that's why the doc's bar is owned.
1107
- What drift is left on a tall doc is Chromium clamping the thumb to a minimum height, shortening
1108
- its travel; a static ruler can't follow that. The wrapper keeps it overlaid rather than scrolling
1109
- with the body. Marks are pointer-inert so a click anywhere on the track jumps. */
1138
+ /* The ruler's diff clothing — red/green per change run; it rides in the channel the body's right
1139
+ padding reserves, where the scrollbar it replaces used to be. Geometry, the window and the click
1140
+ are the shared widget's. The wrapper keeps it overlaid rather than scrolling with the body. */
1110
1141
  .diff-doc-scroll { position: relative; flex: 1; min-height: 0; display: flex; flex-direction: column; }
1111
- .diff-ruler { position: absolute; inset-block: 0; right: 15px; width: 6px; cursor: pointer; z-index: 5; }
1112
- .diff-ruler-mark { position: absolute; left: 0; right: 0; min-height: 3px; border-radius: 1px; pointer-events: none; }
1113
- .diff-ruler-mark.add { background: var(--diff-add); opacity: .8; }
1114
- .diff-ruler-mark.del { background: var(--err); opacity: .8; }
1142
+ .diff-ruler { right: 4px; }
1143
+ .diff-ruler .oruler-mark.add { background: var(--diff-add); opacity: .8; }
1144
+ .diff-ruler .oruler-mark.del { background: var(--err); opacity: .8; }
1115
1145
  /* messages */
1116
1146
  .messages {
1117
1147
  flex: 1;
package/dist/index.js CHANGED
@@ -894,9 +894,20 @@ ${gE(t)}`)}var gh=v(()=>{Bi()});var wt,Fn,Nl,qn=v(()=>{wt="https://esm.sh",Fn="h
894
894
  if (lines.length > MAX_LINES) { lines.length = MAX_LINES; lines.push('- \u2026 (truncated)'); }
895
895
  return { lines, targets };
896
896
  };
897
+ // The scrolling element's client box is the usable area minus scrollbars \u2014 the only basis on
898
+ // which "content \u2264 viewport" is the same verdict the scrollbar's presence gives the user.
899
+ const pageGeometry = () => {
900
+ const de = document.scrollingElement || document.documentElement;
901
+ const over = [];
902
+ if (de.scrollHeight > de.clientHeight) over.push('vertically by ' + (de.scrollHeight - de.clientHeight) + 'px');
903
+ if (de.scrollWidth > de.clientWidth) over.push('horizontally by ' + (de.scrollWidth - de.clientWidth) + 'px');
904
+ return '- page: viewport ' + de.clientWidth + '\xD7' + de.clientHeight
905
+ + ', content ' + de.scrollWidth + '\xD7' + de.scrollHeight
906
+ + ' \u2014 ' + (over.length ? 'overflows ' + over.join(', ') : 'fits');
907
+ };
897
908
  const snapshot = () => {
898
909
  const { lines } = collectOutline();
899
- return lines.length ? lines.join('\\n') : '(empty page)';
910
+ return pageGeometry() + '\\n' + (lines.length ? lines.join('\\n') : '(empty page)');
900
911
  };
901
912
 
902
913
  // tb:click / tb:set (TB-Interrogation-Actuation.md). A target is role + name exactly as the
@@ -968,6 +979,20 @@ ${gE(t)}`)}var gh=v(()=>{Bi()});var wt,Fn,Nl,qn=v(()=>{wt="https://esm.sh",Fn="h
968
979
  }
969
980
  return postFrameSnapshot();
970
981
  };
982
+ // tb:rect \u2014 a read in the actuation grammar: the named control's viewport-relative rect plus the
983
+ // viewport itself, so the reply is self-interpreting. No scrollIntoView \u2014 a read disturbs nothing.
984
+ const readRect = (rest) => {
985
+ const t = parseActuation('tb:rect <role> "<name>"', rest);
986
+ if (t.rest) throw new Error('usage: tb:rect <role> "<name>"');
987
+ const el = resolveTarget(t.role, t.name);
988
+ const r = el.getBoundingClientRect();
989
+ const de = document.scrollingElement || document.documentElement;
990
+ return {
991
+ x: Math.round(r.x), y: Math.round(r.y),
992
+ width: Math.round(r.width), height: Math.round(r.height),
993
+ viewport: { width: de.clientWidth, height: de.clientHeight }
994
+ };
995
+ };
971
996
  const actSet = (rest) => {
972
997
  const t = parseActuation('tb:set <role> "<name>" = <value>', rest);
973
998
  const vm = /^=\\s*([\\s\\S]*)$/.exec(t.rest.trim());
@@ -1024,7 +1049,8 @@ ${Gi}
1024
1049
  if (p === 'tb:snapshot') return snapshot();
1025
1050
  if (p === 'tb:click' || p.indexOf('tb:click ') === 0) return actClick(p.slice(9));
1026
1051
  if (p === 'tb:set' || p.indexOf('tb:set ') === 0) return actSet(p.slice(7));
1027
- throw new Error('unknown reserved message: ' + p + ' (known: tb:snapshot, tb:click, tb:set)');
1052
+ if (p === 'tb:rect' || p.indexOf('tb:rect ') === 0) return readRect(p.slice(8));
1053
+ throw new Error('unknown reserved message: ' + p + ' (known: tb:snapshot, tb:rect, tb:click, tb:set)');
1028
1054
  };
1029
1055
  const value = parseMsg(env.payload);
1030
1056
  const calls = typeof env.payload === 'string' && env.payload.indexOf('tb:') === 0
@@ -1444,11 +1470,14 @@ Usage:
1444
1470
  With --wait, a handler's non-undefined return
1445
1471
  prints on stdout (JSON; a bare string raw).
1446
1472
  msg 'tb:snapshot' instead prints the page's
1447
- rendered outline (roles, names, visible text).
1473
+ rendered outline (roles, names, visible text),
1474
+ headed by a viewport/content fit line.
1475
+ 'tb:rect <role> "<name>"' prints that control's
1476
+ rect ({x,y,width,height} + viewport).
1448
1477
  'tb:click <role> "<name>"' clicks the control
1449
- that outline names; 'tb:set \u2026 = <value>' fills
1450
- a form control. Both reply with a fresh
1451
- snapshot and need exactly one page open.
1478
+ the outline names; 'tb:set \u2026 = <value>' fills
1479
+ a form control. Both gestures reply with a
1480
+ fresh snapshot and need exactly one page open.
1452
1481
  typebulb get <file> <kind> Print one block's content to stdout (kind: code,
1453
1482
  css, html, data, infer, insight, config, notes).
1454
1483
  No content (absent or empty): exit 2;
@@ -2171,5 +2200,5 @@ ${AC}
2171
2200
  `);let c=0,l=Promise.resolve();Nt({target:r,onChange:()=>{let d=++c;l=l.then(async()=>{if(d===c)try{console.log("Re-running..."),await a()}catch(u){console.error("Error:",u)}})}})}}Ot();Ke();import{Console as RC}from"node:console";Ie();async function rw(r,e,t,n,s){LC();try{let p=kr(await Q(),r)[0];p&&(Ni(p.pid,Je(p.pid).offset),console.error(`note: 'call' boots a fresh server.ts instance; the running server (${p.url}) is not contacted \u2014 state shared with it must live on disk.`))}catch{}let i=Ae(t),{bulb:o,config:a}=await De(r);o.server||ws("This bulb has no **server.ts** block; nothing to call."),Tt(i,r,o.server),s&&await Mr(r,s);let c;try{c=await es(o.server,r,n,a.dependencies,s)}catch(p){ws(p instanceof Error?p.message:String(p))}let l=ca(c,e.fn);if(!l){let p=[...Object.keys(c).filter(h=>typeof c[h]=="function"),...Object.keys(aa)];ws(`Function '${e.fn}' not found. Available: ${p.length?p.join(", "):"(none)"}.`)}let d=await _C(e);if(la(l)){try{for await(let p of l(...d))await tw(JSON.stringify(p,nw)+`
2172
2201
  `)}catch(p){ws(p instanceof Error?p.stack??p.message:String(p))}return ew(0)}let u;try{u=await l(...d)}catch(p){ws(p instanceof Error?p.stack??p.message:String(p))}let f=$C(u);f!==void 0&&await tw(f+`
2173
2202
  `),ew(0)}function ew(r){process.exitCode=r,setTimeout(()=>process.exit(r),2e3).unref?.()}async function _C(r){if(r.hasArgsFlag){let e=r.argsJson??"";return e==="-"&&(e=await MC()),IC(e)}return NC(r.positional)}function NC(r){return r.map(e=>{try{return JSON.parse(e)}catch{return e}})}function IC(r){let e;try{e=JSON.parse(r)}catch(t){throw new Error(`--args must be a JSON array: ${t instanceof Error?t.message:String(t)}`)}if(!Array.isArray(e))throw new Error(`--args must be a JSON array, got ${e===null?"null":typeof e}`);return e}function $C(r){if(r!==void 0)return JSON.stringify(r,nw,2)}function nw(r,e){return typeof e=="bigint"?e.toString():e}function ws(r){process.stderr.write(r+`
2174
- `),process.exit(1)}function LC(){let r=new RC(process.stderr,process.stderr);console.log=r.log.bind(r),console.info=r.info.bind(r),console.debug=r.debug.bind(r),console.dir=r.dir.bind(r)}function tw(r){return new Promise((e,t)=>{process.stdout.write(r,n=>n?t(n):e())})}async function MC(){let r=[];for await(let e of process.stdin)r.push(e);return Buffer.concat(r).toString("utf-8")}var sw="0.44.2";for(let r of[process.stdout,process.stderr])r.on("error",e=>{if(e?.code!=="EPIPE")throw e});function iw(r,e){r||(console.error(`This bulb runs server-side Node code (server.ts), which --trust must authorize:
2203
+ `),process.exit(1)}function LC(){let r=new RC(process.stderr,process.stderr);console.log=r.log.bind(r),console.info=r.info.bind(r),console.debug=r.debug.bind(r),console.dir=r.dir.bind(r)}function tw(r){return new Promise((e,t)=>{process.stdout.write(r,n=>n?t(n):e())})}async function MC(){let r=[];for await(let e of process.stdin)r.push(e);return Buffer.concat(r).toString("utf-8")}var sw="0.45.0";for(let r of[process.stdout,process.stderr])r.on("error",e=>{if(e?.code!=="EPIPE")throw e});function iw(r,e){r||(console.error(`This bulb runs server-side Node code (server.ts), which --trust must authorize:
2175
2204
  ${e}`),process.exit(1))}async function BC(){let r=Zy(process.argv.slice(2));if(r.version&&(console.log(`typebulb ${sw}`),process.exit(0)),r.help&&(eb(),process.exit(0)),Xy(),r.subcommand==="logs"){await Pb(r.file||void 0,{follow:r.follow,clear:r.clear,run:r.run,lines:r.lines});return}if(r.subcommand==="wait"){await Cb(r.file||void 0,{match:r.match,timeoutSec:r.timeoutSec});return}if(r.subcommand==="stop"){r.stopScope?await _b(r.stopScope):await Rb(r.file||void 0);return}if(r.subcommand==="send"){await $b(r.file,r.sendMessage,r.sendWaitMs??0);return}if(r.subcommand==="models"){await Eb(r.mode);return}if(r.subcommand==="slug"){Ab(r.slugName);return}if(r.subcommand==="pull"){await kg(r.file||void 0,{force:r.force,mode:r.mode});return}if(r.subcommand==="push"){await Eg(r.file||void 0,{force:r.force,mode:r.mode});return}if(r.subcommand==="agent"){await(r.agentTarget?Xb(r):xb(sw));return}if(r.subcommand==="trust"||r.subcommand==="untrust"){await vb(r.file||void 0,r.subcommand==="trust");return}let e;if(!r.file||r.file==="."){let c=await eg(process.cwd());c||(console.error("No .bulb.md file found in current directory"),process.exit(1)),e=c}else e=ir.resolve(r.file);if(await ow.access(e).then(()=>!0,()=>!1)||(Vr().includes(r.file)&&(console.error(`To open the ${r.file} agent mirror, run: npx typebulb agent:${r.file}`),process.exit(1)),console.error(`File not found: ${e}`),process.exit(1)),e.endsWith(".bulb.md")||(console.error("File must have .bulb.md extension"),process.exit(1)),r.subcommand==="get"){await Mb(e,r.blockKind);return}if(r.subcommand==="put"){await jb(e,r.blockPairs);return}let n=r.file&&r.file!=="."?r.file:ir.relative(process.cwd(),e)||ir.basename(e),s=`npx typebulb --trust ${n.includes(" ")?`"${n}"`:n}`;if(r.subcommand==="predict"){await wb(e,s);return}let i=!r.noTrust&&Qt(e);i&&!r.trust&&console.error("trust: granted from memory (run `typebulb untrust` to revoke)"),r.trust=r.noTrust?!1:r.trust||i;let o;try{o=await De(e)}catch{}let a;if(r.local){o&&!(r.local.name in(o.config.dependencies??{}))&&(console.error(`--replace: '${r.local.name}' is not a dependency in this bulb's config.json; nothing to replace.`),process.exit(1)),o&&r.subcommand!=="call"&&(!o.bulb.code||r.server)&&console.warn("warning: --replace has no effect in server mode (the override is client-only).");try{a=await bd(r.local)}catch(c){console.error(c instanceof Error?c.message:String(c)),process.exit(1)}console.error(`replace: ${a.name} \u2192 ${ir.relative(process.cwd(),a.dir)||"."}`)}if(r.subcommand==="check"){await bb(e,a);return}if(r.subcommand==="call"){iw(r.trust,s),await rw(e,{fn:r.fn,positional:r.callArgs,argsJson:r.argsJson,hasArgsFlag:r.hasArgsFlag},r.mode,a,r.batch);return}if(o&&o.bulb.server&&(Li(o.bulb)||r.server)){iw(r.trust,s),await Zb(e,r.watch,r.mode,a,r.batch);return}await Yb(e,r,s,a)}BC().catch(r=>{console.error("Error:",r.message),process.exit(1)});