@agimon-ai/doompi-read 0.0.1-alpha.4 → 0.0.1-alpha.41

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/README.md CHANGED
@@ -15,7 +15,7 @@ people and preserves syntax highlighting. Image reads retain Pi's native attachm
15
15
  ## Requirements
16
16
 
17
17
  - Node.js 22.19.0 or newer
18
- - Pi 0.84.2 and Pi TUI 0.84.2
18
+ - Pi 0.85.0 and Pi TUI 0.85.0
19
19
 
20
20
  ## Install
21
21
 
@@ -2,9 +2,9 @@ import { Context } from "@deepseek-ai/cordis";
2
2
  import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
  //#region src/adapters/pi/extension.d.ts
4
4
  /** Install the hashline read tool after claiming Pi's shared read name. */
5
- declare function installDoomPiReadRuntime(cordis: Context, pi: ExtensionAPI): void;
5
+ export declare function installDoomPiReadRuntime(cordis: Context, pi: ExtensionAPI): void;
6
6
  /** Connect the package's standard Pi entry to DoomPi's shared Cordis host. */
7
- declare function activateDoomPiReadExtension(pi: ExtensionAPI): Promise<void>;
7
+ export declare function activateDoomPiReadExtension(pi: ExtensionAPI): Promise<void>;
8
8
  //#endregion
9
- export { activateDoomPiReadExtension, activateDoomPiReadExtension as default, installDoomPiReadRuntime };
9
+ export { activateDoomPiReadExtension as default };
10
10
  //# sourceMappingURL=extension.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"extension.d.cts","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"mappings":";;;;iBAYgB,yBAAyB,QAAQ,SAAS,IAAI;;iBAkBxC,4BAA4B,IAAI,eAAe"}
1
+ {"version":3,"file":"extension.d.cts","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"mappings":";;;;wBAYgB,yBAAyB,QAAQ,SAAS,IAAI;;wBAkBxC,4BAA4B,IAAI,eAAe"}
@@ -2,9 +2,9 @@ import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Context } from "@deepseek-ai/cordis";
3
3
  //#region src/adapters/pi/extension.d.ts
4
4
  /** Install the hashline read tool after claiming Pi's shared read name. */
5
- declare function installDoomPiReadRuntime(cordis: Context, pi: ExtensionAPI): void;
5
+ export declare function installDoomPiReadRuntime(cordis: Context, pi: ExtensionAPI): void;
6
6
  /** Connect the package's standard Pi entry to DoomPi's shared Cordis host. */
7
- declare function activateDoomPiReadExtension(pi: ExtensionAPI): Promise<void>;
7
+ export declare function activateDoomPiReadExtension(pi: ExtensionAPI): Promise<void>;
8
8
  //#endregion
9
- export { activateDoomPiReadExtension, activateDoomPiReadExtension as default, installDoomPiReadRuntime };
9
+ export { activateDoomPiReadExtension as default };
10
10
  //# sourceMappingURL=extension.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"extension.d.mts","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"mappings":";;;;iBAYgB,yBAAyB,QAAQ,SAAS,IAAI;;iBAkBxC,4BAA4B,IAAI,eAAe"}
1
+ {"version":3,"file":"extension.d.mts","names":[],"sources":["../../../src/adapters/pi/extension.ts"],"mappings":";;;;wBAYgB,yBAAyB,QAAQ,SAAS,IAAI;;wBAkBxC,4BAA4B,IAAI,eAAe"}
@@ -0,0 +1,2 @@
1
+ let e=require("@earendil-works/pi-coding-agent"),t=require("@agimon-ai/doompi-config/config/piConfig");function n(e){return e.type===`image`}function r(){return(0,t.loadPiImageSettings)()}async function i(t,r){if(!r.autoResize||!t.some(n))return t;let i=[];for(let a of t){if(!n(a)){i.push(a);continue}let t=await(0,e.resizeImage)(Buffer.from(a.data,`base64`),a.mimeType,{maxWidth:r.maxDimension,maxHeight:r.maxDimension});if(!t){i.push({type:`text`,text:`[Image omitted: could not be resized below the inline image size limit.]`});continue}i.push({type:`image`,data:t.data,mimeType:t.mimeType});let o=(0,e.formatDimensionNote)(t);o&&i.push({type:`text`,text:o})}return i}exports.applyImageLimits=i,exports.imageLimits=r;
2
+ //# sourceMappingURL=readImage.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readImage.cjs","names":["loadPiImageSettings","resizeImage","formatDimensionNote"],"sources":["../../../src/adapters/pi/readImage.ts"],"sourcesContent":["/**\n * The image half of the read override.\n *\n * DESIGN PATTERNS:\n * - Pi's read tool already decodes, converts and resizes images, but its only\n * knob is on/off: the 2000px cap is a constant inside its resize core. So the\n * native tool is asked not to resize, and the block it returns is resized\n * here against the machine's configured cap, with Pi's own exported resize so\n * the two surfaces cannot drift apart in filter, format ladder or byte limit.\n * - The limits are read per image read rather than captured at registration,\n * because the cockpit settings page writes them while a session is running.\n *\n * CODING STANDARDS:\n * - Keep Pi's wording for the omission and the coordinate note. A model that\n * learned Pi's phrasing should not have to learn a second one.\n *\n * AVOID:\n * - Dropping an image silently. An image that cannot be brought under the limit\n * is reported in the text, exactly as Pi's own read reports it.\n */\n\nimport { loadPiImageSettings, type PiImageSettings } from '@agimon-ai/doompi-config/config/piConfig';\nimport { formatDimensionNote, resizeImage } from '@earendil-works/pi-coding-agent';\n\n/** Pi's own message for an image no resize pass could bring under the limit. */\nconst OMITTED_NOTE = '[Image omitted: could not be resized below the inline image size limit.]';\n\n/** Structurally Pi's own TextContent and ImageContent, without the pi-ai dependency. */\nexport interface ReadTextPart {\n type: 'text';\n text: string;\n}\n\nexport interface ReadImagePart {\n type: 'image';\n data: string;\n mimeType: string;\n}\n\nexport type ReadContentPart = ReadTextPart | ReadImagePart;\n\nfunction isImagePart(part: ReadContentPart): part is ReadImagePart {\n return part.type === 'image';\n}\n\nexport function imageLimits(): PiImageSettings {\n return loadPiImageSettings();\n}\n\n/**\n * Applies the configured cap to every image block, leaving text untouched.\n *\n * Returns the same array when nothing changed, so a caller can hand the native\n * result straight back.\n */\nexport async function applyImageLimits(\n content: ReadContentPart[],\n limits: PiImageSettings,\n): Promise<ReadContentPart[]> {\n if (!limits.autoResize || !content.some(isImagePart)) return content;\n const resolved: ReadContentPart[] = [];\n for (const part of content) {\n if (!isImagePart(part)) {\n resolved.push(part);\n continue;\n }\n const resized = await resizeImage(Buffer.from(part.data, 'base64'), part.mimeType, {\n maxWidth: limits.maxDimension,\n maxHeight: limits.maxDimension,\n });\n if (!resized) {\n resolved.push({ type: 'text', text: OMITTED_NOTE });\n continue;\n }\n resolved.push({ type: 'image', data: resized.data, mimeType: resized.mimeType });\n const note = formatDimensionNote(resized);\n if (note) resolved.push({ type: 'text', text: note });\n }\n return resolved;\n}\n"],"mappings":"uGAyCA,SAAS,EAAY,EAA8C,CACjE,OAAO,EAAK,OAAS,OACvB,CAEA,SAAgB,GAA+B,CAC7C,OAAA,EAAOA,EAAAA,oBAAAA,CAAoB,CAC7B,CAQA,eAAsB,EACpB,EACA,EAC4B,CAC5B,GAAI,CAAC,EAAO,YAAc,CAAC,EAAQ,KAAK,CAAW,EAAG,OAAO,EAC7D,IAAM,EAA8B,CAAC,EACrC,IAAK,IAAM,KAAQ,EAAS,CAC1B,GAAI,CAAC,EAAY,CAAI,EAAG,CACtB,EAAS,KAAK,CAAI,EAClB,QACF,CACA,IAAM,EAAU,MAAA,EAAMC,EAAAA,YAAAA,CAAY,OAAO,KAAK,EAAK,KAAM,QAAQ,EAAG,EAAK,SAAU,CACjF,SAAU,EAAO,aACjB,UAAW,EAAO,YACpB,CAAC,EACD,GAAI,CAAC,EAAS,CACZ,EAAS,KAAK,CAAE,KAAM,OAAQ,KAAM,0EAAa,CAAC,EAClD,QACF,CACA,EAAS,KAAK,CAAE,KAAM,QAAS,KAAM,EAAQ,KAAM,SAAU,EAAQ,QAAS,CAAC,EAC/E,IAAM,GAAA,EAAOC,EAAAA,oBAAAA,CAAoB,CAAO,EACpC,GAAM,EAAS,KAAK,CAAE,KAAM,OAAQ,KAAM,CAAK,CAAC,CACtD,CACA,OAAO,CACT"}
@@ -0,0 +1,2 @@
1
+ import{formatDimensionNote as e,resizeImage as t}from"@earendil-works/pi-coding-agent";import{loadPiImageSettings as n}from"@agimon-ai/doompi-config/config/piConfig";function r(e){return e.type===`image`}function i(){return n()}async function a(n,i){if(!i.autoResize||!n.some(r))return n;let a=[];for(let o of n){if(!r(o)){a.push(o);continue}let n=await t(Buffer.from(o.data,`base64`),o.mimeType,{maxWidth:i.maxDimension,maxHeight:i.maxDimension});if(!n){a.push({type:`text`,text:`[Image omitted: could not be resized below the inline image size limit.]`});continue}a.push({type:`image`,data:n.data,mimeType:n.mimeType});let s=e(n);s&&a.push({type:`text`,text:s})}return a}export{a as applyImageLimits,i as imageLimits};
2
+ //# sourceMappingURL=readImage.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readImage.mjs","names":[],"sources":["../../../src/adapters/pi/readImage.ts"],"sourcesContent":["/**\n * The image half of the read override.\n *\n * DESIGN PATTERNS:\n * - Pi's read tool already decodes, converts and resizes images, but its only\n * knob is on/off: the 2000px cap is a constant inside its resize core. So the\n * native tool is asked not to resize, and the block it returns is resized\n * here against the machine's configured cap, with Pi's own exported resize so\n * the two surfaces cannot drift apart in filter, format ladder or byte limit.\n * - The limits are read per image read rather than captured at registration,\n * because the cockpit settings page writes them while a session is running.\n *\n * CODING STANDARDS:\n * - Keep Pi's wording for the omission and the coordinate note. A model that\n * learned Pi's phrasing should not have to learn a second one.\n *\n * AVOID:\n * - Dropping an image silently. An image that cannot be brought under the limit\n * is reported in the text, exactly as Pi's own read reports it.\n */\n\nimport { loadPiImageSettings, type PiImageSettings } from '@agimon-ai/doompi-config/config/piConfig';\nimport { formatDimensionNote, resizeImage } from '@earendil-works/pi-coding-agent';\n\n/** Pi's own message for an image no resize pass could bring under the limit. */\nconst OMITTED_NOTE = '[Image omitted: could not be resized below the inline image size limit.]';\n\n/** Structurally Pi's own TextContent and ImageContent, without the pi-ai dependency. */\nexport interface ReadTextPart {\n type: 'text';\n text: string;\n}\n\nexport interface ReadImagePart {\n type: 'image';\n data: string;\n mimeType: string;\n}\n\nexport type ReadContentPart = ReadTextPart | ReadImagePart;\n\nfunction isImagePart(part: ReadContentPart): part is ReadImagePart {\n return part.type === 'image';\n}\n\nexport function imageLimits(): PiImageSettings {\n return loadPiImageSettings();\n}\n\n/**\n * Applies the configured cap to every image block, leaving text untouched.\n *\n * Returns the same array when nothing changed, so a caller can hand the native\n * result straight back.\n */\nexport async function applyImageLimits(\n content: ReadContentPart[],\n limits: PiImageSettings,\n): Promise<ReadContentPart[]> {\n if (!limits.autoResize || !content.some(isImagePart)) return content;\n const resolved: ReadContentPart[] = [];\n for (const part of content) {\n if (!isImagePart(part)) {\n resolved.push(part);\n continue;\n }\n const resized = await resizeImage(Buffer.from(part.data, 'base64'), part.mimeType, {\n maxWidth: limits.maxDimension,\n maxHeight: limits.maxDimension,\n });\n if (!resized) {\n resolved.push({ type: 'text', text: OMITTED_NOTE });\n continue;\n }\n resolved.push({ type: 'image', data: resized.data, mimeType: resized.mimeType });\n const note = formatDimensionNote(resized);\n if (note) resolved.push({ type: 'text', text: note });\n }\n return resolved;\n}\n"],"mappings":"sKAyCA,SAAS,EAAY,EAA8C,CACjE,OAAO,EAAK,OAAS,OACvB,CAEA,SAAgB,GAA+B,CAC7C,OAAO,EAAoB,CAC7B,CAQA,eAAsB,EACpB,EACA,EAC4B,CAC5B,GAAI,CAAC,EAAO,YAAc,CAAC,EAAQ,KAAK,CAAW,EAAG,OAAO,EAC7D,IAAM,EAA8B,CAAC,EACrC,IAAK,IAAM,KAAQ,EAAS,CAC1B,GAAI,CAAC,EAAY,CAAI,EAAG,CACtB,EAAS,KAAK,CAAI,EAClB,QACF,CACA,IAAM,EAAU,MAAM,EAAY,OAAO,KAAK,EAAK,KAAM,QAAQ,EAAG,EAAK,SAAU,CACjF,SAAU,EAAO,aACjB,UAAW,EAAO,YACpB,CAAC,EACD,GAAI,CAAC,EAAS,CACZ,EAAS,KAAK,CAAE,KAAM,OAAQ,KAAM,0EAAa,CAAC,EAClD,QACF,CACA,EAAS,KAAK,CAAE,KAAM,QAAS,KAAM,EAAQ,KAAM,SAAU,EAAQ,QAAS,CAAC,EAC/E,IAAM,EAAO,EAAoB,CAAO,EACpC,GAAM,EAAS,KAAK,CAAE,KAAM,OAAQ,KAAM,CAAK,CAAC,CACtD,CACA,OAAO,CACT"}
@@ -1,3 +1,3 @@
1
- const e=require("../../schemas/readTool.cjs");let t=require("@agimon-ai/doompi-hashline"),n=require("@agimon-ai/doompi-hashline/files"),r=require("@agimon-ai/doompi-ui/hashlineRendering"),i=require("node:fs/promises"),a=require("@earendil-works/pi-coding-agent");function o(t,o=n.isWritableFile){t.registerTool({name:`read`,label:`read`,description:`Read a writable text file with an exact-byte file tag and stable line anchors. Non-writable files and images retain Pi's native behavior. Text is truncated to ${(0,a.formatSize)(a.DEFAULT_MAX_BYTES)}.`,promptSnippet:`Read file contents with snapshot-bound line anchors`,promptGuidelines:[`Use read before edit. When hashline metadata is present, preserve the @file hash and anchors such as 5#abc exactly.`,`Continue large reads with offset until the required anchored lines are visible.`],parameters:e.ReadParamsSchema,executionMode:`parallel`,renderShell:`self`,async execute(e,t,r,u,d){let f=t;l(r);let p=await(0,n.resolveReadInputPath)(f.path,d.cwd),m=await(0,a.createReadToolDefinition)(d.cwd).execute(e,{...f,path:p},r,u,d);if(c(m.content))return m;l(r);let h=await o(p);if(l(r),!h)return m;let g=await(0,i.readFile)(p);return l(r),s(g,(0,n.displayPath)(p,d.cwd),f)},renderCall(e,t){let n=e,i=[n.offset===void 0?void 0:`from ${n.offset}`,n.limit===void 0?void 0:`${n.limit} lines`].filter(e=>e!==void 0);return(0,r.renderHashlineCall)(`read`,n.path,i,t)},renderResult(e,t,n,i){return(0,r.renderHashlineResult)(e,t,n,i,`read`)}})}function s(e,r,i){let o=(0,t.splitLines)((0,n.decodeUtf8)(e,r)),s=i.offset===void 0?0:i.offset-1;if(s>=o.length)throw Error(`Offset ${i.offset} is beyond end of file (${o.length} lines total).`);let c=i.limit===void 0?o.length:Math.min(o.length,s+i.limit),l=o.slice(s,c),u=(0,t.formatFileHeader)(r,(0,n.computeFileTag)(e)),d=[],f=Buffer.byteLength(u,`utf8`)+1,p=l.map((e,n)=>{let r=s+n+1,i=(0,t.formatTaggedLine)(e,r);return Buffer.byteLength(i,`utf8`)<=a.DEFAULT_MAX_BYTES-f?i:(d.push(r),(0,t.formatTaggedLine)((0,a.truncateLine)(e).text,r,``,e))}),m=(0,a.truncateHead)([u,...p].join(`
2
- `)),h=m.content,g;if(m.truncated){let e=Math.max(0,m.outputLines-1),t=s+e+1,n=m.truncatedBy===`bytes`?`, ${(0,a.formatSize)(a.DEFAULT_MAX_BYTES)} limit`:``;h+=`\n\n[Showing ${e} anchored lines${n}. Use offset=${t} to continue.]`,g={truncation:m}}else{let e=[];d.length>0&&e.push(`Lines ${d.join(`, `)} shown compactly. Their anchors hash the full original lines`),c<o.length&&e.push(`${o.length-c} more lines in file. Use offset=${c+1} to continue`),e.length>0&&(h+=`\n\n[${e.join(`. `)}.]`)}return{content:[{type:`text`,text:h}],details:g}}function c(e){return e.some(e=>e.type===`image`||e.text?.startsWith(`Read image file`)===!0)}function l(e){if(e?.aborted)throw Error(`Operation aborted`)}exports.assertNotAborted=l,exports.createTaggedReadResult=s,exports.isImageRead=c,exports.registerHashlineReadTool=o;
1
+ const e=require("../../schemas/readTool.cjs"),t=require("./readImage.cjs");let n=require("@agimon-ai/doompi-hashline"),r=require("@agimon-ai/doompi-hashline/files"),i=require("@agimon-ai/doompi-ui/hashlineRendering"),a=require("node:fs/promises"),o=require("@earendil-works/pi-coding-agent");function s(n,s=r.isWritableFile){n.registerTool({name:`read`,label:`read`,description:`Read a writable text file with an exact-byte file tag and stable line anchors. Non-writable files and images retain Pi's native behavior. Text is truncated to ${(0,o.formatSize)(o.DEFAULT_MAX_BYTES)}.`,promptSnippet:`Read file contents with snapshot-bound line anchors`,promptGuidelines:[`Use read before edit. When hashline metadata is present, preserve the @file hash and anchors such as 5#abc exactly.`,`Continue large reads with offset until the required anchored lines are visible.`],parameters:e.ReadParamsSchema,executionMode:`parallel`,renderShell:`self`,async execute(e,n,i,d,f){let p=n;u(i);let m=await(0,r.resolveReadInputPath)(p.path,f.cwd),h=await(0,o.createReadToolDefinition)(f.cwd,{autoResizeImages:!1}).execute(e,{...p,path:m},i,d,f);if(l(h.content))return u(i),{...h,content:await t.applyImageLimits(h.content,t.imageLimits())};u(i);let g=await s(m);if(u(i),!g)return h;let _=await(0,a.readFile)(m);return u(i),c(_,(0,r.displayPath)(m,f.cwd),p)},renderCall(e,t){let n=e,r=[n.offset===void 0?void 0:`from ${n.offset}`,n.limit===void 0?void 0:`${n.limit} lines`].filter(e=>e!==void 0);return(0,i.renderHashlineCall)(`read`,n.path,r,t)},renderResult(e,t,n,r){return(0,i.renderHashlineResult)(e,t,n,r,`read`)}})}function c(e,t,i){let a=(0,n.splitLines)((0,r.decodeUtf8)(e,t)),s=i.offset===void 0?0:i.offset-1;if(s>=a.length)throw Error(`Offset ${i.offset} is beyond end of file (${a.length} lines total).`);let c=i.limit===void 0?a.length:Math.min(a.length,s+i.limit),l=a.slice(s,c),u=(0,n.formatFileHeader)(t,(0,r.computeFileTag)(e)),d=[],f=Buffer.byteLength(u,`utf8`)+1,p=l.map((e,t)=>{let r=s+t+1,i=(0,n.formatTaggedLine)(e,r);return Buffer.byteLength(i,`utf8`)<=o.DEFAULT_MAX_BYTES-f?i:(d.push(r),(0,n.formatTaggedLine)((0,o.truncateLine)(e).text,r,``,e))}),m=(0,o.truncateHead)([u,...p].join(`
2
+ `)),h=m.content,g;if(m.truncated){let e=Math.max(0,m.outputLines-1),t=s+e+1,n=m.truncatedBy===`bytes`?`, ${(0,o.formatSize)(o.DEFAULT_MAX_BYTES)} limit`:``;h+=`\n\n[Showing ${e} anchored lines${n}. Use offset=${t} to continue.]`,g={truncation:m}}else{let e=[];d.length>0&&e.push(`Lines ${d.join(`, `)} shown compactly. Their anchors hash the full original lines`),c<a.length&&e.push(`${a.length-c} more lines in file. Use offset=${c+1} to continue`),e.length>0&&(h+=`\n\n[${e.join(`. `)}.]`)}return{content:[{type:`text`,text:h}],details:g}}function l(e){return e.some(e=>e.type===`image`||e.text?.startsWith(`Read image file`)===!0)}function u(e){if(e?.aborted)throw Error(`Operation aborted`)}exports.assertNotAborted=u,exports.createTaggedReadResult=c,exports.isImageRead=l,exports.registerHashlineReadTool=s;
3
3
  //# sourceMappingURL=readTool.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"readTool.cjs","names":["isWritableFile","formatSize","DEFAULT_MAX_BYTES","ReadParamsSchema","resolveReadInputPath","createReadToolDefinition","readFile","displayPath","renderHashlineCall","renderHashlineResult","splitLines","decodeUtf8","formatFileHeader","computeFileTag","formatTaggedLine","truncateLine","truncateHead"],"sources":["../../../src/adapters/pi/readTool.ts"],"sourcesContent":["import { formatFileHeader, formatTaggedLine, splitLines } from '@agimon-ai/doompi-hashline';\nimport {\n computeFileTag,\n decodeUtf8,\n displayPath,\n isWritableFile,\n resolveReadInputPath,\n} from '@agimon-ai/doompi-hashline/files';\nimport { renderHashlineCall, renderHashlineResult } from '@agimon-ai/doompi-ui/hashlineRendering';\nimport { readFile } from 'node:fs/promises';\nimport {\n createReadToolDefinition,\n DEFAULT_MAX_BYTES,\n formatSize,\n truncateHead,\n truncateLine,\n type ExtensionAPI,\n type ReadToolDetails,\n} from '@earendil-works/pi-coding-agent';\nimport { ReadParamsSchema, type ReadParams } from '../../schemas/readTool.ts';\n\ntype WritableCheck = (path: string) => Promise<boolean>;\n\n/** Register the snapshot-bound replacement for Pi's read tool. */\nexport function registerHashlineReadTool(\n pi: Pick<ExtensionAPI, 'registerTool'>,\n writable: WritableCheck = isWritableFile,\n): void {\n pi.registerTool({\n name: 'read',\n label: 'read',\n description: `Read a writable text file with an exact-byte file tag and stable line anchors. Non-writable files and images retain Pi's native behavior. Text is truncated to ${formatSize(DEFAULT_MAX_BYTES)}.`,\n promptSnippet: 'Read file contents with snapshot-bound line anchors',\n promptGuidelines: [\n 'Use read before edit. When hashline metadata is present, preserve the @file hash and anchors such as 5#abc exactly.',\n 'Continue large reads with offset until the required anchored lines are visible.',\n ],\n parameters: ReadParamsSchema,\n executionMode: 'parallel',\n renderShell: 'self',\n async execute(toolCallId, params, signal, onUpdate, ctx) {\n const input = params as ReadParams;\n assertNotAborted(signal);\n const absolutePath = await resolveReadInputPath(input.path, ctx.cwd);\n const nativeRead = createReadToolDefinition(ctx.cwd);\n const nativeResult = await nativeRead.execute(\n toolCallId,\n { ...input, path: absolutePath },\n signal,\n onUpdate,\n ctx,\n );\n if (isImageRead(nativeResult.content)) return nativeResult;\n\n assertNotAborted(signal);\n const canEdit = await writable(absolutePath);\n assertNotAborted(signal);\n if (!canEdit) return nativeResult;\n const bytes = await readFile(absolutePath);\n assertNotAborted(signal);\n return createTaggedReadResult(bytes, displayPath(absolutePath, ctx.cwd), input);\n },\n renderCall(args, theme) {\n const input = args as ReadParams;\n const details = [\n input.offset === undefined ? undefined : `from ${input.offset}`,\n input.limit === undefined ? undefined : `${input.limit} lines`,\n ].filter((value): value is string => value !== undefined);\n return renderHashlineCall('read', input.path, details, theme);\n },\n renderResult(result, options, theme, context) {\n return renderHashlineResult(result, options, theme, context, 'read');\n },\n });\n}\n\nexport function createTaggedReadResult(\n bytes: Buffer,\n path: string,\n params: ReadParams,\n): { content: [{ type: 'text'; text: string }]; details: ReadToolDetails | undefined } {\n const lines = splitLines(decodeUtf8(bytes, path));\n const startIndex = params.offset === undefined ? 0 : params.offset - 1;\n if (startIndex >= lines.length) {\n throw new Error(`Offset ${params.offset} is beyond end of file (${lines.length} lines total).`);\n }\n\n const endIndex = params.limit === undefined ? lines.length : Math.min(lines.length, startIndex + params.limit);\n const selected = lines.slice(startIndex, endIndex);\n const header = formatFileHeader(path, computeFileTag(bytes));\n const compactedLines: number[] = [];\n const headerBytes = Buffer.byteLength(header, 'utf8') + 1;\n const tagged = selected.map((line, index) => {\n const lineNumber = startIndex + index + 1;\n const full = formatTaggedLine(line, lineNumber);\n if (Buffer.byteLength(full, 'utf8') <= DEFAULT_MAX_BYTES - headerBytes) return full;\n compactedLines.push(lineNumber);\n return formatTaggedLine(truncateLine(line).text, lineNumber, '', line);\n });\n const truncation = truncateHead([header, ...tagged].join('\\n'));\n let text = truncation.content;\n let details: ReadToolDetails | undefined;\n\n if (truncation.truncated) {\n const shownLines = Math.max(0, truncation.outputLines - 1);\n const nextOffset = startIndex + shownLines + 1;\n const reason = truncation.truncatedBy === 'bytes' ? `, ${formatSize(DEFAULT_MAX_BYTES)} limit` : '';\n text += `\\n\\n[Showing ${shownLines} anchored lines${reason}. Use offset=${nextOffset} to continue.]`;\n details = { truncation };\n } else {\n const notices: string[] = [];\n if (compactedLines.length > 0) {\n notices.push(`Lines ${compactedLines.join(', ')} shown compactly. Their anchors hash the full original lines`);\n }\n if (endIndex < lines.length) {\n notices.push(`${lines.length - endIndex} more lines in file. Use offset=${endIndex + 1} to continue`);\n }\n if (notices.length > 0) text += `\\n\\n[${notices.join('. ')}.]`;\n }\n\n return { content: [{ type: 'text', text }], details };\n}\n\nexport function isImageRead(content: readonly { readonly type: string; readonly text?: string }[]): boolean {\n return content.some((part) => part.type === 'image' || part.text?.startsWith('Read image file') === true);\n}\n\nexport function assertNotAborted(signal: AbortSignal | undefined): void {\n if (signal?.aborted) throw new Error('Operation aborted');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"readTool.cjs","names":["isWritableFile","formatSize","DEFAULT_MAX_BYTES","ReadParamsSchema","resolveReadInputPath","createReadToolDefinition","applyImageLimits","imageLimits","readFile","displayPath","renderHashlineCall","renderHashlineResult","splitLines","decodeUtf8","formatFileHeader","computeFileTag","formatTaggedLine","truncateLine","truncateHead"],"sources":["../../../src/adapters/pi/readTool.ts"],"sourcesContent":["import { formatFileHeader, formatTaggedLine, splitLines } from '@agimon-ai/doompi-hashline';\nimport {\n computeFileTag,\n decodeUtf8,\n displayPath,\n isWritableFile,\n resolveReadInputPath,\n} from '@agimon-ai/doompi-hashline/files';\nimport { renderHashlineCall, renderHashlineResult } from '@agimon-ai/doompi-ui/hashlineRendering';\nimport { readFile } from 'node:fs/promises';\nimport {\n createReadToolDefinition,\n DEFAULT_MAX_BYTES,\n formatSize,\n truncateHead,\n truncateLine,\n type ExtensionAPI,\n type ReadToolDetails,\n} from '@earendil-works/pi-coding-agent';\nimport { ReadParamsSchema, type ReadParams } from '../../schemas/readTool.ts';\nimport { applyImageLimits, imageLimits } from './readImage.ts';\n\ntype WritableCheck = (path: string) => Promise<boolean>;\n\n/** Register the snapshot-bound replacement for Pi's read tool. */\nexport function registerHashlineReadTool(\n pi: Pick<ExtensionAPI, 'registerTool'>,\n writable: WritableCheck = isWritableFile,\n): void {\n pi.registerTool({\n name: 'read',\n label: 'read',\n description: `Read a writable text file with an exact-byte file tag and stable line anchors. Non-writable files and images retain Pi's native behavior. Text is truncated to ${formatSize(DEFAULT_MAX_BYTES)}.`,\n promptSnippet: 'Read file contents with snapshot-bound line anchors',\n promptGuidelines: [\n 'Use read before edit. When hashline metadata is present, preserve the @file hash and anchors such as 5#abc exactly.',\n 'Continue large reads with offset until the required anchored lines are visible.',\n ],\n parameters: ReadParamsSchema,\n executionMode: 'parallel',\n renderShell: 'self',\n async execute(toolCallId, params, signal, onUpdate, ctx) {\n const input = params as ReadParams;\n assertNotAborted(signal);\n const absolutePath = await resolveReadInputPath(input.path, ctx.cwd);\n // Pi resizes at a cap it hardcodes, so it is told not to: the block comes\n // back converted but full size, and the machine's configured cap is\n // applied below. Everything else about the native read is kept.\n const nativeRead = createReadToolDefinition(ctx.cwd, { autoResizeImages: false });\n const nativeResult = await nativeRead.execute(\n toolCallId,\n { ...input, path: absolutePath },\n signal,\n onUpdate,\n ctx,\n );\n if (isImageRead(nativeResult.content)) {\n assertNotAborted(signal);\n return { ...nativeResult, content: await applyImageLimits(nativeResult.content, imageLimits()) };\n }\n\n assertNotAborted(signal);\n const canEdit = await writable(absolutePath);\n assertNotAborted(signal);\n if (!canEdit) return nativeResult;\n const bytes = await readFile(absolutePath);\n assertNotAborted(signal);\n return createTaggedReadResult(bytes, displayPath(absolutePath, ctx.cwd), input);\n },\n renderCall(args, theme) {\n const input = args as ReadParams;\n const details = [\n input.offset === undefined ? undefined : `from ${input.offset}`,\n input.limit === undefined ? undefined : `${input.limit} lines`,\n ].filter((value): value is string => value !== undefined);\n return renderHashlineCall('read', input.path, details, theme);\n },\n renderResult(result, options, theme, context) {\n return renderHashlineResult(result, options, theme, context, 'read');\n },\n });\n}\n\nexport function createTaggedReadResult(\n bytes: Buffer,\n path: string,\n params: ReadParams,\n): { content: [{ type: 'text'; text: string }]; details: ReadToolDetails | undefined } {\n const lines = splitLines(decodeUtf8(bytes, path));\n const startIndex = params.offset === undefined ? 0 : params.offset - 1;\n if (startIndex >= lines.length) {\n throw new Error(`Offset ${params.offset} is beyond end of file (${lines.length} lines total).`);\n }\n\n const endIndex = params.limit === undefined ? lines.length : Math.min(lines.length, startIndex + params.limit);\n const selected = lines.slice(startIndex, endIndex);\n const header = formatFileHeader(path, computeFileTag(bytes));\n const compactedLines: number[] = [];\n const headerBytes = Buffer.byteLength(header, 'utf8') + 1;\n const tagged = selected.map((line, index) => {\n const lineNumber = startIndex + index + 1;\n const full = formatTaggedLine(line, lineNumber);\n if (Buffer.byteLength(full, 'utf8') <= DEFAULT_MAX_BYTES - headerBytes) return full;\n compactedLines.push(lineNumber);\n return formatTaggedLine(truncateLine(line).text, lineNumber, '', line);\n });\n const truncation = truncateHead([header, ...tagged].join('\\n'));\n let text = truncation.content;\n let details: ReadToolDetails | undefined;\n\n if (truncation.truncated) {\n const shownLines = Math.max(0, truncation.outputLines - 1);\n const nextOffset = startIndex + shownLines + 1;\n const reason = truncation.truncatedBy === 'bytes' ? `, ${formatSize(DEFAULT_MAX_BYTES)} limit` : '';\n text += `\\n\\n[Showing ${shownLines} anchored lines${reason}. Use offset=${nextOffset} to continue.]`;\n details = { truncation };\n } else {\n const notices: string[] = [];\n if (compactedLines.length > 0) {\n notices.push(`Lines ${compactedLines.join(', ')} shown compactly. Their anchors hash the full original lines`);\n }\n if (endIndex < lines.length) {\n notices.push(`${lines.length - endIndex} more lines in file. Use offset=${endIndex + 1} to continue`);\n }\n if (notices.length > 0) text += `\\n\\n[${notices.join('. ')}.]`;\n }\n\n return { content: [{ type: 'text', text }], details };\n}\n\nexport function isImageRead(content: readonly { readonly type: string; readonly text?: string }[]): boolean {\n return content.some((part) => part.type === 'image' || part.text?.startsWith('Read image file') === true);\n}\n\nexport function assertNotAborted(signal: AbortSignal | undefined): void {\n if (signal?.aborted) throw new Error('Operation aborted');\n}\n"],"mappings":"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"}
@@ -3,7 +3,6 @@ import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
  //#region src/adapters/pi/readTool.d.ts
4
4
  type WritableCheck = (path: string) => Promise<boolean>;
5
5
  /** Register the snapshot-bound replacement for Pi's read tool. */
6
- declare function registerHashlineReadTool(pi: Pick<ExtensionAPI, 'registerTool'>, writable?: WritableCheck): void;
6
+ export declare function registerHashlineReadTool(pi: Pick<ExtensionAPI, 'registerTool'>, writable?: WritableCheck): void;
7
7
  //#endregion
8
- export { registerHashlineReadTool };
9
8
  //# sourceMappingURL=readTool.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"readTool.d.cts","names":[],"sources":["../../../src/adapters/pi/readTool.ts"],"mappings":";;;KAqBK,iBAAiB,iBAAiB;;iBAGvB,yBACd,IAAI,KAAK,+BACT,WAAU"}
1
+ {"version":3,"file":"readTool.d.cts","names":[],"sources":["../../../src/adapters/pi/readTool.ts"],"mappings":";;;KAsBK,iBAAiB,iBAAiB;;wBAGvB,yBACd,IAAI,KAAK,+BACT,WAAU"}
@@ -3,7 +3,6 @@ import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
  //#region src/adapters/pi/readTool.d.ts
4
4
  type WritableCheck = (path: string) => Promise<boolean>;
5
5
  /** Register the snapshot-bound replacement for Pi's read tool. */
6
- declare function registerHashlineReadTool(pi: Pick<ExtensionAPI, 'registerTool'>, writable?: WritableCheck): void;
6
+ export declare function registerHashlineReadTool(pi: Pick<ExtensionAPI, 'registerTool'>, writable?: WritableCheck): void;
7
7
  //#endregion
8
- export { registerHashlineReadTool };
9
8
  //# sourceMappingURL=readTool.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"readTool.d.mts","names":[],"sources":["../../../src/adapters/pi/readTool.ts"],"mappings":";;;KAqBK,iBAAiB,iBAAiB;;iBAGvB,yBACd,IAAI,KAAK,+BACT,WAAU"}
1
+ {"version":3,"file":"readTool.d.mts","names":[],"sources":["../../../src/adapters/pi/readTool.ts"],"mappings":";;;KAsBK,iBAAiB,iBAAiB;;wBAGvB,yBACd,IAAI,KAAK,+BACT,WAAU"}
@@ -1,3 +1,3 @@
1
- import{ReadParamsSchema as e}from"../../schemas/readTool.mjs";import{formatFileHeader as t,formatTaggedLine as n,splitLines as r}from"@agimon-ai/doompi-hashline";import{computeFileTag as i,decodeUtf8 as a,displayPath as o,isWritableFile as s,resolveReadInputPath as c}from"@agimon-ai/doompi-hashline/files";import{renderHashlineCall as l,renderHashlineResult as u}from"@agimon-ai/doompi-ui/hashlineRendering";import{readFile as d}from"node:fs/promises";import{DEFAULT_MAX_BYTES as f,createReadToolDefinition as p,formatSize as m,truncateHead as h,truncateLine as g}from"@earendil-works/pi-coding-agent";function _(t,n=s){t.registerTool({name:`read`,label:`read`,description:`Read a writable text file with an exact-byte file tag and stable line anchors. Non-writable files and images retain Pi's native behavior. Text is truncated to ${m(f)}.`,promptSnippet:`Read file contents with snapshot-bound line anchors`,promptGuidelines:[`Use read before edit. When hashline metadata is present, preserve the @file hash and anchors such as 5#abc exactly.`,`Continue large reads with offset until the required anchored lines are visible.`],parameters:e,executionMode:`parallel`,renderShell:`self`,async execute(e,t,r,i,a){let s=t;b(r);let l=await c(s.path,a.cwd),u=await p(a.cwd).execute(e,{...s,path:l},r,i,a);if(y(u.content))return u;b(r);let f=await n(l);if(b(r),!f)return u;let m=await d(l);return b(r),v(m,o(l,a.cwd),s)},renderCall(e,t){let n=e,r=[n.offset===void 0?void 0:`from ${n.offset}`,n.limit===void 0?void 0:`${n.limit} lines`].filter(e=>e!==void 0);return l(`read`,n.path,r,t)},renderResult(e,t,n,r){return u(e,t,n,r,`read`)}})}function v(e,o,s){let c=r(a(e,o)),l=s.offset===void 0?0:s.offset-1;if(l>=c.length)throw Error(`Offset ${s.offset} is beyond end of file (${c.length} lines total).`);let u=s.limit===void 0?c.length:Math.min(c.length,l+s.limit),d=c.slice(l,u),p=t(o,i(e)),_=[],v=Buffer.byteLength(p,`utf8`)+1,y=d.map((e,t)=>{let r=l+t+1,i=n(e,r);return Buffer.byteLength(i,`utf8`)<=f-v?i:(_.push(r),n(g(e).text,r,``,e))}),b=h([p,...y].join(`
2
- `)),x=b.content,S;if(b.truncated){let e=Math.max(0,b.outputLines-1),t=l+e+1,n=b.truncatedBy===`bytes`?`, ${m(f)} limit`:``;x+=`\n\n[Showing ${e} anchored lines${n}. Use offset=${t} to continue.]`,S={truncation:b}}else{let e=[];_.length>0&&e.push(`Lines ${_.join(`, `)} shown compactly. Their anchors hash the full original lines`),u<c.length&&e.push(`${c.length-u} more lines in file. Use offset=${u+1} to continue`),e.length>0&&(x+=`\n\n[${e.join(`. `)}.]`)}return{content:[{type:`text`,text:x}],details:S}}function y(e){return e.some(e=>e.type===`image`||e.text?.startsWith(`Read image file`)===!0)}function b(e){if(e?.aborted)throw Error(`Operation aborted`)}export{b as assertNotAborted,v as createTaggedReadResult,y as isImageRead,_ as registerHashlineReadTool};
1
+ import{ReadParamsSchema as e}from"../../schemas/readTool.mjs";import{applyImageLimits as t,imageLimits as n}from"./readImage.mjs";import{formatFileHeader as r,formatTaggedLine as i,splitLines as a}from"@agimon-ai/doompi-hashline";import{computeFileTag as o,decodeUtf8 as s,displayPath as c,isWritableFile as l,resolveReadInputPath as u}from"@agimon-ai/doompi-hashline/files";import{renderHashlineCall as d,renderHashlineResult as f}from"@agimon-ai/doompi-ui/hashlineRendering";import{readFile as p}from"node:fs/promises";import{DEFAULT_MAX_BYTES as m,createReadToolDefinition as h,formatSize as g,truncateHead as _,truncateLine as v}from"@earendil-works/pi-coding-agent";function y(r,i=l){r.registerTool({name:`read`,label:`read`,description:`Read a writable text file with an exact-byte file tag and stable line anchors. Non-writable files and images retain Pi's native behavior. Text is truncated to ${g(m)}.`,promptSnippet:`Read file contents with snapshot-bound line anchors`,promptGuidelines:[`Use read before edit. When hashline metadata is present, preserve the @file hash and anchors such as 5#abc exactly.`,`Continue large reads with offset until the required anchored lines are visible.`],parameters:e,executionMode:`parallel`,renderShell:`self`,async execute(e,r,a,o,s){let l=r;S(a);let d=await u(l.path,s.cwd),f=await h(s.cwd,{autoResizeImages:!1}).execute(e,{...l,path:d},a,o,s);if(x(f.content))return S(a),{...f,content:await t(f.content,n())};S(a);let m=await i(d);if(S(a),!m)return f;let g=await p(d);return S(a),b(g,c(d,s.cwd),l)},renderCall(e,t){let n=e,r=[n.offset===void 0?void 0:`from ${n.offset}`,n.limit===void 0?void 0:`${n.limit} lines`].filter(e=>e!==void 0);return d(`read`,n.path,r,t)},renderResult(e,t,n,r){return f(e,t,n,r,`read`)}})}function b(e,t,n){let c=a(s(e,t)),l=n.offset===void 0?0:n.offset-1;if(l>=c.length)throw Error(`Offset ${n.offset} is beyond end of file (${c.length} lines total).`);let u=n.limit===void 0?c.length:Math.min(c.length,l+n.limit),d=c.slice(l,u),f=r(t,o(e)),p=[],h=Buffer.byteLength(f,`utf8`)+1,y=d.map((e,t)=>{let n=l+t+1,r=i(e,n);return Buffer.byteLength(r,`utf8`)<=m-h?r:(p.push(n),i(v(e).text,n,``,e))}),b=_([f,...y].join(`
2
+ `)),x=b.content,S;if(b.truncated){let e=Math.max(0,b.outputLines-1),t=l+e+1,n=b.truncatedBy===`bytes`?`, ${g(m)} limit`:``;x+=`\n\n[Showing ${e} anchored lines${n}. Use offset=${t} to continue.]`,S={truncation:b}}else{let e=[];p.length>0&&e.push(`Lines ${p.join(`, `)} shown compactly. Their anchors hash the full original lines`),u<c.length&&e.push(`${c.length-u} more lines in file. Use offset=${u+1} to continue`),e.length>0&&(x+=`\n\n[${e.join(`. `)}.]`)}return{content:[{type:`text`,text:x}],details:S}}function x(e){return e.some(e=>e.type===`image`||e.text?.startsWith(`Read image file`)===!0)}function S(e){if(e?.aborted)throw Error(`Operation aborted`)}export{S as assertNotAborted,b as createTaggedReadResult,x as isImageRead,y as registerHashlineReadTool};
3
3
  //# sourceMappingURL=readTool.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"readTool.mjs","names":[],"sources":["../../../src/adapters/pi/readTool.ts"],"sourcesContent":["import { formatFileHeader, formatTaggedLine, splitLines } from '@agimon-ai/doompi-hashline';\nimport {\n computeFileTag,\n decodeUtf8,\n displayPath,\n isWritableFile,\n resolveReadInputPath,\n} from '@agimon-ai/doompi-hashline/files';\nimport { renderHashlineCall, renderHashlineResult } from '@agimon-ai/doompi-ui/hashlineRendering';\nimport { readFile } from 'node:fs/promises';\nimport {\n createReadToolDefinition,\n DEFAULT_MAX_BYTES,\n formatSize,\n truncateHead,\n truncateLine,\n type ExtensionAPI,\n type ReadToolDetails,\n} from '@earendil-works/pi-coding-agent';\nimport { ReadParamsSchema, type ReadParams } from '../../schemas/readTool.ts';\n\ntype WritableCheck = (path: string) => Promise<boolean>;\n\n/** Register the snapshot-bound replacement for Pi's read tool. */\nexport function registerHashlineReadTool(\n pi: Pick<ExtensionAPI, 'registerTool'>,\n writable: WritableCheck = isWritableFile,\n): void {\n pi.registerTool({\n name: 'read',\n label: 'read',\n description: `Read a writable text file with an exact-byte file tag and stable line anchors. Non-writable files and images retain Pi's native behavior. Text is truncated to ${formatSize(DEFAULT_MAX_BYTES)}.`,\n promptSnippet: 'Read file contents with snapshot-bound line anchors',\n promptGuidelines: [\n 'Use read before edit. When hashline metadata is present, preserve the @file hash and anchors such as 5#abc exactly.',\n 'Continue large reads with offset until the required anchored lines are visible.',\n ],\n parameters: ReadParamsSchema,\n executionMode: 'parallel',\n renderShell: 'self',\n async execute(toolCallId, params, signal, onUpdate, ctx) {\n const input = params as ReadParams;\n assertNotAborted(signal);\n const absolutePath = await resolveReadInputPath(input.path, ctx.cwd);\n const nativeRead = createReadToolDefinition(ctx.cwd);\n const nativeResult = await nativeRead.execute(\n toolCallId,\n { ...input, path: absolutePath },\n signal,\n onUpdate,\n ctx,\n );\n if (isImageRead(nativeResult.content)) return nativeResult;\n\n assertNotAborted(signal);\n const canEdit = await writable(absolutePath);\n assertNotAborted(signal);\n if (!canEdit) return nativeResult;\n const bytes = await readFile(absolutePath);\n assertNotAborted(signal);\n return createTaggedReadResult(bytes, displayPath(absolutePath, ctx.cwd), input);\n },\n renderCall(args, theme) {\n const input = args as ReadParams;\n const details = [\n input.offset === undefined ? undefined : `from ${input.offset}`,\n input.limit === undefined ? undefined : `${input.limit} lines`,\n ].filter((value): value is string => value !== undefined);\n return renderHashlineCall('read', input.path, details, theme);\n },\n renderResult(result, options, theme, context) {\n return renderHashlineResult(result, options, theme, context, 'read');\n },\n });\n}\n\nexport function createTaggedReadResult(\n bytes: Buffer,\n path: string,\n params: ReadParams,\n): { content: [{ type: 'text'; text: string }]; details: ReadToolDetails | undefined } {\n const lines = splitLines(decodeUtf8(bytes, path));\n const startIndex = params.offset === undefined ? 0 : params.offset - 1;\n if (startIndex >= lines.length) {\n throw new Error(`Offset ${params.offset} is beyond end of file (${lines.length} lines total).`);\n }\n\n const endIndex = params.limit === undefined ? lines.length : Math.min(lines.length, startIndex + params.limit);\n const selected = lines.slice(startIndex, endIndex);\n const header = formatFileHeader(path, computeFileTag(bytes));\n const compactedLines: number[] = [];\n const headerBytes = Buffer.byteLength(header, 'utf8') + 1;\n const tagged = selected.map((line, index) => {\n const lineNumber = startIndex + index + 1;\n const full = formatTaggedLine(line, lineNumber);\n if (Buffer.byteLength(full, 'utf8') <= DEFAULT_MAX_BYTES - headerBytes) return full;\n compactedLines.push(lineNumber);\n return formatTaggedLine(truncateLine(line).text, lineNumber, '', line);\n });\n const truncation = truncateHead([header, ...tagged].join('\\n'));\n let text = truncation.content;\n let details: ReadToolDetails | undefined;\n\n if (truncation.truncated) {\n const shownLines = Math.max(0, truncation.outputLines - 1);\n const nextOffset = startIndex + shownLines + 1;\n const reason = truncation.truncatedBy === 'bytes' ? `, ${formatSize(DEFAULT_MAX_BYTES)} limit` : '';\n text += `\\n\\n[Showing ${shownLines} anchored lines${reason}. Use offset=${nextOffset} to continue.]`;\n details = { truncation };\n } else {\n const notices: string[] = [];\n if (compactedLines.length > 0) {\n notices.push(`Lines ${compactedLines.join(', ')} shown compactly. Their anchors hash the full original lines`);\n }\n if (endIndex < lines.length) {\n notices.push(`${lines.length - endIndex} more lines in file. Use offset=${endIndex + 1} to continue`);\n }\n if (notices.length > 0) text += `\\n\\n[${notices.join('. ')}.]`;\n }\n\n return { content: [{ type: 'text', text }], details };\n}\n\nexport function isImageRead(content: readonly { readonly type: string; readonly text?: string }[]): boolean {\n return content.some((part) => part.type === 'image' || part.text?.startsWith('Read image file') === true);\n}\n\nexport function assertNotAborted(signal: AbortSignal | undefined): void {\n if (signal?.aborted) throw new Error('Operation aborted');\n}\n"],"mappings":"2lBAwBA,SAAgB,EACd,EACA,EAA0B,EACpB,CACN,EAAG,aAAa,CACd,KAAM,OACN,MAAO,OACP,YAAa,kKAAkK,EAAW,CAAiB,EAAE,GAC7M,cAAe,sDACf,iBAAkB,CAChB,sHACA,iFACF,EACA,WAAY,EACZ,cAAe,WACf,YAAa,OACb,MAAM,QAAQ,EAAY,EAAQ,EAAQ,EAAU,EAAK,CACvD,IAAM,EAAQ,EACd,EAAiB,CAAM,EACvB,IAAM,EAAe,MAAM,EAAqB,EAAM,KAAM,EAAI,GAAG,EAE7D,EAAe,MADF,EAAyB,EAAI,GACZ,CAAC,CAAC,QACpC,EACA,CAAE,GAAG,EAAO,KAAM,CAAa,EAC/B,EACA,EACA,CACF,EACA,GAAI,EAAY,EAAa,OAAO,EAAG,OAAO,EAE9C,EAAiB,CAAM,EACvB,IAAM,EAAU,MAAM,EAAS,CAAY,EAE3C,GADA,EAAiB,CAAM,EACnB,CAAC,EAAS,OAAO,EACrB,IAAM,EAAQ,MAAM,EAAS,CAAY,EAEzC,OADA,EAAiB,CAAM,EAChB,EAAuB,EAAO,EAAY,EAAc,EAAI,GAAG,EAAG,CAAK,CAChF,EACA,WAAW,EAAM,EAAO,CACtB,IAAM,EAAQ,EACR,EAAU,CACd,EAAM,SAAW,IAAA,GAAY,IAAA,GAAY,QAAQ,EAAM,SACvD,EAAM,QAAU,IAAA,GAAY,IAAA,GAAY,GAAG,EAAM,MAAM,OACzD,CAAC,CAAC,OAAQ,GAA2B,IAAU,IAAA,EAAS,EACxD,OAAO,EAAmB,OAAQ,EAAM,KAAM,EAAS,CAAK,CAC9D,EACA,aAAa,EAAQ,EAAS,EAAO,EAAS,CAC5C,OAAO,EAAqB,EAAQ,EAAS,EAAO,EAAS,MAAM,CACrE,CACF,CAAC,CACH,CAEA,SAAgB,EACd,EACA,EACA,EACqF,CACrF,IAAM,EAAQ,EAAW,EAAW,EAAO,CAAI,CAAC,EAC1C,EAAa,EAAO,SAAW,IAAA,GAAY,EAAI,EAAO,OAAS,EACrE,GAAI,GAAc,EAAM,OACtB,MAAU,MAAM,UAAU,EAAO,OAAO,0BAA0B,EAAM,OAAO,eAAe,EAGhG,IAAM,EAAW,EAAO,QAAU,IAAA,GAAY,EAAM,OAAS,KAAK,IAAI,EAAM,OAAQ,EAAa,EAAO,KAAK,EACvG,EAAW,EAAM,MAAM,EAAY,CAAQ,EAC3C,EAAS,EAAiB,EAAM,EAAe,CAAK,CAAC,EACrD,EAA2B,CAAC,EAC5B,EAAc,OAAO,WAAW,EAAQ,MAAM,EAAI,EAClD,EAAS,EAAS,KAAK,EAAM,IAAU,CAC3C,IAAM,EAAa,EAAa,EAAQ,EAClC,EAAO,EAAiB,EAAM,CAAU,EAG9C,OAFI,OAAO,WAAW,EAAM,MAAM,GAAK,EAAoB,EAAoB,GAC/E,EAAe,KAAK,CAAU,EACvB,EAAiB,EAAa,CAAI,CAAC,CAAC,KAAM,EAAY,GAAI,CAAI,EACvE,CAAC,EACK,EAAa,EAAa,CAAC,EAAQ,GAAG,CAAM,CAAC,CAAC,KAAK;CAAI,CAAC,EAC1D,EAAO,EAAW,QAClB,EAEJ,GAAI,EAAW,UAAW,CACxB,IAAM,EAAa,KAAK,IAAI,EAAG,EAAW,YAAc,CAAC,EACnD,EAAa,EAAa,EAAa,EACvC,EAAS,EAAW,cAAgB,QAAU,KAAK,EAAW,CAAiB,EAAE,QAAU,GACjG,GAAQ,gBAAgB,EAAW,iBAAiB,EAAO,eAAe,EAAW,gBACrF,EAAU,CAAE,YAAW,CACzB,KAAO,CACL,IAAM,EAAoB,CAAC,EACvB,EAAe,OAAS,GAC1B,EAAQ,KAAK,SAAS,EAAe,KAAK,IAAI,EAAE,6DAA6D,EAE3G,EAAW,EAAM,QACnB,EAAQ,KAAK,GAAG,EAAM,OAAS,EAAS,kCAAkC,EAAW,EAAE,aAAa,EAElG,EAAQ,OAAS,IAAG,GAAQ,QAAQ,EAAQ,KAAK,IAAI,EAAE,IAC7D,CAEA,MAAO,CAAE,QAAS,CAAC,CAAE,KAAM,OAAQ,MAAK,CAAC,EAAG,SAAQ,CACtD,CAEA,SAAgB,EAAY,EAAgF,CAC1G,OAAO,EAAQ,KAAM,GAAS,EAAK,OAAS,SAAW,EAAK,MAAM,WAAW,iBAAiB,IAAM,EAAI,CAC1G,CAEA,SAAgB,EAAiB,EAAuC,CACtE,GAAI,GAAQ,QAAS,MAAU,MAAM,mBAAmB,CAC1D"}
1
+ {"version":3,"file":"readTool.mjs","names":[],"sources":["../../../src/adapters/pi/readTool.ts"],"sourcesContent":["import { formatFileHeader, formatTaggedLine, splitLines } from '@agimon-ai/doompi-hashline';\nimport {\n computeFileTag,\n decodeUtf8,\n displayPath,\n isWritableFile,\n resolveReadInputPath,\n} from '@agimon-ai/doompi-hashline/files';\nimport { renderHashlineCall, renderHashlineResult } from '@agimon-ai/doompi-ui/hashlineRendering';\nimport { readFile } from 'node:fs/promises';\nimport {\n createReadToolDefinition,\n DEFAULT_MAX_BYTES,\n formatSize,\n truncateHead,\n truncateLine,\n type ExtensionAPI,\n type ReadToolDetails,\n} from '@earendil-works/pi-coding-agent';\nimport { ReadParamsSchema, type ReadParams } from '../../schemas/readTool.ts';\nimport { applyImageLimits, imageLimits } from './readImage.ts';\n\ntype WritableCheck = (path: string) => Promise<boolean>;\n\n/** Register the snapshot-bound replacement for Pi's read tool. */\nexport function registerHashlineReadTool(\n pi: Pick<ExtensionAPI, 'registerTool'>,\n writable: WritableCheck = isWritableFile,\n): void {\n pi.registerTool({\n name: 'read',\n label: 'read',\n description: `Read a writable text file with an exact-byte file tag and stable line anchors. Non-writable files and images retain Pi's native behavior. Text is truncated to ${formatSize(DEFAULT_MAX_BYTES)}.`,\n promptSnippet: 'Read file contents with snapshot-bound line anchors',\n promptGuidelines: [\n 'Use read before edit. When hashline metadata is present, preserve the @file hash and anchors such as 5#abc exactly.',\n 'Continue large reads with offset until the required anchored lines are visible.',\n ],\n parameters: ReadParamsSchema,\n executionMode: 'parallel',\n renderShell: 'self',\n async execute(toolCallId, params, signal, onUpdate, ctx) {\n const input = params as ReadParams;\n assertNotAborted(signal);\n const absolutePath = await resolveReadInputPath(input.path, ctx.cwd);\n // Pi resizes at a cap it hardcodes, so it is told not to: the block comes\n // back converted but full size, and the machine's configured cap is\n // applied below. Everything else about the native read is kept.\n const nativeRead = createReadToolDefinition(ctx.cwd, { autoResizeImages: false });\n const nativeResult = await nativeRead.execute(\n toolCallId,\n { ...input, path: absolutePath },\n signal,\n onUpdate,\n ctx,\n );\n if (isImageRead(nativeResult.content)) {\n assertNotAborted(signal);\n return { ...nativeResult, content: await applyImageLimits(nativeResult.content, imageLimits()) };\n }\n\n assertNotAborted(signal);\n const canEdit = await writable(absolutePath);\n assertNotAborted(signal);\n if (!canEdit) return nativeResult;\n const bytes = await readFile(absolutePath);\n assertNotAborted(signal);\n return createTaggedReadResult(bytes, displayPath(absolutePath, ctx.cwd), input);\n },\n renderCall(args, theme) {\n const input = args as ReadParams;\n const details = [\n input.offset === undefined ? undefined : `from ${input.offset}`,\n input.limit === undefined ? undefined : `${input.limit} lines`,\n ].filter((value): value is string => value !== undefined);\n return renderHashlineCall('read', input.path, details, theme);\n },\n renderResult(result, options, theme, context) {\n return renderHashlineResult(result, options, theme, context, 'read');\n },\n });\n}\n\nexport function createTaggedReadResult(\n bytes: Buffer,\n path: string,\n params: ReadParams,\n): { content: [{ type: 'text'; text: string }]; details: ReadToolDetails | undefined } {\n const lines = splitLines(decodeUtf8(bytes, path));\n const startIndex = params.offset === undefined ? 0 : params.offset - 1;\n if (startIndex >= lines.length) {\n throw new Error(`Offset ${params.offset} is beyond end of file (${lines.length} lines total).`);\n }\n\n const endIndex = params.limit === undefined ? lines.length : Math.min(lines.length, startIndex + params.limit);\n const selected = lines.slice(startIndex, endIndex);\n const header = formatFileHeader(path, computeFileTag(bytes));\n const compactedLines: number[] = [];\n const headerBytes = Buffer.byteLength(header, 'utf8') + 1;\n const tagged = selected.map((line, index) => {\n const lineNumber = startIndex + index + 1;\n const full = formatTaggedLine(line, lineNumber);\n if (Buffer.byteLength(full, 'utf8') <= DEFAULT_MAX_BYTES - headerBytes) return full;\n compactedLines.push(lineNumber);\n return formatTaggedLine(truncateLine(line).text, lineNumber, '', line);\n });\n const truncation = truncateHead([header, ...tagged].join('\\n'));\n let text = truncation.content;\n let details: ReadToolDetails | undefined;\n\n if (truncation.truncated) {\n const shownLines = Math.max(0, truncation.outputLines - 1);\n const nextOffset = startIndex + shownLines + 1;\n const reason = truncation.truncatedBy === 'bytes' ? `, ${formatSize(DEFAULT_MAX_BYTES)} limit` : '';\n text += `\\n\\n[Showing ${shownLines} anchored lines${reason}. Use offset=${nextOffset} to continue.]`;\n details = { truncation };\n } else {\n const notices: string[] = [];\n if (compactedLines.length > 0) {\n notices.push(`Lines ${compactedLines.join(', ')} shown compactly. Their anchors hash the full original lines`);\n }\n if (endIndex < lines.length) {\n notices.push(`${lines.length - endIndex} more lines in file. Use offset=${endIndex + 1} to continue`);\n }\n if (notices.length > 0) text += `\\n\\n[${notices.join('. ')}.]`;\n }\n\n return { content: [{ type: 'text', text }], details };\n}\n\nexport function isImageRead(content: readonly { readonly type: string; readonly text?: string }[]): boolean {\n return content.some((part) => part.type === 'image' || part.text?.startsWith('Read image file') === true);\n}\n\nexport function assertNotAborted(signal: AbortSignal | undefined): void {\n if (signal?.aborted) throw new Error('Operation aborted');\n}\n"],"mappings":"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"}
@@ -1,11 +1,10 @@
1
1
  import { Static, Type } from "typebox";
2
2
  //#region src/schemas/readTool.d.ts
3
- declare const ReadParamsSchema: Type.TObject<{
3
+ export declare const ReadParamsSchema: Type.TObject<{
4
4
  path: Type.TString;
5
5
  offset: Type.TOptional<Type.TInteger>;
6
6
  limit: Type.TOptional<Type.TInteger>;
7
7
  }>;
8
- type ReadParams = Static<typeof ReadParamsSchema>;
8
+ export type ReadParams = Static<typeof ReadParamsSchema>;
9
9
  //#endregion
10
- export { ReadParams, ReadParamsSchema };
11
10
  //# sourceMappingURL=readTool.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"readTool.d.cts","names":[],"sources":["../../src/schemas/readTool.ts"],"mappings":";;cAEa,kBAAgB,KAAA;;;;;KAMjB,aAAa,cAAc"}
1
+ {"version":3,"file":"readTool.d.cts","names":[],"sources":["../../src/schemas/readTool.ts"],"mappings":";;qBAEa,kBAAgB,KAAA;;;;;YAMjB,aAAa,cAAc"}
@@ -1,11 +1,10 @@
1
1
  import { Static, Type } from "typebox";
2
2
  //#region src/schemas/readTool.d.ts
3
- declare const ReadParamsSchema: Type.TObject<{
3
+ export declare const ReadParamsSchema: Type.TObject<{
4
4
  path: Type.TString;
5
5
  offset: Type.TOptional<Type.TInteger>;
6
6
  limit: Type.TOptional<Type.TInteger>;
7
7
  }>;
8
- type ReadParams = Static<typeof ReadParamsSchema>;
8
+ export type ReadParams = Static<typeof ReadParamsSchema>;
9
9
  //#endregion
10
- export { ReadParams, ReadParamsSchema };
11
10
  //# sourceMappingURL=readTool.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"readTool.d.mts","names":[],"sources":["../../src/schemas/readTool.ts"],"mappings":";;cAEa,kBAAgB,KAAA;;;;;KAMjB,aAAa,cAAc"}
1
+ {"version":3,"file":"readTool.d.mts","names":[],"sources":["../../src/schemas/readTool.ts"],"mappings":";;qBAEa,kBAAgB,KAAA;;;;;YAMjB,aAAa,cAAc"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-read",
3
- "version": "0.0.1-alpha.4",
3
+ "version": "0.0.1-alpha.41",
4
4
  "description": "Snapshot-bound hashline read tool for Pi and DoomPi.",
5
5
  "keywords": [
6
6
  "ai",
@@ -14,10 +14,21 @@
14
14
  "read"
15
15
  ],
16
16
  "homepage": "https://agimon.ai",
17
+ "bugs": {
18
+ "url": "https://github.com/AgiFlow/doompi/issues"
19
+ },
17
20
  "license": "MIT",
18
21
  "author": "Vuong Ngo",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/AgiFlow/doompi.git",
25
+ "directory": "packages/default/doompi-read"
26
+ },
19
27
  "files": [
20
- "dist"
28
+ "dist",
29
+ "src/web",
30
+ "src/exports/webClient.ts",
31
+ "!src/web/**/*.stories.tsx"
21
32
  ],
22
33
  "type": "module",
23
34
  "main": "./dist/index.cjs",
@@ -40,24 +51,32 @@
40
51
  "access": "public"
41
52
  },
42
53
  "dependencies": {
43
- "@deepseek-ai/cordis": "4.0.1",
44
- "typebox": "1.3.16",
45
- "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.32",
46
- "@agimon-ai/doompi-hashline": "0.0.1-alpha.4",
47
- "@agimon-ai/doompi-ui": "0.0.1-alpha.32"
54
+ "@deepseek-ai/cordis": "4.0.2",
55
+ "typebox": "1.3.25",
56
+ "@agimon-ai/doompi-config": "0.0.1-alpha.68",
57
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.68",
58
+ "@agimon-ai/doompi-hashline": "0.0.1-alpha.39",
59
+ "@agimon-ai/doompi-ui": "0.0.1-alpha.69",
60
+ "@agimon-ai/doompi-web-components": "0.0.1-alpha.28",
61
+ "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.31"
48
62
  },
49
63
  "devDependencies": {
50
- "@earendil-works/pi-coding-agent": "0.84.2",
51
- "@earendil-works/pi-tui": "0.84.2",
52
- "@types/node": "26.2.0",
53
- "@vitest/coverage-v8": "4.1.11",
54
- "tsdown": "0.22.14",
64
+ "@earendil-works/pi-coding-agent": "0.85.1",
65
+ "@earendil-works/pi-tui": "0.85.1",
66
+ "@tanstack/react-store": "0.11.1",
67
+ "@tanstack/store": "0.11.1",
68
+ "@types/node": "26.4.1",
69
+ "@types/react": "19.2.18",
70
+ "@vitest/coverage-v8": "5.0.0",
71
+ "react": "19.2.8",
72
+ "react-dom": "19.2.8",
73
+ "tsdown": "0.23.0",
55
74
  "typescript": "7.0.2",
56
- "vitest": "4.1.11"
75
+ "vitest": "5.0.0"
57
76
  },
58
77
  "peerDependencies": {
59
- "@earendil-works/pi-coding-agent": "0.84.2",
60
- "@earendil-works/pi-tui": "0.84.2"
78
+ "@earendil-works/pi-coding-agent": "0.85.1",
79
+ "@earendil-works/pi-tui": "0.85.1"
61
80
  },
62
81
  "peerDependenciesMeta": {
63
82
  "@earendil-works/pi-coding-agent": {
@@ -70,6 +89,11 @@
70
89
  "engines": {
71
90
  "node": ">=22.19.0"
72
91
  },
92
+ "doompiWeb": {
93
+ "pluginId": "read",
94
+ "channels": [],
95
+ "client": "./src/exports/webClient.ts"
96
+ },
73
97
  "pi": {
74
98
  "extensions": [
75
99
  "./dist/extensions/pi.mjs"
@@ -78,7 +102,7 @@
78
102
  "scripts": {
79
103
  "build": "tsdown",
80
104
  "test": "vitest --run",
81
- "typecheck": "tsc --noEmit",
105
+ "typecheck": "tsc --noEmit && tsc --noEmit -p src/web/tsconfig.json",
82
106
  "lint": "oxlint . && oxfmt . --check",
83
107
  "fixcode": "oxlint . --fix && oxfmt ."
84
108
  }
@@ -0,0 +1 @@
1
+ export { webPlugin } from '../web/index.ts';
@@ -0,0 +1,115 @@
1
+ import {
2
+ HashlineLines,
3
+ hashlineBody,
4
+ MessageItem,
5
+ MessageItemBody,
6
+ MessageItemHeader,
7
+ MessageItemStatus,
8
+ resultTextLines,
9
+ ToolPathLink,
10
+ toolTone,
11
+ } from '@agimon-ai/doompi-web-components';
12
+ import type { ToolMessageRenderProps } from '@agimon-ai/doompi-web-contracts';
13
+ import { readCallView } from '../lib/readToolView.ts';
14
+
15
+ const IMAGE_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
16
+
17
+ function resultImages(content: readonly unknown[]): Array<{ data: string; mimeType: string }> {
18
+ return content.flatMap((block) => {
19
+ if (typeof block !== 'object' || block === null || Array.isArray(block)) return [];
20
+ const value = block as Record<string, unknown>;
21
+ return value.type === 'image' &&
22
+ typeof value.data === 'string' &&
23
+ typeof value.mimeType === 'string' &&
24
+ IMAGE_MIME_TYPES.has(value.mimeType)
25
+ ? [{ data: value.data, mimeType: value.mimeType }]
26
+ : [];
27
+ });
28
+ }
29
+ /**
30
+ * The read tool's timeline item, the web half of its renderCall and
31
+ * renderResult: the call summary in the header and, when expanded, the
32
+ * anchored lines with a line-number gutter in the body. A failure shows the
33
+ * tool's message in red inside the expanded body instead.
34
+ */
35
+ export function ReadToolMessage({
36
+ args,
37
+ result,
38
+ output,
39
+ running,
40
+ isError,
41
+ fileTabFor,
42
+ openTransientTab,
43
+ }: ToolMessageRenderProps) {
44
+ const view = readCallView(args);
45
+ const images = resultImages(result?.content ?? []);
46
+ // A read is the one call whose file the session usually has not changed, so
47
+ // this is the link that opens it read-only rather than in its history.
48
+ const tab = fileTabFor(view.path);
49
+ const body = result === null || isError ? null : hashlineBody(result, output, 'read', true);
50
+ return (
51
+ <MessageItem tone={toolTone({ running, isError })} expandable defaultExpanded={false}>
52
+ {({ expanded }) => {
53
+ return (
54
+ <>
55
+ <MessageItemHeader title="read">
56
+ <span data-testid="tool-call-read" className="flex min-w-0 flex-1 items-center gap-2">
57
+ <ToolPathLink
58
+ path={view.path}
59
+ {...(tab === undefined ? {} : { onOpen: () => openTransientTab(tab) })}
60
+ />
61
+ {view.details.length > 0 ? (
62
+ <span className="shrink-0 text-doom-faint">· {view.details.join(' · ')}</span>
63
+ ) : null}
64
+ </span>
65
+ </MessageItemHeader>
66
+ {expanded ? (
67
+ isError ? (
68
+ <MessageItemBody data-testid="tool-result-read" className="flex flex-col">
69
+ <MessageItemStatus tone="error">failed</MessageItemStatus>
70
+ {resultTextLines(result, output).map((line, index) => (
71
+ <span key={`${String(index)}-${line}`} className="whitespace-pre-wrap break-words text-doom-red">
72
+ {line}
73
+ </span>
74
+ ))}
75
+ </MessageItemBody>
76
+ ) : body === null ? (
77
+ running ? (
78
+ <MessageItemBody data-testid="tool-result-read">
79
+ <MessageItemStatus tone="running">running</MessageItemStatus>
80
+ </MessageItemBody>
81
+ ) : null
82
+ ) : body.shown.length === 0 && body.notice === undefined && images.length === 0 ? null : (
83
+ <MessageItemBody data-testid="tool-result-read" className="flex flex-col gap-1">
84
+ {/* A read body is one file with no heading of its own, so the
85
+ call's path is what names the grammar for it. */}
86
+ <HashlineLines lines={body.shown} gutter={body.gutter} path={view.path} />
87
+ {images.map((image, index) => {
88
+ const url = `data:${image.mimeType};base64,${image.data}`;
89
+ return (
90
+ <a
91
+ key={`${image.mimeType}:${String(index)}`}
92
+ href={url}
93
+ target="_blank"
94
+ rel="noreferrer"
95
+ className="self-start"
96
+ >
97
+ <img
98
+ src={url}
99
+ alt={`Read result ${String(index + 1)}`}
100
+ data-testid="tool-result-read-image"
101
+ className="h-auto max-h-96 max-w-full rounded-md border border-doom-border-soft object-contain"
102
+ />
103
+ </a>
104
+ );
105
+ })}
106
+ {body.notice !== undefined ? <span className="text-doom-faint">{body.notice}</span> : null}
107
+ </MessageItemBody>
108
+ )
109
+ ) : null}
110
+ </>
111
+ );
112
+ }}
113
+ </MessageItem>
114
+ );
115
+ }
@@ -0,0 +1,11 @@
1
+ import { defineWebPlugin } from '@agimon-ai/doompi-web-contracts';
2
+ import { ReadToolMessage } from './components/ReadToolMessage.tsx';
3
+
4
+ /**
5
+ * This package's cockpit presence: the read tool's timeline card, the web
6
+ * half of the TUI's renderCall and renderResult for hashline reads.
7
+ */
8
+ export const webPlugin = defineWebPlugin({
9
+ id: 'read',
10
+ toolRenderers: [{ tools: ['read'], message: ReadToolMessage }],
11
+ });
@@ -0,0 +1,24 @@
1
+ import { compactDetails } from '@agimon-ai/doompi-web-components';
2
+
3
+ export interface ReadCallView {
4
+ readonly path: string;
5
+ /** `from N` and `N lines`, as the TUI's call heading lists them. */
6
+ readonly details: readonly string[];
7
+ }
8
+
9
+ function optionalNumber(value: unknown): number | undefined {
10
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
11
+ }
12
+
13
+ /** What the read call heading shows: the path, then the offset and limit when given. */
14
+ export function readCallView(args: Readonly<Record<string, unknown>>): ReadCallView {
15
+ const offset = optionalNumber(args.offset);
16
+ const limit = optionalNumber(args.limit);
17
+ return {
18
+ path: typeof args.path === 'string' ? args.path : '',
19
+ details: compactDetails([
20
+ offset === undefined ? undefined : `from ${offset}`,
21
+ limit === undefined ? undefined : `${limit} lines`,
22
+ ]),
23
+ };
24
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "noImplicitAny": true,
10
+ "noUnusedLocals": true,
11
+ "noUnusedParameters": true,
12
+ "noImplicitReturns": true,
13
+ "noFallthroughCasesInSwitch": true,
14
+ "noEmit": true,
15
+ "resolveJsonModule": true,
16
+ "esModuleInterop": true,
17
+ "skipLibCheck": true,
18
+ "allowImportingTsExtensions": true,
19
+ "forceConsistentCasingInFileNames": true,
20
+ "types": ["node"]
21
+ },
22
+ "include": [".", "../types", "../exports/webClient.ts"]
23
+ }