@kolisachint/hoocode-agent 0.5.47 → 0.5.49
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/CHANGELOG.md +19 -0
- package/dist/core/agent-session-retry.d.ts.map +1 -1
- package/dist/core/agent-session-retry.js +10 -1
- package/dist/core/agent-session-retry.js.map +1 -1
- package/dist/core/session-identity.d.ts +10 -0
- package/dist/core/session-identity.d.ts.map +1 -1
- package/dist/core/session-identity.js +48 -0
- package/dist/core/session-identity.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -0
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +7 -3
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +156 -65
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +5 -4
- package/dist/core/tools/read.js.map +1 -1
- package/dist/modes/interactive/command-executor.d.ts +9 -3
- package/dist/modes/interactive/command-executor.d.ts.map +1 -1
- package/dist/modes/interactive/command-executor.js +16 -8
- package/dist/modes/interactive/command-executor.js.map +1 -1
- package/dist/modes/interactive/components/session-color-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-color-selector.js +5 -2
- package/dist/modes/interactive/components/session-color-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +7 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/sleep.d.ts +9 -0
- package/dist/utils/sleep.d.ts.map +1 -1
- package/dist/utils/sleep.js +11 -1
- package/dist/utils/sleep.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../../src/core/tools/read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAMjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAQ5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAKtF,OAAO,EAAoD,KAAK,gBAAgB,EAAgB,MAAM,eAAe,CAAC;AAEtH,QAAA,MAAM,UAAU;;;;EAId,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAEtD,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AA2BD;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B,qCAAqC;IACrC,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACpD,+CAA+C;IAC/C,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,sEAAsE;IACtE,mBAAmB,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACnF;AAQD,MAAM,WAAW,eAAe;IAC/B,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oEAAoE;IACpE,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,mFAAmF;IACnF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mFAAmF;IACnF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAmJD,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,CAAC,CA0LhE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAEnG","sourcesContent":["import { basename, dirname, isAbsolute, relative, resolve as resolvePath, sep } from \"node:path\";\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { compressReadOutput } from \"@kolisachint/hoocode-agent-core\";\nimport type { Api, ImageContent, Model, TextContent } from \"@kolisachint/hoocode-ai\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { constants } from \"fs\";\nimport { access as fsAccess, readFile as fsReadFile } from \"fs/promises\";\nimport { type Static, Type } from \"typebox\";\nimport { getReadmePath } from \"../../config.js\";\nimport { keyHint, keyText } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { renderReadOutput } from \"../../modes/interactive/components/read-output.js\";\nimport { getLanguageFromPath, highlightCode, messageLabel, type Theme } from \"../../modes/interactive/theme/theme.js\";\nimport { formatDimensionNote, resizeImage } from \"../../utils/image-resize.js\";\nimport { detectSupportedImageMimeTypeFromFile } from \"../../utils/mime.js\";\nimport { formatPathRelativeToCwdOrAbsolute } from \"../../utils/paths.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { buildDedupPointerText, findCoveringRead, readRangeFromArgs } from \"./read-dedup.js\";\nimport { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nexport type ReadToolInput = Static<typeof readSchema>;\n\nexport interface ReadToolDetails {\n\ttruncation?: TruncationResult;\n}\n\ninterface CompactReadClassification {\n\tkind: \"docs\" | \"resource\" | \"skill\";\n\tlabel: string;\n}\n\nconst COMPACT_RESOURCE_FILE_NAMES = new Set([\"AGENTS.md\", \"AGENTS.MD\", \"CLAUDE.md\", \"CLAUDE.MD\"]);\n\n/**\n * Structured/binary document formats. These are container/binary formats (OOXML\n * is a zip, PDF is binary), so a plain utf-8 read yields garbage — read\n * short-circuits with a note instead of dumping bytes into the context.\n */\nconst DOC_FORMAT_EXTENSIONS = new Map<string, string>([\n\t[\".docx\", \"Word (OOXML)\"],\n\t[\".xlsx\", \"Excel (OOXML)\"],\n\t[\".pptx\", \"PowerPoint (OOXML)\"],\n\t[\".pdf\", \"PDF\"],\n]);\n\nfunction getDocFormatHint(absolutePath: string): string | undefined {\n\tconst dot = absolutePath.lastIndexOf(\".\");\n\tif (dot === -1) return undefined;\n\treturn DOC_FORMAT_EXTENSIONS.get(absolutePath.slice(dot).toLowerCase());\n}\n\n/**\n * Pluggable operations for the read tool.\n * Override these to delegate file reading to remote systems (for example SSH).\n */\nexport interface ReadOperations {\n\t/** Read file contents as a Buffer */\n\treadFile: (absolutePath: string) => Promise<Buffer>;\n\t/** Check if file is readable (throw if not) */\n\taccess: (absolutePath: string) => Promise<void>;\n\t/** Detect image MIME type, return null or undefined for non-images */\n\tdetectImageMimeType?: (absolutePath: string) => Promise<string | null | undefined>;\n}\n\nconst defaultReadOperations: ReadOperations = {\n\treadFile: (path) => fsReadFile(path),\n\taccess: (path) => fsAccess(path, constants.R_OK),\n\tdetectImageMimeType: detectSupportedImageMimeTypeFromFile,\n};\n\nexport interface ReadToolOptions {\n\t/** Whether to auto-resize images to 2000x2000 max. Default: true */\n\tautoResizeImages?: boolean;\n\t/** Custom operations for file reading. Default: local filesystem */\n\toperations?: ReadOperations;\n\t/** Byte cap on the returned text before truncation. Default: DEFAULT_MAX_BYTES. */\n\tmaxOutputBytes?: number;\n\t/** Line cap on the returned text before truncation. Default: DEFAULT_MAX_LINES. */\n\tmaxOutputLines?: number;\n\t/**\n\t * When true, a text read whose requested line range is already fully covered\n\t * by an earlier, still-live read in the current session short-circuits with a\n\t * pointer instead of re-fetching the file. Complements the post-hoc context\n\t * GC; gated on the same setting. Default: false.\n\t */\n\tdedupReads?: boolean;\n}\n\ntype ReadRenderArgs = { path?: string; file_path?: string; offset?: number; limit?: number };\n\nfunction formatReadLineRange(args: ReadRenderArgs | undefined, theme: Theme): string {\n\tif (args?.offset === undefined && args?.limit === undefined) return \"\";\n\tconst startLine = args.offset ?? 1;\n\tconst endLine = args.limit !== undefined ? startLine + args.limit - 1 : \"\";\n\treturn theme.fg(\"warning\", `:${startLine}${endLine ? `-${endLine}` : \"\"}`);\n}\n\nfunction formatReadCall(args: ReadRenderArgs | undefined, theme: Theme): string {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst path = rawPath !== null ? shortenPath(rawPath) : null;\n\tconst invalidArg = invalidArgText(theme);\n\tconst pathDisplay = path === null ? invalidArg : path ? theme.fg(\"accent\", path) : theme.fg(\"toolOutput\", \"...\");\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"read\"))} ${pathDisplay}${formatReadLineRange(args, theme)}`;\n}\n\nfunction trimTrailingEmptyLines(lines: string[]): string[] {\n\tlet end = lines.length;\n\twhile (end > 0 && lines[end - 1] === \"\") {\n\t\tend--;\n\t}\n\treturn lines.slice(0, end);\n}\n\nfunction getNonVisionImageNote(model: Model<Api> | undefined): string | undefined {\n\tif (!model || model.input.includes(\"image\")) {\n\t\treturn undefined;\n\t}\n\treturn \"[Current model does not support images. The image will be omitted from this request.]\";\n}\n\nfunction toPosixPath(filePath: string): string {\n\treturn filePath.split(sep).join(\"/\");\n}\n\nfunction getHooCodeDocsClassification(absolutePath: string): CompactReadClassification | undefined {\n\tconst packageRoot = dirname(getReadmePath());\n\tconst relativePath = relative(resolvePath(packageRoot), resolvePath(absolutePath));\n\tif (\n\t\trelativePath === \"\" ||\n\t\trelativePath === \"..\" ||\n\t\trelativePath.startsWith(`..${sep}`) ||\n\t\tisAbsolute(relativePath)\n\t) {\n\t\treturn undefined;\n\t}\n\n\tconst label = toPosixPath(relativePath);\n\tif (label === \"README.md\" || label.startsWith(\"docs/\") || label.startsWith(\"examples/\")) {\n\t\treturn { kind: \"docs\", label };\n\t}\n\treturn undefined;\n}\n\nfunction getCompactReadClassification(\n\targs: ReadRenderArgs | undefined,\n\tcwd: string,\n): CompactReadClassification | undefined {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tif (!rawPath) return undefined;\n\n\tconst absolutePath = resolveReadPath(rawPath, cwd);\n\tconst fileName = basename(absolutePath);\n\tif (fileName === \"SKILL.md\") {\n\t\treturn { kind: \"skill\", label: basename(dirname(absolutePath)) || fileName };\n\t}\n\n\tconst docsClassification = getHooCodeDocsClassification(absolutePath);\n\tif (docsClassification) return docsClassification;\n\n\tif (COMPACT_RESOURCE_FILE_NAMES.has(fileName)) {\n\t\treturn { kind: \"resource\", label: formatPathRelativeToCwdOrAbsolute(absolutePath, cwd) };\n\t}\n\n\treturn undefined;\n}\n\nfunction formatCompactReadCall(\n\tclassification: CompactReadClassification,\n\targs: ReadRenderArgs | undefined,\n\ttheme: Theme,\n): string {\n\tconst expandHint = theme.fg(\"dim\", ` (${keyText(\"app.tools.expand\")} to expand)`);\n\tif (classification.kind === \"skill\") {\n\t\treturn (\n\t\t\t`${messageLabel(\"skill\")} ` +\n\t\t\ttheme.fg(\"customMessageText\", classification.label) +\n\t\t\tformatReadLineRange(args, theme) +\n\t\t\texpandHint\n\t\t);\n\t}\n\n\treturn (\n\t\ttheme.fg(\"toolTitle\", theme.bold(`read ${classification.kind}`)) +\n\t\t\" \" +\n\t\ttheme.fg(\"accent\", classification.label) +\n\t\tformatReadLineRange(args, theme) +\n\t\texpandHint\n\t);\n}\n\nfunction formatReadResult(\n\targs: ReadRenderArgs | undefined,\n\tresult: { content: (TextContent | ImageContent)[]; details?: ReadToolDetails },\n\toptions: ToolRenderResultOptions,\n\ttheme: Theme,\n\tshowImages: boolean,\n\tcwd: string,\n\tisError: boolean,\n): string {\n\tif (!options.expanded && !isError && getCompactReadClassification(args, cwd)) {\n\t\treturn \"\";\n\t}\n\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst output = getTextOutput(result, showImages);\n\tconst lang = rawPath ? getLanguageFromPath(rawPath) : undefined;\n\tconst renderedLines = lang ? highlightCode(replaceTabs(output), lang) : output.split(\"\\n\");\n\tconst lines = trimTrailingEmptyLines(renderedLines);\n\tconst maxLines = options.expanded ? lines.length : 10;\n\tconst displayLines = lines.slice(0, maxLines);\n\tconst remaining = lines.length - maxLines;\n\t// Calculate starting line number from args\n\tconst startLine = (args?.offset as number) ?? 1;\n\t// Use the new renderReadOutput function for line numbers\n\tconst numberedLines = renderReadOutput(displayLines.join(\"\\n\"), { startLine });\n\tlet text = `\\n${numberedLines.join(\"\\n\")}`;\n\tif (remaining > 0) {\n\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t}\n\n\tconst truncation = result.details?.truncation;\n\tif (truncation?.truncated) {\n\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\ttext += `\\n${theme.fg(\"warning\", `[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`)}`;\n\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)]`)}`;\n\t\t} else {\n\t\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)]`)}`;\n\t\t}\n\t}\n\treturn text;\n}\n\nexport function createReadToolDefinition(\n\tcwd: string,\n\toptions?: ReadToolOptions,\n): ToolDefinition<typeof readSchema, ReadToolDetails | undefined> {\n\tconst autoResizeImages = options?.autoResizeImages ?? true;\n\tconst ops = options?.operations ?? defaultReadOperations;\n\tconst maxBytes = options?.maxOutputBytes ?? DEFAULT_MAX_BYTES;\n\tconst maxLines = options?.maxOutputLines ?? DEFAULT_MAX_LINES;\n\tconst dedupReads = options?.dedupReads ?? false;\n\treturn {\n\t\tname: \"read\",\n\t\tlabel: \"read\",\n\t\tdescription: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${maxLines} lines or ${Math.round(maxBytes / 1024)}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,\n\t\tpromptSnippet: \"Read file contents\",\n\t\t// No promptGuidelines: \"use read instead of cat/sed\" is already covered by the\n\t\t// file-exploration guideline buildSystemPrompt emits, and again by bash's own\n\t\t// snippet. Three copies of one rule.\n\t\tparameters: readSchema,\n\t\tasync execute(\n\t\t\ttoolCallId,\n\t\t\t{ path, offset, limit }: { path: string; offset?: number; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\tctx?,\n\t\t) {\n\t\t\tconst absolutePath = resolveReadPath(path, cwd);\n\t\t\treturn new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(\n\t\t\t\t(resolve, reject) => {\n\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tlet aborted = false;\n\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t};\n\t\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t\t\t(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Check if file exists and is readable.\n\t\t\t\t\t\t\tawait ops.access(absolutePath);\n\t\t\t\t\t\t\tif (aborted) return;\n\t\t\t\t\t\t\tconst mimeType = ops.detectImageMimeType ? await ops.detectImageMimeType(absolutePath) : undefined;\n\t\t\t\t\t\t\tlet content: (TextContent | ImageContent)[];\n\t\t\t\t\t\t\tlet details: ReadToolDetails | undefined;\n\t\t\t\t\t\t\tconst nonVisionImageNote = getNonVisionImageNote(ctx?.model);\n\t\t\t\t\t\t\t// At-call dedup: for plain-text reads, if an earlier still-live read\n\t\t\t\t\t\t\t// in this session already covers the requested range, point at it\n\t\t\t\t\t\t\t// instead of re-fetching. Skipped for images and structured docs.\n\t\t\t\t\t\t\tconst dedupCovering =\n\t\t\t\t\t\t\t\tdedupReads && !mimeType && !getDocFormatHint(absolutePath) && ctx?.sessionManager\n\t\t\t\t\t\t\t\t\t? findCoveringRead(ctx.sessionManager.getBranch(), {\n\t\t\t\t\t\t\t\t\t\t\tresolvedPath: absolutePath,\n\t\t\t\t\t\t\t\t\t\t\trequestedRange: readRangeFromArgs({ offset, limit }),\n\t\t\t\t\t\t\t\t\t\t\tcurrentCallId: toolCallId,\n\t\t\t\t\t\t\t\t\t\t\tresolvePath: (raw) => resolveReadPath(raw, cwd),\n\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t: null;\n\t\t\t\t\t\t\tif (dedupCovering) {\n\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: buildDedupPointerText(dedupCovering) }];\n\t\t\t\t\t\t\t} else if (mimeType) {\n\t\t\t\t\t\t\t\t// Read image as binary.\n\t\t\t\t\t\t\t\tconst buffer = await ops.readFile(absolutePath);\n\t\t\t\t\t\t\t\tconst base64 = buffer.toString(\"base64\");\n\t\t\t\t\t\t\t\tif (autoResizeImages) {\n\t\t\t\t\t\t\t\t\t// Resize image if needed before sending it back to the model.\n\t\t\t\t\t\t\t\t\tconst resized = await resizeImage({ type: \"image\", data: base64, mimeType });\n\t\t\t\t\t\t\t\t\tif (!resized) {\n\t\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${mimeType}]\\n[Image omitted: could not be resized below the inline image size limit.]`;\n\t\t\t\t\t\t\t\t\t\tif (nonVisionImageNote) textNote += `\\n${nonVisionImageNote}`;\n\t\t\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: textNote }];\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tconst dimensionNote = formatDimensionNote(resized);\n\t\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${resized.mimeType}]`;\n\t\t\t\t\t\t\t\t\t\tif (dimensionNote) textNote += `\\n${dimensionNote}`;\n\t\t\t\t\t\t\t\t\t\tif (nonVisionImageNote) textNote += `\\n${nonVisionImageNote}`;\n\t\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: textNote },\n\t\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: resized.data, mimeType: resized.mimeType },\n\t\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${mimeType}]`;\n\t\t\t\t\t\t\t\t\tif (nonVisionImageNote) textNote += `\\n${nonVisionImageNote}`;\n\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: textNote },\n\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if (getDocFormatHint(absolutePath)) {\n\t\t\t\t\t\t\t\t// Structured/binary document: a utf-8 read is meaningless, and dumping\n\t\t\t\t\t\t\t\t// the bytes would burn context for nothing. Stop here and let the model\n\t\t\t\t\t\t\t\t// choose an extraction path (bash + an appropriate converter).\n\t\t\t\t\t\t\t\tconst label = getDocFormatHint(absolutePath);\n\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: `[${label} document — not plain text; read cannot show it. Extract it with a converter via bash if you need its contents.]`,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Read text content.\n\t\t\t\t\t\t\t\tconst buffer = await ops.readFile(absolutePath);\n\t\t\t\t\t\t\t\tconst textContent = buffer.toString(\"utf-8\");\n\t\t\t\t\t\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\t\t\t\t\t\tconst totalFileLines = allLines.length;\n\t\t\t\t\t\t\t\t// Apply offset if specified. Convert from 1-indexed input to 0-indexed array access.\n\t\t\t\t\t\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\t\t\t\t\t\tconst startLineDisplay = startLine + 1;\n\t\t\t\t\t\t\t\t// Check if offset is out of bounds.\n\t\t\t\t\t\t\t\tif (startLine >= allLines.length) {\n\t\t\t\t\t\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tlet selectedContent: string;\n\t\t\t\t\t\t\t\tlet userLimitedLines: number | undefined;\n\t\t\t\t\t\t\t\t// If limit is specified by the user, honor it first. Otherwise truncateHead decides.\n\t\t\t\t\t\t\t\tif (limit !== undefined) {\n\t\t\t\t\t\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t\t\t\t\t\t} else if (startLine === 0) {\n\t\t\t\t\t\t\t\t\t// Whole-file read (common case): reuse the original string instead of\n\t\t\t\t\t\t\t\t\t// rebuilding it from the split lines.\n\t\t\t\t\t\t\t\t\tselectedContent = textContent;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Apply truncation, respecting both line and byte limits.\n\t\t\t\t\t\t\t\t// Apply lossless compression then truncation.\n\t\t\t\t\t\t\t\tconst compressedContent = compressReadOutput(selectedContent);\n\t\t\t\t\t\t\t\tconst truncation = truncateHead(compressedContent, { maxBytes, maxLines });\n\t\t\t\t\t\t\t\tlet outputText: string;\n\t\t\t\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\t\t\t\t// First line alone exceeds the byte limit. Point the model at a bash fallback.\n\t\t\t\t\t\t\t\t\tconst firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], \"utf-8\"));\n\t\t\t\t\t\t\t\t\toutputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(maxBytes)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${maxBytes}]`;\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (truncation.truncated) {\n\t\t\t\t\t\t\t\t\t// Truncation occurred. Build an actionable continuation notice.\n\t\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\t\t\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(maxBytes)} limit). Use offset=${nextOffset} to continue.]`;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {\n\t\t\t\t\t\t\t\t\t// User-specified limit stopped early, but the file still has more content.\n\t\t\t\t\t\t\t\t\tconst remaining = allLines.length - (startLine + userLimitedLines);\n\t\t\t\t\t\t\t\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\t\t\t\t\t\t\t\t\toutputText = `${truncation.content}\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// No truncation and no remaining user-limited content.\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: outputText }];\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (aborted) return;\n\t\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\tresolve({ content, details });\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\tif (!aborted) reject(error);\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\tconst classification = !context.expanded ? getCompactReadClassification(args, context.cwd) : undefined;\n\t\t\ttext.setText(\n\t\t\t\tclassification ? formatCompactReadCall(classification, args, theme) : formatReadCall(args, theme),\n\t\t\t);\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(\n\t\t\t\tformatReadResult(context.args, result, options, theme, context.showImages, context.cwd, context.isError),\n\t\t\t);\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool<typeof readSchema> {\n\treturn wrapToolDefinition(createReadToolDefinition(cwd, options));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../../src/core/tools/read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAKjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAQ5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAKtF,OAAO,EAAoD,KAAK,gBAAgB,EAAgB,MAAM,eAAe,CAAC;AAEtH,QAAA,MAAM,UAAU;;;;EAId,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAEtD,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AA2BD;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B,qCAAqC;IACrC,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACpD,+CAA+C;IAC/C,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,sEAAsE;IACtE,mBAAmB,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACnF;AAQD,MAAM,WAAW,eAAe;IAC/B,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oEAAoE;IACpE,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,mFAAmF;IACnF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mFAAmF;IACnF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAmJD,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,CAAC,CA4LhE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAEnG","sourcesContent":["import { basename, dirname, isAbsolute, relative, resolve as resolvePath, sep } from \"node:path\";\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport type { Api, ImageContent, Model, TextContent } from \"@kolisachint/hoocode-ai\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { constants } from \"fs\";\nimport { access as fsAccess, readFile as fsReadFile } from \"fs/promises\";\nimport { type Static, Type } from \"typebox\";\nimport { getReadmePath } from \"../../config.js\";\nimport { keyHint, keyText } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { renderReadOutput } from \"../../modes/interactive/components/read-output.js\";\nimport { getLanguageFromPath, highlightCode, messageLabel, type Theme } from \"../../modes/interactive/theme/theme.js\";\nimport { formatDimensionNote, resizeImage } from \"../../utils/image-resize.js\";\nimport { detectSupportedImageMimeTypeFromFile } from \"../../utils/mime.js\";\nimport { formatPathRelativeToCwdOrAbsolute } from \"../../utils/paths.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { buildDedupPointerText, findCoveringRead, readRangeFromArgs } from \"./read-dedup.js\";\nimport { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nexport type ReadToolInput = Static<typeof readSchema>;\n\nexport interface ReadToolDetails {\n\ttruncation?: TruncationResult;\n}\n\ninterface CompactReadClassification {\n\tkind: \"docs\" | \"resource\" | \"skill\";\n\tlabel: string;\n}\n\nconst COMPACT_RESOURCE_FILE_NAMES = new Set([\"AGENTS.md\", \"AGENTS.MD\", \"CLAUDE.md\", \"CLAUDE.MD\"]);\n\n/**\n * Structured/binary document formats. These are container/binary formats (OOXML\n * is a zip, PDF is binary), so a plain utf-8 read yields garbage — read\n * short-circuits with a note instead of dumping bytes into the context.\n */\nconst DOC_FORMAT_EXTENSIONS = new Map<string, string>([\n\t[\".docx\", \"Word (OOXML)\"],\n\t[\".xlsx\", \"Excel (OOXML)\"],\n\t[\".pptx\", \"PowerPoint (OOXML)\"],\n\t[\".pdf\", \"PDF\"],\n]);\n\nfunction getDocFormatHint(absolutePath: string): string | undefined {\n\tconst dot = absolutePath.lastIndexOf(\".\");\n\tif (dot === -1) return undefined;\n\treturn DOC_FORMAT_EXTENSIONS.get(absolutePath.slice(dot).toLowerCase());\n}\n\n/**\n * Pluggable operations for the read tool.\n * Override these to delegate file reading to remote systems (for example SSH).\n */\nexport interface ReadOperations {\n\t/** Read file contents as a Buffer */\n\treadFile: (absolutePath: string) => Promise<Buffer>;\n\t/** Check if file is readable (throw if not) */\n\taccess: (absolutePath: string) => Promise<void>;\n\t/** Detect image MIME type, return null or undefined for non-images */\n\tdetectImageMimeType?: (absolutePath: string) => Promise<string | null | undefined>;\n}\n\nconst defaultReadOperations: ReadOperations = {\n\treadFile: (path) => fsReadFile(path),\n\taccess: (path) => fsAccess(path, constants.R_OK),\n\tdetectImageMimeType: detectSupportedImageMimeTypeFromFile,\n};\n\nexport interface ReadToolOptions {\n\t/** Whether to auto-resize images to 2000x2000 max. Default: true */\n\tautoResizeImages?: boolean;\n\t/** Custom operations for file reading. Default: local filesystem */\n\toperations?: ReadOperations;\n\t/** Byte cap on the returned text before truncation. Default: DEFAULT_MAX_BYTES. */\n\tmaxOutputBytes?: number;\n\t/** Line cap on the returned text before truncation. Default: DEFAULT_MAX_LINES. */\n\tmaxOutputLines?: number;\n\t/**\n\t * When true, a text read whose requested line range is already fully covered\n\t * by an earlier, still-live read in the current session short-circuits with a\n\t * pointer instead of re-fetching the file. Complements the post-hoc context\n\t * GC; gated on the same setting. Default: false.\n\t */\n\tdedupReads?: boolean;\n}\n\ntype ReadRenderArgs = { path?: string; file_path?: string; offset?: number; limit?: number };\n\nfunction formatReadLineRange(args: ReadRenderArgs | undefined, theme: Theme): string {\n\tif (args?.offset === undefined && args?.limit === undefined) return \"\";\n\tconst startLine = args.offset ?? 1;\n\tconst endLine = args.limit !== undefined ? startLine + args.limit - 1 : \"\";\n\treturn theme.fg(\"warning\", `:${startLine}${endLine ? `-${endLine}` : \"\"}`);\n}\n\nfunction formatReadCall(args: ReadRenderArgs | undefined, theme: Theme): string {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst path = rawPath !== null ? shortenPath(rawPath) : null;\n\tconst invalidArg = invalidArgText(theme);\n\tconst pathDisplay = path === null ? invalidArg : path ? theme.fg(\"accent\", path) : theme.fg(\"toolOutput\", \"...\");\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"read\"))} ${pathDisplay}${formatReadLineRange(args, theme)}`;\n}\n\nfunction trimTrailingEmptyLines(lines: string[]): string[] {\n\tlet end = lines.length;\n\twhile (end > 0 && lines[end - 1] === \"\") {\n\t\tend--;\n\t}\n\treturn lines.slice(0, end);\n}\n\nfunction getNonVisionImageNote(model: Model<Api> | undefined): string | undefined {\n\tif (!model || model.input.includes(\"image\")) {\n\t\treturn undefined;\n\t}\n\treturn \"[Current model does not support images. The image will be omitted from this request.]\";\n}\n\nfunction toPosixPath(filePath: string): string {\n\treturn filePath.split(sep).join(\"/\");\n}\n\nfunction getHooCodeDocsClassification(absolutePath: string): CompactReadClassification | undefined {\n\tconst packageRoot = dirname(getReadmePath());\n\tconst relativePath = relative(resolvePath(packageRoot), resolvePath(absolutePath));\n\tif (\n\t\trelativePath === \"\" ||\n\t\trelativePath === \"..\" ||\n\t\trelativePath.startsWith(`..${sep}`) ||\n\t\tisAbsolute(relativePath)\n\t) {\n\t\treturn undefined;\n\t}\n\n\tconst label = toPosixPath(relativePath);\n\tif (label === \"README.md\" || label.startsWith(\"docs/\") || label.startsWith(\"examples/\")) {\n\t\treturn { kind: \"docs\", label };\n\t}\n\treturn undefined;\n}\n\nfunction getCompactReadClassification(\n\targs: ReadRenderArgs | undefined,\n\tcwd: string,\n): CompactReadClassification | undefined {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tif (!rawPath) return undefined;\n\n\tconst absolutePath = resolveReadPath(rawPath, cwd);\n\tconst fileName = basename(absolutePath);\n\tif (fileName === \"SKILL.md\") {\n\t\treturn { kind: \"skill\", label: basename(dirname(absolutePath)) || fileName };\n\t}\n\n\tconst docsClassification = getHooCodeDocsClassification(absolutePath);\n\tif (docsClassification) return docsClassification;\n\n\tif (COMPACT_RESOURCE_FILE_NAMES.has(fileName)) {\n\t\treturn { kind: \"resource\", label: formatPathRelativeToCwdOrAbsolute(absolutePath, cwd) };\n\t}\n\n\treturn undefined;\n}\n\nfunction formatCompactReadCall(\n\tclassification: CompactReadClassification,\n\targs: ReadRenderArgs | undefined,\n\ttheme: Theme,\n): string {\n\tconst expandHint = theme.fg(\"dim\", ` (${keyText(\"app.tools.expand\")} to expand)`);\n\tif (classification.kind === \"skill\") {\n\t\treturn (\n\t\t\t`${messageLabel(\"skill\")} ` +\n\t\t\ttheme.fg(\"customMessageText\", classification.label) +\n\t\t\tformatReadLineRange(args, theme) +\n\t\t\texpandHint\n\t\t);\n\t}\n\n\treturn (\n\t\ttheme.fg(\"toolTitle\", theme.bold(`read ${classification.kind}`)) +\n\t\t\" \" +\n\t\ttheme.fg(\"accent\", classification.label) +\n\t\tformatReadLineRange(args, theme) +\n\t\texpandHint\n\t);\n}\n\nfunction formatReadResult(\n\targs: ReadRenderArgs | undefined,\n\tresult: { content: (TextContent | ImageContent)[]; details?: ReadToolDetails },\n\toptions: ToolRenderResultOptions,\n\ttheme: Theme,\n\tshowImages: boolean,\n\tcwd: string,\n\tisError: boolean,\n): string {\n\tif (!options.expanded && !isError && getCompactReadClassification(args, cwd)) {\n\t\treturn \"\";\n\t}\n\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst output = getTextOutput(result, showImages);\n\tconst lang = rawPath ? getLanguageFromPath(rawPath) : undefined;\n\tconst renderedLines = lang ? highlightCode(replaceTabs(output), lang) : output.split(\"\\n\");\n\tconst lines = trimTrailingEmptyLines(renderedLines);\n\tconst maxLines = options.expanded ? lines.length : 10;\n\tconst displayLines = lines.slice(0, maxLines);\n\tconst remaining = lines.length - maxLines;\n\t// Calculate starting line number from args\n\tconst startLine = (args?.offset as number) ?? 1;\n\t// Use the new renderReadOutput function for line numbers\n\tconst numberedLines = renderReadOutput(displayLines.join(\"\\n\"), { startLine });\n\tlet text = `\\n${numberedLines.join(\"\\n\")}`;\n\tif (remaining > 0) {\n\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t}\n\n\tconst truncation = result.details?.truncation;\n\tif (truncation?.truncated) {\n\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\ttext += `\\n${theme.fg(\"warning\", `[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`)}`;\n\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)]`)}`;\n\t\t} else {\n\t\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)]`)}`;\n\t\t}\n\t}\n\treturn text;\n}\n\nexport function createReadToolDefinition(\n\tcwd: string,\n\toptions?: ReadToolOptions,\n): ToolDefinition<typeof readSchema, ReadToolDetails | undefined> {\n\tconst autoResizeImages = options?.autoResizeImages ?? true;\n\tconst ops = options?.operations ?? defaultReadOperations;\n\tconst maxBytes = options?.maxOutputBytes ?? DEFAULT_MAX_BYTES;\n\tconst maxLines = options?.maxOutputLines ?? DEFAULT_MAX_LINES;\n\tconst dedupReads = options?.dedupReads ?? false;\n\treturn {\n\t\tname: \"read\",\n\t\tlabel: \"read\",\n\t\tdescription: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${maxLines} lines or ${Math.round(maxBytes / 1024)}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,\n\t\tpromptSnippet: \"Read file contents\",\n\t\t// No promptGuidelines: \"use read instead of cat/sed\" is already covered by the\n\t\t// file-exploration guideline buildSystemPrompt emits, and again by bash's own\n\t\t// snippet. Three copies of one rule.\n\t\tparameters: readSchema,\n\t\tasync execute(\n\t\t\ttoolCallId,\n\t\t\t{ path, offset, limit }: { path: string; offset?: number; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\tctx?,\n\t\t) {\n\t\t\tconst absolutePath = resolveReadPath(path, cwd);\n\t\t\treturn new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(\n\t\t\t\t(resolve, reject) => {\n\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tlet aborted = false;\n\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t};\n\t\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t\t\t(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Check if file exists and is readable.\n\t\t\t\t\t\t\tawait ops.access(absolutePath);\n\t\t\t\t\t\t\tif (aborted) return;\n\t\t\t\t\t\t\tconst mimeType = ops.detectImageMimeType ? await ops.detectImageMimeType(absolutePath) : undefined;\n\t\t\t\t\t\t\tlet content: (TextContent | ImageContent)[];\n\t\t\t\t\t\t\tlet details: ReadToolDetails | undefined;\n\t\t\t\t\t\t\tconst nonVisionImageNote = getNonVisionImageNote(ctx?.model);\n\t\t\t\t\t\t\t// At-call dedup: for plain-text reads, if an earlier still-live read\n\t\t\t\t\t\t\t// in this session already covers the requested range, point at it\n\t\t\t\t\t\t\t// instead of re-fetching. Skipped for images and structured docs.\n\t\t\t\t\t\t\tconst dedupCovering =\n\t\t\t\t\t\t\t\tdedupReads && !mimeType && !getDocFormatHint(absolutePath) && ctx?.sessionManager\n\t\t\t\t\t\t\t\t\t? findCoveringRead(ctx.sessionManager.getBranch(), {\n\t\t\t\t\t\t\t\t\t\t\tresolvedPath: absolutePath,\n\t\t\t\t\t\t\t\t\t\t\trequestedRange: readRangeFromArgs({ offset, limit }),\n\t\t\t\t\t\t\t\t\t\t\tcurrentCallId: toolCallId,\n\t\t\t\t\t\t\t\t\t\t\tresolvePath: (raw) => resolveReadPath(raw, cwd),\n\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t: null;\n\t\t\t\t\t\t\tif (dedupCovering) {\n\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: buildDedupPointerText(dedupCovering) }];\n\t\t\t\t\t\t\t} else if (mimeType) {\n\t\t\t\t\t\t\t\t// Read image as binary.\n\t\t\t\t\t\t\t\tconst buffer = await ops.readFile(absolutePath);\n\t\t\t\t\t\t\t\tconst base64 = buffer.toString(\"base64\");\n\t\t\t\t\t\t\t\tif (autoResizeImages) {\n\t\t\t\t\t\t\t\t\t// Resize image if needed before sending it back to the model.\n\t\t\t\t\t\t\t\t\tconst resized = await resizeImage({ type: \"image\", data: base64, mimeType });\n\t\t\t\t\t\t\t\t\tif (!resized) {\n\t\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${mimeType}]\\n[Image omitted: could not be resized below the inline image size limit.]`;\n\t\t\t\t\t\t\t\t\t\tif (nonVisionImageNote) textNote += `\\n${nonVisionImageNote}`;\n\t\t\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: textNote }];\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tconst dimensionNote = formatDimensionNote(resized);\n\t\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${resized.mimeType}]`;\n\t\t\t\t\t\t\t\t\t\tif (dimensionNote) textNote += `\\n${dimensionNote}`;\n\t\t\t\t\t\t\t\t\t\tif (nonVisionImageNote) textNote += `\\n${nonVisionImageNote}`;\n\t\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: textNote },\n\t\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: resized.data, mimeType: resized.mimeType },\n\t\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${mimeType}]`;\n\t\t\t\t\t\t\t\t\tif (nonVisionImageNote) textNote += `\\n${nonVisionImageNote}`;\n\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: textNote },\n\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if (getDocFormatHint(absolutePath)) {\n\t\t\t\t\t\t\t\t// Structured/binary document: a utf-8 read is meaningless, and dumping\n\t\t\t\t\t\t\t\t// the bytes would burn context for nothing. Stop here and let the model\n\t\t\t\t\t\t\t\t// choose an extraction path (bash + an appropriate converter).\n\t\t\t\t\t\t\t\tconst label = getDocFormatHint(absolutePath);\n\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: `[${label} document — not plain text; read cannot show it. Extract it with a converter via bash if you need its contents.]`,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Read text content.\n\t\t\t\t\t\t\t\tconst buffer = await ops.readFile(absolutePath);\n\t\t\t\t\t\t\t\tconst textContent = buffer.toString(\"utf-8\");\n\t\t\t\t\t\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\t\t\t\t\t\tconst totalFileLines = allLines.length;\n\t\t\t\t\t\t\t\t// Apply offset if specified. Convert from 1-indexed input to 0-indexed array access.\n\t\t\t\t\t\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\t\t\t\t\t\tconst startLineDisplay = startLine + 1;\n\t\t\t\t\t\t\t\t// Check if offset is out of bounds.\n\t\t\t\t\t\t\t\tif (startLine >= allLines.length) {\n\t\t\t\t\t\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tlet selectedContent: string;\n\t\t\t\t\t\t\t\tlet userLimitedLines: number | undefined;\n\t\t\t\t\t\t\t\t// If limit is specified by the user, honor it first. Otherwise truncateHead decides.\n\t\t\t\t\t\t\t\tif (limit !== undefined) {\n\t\t\t\t\t\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t\t\t\t\t\t} else if (startLine === 0) {\n\t\t\t\t\t\t\t\t\t// Whole-file read (common case): reuse the original string instead of\n\t\t\t\t\t\t\t\t\t// rebuilding it from the split lines.\n\t\t\t\t\t\t\t\t\tselectedContent = textContent;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Apply truncation, respecting both line and byte limits.\n\t\t\t\t\t\t\t\t// The text is handed over byte-for-byte. Whatever the model reads here is\n\t\t\t\t\t\t\t\t// what it sends back as an edit's `oldText`, so altering it (the trailing\n\t\t\t\t\t\t\t\t// whitespace strip that used to run at this point) makes the model's own\n\t\t\t\t\t\t\t\t// faithful copy fail to match the file it came from.\n\t\t\t\t\t\t\t\tconst truncation = truncateHead(selectedContent, { maxBytes, maxLines });\n\t\t\t\t\t\t\t\tlet outputText: string;\n\t\t\t\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\t\t\t\t// First line alone exceeds the byte limit. Point the model at a bash fallback.\n\t\t\t\t\t\t\t\t\tconst firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], \"utf-8\"));\n\t\t\t\t\t\t\t\t\toutputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(maxBytes)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${maxBytes}]`;\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (truncation.truncated) {\n\t\t\t\t\t\t\t\t\t// Truncation occurred. Build an actionable continuation notice.\n\t\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\t\t\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(maxBytes)} limit). Use offset=${nextOffset} to continue.]`;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {\n\t\t\t\t\t\t\t\t\t// User-specified limit stopped early, but the file still has more content.\n\t\t\t\t\t\t\t\t\tconst remaining = allLines.length - (startLine + userLimitedLines);\n\t\t\t\t\t\t\t\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\t\t\t\t\t\t\t\t\toutputText = `${truncation.content}\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// No truncation and no remaining user-limited content.\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: outputText }];\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (aborted) return;\n\t\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\tresolve({ content, details });\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\tif (!aborted) reject(error);\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\tconst classification = !context.expanded ? getCompactReadClassification(args, context.cwd) : undefined;\n\t\t\ttext.setText(\n\t\t\t\tclassification ? formatCompactReadCall(classification, args, theme) : formatReadCall(args, theme),\n\t\t\t);\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(\n\t\t\t\tformatReadResult(context.args, result, options, theme, context.showImages, context.cwd, context.isError),\n\t\t\t);\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool<typeof readSchema> {\n\treturn wrapToolDefinition(createReadToolDefinition(cwd, options));\n}\n"]}
|
package/dist/core/tools/read.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { basename, dirname, isAbsolute, relative, resolve as resolvePath, sep } from "node:path";
|
|
2
|
-
import { compressReadOutput } from "@kolisachint/hoocode-agent-core";
|
|
3
2
|
import { Text } from "@kolisachint/hoocode-tui";
|
|
4
3
|
import { constants } from "fs";
|
|
5
4
|
import { access as fsAccess, readFile as fsReadFile } from "fs/promises";
|
|
@@ -284,9 +283,11 @@ export function createReadToolDefinition(cwd, options) {
|
|
|
284
283
|
selectedContent = allLines.slice(startLine).join("\n");
|
|
285
284
|
}
|
|
286
285
|
// Apply truncation, respecting both line and byte limits.
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
286
|
+
// The text is handed over byte-for-byte. Whatever the model reads here is
|
|
287
|
+
// what it sends back as an edit's `oldText`, so altering it (the trailing
|
|
288
|
+
// whitespace strip that used to run at this point) makes the model's own
|
|
289
|
+
// faithful copy fail to match the file it came from.
|
|
290
|
+
const truncation = truncateHead(selectedContent, { maxBytes, maxLines });
|
|
290
291
|
let outputText;
|
|
291
292
|
if (truncation.firstLineExceedsLimit) {
|
|
292
293
|
// First line alone exceeds the byte limit. Point the model at a bash fallback.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read.js","sourceRoot":"","sources":["../../../src/core/tools/read.ts"],"names":[],"mappings":"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","sourcesContent":["import { basename, dirname, isAbsolute, relative, resolve as resolvePath, sep } from \"node:path\";\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport { compressReadOutput } from \"@kolisachint/hoocode-agent-core\";\nimport type { Api, ImageContent, Model, TextContent } from \"@kolisachint/hoocode-ai\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { constants } from \"fs\";\nimport { access as fsAccess, readFile as fsReadFile } from \"fs/promises\";\nimport { type Static, Type } from \"typebox\";\nimport { getReadmePath } from \"../../config.js\";\nimport { keyHint, keyText } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { renderReadOutput } from \"../../modes/interactive/components/read-output.js\";\nimport { getLanguageFromPath, highlightCode, messageLabel, type Theme } from \"../../modes/interactive/theme/theme.js\";\nimport { formatDimensionNote, resizeImage } from \"../../utils/image-resize.js\";\nimport { detectSupportedImageMimeTypeFromFile } from \"../../utils/mime.js\";\nimport { formatPathRelativeToCwdOrAbsolute } from \"../../utils/paths.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { buildDedupPointerText, findCoveringRead, readRangeFromArgs } from \"./read-dedup.js\";\nimport { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nexport type ReadToolInput = Static<typeof readSchema>;\n\nexport interface ReadToolDetails {\n\ttruncation?: TruncationResult;\n}\n\ninterface CompactReadClassification {\n\tkind: \"docs\" | \"resource\" | \"skill\";\n\tlabel: string;\n}\n\nconst COMPACT_RESOURCE_FILE_NAMES = new Set([\"AGENTS.md\", \"AGENTS.MD\", \"CLAUDE.md\", \"CLAUDE.MD\"]);\n\n/**\n * Structured/binary document formats. These are container/binary formats (OOXML\n * is a zip, PDF is binary), so a plain utf-8 read yields garbage — read\n * short-circuits with a note instead of dumping bytes into the context.\n */\nconst DOC_FORMAT_EXTENSIONS = new Map<string, string>([\n\t[\".docx\", \"Word (OOXML)\"],\n\t[\".xlsx\", \"Excel (OOXML)\"],\n\t[\".pptx\", \"PowerPoint (OOXML)\"],\n\t[\".pdf\", \"PDF\"],\n]);\n\nfunction getDocFormatHint(absolutePath: string): string | undefined {\n\tconst dot = absolutePath.lastIndexOf(\".\");\n\tif (dot === -1) return undefined;\n\treturn DOC_FORMAT_EXTENSIONS.get(absolutePath.slice(dot).toLowerCase());\n}\n\n/**\n * Pluggable operations for the read tool.\n * Override these to delegate file reading to remote systems (for example SSH).\n */\nexport interface ReadOperations {\n\t/** Read file contents as a Buffer */\n\treadFile: (absolutePath: string) => Promise<Buffer>;\n\t/** Check if file is readable (throw if not) */\n\taccess: (absolutePath: string) => Promise<void>;\n\t/** Detect image MIME type, return null or undefined for non-images */\n\tdetectImageMimeType?: (absolutePath: string) => Promise<string | null | undefined>;\n}\n\nconst defaultReadOperations: ReadOperations = {\n\treadFile: (path) => fsReadFile(path),\n\taccess: (path) => fsAccess(path, constants.R_OK),\n\tdetectImageMimeType: detectSupportedImageMimeTypeFromFile,\n};\n\nexport interface ReadToolOptions {\n\t/** Whether to auto-resize images to 2000x2000 max. Default: true */\n\tautoResizeImages?: boolean;\n\t/** Custom operations for file reading. Default: local filesystem */\n\toperations?: ReadOperations;\n\t/** Byte cap on the returned text before truncation. Default: DEFAULT_MAX_BYTES. */\n\tmaxOutputBytes?: number;\n\t/** Line cap on the returned text before truncation. Default: DEFAULT_MAX_LINES. */\n\tmaxOutputLines?: number;\n\t/**\n\t * When true, a text read whose requested line range is already fully covered\n\t * by an earlier, still-live read in the current session short-circuits with a\n\t * pointer instead of re-fetching the file. Complements the post-hoc context\n\t * GC; gated on the same setting. Default: false.\n\t */\n\tdedupReads?: boolean;\n}\n\ntype ReadRenderArgs = { path?: string; file_path?: string; offset?: number; limit?: number };\n\nfunction formatReadLineRange(args: ReadRenderArgs | undefined, theme: Theme): string {\n\tif (args?.offset === undefined && args?.limit === undefined) return \"\";\n\tconst startLine = args.offset ?? 1;\n\tconst endLine = args.limit !== undefined ? startLine + args.limit - 1 : \"\";\n\treturn theme.fg(\"warning\", `:${startLine}${endLine ? `-${endLine}` : \"\"}`);\n}\n\nfunction formatReadCall(args: ReadRenderArgs | undefined, theme: Theme): string {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst path = rawPath !== null ? shortenPath(rawPath) : null;\n\tconst invalidArg = invalidArgText(theme);\n\tconst pathDisplay = path === null ? invalidArg : path ? theme.fg(\"accent\", path) : theme.fg(\"toolOutput\", \"...\");\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"read\"))} ${pathDisplay}${formatReadLineRange(args, theme)}`;\n}\n\nfunction trimTrailingEmptyLines(lines: string[]): string[] {\n\tlet end = lines.length;\n\twhile (end > 0 && lines[end - 1] === \"\") {\n\t\tend--;\n\t}\n\treturn lines.slice(0, end);\n}\n\nfunction getNonVisionImageNote(model: Model<Api> | undefined): string | undefined {\n\tif (!model || model.input.includes(\"image\")) {\n\t\treturn undefined;\n\t}\n\treturn \"[Current model does not support images. The image will be omitted from this request.]\";\n}\n\nfunction toPosixPath(filePath: string): string {\n\treturn filePath.split(sep).join(\"/\");\n}\n\nfunction getHooCodeDocsClassification(absolutePath: string): CompactReadClassification | undefined {\n\tconst packageRoot = dirname(getReadmePath());\n\tconst relativePath = relative(resolvePath(packageRoot), resolvePath(absolutePath));\n\tif (\n\t\trelativePath === \"\" ||\n\t\trelativePath === \"..\" ||\n\t\trelativePath.startsWith(`..${sep}`) ||\n\t\tisAbsolute(relativePath)\n\t) {\n\t\treturn undefined;\n\t}\n\n\tconst label = toPosixPath(relativePath);\n\tif (label === \"README.md\" || label.startsWith(\"docs/\") || label.startsWith(\"examples/\")) {\n\t\treturn { kind: \"docs\", label };\n\t}\n\treturn undefined;\n}\n\nfunction getCompactReadClassification(\n\targs: ReadRenderArgs | undefined,\n\tcwd: string,\n): CompactReadClassification | undefined {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tif (!rawPath) return undefined;\n\n\tconst absolutePath = resolveReadPath(rawPath, cwd);\n\tconst fileName = basename(absolutePath);\n\tif (fileName === \"SKILL.md\") {\n\t\treturn { kind: \"skill\", label: basename(dirname(absolutePath)) || fileName };\n\t}\n\n\tconst docsClassification = getHooCodeDocsClassification(absolutePath);\n\tif (docsClassification) return docsClassification;\n\n\tif (COMPACT_RESOURCE_FILE_NAMES.has(fileName)) {\n\t\treturn { kind: \"resource\", label: formatPathRelativeToCwdOrAbsolute(absolutePath, cwd) };\n\t}\n\n\treturn undefined;\n}\n\nfunction formatCompactReadCall(\n\tclassification: CompactReadClassification,\n\targs: ReadRenderArgs | undefined,\n\ttheme: Theme,\n): string {\n\tconst expandHint = theme.fg(\"dim\", ` (${keyText(\"app.tools.expand\")} to expand)`);\n\tif (classification.kind === \"skill\") {\n\t\treturn (\n\t\t\t`${messageLabel(\"skill\")} ` +\n\t\t\ttheme.fg(\"customMessageText\", classification.label) +\n\t\t\tformatReadLineRange(args, theme) +\n\t\t\texpandHint\n\t\t);\n\t}\n\n\treturn (\n\t\ttheme.fg(\"toolTitle\", theme.bold(`read ${classification.kind}`)) +\n\t\t\" \" +\n\t\ttheme.fg(\"accent\", classification.label) +\n\t\tformatReadLineRange(args, theme) +\n\t\texpandHint\n\t);\n}\n\nfunction formatReadResult(\n\targs: ReadRenderArgs | undefined,\n\tresult: { content: (TextContent | ImageContent)[]; details?: ReadToolDetails },\n\toptions: ToolRenderResultOptions,\n\ttheme: Theme,\n\tshowImages: boolean,\n\tcwd: string,\n\tisError: boolean,\n): string {\n\tif (!options.expanded && !isError && getCompactReadClassification(args, cwd)) {\n\t\treturn \"\";\n\t}\n\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst output = getTextOutput(result, showImages);\n\tconst lang = rawPath ? getLanguageFromPath(rawPath) : undefined;\n\tconst renderedLines = lang ? highlightCode(replaceTabs(output), lang) : output.split(\"\\n\");\n\tconst lines = trimTrailingEmptyLines(renderedLines);\n\tconst maxLines = options.expanded ? lines.length : 10;\n\tconst displayLines = lines.slice(0, maxLines);\n\tconst remaining = lines.length - maxLines;\n\t// Calculate starting line number from args\n\tconst startLine = (args?.offset as number) ?? 1;\n\t// Use the new renderReadOutput function for line numbers\n\tconst numberedLines = renderReadOutput(displayLines.join(\"\\n\"), { startLine });\n\tlet text = `\\n${numberedLines.join(\"\\n\")}`;\n\tif (remaining > 0) {\n\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t}\n\n\tconst truncation = result.details?.truncation;\n\tif (truncation?.truncated) {\n\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\ttext += `\\n${theme.fg(\"warning\", `[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`)}`;\n\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)]`)}`;\n\t\t} else {\n\t\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)]`)}`;\n\t\t}\n\t}\n\treturn text;\n}\n\nexport function createReadToolDefinition(\n\tcwd: string,\n\toptions?: ReadToolOptions,\n): ToolDefinition<typeof readSchema, ReadToolDetails | undefined> {\n\tconst autoResizeImages = options?.autoResizeImages ?? true;\n\tconst ops = options?.operations ?? defaultReadOperations;\n\tconst maxBytes = options?.maxOutputBytes ?? DEFAULT_MAX_BYTES;\n\tconst maxLines = options?.maxOutputLines ?? DEFAULT_MAX_LINES;\n\tconst dedupReads = options?.dedupReads ?? false;\n\treturn {\n\t\tname: \"read\",\n\t\tlabel: \"read\",\n\t\tdescription: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${maxLines} lines or ${Math.round(maxBytes / 1024)}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,\n\t\tpromptSnippet: \"Read file contents\",\n\t\t// No promptGuidelines: \"use read instead of cat/sed\" is already covered by the\n\t\t// file-exploration guideline buildSystemPrompt emits, and again by bash's own\n\t\t// snippet. Three copies of one rule.\n\t\tparameters: readSchema,\n\t\tasync execute(\n\t\t\ttoolCallId,\n\t\t\t{ path, offset, limit }: { path: string; offset?: number; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\tctx?,\n\t\t) {\n\t\t\tconst absolutePath = resolveReadPath(path, cwd);\n\t\t\treturn new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(\n\t\t\t\t(resolve, reject) => {\n\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tlet aborted = false;\n\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t};\n\t\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t\t\t(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Check if file exists and is readable.\n\t\t\t\t\t\t\tawait ops.access(absolutePath);\n\t\t\t\t\t\t\tif (aborted) return;\n\t\t\t\t\t\t\tconst mimeType = ops.detectImageMimeType ? await ops.detectImageMimeType(absolutePath) : undefined;\n\t\t\t\t\t\t\tlet content: (TextContent | ImageContent)[];\n\t\t\t\t\t\t\tlet details: ReadToolDetails | undefined;\n\t\t\t\t\t\t\tconst nonVisionImageNote = getNonVisionImageNote(ctx?.model);\n\t\t\t\t\t\t\t// At-call dedup: for plain-text reads, if an earlier still-live read\n\t\t\t\t\t\t\t// in this session already covers the requested range, point at it\n\t\t\t\t\t\t\t// instead of re-fetching. Skipped for images and structured docs.\n\t\t\t\t\t\t\tconst dedupCovering =\n\t\t\t\t\t\t\t\tdedupReads && !mimeType && !getDocFormatHint(absolutePath) && ctx?.sessionManager\n\t\t\t\t\t\t\t\t\t? findCoveringRead(ctx.sessionManager.getBranch(), {\n\t\t\t\t\t\t\t\t\t\t\tresolvedPath: absolutePath,\n\t\t\t\t\t\t\t\t\t\t\trequestedRange: readRangeFromArgs({ offset, limit }),\n\t\t\t\t\t\t\t\t\t\t\tcurrentCallId: toolCallId,\n\t\t\t\t\t\t\t\t\t\t\tresolvePath: (raw) => resolveReadPath(raw, cwd),\n\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t: null;\n\t\t\t\t\t\t\tif (dedupCovering) {\n\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: buildDedupPointerText(dedupCovering) }];\n\t\t\t\t\t\t\t} else if (mimeType) {\n\t\t\t\t\t\t\t\t// Read image as binary.\n\t\t\t\t\t\t\t\tconst buffer = await ops.readFile(absolutePath);\n\t\t\t\t\t\t\t\tconst base64 = buffer.toString(\"base64\");\n\t\t\t\t\t\t\t\tif (autoResizeImages) {\n\t\t\t\t\t\t\t\t\t// Resize image if needed before sending it back to the model.\n\t\t\t\t\t\t\t\t\tconst resized = await resizeImage({ type: \"image\", data: base64, mimeType });\n\t\t\t\t\t\t\t\t\tif (!resized) {\n\t\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${mimeType}]\\n[Image omitted: could not be resized below the inline image size limit.]`;\n\t\t\t\t\t\t\t\t\t\tif (nonVisionImageNote) textNote += `\\n${nonVisionImageNote}`;\n\t\t\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: textNote }];\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tconst dimensionNote = formatDimensionNote(resized);\n\t\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${resized.mimeType}]`;\n\t\t\t\t\t\t\t\t\t\tif (dimensionNote) textNote += `\\n${dimensionNote}`;\n\t\t\t\t\t\t\t\t\t\tif (nonVisionImageNote) textNote += `\\n${nonVisionImageNote}`;\n\t\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: textNote },\n\t\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: resized.data, mimeType: resized.mimeType },\n\t\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${mimeType}]`;\n\t\t\t\t\t\t\t\t\tif (nonVisionImageNote) textNote += `\\n${nonVisionImageNote}`;\n\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: textNote },\n\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if (getDocFormatHint(absolutePath)) {\n\t\t\t\t\t\t\t\t// Structured/binary document: a utf-8 read is meaningless, and dumping\n\t\t\t\t\t\t\t\t// the bytes would burn context for nothing. Stop here and let the model\n\t\t\t\t\t\t\t\t// choose an extraction path (bash + an appropriate converter).\n\t\t\t\t\t\t\t\tconst label = getDocFormatHint(absolutePath);\n\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: `[${label} document — not plain text; read cannot show it. Extract it with a converter via bash if you need its contents.]`,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Read text content.\n\t\t\t\t\t\t\t\tconst buffer = await ops.readFile(absolutePath);\n\t\t\t\t\t\t\t\tconst textContent = buffer.toString(\"utf-8\");\n\t\t\t\t\t\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\t\t\t\t\t\tconst totalFileLines = allLines.length;\n\t\t\t\t\t\t\t\t// Apply offset if specified. Convert from 1-indexed input to 0-indexed array access.\n\t\t\t\t\t\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\t\t\t\t\t\tconst startLineDisplay = startLine + 1;\n\t\t\t\t\t\t\t\t// Check if offset is out of bounds.\n\t\t\t\t\t\t\t\tif (startLine >= allLines.length) {\n\t\t\t\t\t\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tlet selectedContent: string;\n\t\t\t\t\t\t\t\tlet userLimitedLines: number | undefined;\n\t\t\t\t\t\t\t\t// If limit is specified by the user, honor it first. Otherwise truncateHead decides.\n\t\t\t\t\t\t\t\tif (limit !== undefined) {\n\t\t\t\t\t\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t\t\t\t\t\t} else if (startLine === 0) {\n\t\t\t\t\t\t\t\t\t// Whole-file read (common case): reuse the original string instead of\n\t\t\t\t\t\t\t\t\t// rebuilding it from the split lines.\n\t\t\t\t\t\t\t\t\tselectedContent = textContent;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Apply truncation, respecting both line and byte limits.\n\t\t\t\t\t\t\t\t// Apply lossless compression then truncation.\n\t\t\t\t\t\t\t\tconst compressedContent = compressReadOutput(selectedContent);\n\t\t\t\t\t\t\t\tconst truncation = truncateHead(compressedContent, { maxBytes, maxLines });\n\t\t\t\t\t\t\t\tlet outputText: string;\n\t\t\t\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\t\t\t\t// First line alone exceeds the byte limit. Point the model at a bash fallback.\n\t\t\t\t\t\t\t\t\tconst firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], \"utf-8\"));\n\t\t\t\t\t\t\t\t\toutputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(maxBytes)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${maxBytes}]`;\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (truncation.truncated) {\n\t\t\t\t\t\t\t\t\t// Truncation occurred. Build an actionable continuation notice.\n\t\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\t\t\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(maxBytes)} limit). Use offset=${nextOffset} to continue.]`;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {\n\t\t\t\t\t\t\t\t\t// User-specified limit stopped early, but the file still has more content.\n\t\t\t\t\t\t\t\t\tconst remaining = allLines.length - (startLine + userLimitedLines);\n\t\t\t\t\t\t\t\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\t\t\t\t\t\t\t\t\toutputText = `${truncation.content}\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// No truncation and no remaining user-limited content.\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: outputText }];\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (aborted) return;\n\t\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\tresolve({ content, details });\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\tif (!aborted) reject(error);\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\tconst classification = !context.expanded ? getCompactReadClassification(args, context.cwd) : undefined;\n\t\t\ttext.setText(\n\t\t\t\tclassification ? formatCompactReadCall(classification, args, theme) : formatReadCall(args, theme),\n\t\t\t);\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(\n\t\t\t\tformatReadResult(context.args, result, options, theme, context.showImages, context.cwd, context.isError),\n\t\t\t);\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool<typeof readSchema> {\n\treturn wrapToolDefinition(createReadToolDefinition(cwd, options));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"read.js","sourceRoot":"","sources":["../../../src/core/tools/read.ts"],"names":[],"mappings":"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","sourcesContent":["import { basename, dirname, isAbsolute, relative, resolve as resolvePath, sep } from \"node:path\";\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport type { Api, ImageContent, Model, TextContent } from \"@kolisachint/hoocode-ai\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { constants } from \"fs\";\nimport { access as fsAccess, readFile as fsReadFile } from \"fs/promises\";\nimport { type Static, Type } from \"typebox\";\nimport { getReadmePath } from \"../../config.js\";\nimport { keyHint, keyText } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { renderReadOutput } from \"../../modes/interactive/components/read-output.js\";\nimport { getLanguageFromPath, highlightCode, messageLabel, type Theme } from \"../../modes/interactive/theme/theme.js\";\nimport { formatDimensionNote, resizeImage } from \"../../utils/image-resize.js\";\nimport { detectSupportedImageMimeTypeFromFile } from \"../../utils/mime.js\";\nimport { formatPathRelativeToCwdOrAbsolute } from \"../../utils/paths.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { buildDedupPointerText, findCoveringRead, readRangeFromArgs } from \"./read-dedup.js\";\nimport { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nexport type ReadToolInput = Static<typeof readSchema>;\n\nexport interface ReadToolDetails {\n\ttruncation?: TruncationResult;\n}\n\ninterface CompactReadClassification {\n\tkind: \"docs\" | \"resource\" | \"skill\";\n\tlabel: string;\n}\n\nconst COMPACT_RESOURCE_FILE_NAMES = new Set([\"AGENTS.md\", \"AGENTS.MD\", \"CLAUDE.md\", \"CLAUDE.MD\"]);\n\n/**\n * Structured/binary document formats. These are container/binary formats (OOXML\n * is a zip, PDF is binary), so a plain utf-8 read yields garbage — read\n * short-circuits with a note instead of dumping bytes into the context.\n */\nconst DOC_FORMAT_EXTENSIONS = new Map<string, string>([\n\t[\".docx\", \"Word (OOXML)\"],\n\t[\".xlsx\", \"Excel (OOXML)\"],\n\t[\".pptx\", \"PowerPoint (OOXML)\"],\n\t[\".pdf\", \"PDF\"],\n]);\n\nfunction getDocFormatHint(absolutePath: string): string | undefined {\n\tconst dot = absolutePath.lastIndexOf(\".\");\n\tif (dot === -1) return undefined;\n\treturn DOC_FORMAT_EXTENSIONS.get(absolutePath.slice(dot).toLowerCase());\n}\n\n/**\n * Pluggable operations for the read tool.\n * Override these to delegate file reading to remote systems (for example SSH).\n */\nexport interface ReadOperations {\n\t/** Read file contents as a Buffer */\n\treadFile: (absolutePath: string) => Promise<Buffer>;\n\t/** Check if file is readable (throw if not) */\n\taccess: (absolutePath: string) => Promise<void>;\n\t/** Detect image MIME type, return null or undefined for non-images */\n\tdetectImageMimeType?: (absolutePath: string) => Promise<string | null | undefined>;\n}\n\nconst defaultReadOperations: ReadOperations = {\n\treadFile: (path) => fsReadFile(path),\n\taccess: (path) => fsAccess(path, constants.R_OK),\n\tdetectImageMimeType: detectSupportedImageMimeTypeFromFile,\n};\n\nexport interface ReadToolOptions {\n\t/** Whether to auto-resize images to 2000x2000 max. Default: true */\n\tautoResizeImages?: boolean;\n\t/** Custom operations for file reading. Default: local filesystem */\n\toperations?: ReadOperations;\n\t/** Byte cap on the returned text before truncation. Default: DEFAULT_MAX_BYTES. */\n\tmaxOutputBytes?: number;\n\t/** Line cap on the returned text before truncation. Default: DEFAULT_MAX_LINES. */\n\tmaxOutputLines?: number;\n\t/**\n\t * When true, a text read whose requested line range is already fully covered\n\t * by an earlier, still-live read in the current session short-circuits with a\n\t * pointer instead of re-fetching the file. Complements the post-hoc context\n\t * GC; gated on the same setting. Default: false.\n\t */\n\tdedupReads?: boolean;\n}\n\ntype ReadRenderArgs = { path?: string; file_path?: string; offset?: number; limit?: number };\n\nfunction formatReadLineRange(args: ReadRenderArgs | undefined, theme: Theme): string {\n\tif (args?.offset === undefined && args?.limit === undefined) return \"\";\n\tconst startLine = args.offset ?? 1;\n\tconst endLine = args.limit !== undefined ? startLine + args.limit - 1 : \"\";\n\treturn theme.fg(\"warning\", `:${startLine}${endLine ? `-${endLine}` : \"\"}`);\n}\n\nfunction formatReadCall(args: ReadRenderArgs | undefined, theme: Theme): string {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst path = rawPath !== null ? shortenPath(rawPath) : null;\n\tconst invalidArg = invalidArgText(theme);\n\tconst pathDisplay = path === null ? invalidArg : path ? theme.fg(\"accent\", path) : theme.fg(\"toolOutput\", \"...\");\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"read\"))} ${pathDisplay}${formatReadLineRange(args, theme)}`;\n}\n\nfunction trimTrailingEmptyLines(lines: string[]): string[] {\n\tlet end = lines.length;\n\twhile (end > 0 && lines[end - 1] === \"\") {\n\t\tend--;\n\t}\n\treturn lines.slice(0, end);\n}\n\nfunction getNonVisionImageNote(model: Model<Api> | undefined): string | undefined {\n\tif (!model || model.input.includes(\"image\")) {\n\t\treturn undefined;\n\t}\n\treturn \"[Current model does not support images. The image will be omitted from this request.]\";\n}\n\nfunction toPosixPath(filePath: string): string {\n\treturn filePath.split(sep).join(\"/\");\n}\n\nfunction getHooCodeDocsClassification(absolutePath: string): CompactReadClassification | undefined {\n\tconst packageRoot = dirname(getReadmePath());\n\tconst relativePath = relative(resolvePath(packageRoot), resolvePath(absolutePath));\n\tif (\n\t\trelativePath === \"\" ||\n\t\trelativePath === \"..\" ||\n\t\trelativePath.startsWith(`..${sep}`) ||\n\t\tisAbsolute(relativePath)\n\t) {\n\t\treturn undefined;\n\t}\n\n\tconst label = toPosixPath(relativePath);\n\tif (label === \"README.md\" || label.startsWith(\"docs/\") || label.startsWith(\"examples/\")) {\n\t\treturn { kind: \"docs\", label };\n\t}\n\treturn undefined;\n}\n\nfunction getCompactReadClassification(\n\targs: ReadRenderArgs | undefined,\n\tcwd: string,\n): CompactReadClassification | undefined {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tif (!rawPath) return undefined;\n\n\tconst absolutePath = resolveReadPath(rawPath, cwd);\n\tconst fileName = basename(absolutePath);\n\tif (fileName === \"SKILL.md\") {\n\t\treturn { kind: \"skill\", label: basename(dirname(absolutePath)) || fileName };\n\t}\n\n\tconst docsClassification = getHooCodeDocsClassification(absolutePath);\n\tif (docsClassification) return docsClassification;\n\n\tif (COMPACT_RESOURCE_FILE_NAMES.has(fileName)) {\n\t\treturn { kind: \"resource\", label: formatPathRelativeToCwdOrAbsolute(absolutePath, cwd) };\n\t}\n\n\treturn undefined;\n}\n\nfunction formatCompactReadCall(\n\tclassification: CompactReadClassification,\n\targs: ReadRenderArgs | undefined,\n\ttheme: Theme,\n): string {\n\tconst expandHint = theme.fg(\"dim\", ` (${keyText(\"app.tools.expand\")} to expand)`);\n\tif (classification.kind === \"skill\") {\n\t\treturn (\n\t\t\t`${messageLabel(\"skill\")} ` +\n\t\t\ttheme.fg(\"customMessageText\", classification.label) +\n\t\t\tformatReadLineRange(args, theme) +\n\t\t\texpandHint\n\t\t);\n\t}\n\n\treturn (\n\t\ttheme.fg(\"toolTitle\", theme.bold(`read ${classification.kind}`)) +\n\t\t\" \" +\n\t\ttheme.fg(\"accent\", classification.label) +\n\t\tformatReadLineRange(args, theme) +\n\t\texpandHint\n\t);\n}\n\nfunction formatReadResult(\n\targs: ReadRenderArgs | undefined,\n\tresult: { content: (TextContent | ImageContent)[]; details?: ReadToolDetails },\n\toptions: ToolRenderResultOptions,\n\ttheme: Theme,\n\tshowImages: boolean,\n\tcwd: string,\n\tisError: boolean,\n): string {\n\tif (!options.expanded && !isError && getCompactReadClassification(args, cwd)) {\n\t\treturn \"\";\n\t}\n\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst output = getTextOutput(result, showImages);\n\tconst lang = rawPath ? getLanguageFromPath(rawPath) : undefined;\n\tconst renderedLines = lang ? highlightCode(replaceTabs(output), lang) : output.split(\"\\n\");\n\tconst lines = trimTrailingEmptyLines(renderedLines);\n\tconst maxLines = options.expanded ? lines.length : 10;\n\tconst displayLines = lines.slice(0, maxLines);\n\tconst remaining = lines.length - maxLines;\n\t// Calculate starting line number from args\n\tconst startLine = (args?.offset as number) ?? 1;\n\t// Use the new renderReadOutput function for line numbers\n\tconst numberedLines = renderReadOutput(displayLines.join(\"\\n\"), { startLine });\n\tlet text = `\\n${numberedLines.join(\"\\n\")}`;\n\tif (remaining > 0) {\n\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t}\n\n\tconst truncation = result.details?.truncation;\n\tif (truncation?.truncated) {\n\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\ttext += `\\n${theme.fg(\"warning\", `[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`)}`;\n\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)]`)}`;\n\t\t} else {\n\t\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)]`)}`;\n\t\t}\n\t}\n\treturn text;\n}\n\nexport function createReadToolDefinition(\n\tcwd: string,\n\toptions?: ReadToolOptions,\n): ToolDefinition<typeof readSchema, ReadToolDetails | undefined> {\n\tconst autoResizeImages = options?.autoResizeImages ?? true;\n\tconst ops = options?.operations ?? defaultReadOperations;\n\tconst maxBytes = options?.maxOutputBytes ?? DEFAULT_MAX_BYTES;\n\tconst maxLines = options?.maxOutputLines ?? DEFAULT_MAX_LINES;\n\tconst dedupReads = options?.dedupReads ?? false;\n\treturn {\n\t\tname: \"read\",\n\t\tlabel: \"read\",\n\t\tdescription: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${maxLines} lines or ${Math.round(maxBytes / 1024)}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,\n\t\tpromptSnippet: \"Read file contents\",\n\t\t// No promptGuidelines: \"use read instead of cat/sed\" is already covered by the\n\t\t// file-exploration guideline buildSystemPrompt emits, and again by bash's own\n\t\t// snippet. Three copies of one rule.\n\t\tparameters: readSchema,\n\t\tasync execute(\n\t\t\ttoolCallId,\n\t\t\t{ path, offset, limit }: { path: string; offset?: number; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\tctx?,\n\t\t) {\n\t\t\tconst absolutePath = resolveReadPath(path, cwd);\n\t\t\treturn new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(\n\t\t\t\t(resolve, reject) => {\n\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tlet aborted = false;\n\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t};\n\t\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t\t\t(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Check if file exists and is readable.\n\t\t\t\t\t\t\tawait ops.access(absolutePath);\n\t\t\t\t\t\t\tif (aborted) return;\n\t\t\t\t\t\t\tconst mimeType = ops.detectImageMimeType ? await ops.detectImageMimeType(absolutePath) : undefined;\n\t\t\t\t\t\t\tlet content: (TextContent | ImageContent)[];\n\t\t\t\t\t\t\tlet details: ReadToolDetails | undefined;\n\t\t\t\t\t\t\tconst nonVisionImageNote = getNonVisionImageNote(ctx?.model);\n\t\t\t\t\t\t\t// At-call dedup: for plain-text reads, if an earlier still-live read\n\t\t\t\t\t\t\t// in this session already covers the requested range, point at it\n\t\t\t\t\t\t\t// instead of re-fetching. Skipped for images and structured docs.\n\t\t\t\t\t\t\tconst dedupCovering =\n\t\t\t\t\t\t\t\tdedupReads && !mimeType && !getDocFormatHint(absolutePath) && ctx?.sessionManager\n\t\t\t\t\t\t\t\t\t? findCoveringRead(ctx.sessionManager.getBranch(), {\n\t\t\t\t\t\t\t\t\t\t\tresolvedPath: absolutePath,\n\t\t\t\t\t\t\t\t\t\t\trequestedRange: readRangeFromArgs({ offset, limit }),\n\t\t\t\t\t\t\t\t\t\t\tcurrentCallId: toolCallId,\n\t\t\t\t\t\t\t\t\t\t\tresolvePath: (raw) => resolveReadPath(raw, cwd),\n\t\t\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\t\t: null;\n\t\t\t\t\t\t\tif (dedupCovering) {\n\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: buildDedupPointerText(dedupCovering) }];\n\t\t\t\t\t\t\t} else if (mimeType) {\n\t\t\t\t\t\t\t\t// Read image as binary.\n\t\t\t\t\t\t\t\tconst buffer = await ops.readFile(absolutePath);\n\t\t\t\t\t\t\t\tconst base64 = buffer.toString(\"base64\");\n\t\t\t\t\t\t\t\tif (autoResizeImages) {\n\t\t\t\t\t\t\t\t\t// Resize image if needed before sending it back to the model.\n\t\t\t\t\t\t\t\t\tconst resized = await resizeImage({ type: \"image\", data: base64, mimeType });\n\t\t\t\t\t\t\t\t\tif (!resized) {\n\t\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${mimeType}]\\n[Image omitted: could not be resized below the inline image size limit.]`;\n\t\t\t\t\t\t\t\t\t\tif (nonVisionImageNote) textNote += `\\n${nonVisionImageNote}`;\n\t\t\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: textNote }];\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tconst dimensionNote = formatDimensionNote(resized);\n\t\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${resized.mimeType}]`;\n\t\t\t\t\t\t\t\t\t\tif (dimensionNote) textNote += `\\n${dimensionNote}`;\n\t\t\t\t\t\t\t\t\t\tif (nonVisionImageNote) textNote += `\\n${nonVisionImageNote}`;\n\t\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: textNote },\n\t\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: resized.data, mimeType: resized.mimeType },\n\t\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${mimeType}]`;\n\t\t\t\t\t\t\t\t\tif (nonVisionImageNote) textNote += `\\n${nonVisionImageNote}`;\n\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: textNote },\n\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else if (getDocFormatHint(absolutePath)) {\n\t\t\t\t\t\t\t\t// Structured/binary document: a utf-8 read is meaningless, and dumping\n\t\t\t\t\t\t\t\t// the bytes would burn context for nothing. Stop here and let the model\n\t\t\t\t\t\t\t\t// choose an extraction path (bash + an appropriate converter).\n\t\t\t\t\t\t\t\tconst label = getDocFormatHint(absolutePath);\n\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\t\t\ttext: `[${label} document — not plain text; read cannot show it. Extract it with a converter via bash if you need its contents.]`,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Read text content.\n\t\t\t\t\t\t\t\tconst buffer = await ops.readFile(absolutePath);\n\t\t\t\t\t\t\t\tconst textContent = buffer.toString(\"utf-8\");\n\t\t\t\t\t\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\t\t\t\t\t\tconst totalFileLines = allLines.length;\n\t\t\t\t\t\t\t\t// Apply offset if specified. Convert from 1-indexed input to 0-indexed array access.\n\t\t\t\t\t\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\t\t\t\t\t\tconst startLineDisplay = startLine + 1;\n\t\t\t\t\t\t\t\t// Check if offset is out of bounds.\n\t\t\t\t\t\t\t\tif (startLine >= allLines.length) {\n\t\t\t\t\t\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tlet selectedContent: string;\n\t\t\t\t\t\t\t\tlet userLimitedLines: number | undefined;\n\t\t\t\t\t\t\t\t// If limit is specified by the user, honor it first. Otherwise truncateHead decides.\n\t\t\t\t\t\t\t\tif (limit !== undefined) {\n\t\t\t\t\t\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t\t\t\t\t\t} else if (startLine === 0) {\n\t\t\t\t\t\t\t\t\t// Whole-file read (common case): reuse the original string instead of\n\t\t\t\t\t\t\t\t\t// rebuilding it from the split lines.\n\t\t\t\t\t\t\t\t\tselectedContent = textContent;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Apply truncation, respecting both line and byte limits.\n\t\t\t\t\t\t\t\t// The text is handed over byte-for-byte. Whatever the model reads here is\n\t\t\t\t\t\t\t\t// what it sends back as an edit's `oldText`, so altering it (the trailing\n\t\t\t\t\t\t\t\t// whitespace strip that used to run at this point) makes the model's own\n\t\t\t\t\t\t\t\t// faithful copy fail to match the file it came from.\n\t\t\t\t\t\t\t\tconst truncation = truncateHead(selectedContent, { maxBytes, maxLines });\n\t\t\t\t\t\t\t\tlet outputText: string;\n\t\t\t\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\t\t\t\t// First line alone exceeds the byte limit. Point the model at a bash fallback.\n\t\t\t\t\t\t\t\t\tconst firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], \"utf-8\"));\n\t\t\t\t\t\t\t\t\toutputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(maxBytes)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${maxBytes}]`;\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (truncation.truncated) {\n\t\t\t\t\t\t\t\t\t// Truncation occurred. Build an actionable continuation notice.\n\t\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\t\t\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(maxBytes)} limit). Use offset=${nextOffset} to continue.]`;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {\n\t\t\t\t\t\t\t\t\t// User-specified limit stopped early, but the file still has more content.\n\t\t\t\t\t\t\t\t\tconst remaining = allLines.length - (startLine + userLimitedLines);\n\t\t\t\t\t\t\t\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\t\t\t\t\t\t\t\t\toutputText = `${truncation.content}\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// No truncation and no remaining user-limited content.\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: outputText }];\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (aborted) return;\n\t\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\tresolve({ content, details });\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\tif (!aborted) reject(error);\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\tconst classification = !context.expanded ? getCompactReadClassification(args, context.cwd) : undefined;\n\t\t\ttext.setText(\n\t\t\t\tclassification ? formatCompactReadCall(classification, args, theme) : formatReadCall(args, theme),\n\t\t\t);\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(\n\t\t\t\tformatReadResult(context.args, result, options, theme, context.showImages, context.cwd, context.isError),\n\t\t\t);\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool<typeof readSchema> {\n\treturn wrapToolDefinition(createReadToolDefinition(cwd, options));\n}\n"]}
|
|
@@ -55,9 +55,15 @@ export declare class CommandExecutor {
|
|
|
55
55
|
private currentChip;
|
|
56
56
|
handleName(text: string): void;
|
|
57
57
|
/**
|
|
58
|
-
* `/color <
|
|
59
|
-
*
|
|
60
|
-
*
|
|
58
|
+
* `/color <slot>` sets the session's chip colour directly, where a slot is a
|
|
59
|
+
* number, a name (`green`), or a name's initial (`g`). Bare `/color` is
|
|
60
|
+
* handled by the caller, which opens the swatch picker instead — the names
|
|
61
|
+
* describe a hue family rather than the exact colour a theme fills the slot
|
|
62
|
+
* with, so which one reads best here is still a thing you look at.
|
|
63
|
+
*
|
|
64
|
+
* The confirmation names the slot it landed on, because the aliases fold
|
|
65
|
+
* neighbouring hues together: `/color red` answering with "magenta" is how
|
|
66
|
+
* you learn the palette has no red of its own.
|
|
61
67
|
*/
|
|
62
68
|
handleColor(text: string): boolean;
|
|
63
69
|
handleSession(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command-executor.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/command-executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Extracted command handlers from InteractiveMode.\n * All methods receive dependencies via CommandContext rather than\n * reaching back into the parent class.\n */\n\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport type { Model } from \"@kolisachint/hoocode-ai\";\nimport type { EditorComponent, MarkdownTheme, TUI } from \"@kolisachint/hoocode-tui\";\nimport { type Container, Markdown, Spacer, Text, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport { spawn, spawnSync } from \"child_process\";\nimport { getDebugLogPath, getShareViewerUrl } from \"../../config.js\";\nimport { loadAgentRegistry } from \"../../core/agent-registry.js\";\nimport type { AgentSession } from \"../../core/agent-session.js\";\nimport type { AgentSessionRuntime } from \"../../core/agent-session-runtime.js\";\nimport { ChangeDirectoryError, SessionImportFileNotFoundError } from \"../../core/agent-session-runtime.js\";\nimport type { KeybindingsManager } from \"../../core/keybindings.js\";\nimport { MissingSessionCwdError } from \"../../core/session-cwd.js\";\nimport { isSessionColorSlot, SESSION_COLOR_SLOTS } from \"../../core/session-identity.js\";\nimport type { SessionManager } from \"../../core/session-manager.js\";\nimport { getSubagentPool } from \"../../core/subagent-pool-instance.js\";\nimport type { SubagentResultFile } from \"../../core/subagent-result.js\";\nimport { getChangelogPath, parseChangelog } from \"../../utils/changelog.js\";\nimport { copyToClipboard } from \"../../utils/clipboard.js\";\nimport { BorderedLoader } from \"./components/bordered-loader.js\";\nimport { DynamicBorder } from \"./components/dynamic-border.js\";\nimport type { FooterComponent } from \"./components/footer.js\";\nimport { formatKeyText, keyDisplayText } from \"./components/keybinding-hints.js\";\nimport { renderSessionChip } from \"./components/session-chip.js\";\nimport { theme } from \"./theme/theme.js\";\n\nexport interface CommandContext {\n\t// Core dependencies\n\tsession: AgentSession;\n\tsessionManager: SessionManager;\n\truntimeHost: AgentSessionRuntime;\n\tui: TUI;\n\teditor: EditorComponent;\n\teditorContainer: Container;\n\tchatContainer: Container;\n\tstatusContainer: Container;\n\tfooter: FooterComponent;\n\tkeybindings: KeybindingsManager;\n\n\t// UI callbacks\n\tshowStatus: (message: string) => void;\n\tshowError: (message: string) => void;\n\tshowWarning: (message: string) => void;\n\tupdateEditorBorderColor: () => void;\n\trenderCurrentSessionState: () => void;\n\trebuildChatFromMessages: () => void;\n\tgetMarkdownThemeWithSettings: () => MarkdownTheme;\n\tstopLoadingAnimation: () => void;\n\n\t// Auth/model helpers\n\tfindExactModelMatch: (searchTerm: string) => Promise<Model<any> | undefined>;\n\tmaybeWarnAboutAnthropicSubscriptionAuth: (model?: Model<any>) => Promise<void>;\n\n\t// Dialog callbacks\n\tshowModelSelector: (searchTerm?: string) => void;\n\tshowExtensionConfirm: (title: string, message: string) => Promise<boolean>;\n\tpromptForMissingSessionCwd: (error: MissingSessionCwdError) => Promise<string | undefined>;\n\n\t// Fatal error handler\n\thandleFatalRuntimeError: (prefix: string, error: unknown) => Promise<never>;\n}\n\nexport class CommandExecutor {\n\t/** Where `/cd -` returns to. Set on every successful move, session-local. */\n\tprivate previousCwd?: string;\n\n\tconstructor(private readonly ctx: CommandContext) {}\n\n\t// =========================================================================\n\t// Slash command handlers\n\t// =========================================================================\n\n\tasync handleModel(searchTerm?: string): Promise<void> {\n\t\tif (!searchTerm) {\n\t\t\tthis.ctx.showModelSelector();\n\t\t\treturn;\n\t\t}\n\n\t\tconst model = await this.ctx.findExactModelMatch(searchTerm);\n\t\tif (model) {\n\t\t\ttry {\n\t\t\t\tawait this.ctx.session.setModel(model);\n\t\t\t\tthis.ctx.footer.invalidate();\n\t\t\t\tthis.ctx.updateEditorBorderColor();\n\t\t\t\tthis.ctx.showStatus(`Model: ${model.id}`);\n\t\t\t\tvoid this.ctx.maybeWarnAboutAnthropicSubscriptionAuth(model);\n\t\t\t} catch (error) {\n\t\t\t\tthis.ctx.showError(error instanceof Error ? error.message : String(error));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tthis.ctx.showModelSelector(searchTerm);\n\t}\n\n\tasync handleClone(): Promise<void> {\n\t\tconst leafId = this.ctx.sessionManager.getLeafId();\n\t\tif (!leafId) {\n\t\t\tthis.ctx.showStatus(\"Nothing to clone yet\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await this.ctx.runtimeHost.fork(leafId, { position: \"at\" });\n\t\t\tif (result.cancelled) {\n\t\t\t\tthis.ctx.ui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.ctx.renderCurrentSessionState();\n\t\t\tthis.ctx.editor.setText(\"\");\n\t\t\tthis.ctx.showStatus(\"Cloned to new session\");\n\t\t} catch (error: unknown) {\n\t\t\tthis.ctx.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tasync handleSubagent(text: string): Promise<void> {\n\t\tconst prefix = \"/subagent \";\n\t\tconst args = text.startsWith(prefix) ? text.slice(prefix.length).trim() : \"\";\n\t\tif (!args) {\n\t\t\tthis.ctx.showStatus(\"Usage: /subagent <mode> <task>\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst firstSpace = args.indexOf(\" \");\n\t\tif (firstSpace === -1) {\n\t\t\tthis.ctx.showStatus(\"Usage: /subagent <mode> <task>\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst mode = args.slice(0, firstSpace).trim();\n\t\tconst task = args.slice(firstSpace + 1).trim();\n\t\tif (!task) {\n\t\t\tthis.ctx.showStatus(\"Usage: /subagent <mode> <task>\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst validModes = loadAgentRegistry({ cwd: this.ctx.sessionManager.getCwd() })\n\t\t\t.list()\n\t\t\t.map((a) => a.name);\n\t\tif (!validModes.includes(mode)) {\n\t\t\tthis.ctx.showStatus(`Unknown subagent_type: ${mode}. Available: ${validModes.join(\", \")}`);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.ctx.showStatus(`Spawning ${mode} subagent...`);\n\t\ttry {\n\t\t\tconst pool = getSubagentPool(this.ctx.sessionManager.getCwd(), this.ctx.session.modelRegistry.getAvailable());\n\t\t\tconst dispatchResult = await pool.dispatch(task, {\n\t\t\t\tforceAgent: mode,\n\t\t\t\tmodel: this.ctx.session.model?.id,\n\t\t\t\tprovider: this.ctx.session.model?.provider,\n\t\t\t});\n\t\t\tconst result = dispatchResult.result;\n\t\t\tconst resultData = result?.result_data as SubagentResultFile | undefined;\n\t\t\tif (result?.ok) {\n\t\t\t\tthis.ctx.showStatus(`${mode} subagent completed`);\n\t\t\t\t// Inject the subagent answer as a custom message so the user can see it in the chat\n\t\t\t\tthis.ctx.sessionManager.appendMessage({\n\t\t\t\t\trole: \"custom\",\n\t\t\t\t\tcustomType: \"subagent\",\n\t\t\t\t\tcontent: resultData?.summary || \"(no output)\",\n\t\t\t\t\tdisplay: true,\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tthis.ctx.showError(`Subagent (${mode}) failed: ${result?.error ?? \"unknown error\"}`);\n\t\t\t}\n\t\t} catch (error: unknown) {\n\t\t\tthis.ctx.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tasync handleExport(text: string): Promise<void> {\n\t\tconst outputPath = this.getPathArgument(text, \"/export\");\n\n\t\ttry {\n\t\t\tif (outputPath?.endsWith(\".jsonl\")) {\n\t\t\t\tconst filePath = this.ctx.session.exportToJsonl(outputPath);\n\t\t\t\tthis.ctx.showStatus(`Session exported to: ${filePath}`);\n\t\t\t} else {\n\t\t\t\tconst filePath = await this.ctx.session.exportToHtml(outputPath);\n\t\t\t\tthis.ctx.showStatus(`Session exported to: ${filePath}`);\n\t\t\t}\n\t\t} catch (error: unknown) {\n\t\t\tthis.ctx.showError(`Failed to export session: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n\t\t}\n\t}\n\n\tprivate getPathArgument(text: string, command: \"/export\" | \"/import\"): string | undefined {\n\t\tif (text === command) {\n\t\t\treturn undefined;\n\t\t}\n\t\tif (!text.startsWith(`${command} `)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst argsString = text.slice(command.length + 1).trimStart();\n\t\tif (!argsString) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst firstChar = argsString[0];\n\t\tif (firstChar === '\"' || firstChar === \"'\") {\n\t\t\tconst closingQuoteIndex = argsString.indexOf(firstChar, 1);\n\t\t\tif (closingQuoteIndex < 0) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\treturn argsString.slice(1, closingQuoteIndex);\n\t\t}\n\n\t\tconst firstWhitespaceIndex = argsString.search(/\\s/);\n\t\tif (firstWhitespaceIndex < 0) {\n\t\t\treturn argsString;\n\t\t}\n\t\treturn argsString.slice(0, firstWhitespaceIndex);\n\t}\n\n\tasync handleImport(text: string): Promise<void> {\n\t\tconst inputPath = this.getPathArgument(text, \"/import\");\n\t\tif (!inputPath) {\n\t\t\tthis.ctx.showError(\"Usage: /import <path.jsonl>\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst confirmed = await this.ctx.showExtensionConfirm(\n\t\t\t\"Import session\",\n\t\t\t`Replace current session with ${inputPath}?`,\n\t\t);\n\t\tif (!confirmed) {\n\t\t\tthis.ctx.showStatus(\"Import cancelled\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.ctx.stopLoadingAnimation();\n\t\t\tthis.ctx.statusContainer.clear();\n\t\t\tconst result = await this.ctx.runtimeHost.importFromJsonl(inputPath);\n\t\t\tif (result.cancelled) {\n\t\t\t\tthis.ctx.showStatus(\"Import cancelled\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.ctx.renderCurrentSessionState();\n\t\t\tthis.ctx.showStatus(`Session imported from: ${inputPath}`);\n\t\t} catch (error: unknown) {\n\t\t\tif (error instanceof MissingSessionCwdError) {\n\t\t\t\tconst selectedCwd = await this.ctx.promptForMissingSessionCwd(error);\n\t\t\t\tif (!selectedCwd) {\n\t\t\t\t\tthis.ctx.showStatus(\"Import cancelled\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst result = await this.ctx.runtimeHost.importFromJsonl(inputPath, selectedCwd);\n\t\t\t\tif (result.cancelled) {\n\t\t\t\t\tthis.ctx.showStatus(\"Import cancelled\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.ctx.renderCurrentSessionState();\n\t\t\t\tthis.ctx.showStatus(`Session imported from: ${inputPath}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (error instanceof SessionImportFileNotFoundError) {\n\t\t\t\tthis.ctx.showError(`Failed to import session: ${error.message}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait this.ctx.handleFatalRuntimeError(\"Failed to import session\", error);\n\t\t}\n\t}\n\n\tasync handleShare(): Promise<void> {\n\t\t// Check if gh is available and logged in\n\t\ttry {\n\t\t\tconst authResult = spawnSync(\"gh\", [\"auth\", \"status\"], { encoding: \"utf-8\" });\n\t\t\tif (authResult.status !== 0) {\n\t\t\t\tthis.ctx.showError(\"GitHub CLI is not logged in. Run 'gh auth login' first.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch {\n\t\t\tthis.ctx.showError(\"GitHub CLI (gh) is not installed. Install it from https://cli.github.com/\");\n\t\t\treturn;\n\t\t}\n\n\t\t// Export to a temp file\n\t\tconst tmpFile = path.join(os.tmpdir(), \"session.html\");\n\t\ttry {\n\t\t\tawait this.ctx.session.exportToHtml(tmpFile);\n\t\t} catch (error: unknown) {\n\t\t\tthis.ctx.showError(`Failed to export session: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n\t\t\treturn;\n\t\t}\n\n\t\t// Show cancellable loader, replacing the editor\n\t\tconst loader = new BorderedLoader(this.ctx.ui, theme, \"Creating gist...\");\n\t\tthis.ctx.editorContainer.clear();\n\t\tthis.ctx.editorContainer.addChild(loader);\n\t\tthis.ctx.ui.setFocus(loader);\n\t\tthis.ctx.ui.requestRender();\n\n\t\tconst restoreEditor = () => {\n\t\t\tloader.dispose();\n\t\t\tthis.ctx.editorContainer.clear();\n\t\t\tthis.ctx.editorContainer.addChild(this.ctx.editor);\n\t\t\tthis.ctx.ui.setFocus(this.ctx.editor);\n\t\t\ttry {\n\t\t\t\tfs.unlinkSync(tmpFile);\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t};\n\n\t\t// Create a secret gist asynchronously\n\t\tlet proc: ReturnType<typeof spawn> | null = null;\n\n\t\tloader.onAbort = () => {\n\t\t\tproc?.kill();\n\t\t\trestoreEditor();\n\t\t\tthis.ctx.showStatus(\"Share cancelled\");\n\t\t};\n\n\t\ttry {\n\t\t\tconst result = await new Promise<{ stdout: string; stderr: string; code: number | null }>((resolve) => {\n\t\t\t\tproc = spawn(\"gh\", [\"gist\", \"create\", \"--public=false\", tmpFile]);\n\t\t\t\tlet stdout = \"\";\n\t\t\t\tlet stderr = \"\";\n\t\t\t\tproc.stdout?.on(\"data\", (data) => {\n\t\t\t\t\tstdout += data.toString();\n\t\t\t\t});\n\t\t\t\tproc.stderr?.on(\"data\", (data) => {\n\t\t\t\t\tstderr += data.toString();\n\t\t\t\t});\n\t\t\t\tproc.on(\"close\", (code) => resolve({ stdout, stderr, code }));\n\t\t\t});\n\n\t\t\tif (loader.signal.aborted) return;\n\n\t\t\trestoreEditor();\n\n\t\t\tif (result.code !== 0) {\n\t\t\t\tconst errorMsg = result.stderr?.trim() || \"Unknown error\";\n\t\t\t\tthis.ctx.showError(`Failed to create gist: ${errorMsg}`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Extract gist ID from the URL returned by gh\n\t\t\t// gh returns something like: https://gist.github.com/username/GIST_ID\n\t\t\tconst gistUrl = result.stdout?.trim();\n\t\t\tconst gistId = gistUrl?.split(\"/\").pop();\n\t\t\tif (!gistId) {\n\t\t\t\tthis.ctx.showError(\"Failed to parse gist ID from gh output\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Create the preview URL\n\t\t\tconst previewUrl = getShareViewerUrl(gistId);\n\t\t\tthis.ctx.showStatus(`Share URL: ${previewUrl}\\nGist: ${gistUrl}`);\n\t\t} catch (error: unknown) {\n\t\t\tif (!loader.signal.aborted) {\n\t\t\t\trestoreEditor();\n\t\t\t\tthis.ctx.showError(`Failed to create gist: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n\t\t\t}\n\t\t}\n\t}\n\n\tasync handleCopy(): Promise<void> {\n\t\tconst text = this.ctx.session.getLastAssistantText();\n\t\tif (!text) {\n\t\t\tthis.ctx.showError(\"No agent messages to copy yet.\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tawait copyToClipboard(text);\n\t\t\tthis.ctx.showStatus(\"Copied last agent message to clipboard\");\n\t\t} catch (error) {\n\t\t\tthis.ctx.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\t/** The session's chip as it currently renders, for echoing back after a change. */\n\tprivate currentChip(): string {\n\t\tconst chip = renderSessionChip(\n\t\t\tthis.ctx.sessionManager.getDisplayName(),\n\t\t\tthis.ctx.sessionManager.getSessionColorSlot(),\n\t\t);\n\t\treturn chip?.styled ?? this.ctx.sessionManager.getDisplayName();\n\t}\n\n\thandleName(text: string): void {\n\t\tconst name = text.replace(/^\\/name\\s*/, \"\").trim();\n\t\tif (!name) {\n\t\t\t// Every session has a name now — an auto-assigned slug until someone\n\t\t\t// picks one — so there is always something to report, and the reply\n\t\t\t// shows the chip rather than describing it.\n\t\t\tconst chosen = this.ctx.sessionManager.getSessionName();\n\t\t\tconst label = chosen ? \"Session name:\" : \"Session name (auto):\";\n\t\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.ctx.chatContainer.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\t`${theme.fg(\"dim\", label)} ${this.currentChip()} ${theme.fg(\"dim\", \"/name <name> to change\")}`,\n\t\t\t\t\t1,\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\t);\n\t\t\tthis.ctx.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.ctx.session.setSessionName(name);\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(new Text(`${theme.fg(\"dim\", \"Session name set:\")} ${this.currentChip()}`, 1, 0));\n\t\tthis.ctx.ui.requestRender();\n\t}\n\n\t/**\n\t * `/color <1-6>` sets the session's chip colour directly. Bare `/color` is\n\t * handled by the caller, which opens the swatch picker instead — the slots\n\t * have no names worth typing, so seeing them is the point.\n\t */\n\thandleColor(text: string): boolean {\n\t\tconst arg = text.replace(/^\\/color\\s*/, \"\").trim();\n\t\tif (!arg) return false;\n\t\tconst slot = Number(arg);\n\t\tif (!isSessionColorSlot(slot)) {\n\t\t\tthis.ctx.showWarning(`Usage: /color <1-${SESSION_COLOR_SLOTS}>, or /color on its own to pick one`);\n\t\t\treturn true;\n\t\t}\n\t\tthis.ctx.session.setSessionColor(slot);\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(new Text(`${theme.fg(\"dim\", \"Session color set:\")} ${this.currentChip()}`, 1, 0));\n\t\tthis.ctx.ui.requestRender();\n\t\treturn true;\n\t}\n\n\thandleSession(): void {\n\t\tconst stats = this.ctx.session.getSessionStats();\n\t\tconst sessionName = this.ctx.sessionManager.getSessionName();\n\n\t\tlet info = `${theme.bold(\"Session Info\")}\\n\\n`;\n\t\tinfo += `${theme.fg(\"dim\", sessionName ? \"Name:\" : \"Name (auto):\")} ${this.currentChip()}\\n`;\n\t\tconst branch = this.ctx.sessionManager.getSessionBranch();\n\t\tif (branch) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"Branch:\")} ${branch}\\n`;\n\t\t}\n\t\tinfo += `${theme.fg(\"dim\", \"File:\")} ${stats.sessionFile ?? \"In-memory\"}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"ID:\")} ${stats.sessionId}\\n\\n`;\n\t\tinfo += `${theme.bold(\"Messages\")}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"User:\")} ${stats.userMessages}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Assistant:\")} ${stats.assistantMessages}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Tool Calls:\")} ${stats.toolCalls}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Tool Results:\")} ${stats.toolResults}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${stats.totalMessages}\\n\\n`;\n\t\tinfo += `${theme.bold(\"Tokens\")}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Input:\")} ${stats.tokens.input.toLocaleString()}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Output:\")} ${stats.tokens.output.toLocaleString()}\\n`;\n\t\tif (stats.tokens.cacheRead > 0) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"Cache Read:\")} ${stats.tokens.cacheRead.toLocaleString()}\\n`;\n\t\t}\n\t\tif (stats.tokens.cacheWrite > 0) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"Cache Write:\")} ${stats.tokens.cacheWrite.toLocaleString()}\\n`;\n\t\t}\n\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${stats.tokens.total.toLocaleString()}\\n`;\n\n\t\tif (stats.cost > 0) {\n\t\t\tinfo += `\\n${theme.bold(\"Cost\")}\\n`;\n\t\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${stats.cost.toFixed(4)}`;\n\t\t}\n\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(new Text(info, 1, 0));\n\t\tthis.ctx.ui.requestRender();\n\t}\n\n\thandleChangelog(): void {\n\t\tconst changelogPath = getChangelogPath();\n\t\tconst allEntries = parseChangelog(changelogPath);\n\n\t\tif (allEntries.length === 0) {\n\t\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.ctx.chatContainer.addChild(new Text(theme.fg(\"dim\", \"No changelog entries found.\"), 1, 0));\n\t\t\tthis.ctx.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tconst changelogMarkdown = allEntries\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.map((e) => e.content)\n\t\t\t.join(\"\\n\\n\");\n\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg(\"accent\", \"What's New\")), 1, 0));\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, this.ctx.getMarkdownThemeWithSettings()));\n\t\tthis.ctx.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ctx.ui.requestRender();\n\t}\n\n\thandleHotkeys(): void {\n\t\t// Navigation keybindings\n\t\tconst cursorUp = keyDisplayText(\"tui.editor.cursorUp\");\n\t\tconst cursorDown = keyDisplayText(\"tui.editor.cursorDown\");\n\t\tconst cursorLeft = keyDisplayText(\"tui.editor.cursorLeft\");\n\t\tconst cursorRight = keyDisplayText(\"tui.editor.cursorRight\");\n\t\tconst cursorWordLeft = keyDisplayText(\"tui.editor.cursorWordLeft\");\n\t\tconst cursorWordRight = keyDisplayText(\"tui.editor.cursorWordRight\");\n\t\tconst cursorLineStart = keyDisplayText(\"tui.editor.cursorLineStart\");\n\t\tconst cursorLineEnd = keyDisplayText(\"tui.editor.cursorLineEnd\");\n\t\tconst jumpForward = keyDisplayText(\"tui.editor.jumpForward\");\n\t\tconst jumpBackward = keyDisplayText(\"tui.editor.jumpBackward\");\n\t\tconst pageUp = keyDisplayText(\"tui.editor.pageUp\");\n\t\tconst pageDown = keyDisplayText(\"tui.editor.pageDown\");\n\n\t\t// Editing keybindings\n\t\tconst submit = keyDisplayText(\"tui.input.submit\");\n\t\tconst newLine = keyDisplayText(\"tui.input.newLine\");\n\t\tconst deleteWordBackward = keyDisplayText(\"tui.editor.deleteWordBackward\");\n\t\tconst deleteWordForward = keyDisplayText(\"tui.editor.deleteWordForward\");\n\t\tconst deleteToLineStart = keyDisplayText(\"tui.editor.deleteToLineStart\");\n\t\tconst deleteToLineEnd = keyDisplayText(\"tui.editor.deleteToLineEnd\");\n\t\tconst yank = keyDisplayText(\"tui.editor.yank\");\n\t\tconst yankPop = keyDisplayText(\"tui.editor.yankPop\");\n\t\tconst undo = keyDisplayText(\"tui.editor.undo\");\n\t\tconst tab = keyDisplayText(\"tui.input.tab\");\n\n\t\t// App keybindings\n\t\tconst interrupt = keyDisplayText(\"app.interrupt\");\n\t\tconst clear = keyDisplayText(\"app.clear\");\n\t\tconst exit = keyDisplayText(\"app.exit\");\n\t\tconst suspend = keyDisplayText(\"app.suspend\");\n\t\tconst cycleThinkingLevel = keyDisplayText(\"app.thinking.cycle\");\n\t\tconst cycleModelForward = keyDisplayText(\"app.model.cycleForward\");\n\t\tconst selectModel = keyDisplayText(\"app.model.select\");\n\t\tconst expandTools = keyDisplayText(\"app.tools.expand\");\n\t\tconst toggleThinking = keyDisplayText(\"app.thinking.toggle\");\n\t\tconst cycleTaskView = keyDisplayText(\"app.tasks.cycleView\");\n\t\tconst externalEditor = keyDisplayText(\"app.editor.external\");\n\t\tconst cycleModelBackward = keyDisplayText(\"app.model.cycleBackward\");\n\t\tconst followUp = keyDisplayText(\"app.message.followUp\");\n\t\tconst dequeue = keyDisplayText(\"app.message.dequeue\");\n\t\tconst pasteImage = keyDisplayText(\"app.clipboard.pasteImage\");\n\t\tconst viewForward = keyDisplayText(\"app.view.cycleForward\");\n\t\tconst unfoldOne = keyDisplayText(\"app.tools.unfoldOne\");\n\t\tconst foldOne = keyDisplayText(\"app.tools.foldOne\");\n\t\tconst viewBackward = keyDisplayText(\"app.view.cycleBackward\");\n\t\tconst voice = keyDisplayText(\"app.input.voiceTranscribe\");\n\t\tconst changeDirectory = keyDisplayText(\"app.session.changeDirectory\");\n\t\tconst openSettings = keyDisplayText(\"app.settings.open\");\n\t\tconst openHotkeys = keyDisplayText(\"app.hotkeys.open\");\n\t\tconst cycleMode = keyDisplayText(\"app.mode.cycle\");\n\t\tconst sessionTree = keyDisplayText(\"app.session.tree\");\n\t\tconst sessionResume = keyDisplayText(\"app.session.resume\");\n\n\t\tlet hotkeys = `\n**Navigation**\n| Key | Action |\n|-----|--------|\n| \\`${cursorUp}\\` / \\`${cursorDown}\\` / \\`${cursorLeft}\\` / \\`${cursorRight}\\` | Move cursor / browse history (Up when empty) |\n| \\`${cursorWordLeft}\\` / \\`${cursorWordRight}\\` | Move by word |\n| \\`${cursorLineStart}\\` | Start of line |\n| \\`${cursorLineEnd}\\` | End of line |\n| \\`${jumpForward}\\` | Jump forward to character |\n| \\`${jumpBackward}\\` | Jump backward to character |\n| \\`${pageUp}\\` / \\`${pageDown}\\` | Scroll by page |\n\n**Editing**\n| Key | Action |\n|-----|--------|\n| \\`${submit}\\` | Send message |\n| \\`${newLine}\\` | New line${process.platform === \"win32\" ? \" (Ctrl+Enter on Windows Terminal)\" : \"\"} |\n| \\`${deleteWordBackward}\\` | Delete word backwards |\n| \\`${deleteWordForward}\\` | Delete word forwards |\n| \\`${deleteToLineStart}\\` | Delete to start of line |\n| \\`${deleteToLineEnd}\\` | Delete to end of line |\n| \\`${yank}\\` | Paste the most-recently-deleted text |\n| \\`${yankPop}\\` | Cycle through the deleted text after pasting |\n| \\`${undo}\\` | Undo |\n\n**Flow** — the keys you hit without thinking\n| Key | Action |\n|-----|--------|\n| \\`${tab}\\` | Path completion / accept autocomplete |\n| \\`${interrupt}\\` | Cancel autocomplete / abort streaming |\n| \\`${clear}\\` | Clear editor (first) / exit (second) |\n| \\`${exit}\\` | Exit (when editor is empty) |\n| \\`${suspend}\\` | Suspend to background |\n| \\`${followUp}\\` | Queue follow-up message |\n| \\`${dequeue}\\` | Restore queued messages |\n| \\`${pasteImage}\\` | Paste image from clipboard |\n| \\`/\\` | Slash commands |\n| \\`!\\` | Run bash command |\n| \\`!!\\` | Run bash command (excluded from context) |\n\n**View** — what is on screen right now (\\`Ctrl\\`)\n| Key | Action |\n|-----|--------|\n| \\`${expandTools}\\` | Expand or collapse every tool block at once |\n| \\`${unfoldOne}\\` / \\`${foldOne}\\` | Open / re-fold one thing, newest first — a chain in radar, a tool body otherwise |\n| \\`${viewForward}\\` / \\`${viewBackward}\\` | Cycle tool output: radar → glance → full |\n| \\`${toggleThinking}\\` | Toggle thinking block visibility |\n| \\`${cycleTaskView}\\` | Cycle task panel view (tasks → subagents → teams) |\n| \\`${cycleModelForward}\\` / \\`${cycleModelBackward}\\` | Cycle models |\n| \\`${cycleThinkingLevel}\\` | Cycle thinking level |\n\n**Cockpit** — what the agent is, and where it works (\\`Alt\\`)\n| Key | Action |\n|-----|--------|\n| \\`${cycleMode}\\` | Cycle agent mode (ask → plan → build → debug) |\n| \\`${selectModel}\\` | Open model selector |\n| \\`${changeDirectory}\\` | Change working directory (\\`/cd\\`) |\n| \\`${sessionTree}\\` | Open session tree |\n| \\`${sessionResume}\\` | Resume a session from history |\n| \\`${openSettings}\\` | Open settings |\n| \\`${openHotkeys}\\` | Show this list |\n| \\`${externalEditor}\\` | Edit message in external editor |\n| \\`${voice}\\` | Record voice and transcribe |\n\nInside a picker, every \\`Ctrl\\` key still edits the query — the picker's own\nverbs are on \\`Alt\\`, and its hint line names them.\n`;\n\n\t\t// Add extension-registered shortcuts\n\t\tconst extensionRunner = this.ctx.session.extensionRunner;\n\t\tconst shortcuts = extensionRunner.getShortcuts(this.ctx.keybindings.getEffectiveConfig());\n\t\tif (shortcuts.size > 0) {\n\t\t\thotkeys += `\n**Extensions**\n| Key | Action |\n|-----|--------|\n`;\n\t\t\tfor (const [key, shortcut] of shortcuts) {\n\t\t\t\tconst description = shortcut.description ?? shortcut.extensionPath;\n\t\t\t\tconst keyDisplay = formatKeyText(key, { capitalize: true });\n\t\t\t\thotkeys += `| \\`${keyDisplay}\\` | ${description} |\\n`;\n\t\t\t}\n\t\t}\n\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg(\"accent\", \"Keyboard Shortcuts\")), 1, 0));\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(new Markdown(hotkeys.trim(), 1, 1, this.ctx.getMarkdownThemeWithSettings()));\n\t\tthis.ctx.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ctx.ui.requestRender();\n\t}\n\n\tasync handleClear(): Promise<void> {\n\t\tthis.ctx.stopLoadingAnimation();\n\t\tthis.ctx.statusContainer.clear();\n\t\ttry {\n\t\t\tconst result = await this.ctx.runtimeHost.newSession();\n\t\t\tif (result.cancelled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.ctx.renderCurrentSessionState();\n\t\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.ctx.chatContainer.addChild(new Text(`${theme.fg(\"accent\", \"✓ New session started\")}`, 1, 1));\n\t\t\tthis.ctx.ui.requestRender();\n\t\t} catch (error: unknown) {\n\t\t\tawait this.ctx.handleFatalRuntimeError(\"Failed to create session\", error);\n\t\t}\n\t}\n\n\t/**\n\t * `/cd [path]` — move the whole session to another directory.\n\t *\n\t * Written for the case it exists to serve: you are done in one repo and want\n\t * to work in the next one without losing the process, its provider auth, or\n\t * its warmed model list. Bare `/cd` goes home, `/cd -` goes back to where you\n\t * came from, `~` and relative paths resolve as a shell would.\n\t *\n\t * The move starts a fresh session rooted in the target — see\n\t * `AgentSessionRuntime.changeDirectory` for why the whole runtime is rebuilt\n\t * rather than a cwd string reassigned.\n\t */\n\tasync handleChangeDirectory(text: string): Promise<void> {\n\t\tconst prefix = \"/cd\";\n\t\tconst rawArg = text.startsWith(prefix) ? text.slice(prefix.length).trim() : text.trim();\n\t\tconst previousCwd = this.ctx.sessionManager.getCwd();\n\n\t\tlet target: string;\n\t\tif (!rawArg || rawArg === \"~\") {\n\t\t\ttarget = os.homedir();\n\t\t} else if (rawArg === \"-\") {\n\t\t\tif (!this.previousCwd) {\n\t\t\t\tthis.ctx.showWarning(\"No previous directory to return to\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttarget = this.previousCwd;\n\t\t} else {\n\t\t\tconst expanded = rawArg.startsWith(\"~/\") ? path.join(os.homedir(), rawArg.slice(2)) : rawArg;\n\t\t\ttarget = path.resolve(previousCwd, expanded);\n\t\t}\n\n\t\tif (path.resolve(target) === path.resolve(previousCwd)) {\n\t\t\tthis.ctx.showStatus(`Already in ${target}`);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.ctx.stopLoadingAnimation();\n\t\tthis.ctx.statusContainer.clear();\n\t\ttry {\n\t\t\tconst result = await this.ctx.runtimeHost.changeDirectory(target);\n\t\t\tif (result.cancelled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.previousCwd = previousCwd;\n\t\t\tthis.ctx.renderCurrentSessionState();\n\t\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.ctx.chatContainer.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\t`${theme.fg(\"accent\", \"✓ Working directory\")} ${theme.fg(\"muted\", result.cwd)}\\n` +\n\t\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\t\"dim\",\n\t\t\t\t\t\t\t`New session started here. ${keyDisplayText(\"app.session.resume\")} reopens the session you left in ${previousCwd}.`,\n\t\t\t\t\t\t),\n\t\t\t\t\t1,\n\t\t\t\t\t1,\n\t\t\t\t),\n\t\t\t);\n\t\t\tthis.ctx.ui.requestRender();\n\t\t} catch (error: unknown) {\n\t\t\tif (error instanceof ChangeDirectoryError) {\n\t\t\t\tthis.ctx.showError(error.message);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait this.ctx.handleFatalRuntimeError(`Failed to change directory to ${target}`, error);\n\t\t}\n\t}\n\n\t/** Directory completions for `/cd <prefix>`, plus the two shell shorthands. */\n\tgetChangeDirectoryCompletions(argumentPrefix: string): Array<{ value: string; label: string }> {\n\t\tconst cwd = this.ctx.sessionManager.getCwd();\n\t\tconst expanded = argumentPrefix.startsWith(\"~/\")\n\t\t\t? path.join(os.homedir(), argumentPrefix.slice(2))\n\t\t\t: argumentPrefix;\n\t\t// A prefix ending in a separator names the directory to list; otherwise the\n\t\t// last segment is a partial name to filter its parent by.\n\t\tconst endsWithSep = expanded.endsWith(\"/\") || expanded.endsWith(path.sep);\n\t\tconst base = endsWithSep ? expanded : path.dirname(expanded);\n\t\tconst partial = endsWithSep ? \"\" : path.basename(expanded);\n\t\tconst searchDir = path.isAbsolute(base) ? base : path.resolve(cwd, base || \".\");\n\n\t\tconst completions: Array<{ value: string; label: string }> = [];\n\t\tif (!argumentPrefix) {\n\t\t\tif (this.previousCwd) completions.push({ value: \"-\", label: `- (${this.previousCwd})` });\n\t\t\tcompletions.push({ value: \"~\", label: `~ (${os.homedir()})` });\n\t\t}\n\n\t\tlet entries: fs.Dirent[];\n\t\ttry {\n\t\t\tentries = fs.readdirSync(searchDir, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn completions;\n\t\t}\n\n\t\tfor (const entry of entries) {\n\t\t\tif (!entry.isDirectory()) continue;\n\t\t\tif (partial && !entry.name.startsWith(partial)) continue;\n\t\t\tif (!partial && entry.name.startsWith(\".\")) continue;\n\t\t\tconst joined =\n\t\t\t\tbase && !endsWithSep\n\t\t\t\t\t? path.join(path.dirname(argumentPrefix), entry.name)\n\t\t\t\t\t: `${argumentPrefix}${entry.name}`;\n\t\t\tcompletions.push({ value: `${joined}/`, label: `${entry.name}/` });\n\t\t}\n\t\treturn completions.slice(0, 50);\n\t}\n\n\thandleDebug(): void {\n\t\tconst width = this.ctx.ui.terminal.columns;\n\t\tconst height = this.ctx.ui.terminal.rows;\n\t\tconst allLines = this.ctx.ui.render(width);\n\n\t\tconst debugLogPath = getDebugLogPath();\n\t\tconst debugData = [\n\t\t\t`Debug output at ${new Date().toISOString()}`,\n\t\t\t`Terminal: ${width}x${height}`,\n\t\t\t`Total lines: ${allLines.length}`,\n\t\t\t\"\",\n\t\t\t\"=== All rendered lines with visible widths ===\",\n\t\t\t...allLines.map((line, idx) => {\n\t\t\t\tconst vw = visibleWidth(line);\n\t\t\t\tconst escaped = JSON.stringify(line);\n\t\t\t\treturn `[${idx}] (w=${vw}) ${escaped}`;\n\t\t\t}),\n\t\t\t\"\",\n\t\t\t\"=== Agent messages (JSONL) ===\",\n\t\t\t...this.ctx.session.messages.map((msg) => JSON.stringify(msg)),\n\t\t\t\"\",\n\t\t].join(\"\\n\");\n\n\t\tfs.mkdirSync(path.dirname(debugLogPath), { recursive: true });\n\t\tfs.writeFileSync(debugLogPath, debugData);\n\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(\n\t\t\tnew Text(`${theme.fg(\"accent\", \"✓ Debug log written\")}\\n${theme.fg(\"muted\", debugLogPath)}`, 1, 1),\n\t\t);\n\t\tthis.ctx.ui.requestRender();\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"command-executor.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/command-executor.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Extracted command handlers from InteractiveMode.\n * All methods receive dependencies via CommandContext rather than\n * reaching back into the parent class.\n */\n\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport type { Model } from \"@kolisachint/hoocode-ai\";\nimport type { EditorComponent, MarkdownTheme, TUI } from \"@kolisachint/hoocode-tui\";\nimport { type Container, Markdown, Spacer, Text, visibleWidth } from \"@kolisachint/hoocode-tui\";\nimport { spawn, spawnSync } from \"child_process\";\nimport { getDebugLogPath, getShareViewerUrl } from \"../../config.js\";\nimport { loadAgentRegistry } from \"../../core/agent-registry.js\";\nimport type { AgentSession } from \"../../core/agent-session.js\";\nimport type { AgentSessionRuntime } from \"../../core/agent-session-runtime.js\";\nimport { ChangeDirectoryError, SessionImportFileNotFoundError } from \"../../core/agent-session-runtime.js\";\nimport type { KeybindingsManager } from \"../../core/keybindings.js\";\nimport { MissingSessionCwdError } from \"../../core/session-cwd.js\";\nimport {\n\tparseSessionColorSlot,\n\tSESSION_COLOR_NAME_LIST,\n\tSESSION_COLOR_SLOTS,\n\tsessionColorName,\n} from \"../../core/session-identity.js\";\nimport type { SessionManager } from \"../../core/session-manager.js\";\nimport { getSubagentPool } from \"../../core/subagent-pool-instance.js\";\nimport type { SubagentResultFile } from \"../../core/subagent-result.js\";\nimport { getChangelogPath, parseChangelog } from \"../../utils/changelog.js\";\nimport { copyToClipboard } from \"../../utils/clipboard.js\";\nimport { BorderedLoader } from \"./components/bordered-loader.js\";\nimport { DynamicBorder } from \"./components/dynamic-border.js\";\nimport type { FooterComponent } from \"./components/footer.js\";\nimport { formatKeyText, keyDisplayText } from \"./components/keybinding-hints.js\";\nimport { renderSessionChip } from \"./components/session-chip.js\";\nimport { theme } from \"./theme/theme.js\";\n\nexport interface CommandContext {\n\t// Core dependencies\n\tsession: AgentSession;\n\tsessionManager: SessionManager;\n\truntimeHost: AgentSessionRuntime;\n\tui: TUI;\n\teditor: EditorComponent;\n\teditorContainer: Container;\n\tchatContainer: Container;\n\tstatusContainer: Container;\n\tfooter: FooterComponent;\n\tkeybindings: KeybindingsManager;\n\n\t// UI callbacks\n\tshowStatus: (message: string) => void;\n\tshowError: (message: string) => void;\n\tshowWarning: (message: string) => void;\n\tupdateEditorBorderColor: () => void;\n\trenderCurrentSessionState: () => void;\n\trebuildChatFromMessages: () => void;\n\tgetMarkdownThemeWithSettings: () => MarkdownTheme;\n\tstopLoadingAnimation: () => void;\n\n\t// Auth/model helpers\n\tfindExactModelMatch: (searchTerm: string) => Promise<Model<any> | undefined>;\n\tmaybeWarnAboutAnthropicSubscriptionAuth: (model?: Model<any>) => Promise<void>;\n\n\t// Dialog callbacks\n\tshowModelSelector: (searchTerm?: string) => void;\n\tshowExtensionConfirm: (title: string, message: string) => Promise<boolean>;\n\tpromptForMissingSessionCwd: (error: MissingSessionCwdError) => Promise<string | undefined>;\n\n\t// Fatal error handler\n\thandleFatalRuntimeError: (prefix: string, error: unknown) => Promise<never>;\n}\n\nexport class CommandExecutor {\n\t/** Where `/cd -` returns to. Set on every successful move, session-local. */\n\tprivate previousCwd?: string;\n\n\tconstructor(private readonly ctx: CommandContext) {}\n\n\t// =========================================================================\n\t// Slash command handlers\n\t// =========================================================================\n\n\tasync handleModel(searchTerm?: string): Promise<void> {\n\t\tif (!searchTerm) {\n\t\t\tthis.ctx.showModelSelector();\n\t\t\treturn;\n\t\t}\n\n\t\tconst model = await this.ctx.findExactModelMatch(searchTerm);\n\t\tif (model) {\n\t\t\ttry {\n\t\t\t\tawait this.ctx.session.setModel(model);\n\t\t\t\tthis.ctx.footer.invalidate();\n\t\t\t\tthis.ctx.updateEditorBorderColor();\n\t\t\t\tthis.ctx.showStatus(`Model: ${model.id}`);\n\t\t\t\tvoid this.ctx.maybeWarnAboutAnthropicSubscriptionAuth(model);\n\t\t\t} catch (error) {\n\t\t\t\tthis.ctx.showError(error instanceof Error ? error.message : String(error));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tthis.ctx.showModelSelector(searchTerm);\n\t}\n\n\tasync handleClone(): Promise<void> {\n\t\tconst leafId = this.ctx.sessionManager.getLeafId();\n\t\tif (!leafId) {\n\t\t\tthis.ctx.showStatus(\"Nothing to clone yet\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst result = await this.ctx.runtimeHost.fork(leafId, { position: \"at\" });\n\t\t\tif (result.cancelled) {\n\t\t\t\tthis.ctx.ui.requestRender();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tthis.ctx.renderCurrentSessionState();\n\t\t\tthis.ctx.editor.setText(\"\");\n\t\t\tthis.ctx.showStatus(\"Cloned to new session\");\n\t\t} catch (error: unknown) {\n\t\t\tthis.ctx.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tasync handleSubagent(text: string): Promise<void> {\n\t\tconst prefix = \"/subagent \";\n\t\tconst args = text.startsWith(prefix) ? text.slice(prefix.length).trim() : \"\";\n\t\tif (!args) {\n\t\t\tthis.ctx.showStatus(\"Usage: /subagent <mode> <task>\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst firstSpace = args.indexOf(\" \");\n\t\tif (firstSpace === -1) {\n\t\t\tthis.ctx.showStatus(\"Usage: /subagent <mode> <task>\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst mode = args.slice(0, firstSpace).trim();\n\t\tconst task = args.slice(firstSpace + 1).trim();\n\t\tif (!task) {\n\t\t\tthis.ctx.showStatus(\"Usage: /subagent <mode> <task>\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst validModes = loadAgentRegistry({ cwd: this.ctx.sessionManager.getCwd() })\n\t\t\t.list()\n\t\t\t.map((a) => a.name);\n\t\tif (!validModes.includes(mode)) {\n\t\t\tthis.ctx.showStatus(`Unknown subagent_type: ${mode}. Available: ${validModes.join(\", \")}`);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.ctx.showStatus(`Spawning ${mode} subagent...`);\n\t\ttry {\n\t\t\tconst pool = getSubagentPool(this.ctx.sessionManager.getCwd(), this.ctx.session.modelRegistry.getAvailable());\n\t\t\tconst dispatchResult = await pool.dispatch(task, {\n\t\t\t\tforceAgent: mode,\n\t\t\t\tmodel: this.ctx.session.model?.id,\n\t\t\t\tprovider: this.ctx.session.model?.provider,\n\t\t\t});\n\t\t\tconst result = dispatchResult.result;\n\t\t\tconst resultData = result?.result_data as SubagentResultFile | undefined;\n\t\t\tif (result?.ok) {\n\t\t\t\tthis.ctx.showStatus(`${mode} subagent completed`);\n\t\t\t\t// Inject the subagent answer as a custom message so the user can see it in the chat\n\t\t\t\tthis.ctx.sessionManager.appendMessage({\n\t\t\t\t\trole: \"custom\",\n\t\t\t\t\tcustomType: \"subagent\",\n\t\t\t\t\tcontent: resultData?.summary || \"(no output)\",\n\t\t\t\t\tdisplay: true,\n\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tthis.ctx.showError(`Subagent (${mode}) failed: ${result?.error ?? \"unknown error\"}`);\n\t\t\t}\n\t\t} catch (error: unknown) {\n\t\t\tthis.ctx.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\tasync handleExport(text: string): Promise<void> {\n\t\tconst outputPath = this.getPathArgument(text, \"/export\");\n\n\t\ttry {\n\t\t\tif (outputPath?.endsWith(\".jsonl\")) {\n\t\t\t\tconst filePath = this.ctx.session.exportToJsonl(outputPath);\n\t\t\t\tthis.ctx.showStatus(`Session exported to: ${filePath}`);\n\t\t\t} else {\n\t\t\t\tconst filePath = await this.ctx.session.exportToHtml(outputPath);\n\t\t\t\tthis.ctx.showStatus(`Session exported to: ${filePath}`);\n\t\t\t}\n\t\t} catch (error: unknown) {\n\t\t\tthis.ctx.showError(`Failed to export session: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n\t\t}\n\t}\n\n\tprivate getPathArgument(text: string, command: \"/export\" | \"/import\"): string | undefined {\n\t\tif (text === command) {\n\t\t\treturn undefined;\n\t\t}\n\t\tif (!text.startsWith(`${command} `)) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst argsString = text.slice(command.length + 1).trimStart();\n\t\tif (!argsString) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst firstChar = argsString[0];\n\t\tif (firstChar === '\"' || firstChar === \"'\") {\n\t\t\tconst closingQuoteIndex = argsString.indexOf(firstChar, 1);\n\t\t\tif (closingQuoteIndex < 0) {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\treturn argsString.slice(1, closingQuoteIndex);\n\t\t}\n\n\t\tconst firstWhitespaceIndex = argsString.search(/\\s/);\n\t\tif (firstWhitespaceIndex < 0) {\n\t\t\treturn argsString;\n\t\t}\n\t\treturn argsString.slice(0, firstWhitespaceIndex);\n\t}\n\n\tasync handleImport(text: string): Promise<void> {\n\t\tconst inputPath = this.getPathArgument(text, \"/import\");\n\t\tif (!inputPath) {\n\t\t\tthis.ctx.showError(\"Usage: /import <path.jsonl>\");\n\t\t\treturn;\n\t\t}\n\n\t\tconst confirmed = await this.ctx.showExtensionConfirm(\n\t\t\t\"Import session\",\n\t\t\t`Replace current session with ${inputPath}?`,\n\t\t);\n\t\tif (!confirmed) {\n\t\t\tthis.ctx.showStatus(\"Import cancelled\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.ctx.stopLoadingAnimation();\n\t\t\tthis.ctx.statusContainer.clear();\n\t\t\tconst result = await this.ctx.runtimeHost.importFromJsonl(inputPath);\n\t\t\tif (result.cancelled) {\n\t\t\t\tthis.ctx.showStatus(\"Import cancelled\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.ctx.renderCurrentSessionState();\n\t\t\tthis.ctx.showStatus(`Session imported from: ${inputPath}`);\n\t\t} catch (error: unknown) {\n\t\t\tif (error instanceof MissingSessionCwdError) {\n\t\t\t\tconst selectedCwd = await this.ctx.promptForMissingSessionCwd(error);\n\t\t\t\tif (!selectedCwd) {\n\t\t\t\t\tthis.ctx.showStatus(\"Import cancelled\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst result = await this.ctx.runtimeHost.importFromJsonl(inputPath, selectedCwd);\n\t\t\t\tif (result.cancelled) {\n\t\t\t\t\tthis.ctx.showStatus(\"Import cancelled\");\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tthis.ctx.renderCurrentSessionState();\n\t\t\t\tthis.ctx.showStatus(`Session imported from: ${inputPath}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (error instanceof SessionImportFileNotFoundError) {\n\t\t\t\tthis.ctx.showError(`Failed to import session: ${error.message}`);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait this.ctx.handleFatalRuntimeError(\"Failed to import session\", error);\n\t\t}\n\t}\n\n\tasync handleShare(): Promise<void> {\n\t\t// Check if gh is available and logged in\n\t\ttry {\n\t\t\tconst authResult = spawnSync(\"gh\", [\"auth\", \"status\"], { encoding: \"utf-8\" });\n\t\t\tif (authResult.status !== 0) {\n\t\t\t\tthis.ctx.showError(\"GitHub CLI is not logged in. Run 'gh auth login' first.\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t} catch {\n\t\t\tthis.ctx.showError(\"GitHub CLI (gh) is not installed. Install it from https://cli.github.com/\");\n\t\t\treturn;\n\t\t}\n\n\t\t// Export to a temp file\n\t\tconst tmpFile = path.join(os.tmpdir(), \"session.html\");\n\t\ttry {\n\t\t\tawait this.ctx.session.exportToHtml(tmpFile);\n\t\t} catch (error: unknown) {\n\t\t\tthis.ctx.showError(`Failed to export session: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n\t\t\treturn;\n\t\t}\n\n\t\t// Show cancellable loader, replacing the editor\n\t\tconst loader = new BorderedLoader(this.ctx.ui, theme, \"Creating gist...\");\n\t\tthis.ctx.editorContainer.clear();\n\t\tthis.ctx.editorContainer.addChild(loader);\n\t\tthis.ctx.ui.setFocus(loader);\n\t\tthis.ctx.ui.requestRender();\n\n\t\tconst restoreEditor = () => {\n\t\t\tloader.dispose();\n\t\t\tthis.ctx.editorContainer.clear();\n\t\t\tthis.ctx.editorContainer.addChild(this.ctx.editor);\n\t\t\tthis.ctx.ui.setFocus(this.ctx.editor);\n\t\t\ttry {\n\t\t\t\tfs.unlinkSync(tmpFile);\n\t\t\t} catch {\n\t\t\t\t// Ignore cleanup errors\n\t\t\t}\n\t\t};\n\n\t\t// Create a secret gist asynchronously\n\t\tlet proc: ReturnType<typeof spawn> | null = null;\n\n\t\tloader.onAbort = () => {\n\t\t\tproc?.kill();\n\t\t\trestoreEditor();\n\t\t\tthis.ctx.showStatus(\"Share cancelled\");\n\t\t};\n\n\t\ttry {\n\t\t\tconst result = await new Promise<{ stdout: string; stderr: string; code: number | null }>((resolve) => {\n\t\t\t\tproc = spawn(\"gh\", [\"gist\", \"create\", \"--public=false\", tmpFile]);\n\t\t\t\tlet stdout = \"\";\n\t\t\t\tlet stderr = \"\";\n\t\t\t\tproc.stdout?.on(\"data\", (data) => {\n\t\t\t\t\tstdout += data.toString();\n\t\t\t\t});\n\t\t\t\tproc.stderr?.on(\"data\", (data) => {\n\t\t\t\t\tstderr += data.toString();\n\t\t\t\t});\n\t\t\t\tproc.on(\"close\", (code) => resolve({ stdout, stderr, code }));\n\t\t\t});\n\n\t\t\tif (loader.signal.aborted) return;\n\n\t\t\trestoreEditor();\n\n\t\t\tif (result.code !== 0) {\n\t\t\t\tconst errorMsg = result.stderr?.trim() || \"Unknown error\";\n\t\t\t\tthis.ctx.showError(`Failed to create gist: ${errorMsg}`);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Extract gist ID from the URL returned by gh\n\t\t\t// gh returns something like: https://gist.github.com/username/GIST_ID\n\t\t\tconst gistUrl = result.stdout?.trim();\n\t\t\tconst gistId = gistUrl?.split(\"/\").pop();\n\t\t\tif (!gistId) {\n\t\t\t\tthis.ctx.showError(\"Failed to parse gist ID from gh output\");\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Create the preview URL\n\t\t\tconst previewUrl = getShareViewerUrl(gistId);\n\t\t\tthis.ctx.showStatus(`Share URL: ${previewUrl}\\nGist: ${gistUrl}`);\n\t\t} catch (error: unknown) {\n\t\t\tif (!loader.signal.aborted) {\n\t\t\t\trestoreEditor();\n\t\t\t\tthis.ctx.showError(`Failed to create gist: ${error instanceof Error ? error.message : \"Unknown error\"}`);\n\t\t\t}\n\t\t}\n\t}\n\n\tasync handleCopy(): Promise<void> {\n\t\tconst text = this.ctx.session.getLastAssistantText();\n\t\tif (!text) {\n\t\t\tthis.ctx.showError(\"No agent messages to copy yet.\");\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tawait copyToClipboard(text);\n\t\t\tthis.ctx.showStatus(\"Copied last agent message to clipboard\");\n\t\t} catch (error) {\n\t\t\tthis.ctx.showError(error instanceof Error ? error.message : String(error));\n\t\t}\n\t}\n\n\t/** The session's chip as it currently renders, for echoing back after a change. */\n\tprivate currentChip(): string {\n\t\tconst chip = renderSessionChip(\n\t\t\tthis.ctx.sessionManager.getDisplayName(),\n\t\t\tthis.ctx.sessionManager.getSessionColorSlot(),\n\t\t);\n\t\treturn chip?.styled ?? this.ctx.sessionManager.getDisplayName();\n\t}\n\n\thandleName(text: string): void {\n\t\tconst name = text.replace(/^\\/name\\s*/, \"\").trim();\n\t\tif (!name) {\n\t\t\t// Every session has a name now — an auto-assigned slug until someone\n\t\t\t// picks one — so there is always something to report, and the reply\n\t\t\t// shows the chip rather than describing it.\n\t\t\tconst chosen = this.ctx.sessionManager.getSessionName();\n\t\t\tconst label = chosen ? \"Session name:\" : \"Session name (auto):\";\n\t\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.ctx.chatContainer.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\t`${theme.fg(\"dim\", label)} ${this.currentChip()} ${theme.fg(\"dim\", \"/name <name> to change\")}`,\n\t\t\t\t\t1,\n\t\t\t\t\t0,\n\t\t\t\t),\n\t\t\t);\n\t\t\tthis.ctx.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tthis.ctx.session.setSessionName(name);\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(new Text(`${theme.fg(\"dim\", \"Session name set:\")} ${this.currentChip()}`, 1, 0));\n\t\tthis.ctx.ui.requestRender();\n\t}\n\n\t/**\n\t * `/color <slot>` sets the session's chip colour directly, where a slot is a\n\t * number, a name (`green`), or a name's initial (`g`). Bare `/color` is\n\t * handled by the caller, which opens the swatch picker instead — the names\n\t * describe a hue family rather than the exact colour a theme fills the slot\n\t * with, so which one reads best here is still a thing you look at.\n\t *\n\t * The confirmation names the slot it landed on, because the aliases fold\n\t * neighbouring hues together: `/color red` answering with \"magenta\" is how\n\t * you learn the palette has no red of its own.\n\t */\n\thandleColor(text: string): boolean {\n\t\tconst arg = text.replace(/^\\/color\\s*/, \"\").trim();\n\t\tif (!arg) return false;\n\t\tconst slot = parseSessionColorSlot(arg);\n\t\tif (slot === undefined) {\n\t\t\tthis.ctx.showWarning(\n\t\t\t\t`Usage: /color <1-${SESSION_COLOR_SLOTS}> or /color <${SESSION_COLOR_NAME_LIST.join(\"|\")}> ` +\n\t\t\t\t\t\"(first letter works too), or /color on its own to pick one\",\n\t\t\t);\n\t\t\treturn true;\n\t\t}\n\t\tthis.ctx.session.setSessionColor(slot);\n\t\tconst name = sessionColorName(slot);\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(\n\t\t\tnew Text(\n\t\t\t\t`${theme.fg(\"dim\", \"Session color set:\")} ${this.currentChip()}${name ? ` ${theme.fg(\"dim\", name)}` : \"\"}`,\n\t\t\t\t1,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\t\tthis.ctx.ui.requestRender();\n\t\treturn true;\n\t}\n\n\thandleSession(): void {\n\t\tconst stats = this.ctx.session.getSessionStats();\n\t\tconst sessionName = this.ctx.sessionManager.getSessionName();\n\n\t\tlet info = `${theme.bold(\"Session Info\")}\\n\\n`;\n\t\tinfo += `${theme.fg(\"dim\", sessionName ? \"Name:\" : \"Name (auto):\")} ${this.currentChip()}\\n`;\n\t\tconst branch = this.ctx.sessionManager.getSessionBranch();\n\t\tif (branch) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"Branch:\")} ${branch}\\n`;\n\t\t}\n\t\tinfo += `${theme.fg(\"dim\", \"File:\")} ${stats.sessionFile ?? \"In-memory\"}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"ID:\")} ${stats.sessionId}\\n\\n`;\n\t\tinfo += `${theme.bold(\"Messages\")}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"User:\")} ${stats.userMessages}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Assistant:\")} ${stats.assistantMessages}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Tool Calls:\")} ${stats.toolCalls}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Tool Results:\")} ${stats.toolResults}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${stats.totalMessages}\\n\\n`;\n\t\tinfo += `${theme.bold(\"Tokens\")}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Input:\")} ${stats.tokens.input.toLocaleString()}\\n`;\n\t\tinfo += `${theme.fg(\"dim\", \"Output:\")} ${stats.tokens.output.toLocaleString()}\\n`;\n\t\tif (stats.tokens.cacheRead > 0) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"Cache Read:\")} ${stats.tokens.cacheRead.toLocaleString()}\\n`;\n\t\t}\n\t\tif (stats.tokens.cacheWrite > 0) {\n\t\t\tinfo += `${theme.fg(\"dim\", \"Cache Write:\")} ${stats.tokens.cacheWrite.toLocaleString()}\\n`;\n\t\t}\n\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${stats.tokens.total.toLocaleString()}\\n`;\n\n\t\tif (stats.cost > 0) {\n\t\t\tinfo += `\\n${theme.bold(\"Cost\")}\\n`;\n\t\t\tinfo += `${theme.fg(\"dim\", \"Total:\")} ${stats.cost.toFixed(4)}`;\n\t\t}\n\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(new Text(info, 1, 0));\n\t\tthis.ctx.ui.requestRender();\n\t}\n\n\thandleChangelog(): void {\n\t\tconst changelogPath = getChangelogPath();\n\t\tconst allEntries = parseChangelog(changelogPath);\n\n\t\tif (allEntries.length === 0) {\n\t\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.ctx.chatContainer.addChild(new Text(theme.fg(\"dim\", \"No changelog entries found.\"), 1, 0));\n\t\t\tthis.ctx.ui.requestRender();\n\t\t\treturn;\n\t\t}\n\n\t\tconst changelogMarkdown = allEntries\n\t\t\t.slice()\n\t\t\t.reverse()\n\t\t\t.map((e) => e.content)\n\t\t\t.join(\"\\n\\n\");\n\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg(\"accent\", \"What's New\")), 1, 0));\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, this.ctx.getMarkdownThemeWithSettings()));\n\t\tthis.ctx.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ctx.ui.requestRender();\n\t}\n\n\thandleHotkeys(): void {\n\t\t// Navigation keybindings\n\t\tconst cursorUp = keyDisplayText(\"tui.editor.cursorUp\");\n\t\tconst cursorDown = keyDisplayText(\"tui.editor.cursorDown\");\n\t\tconst cursorLeft = keyDisplayText(\"tui.editor.cursorLeft\");\n\t\tconst cursorRight = keyDisplayText(\"tui.editor.cursorRight\");\n\t\tconst cursorWordLeft = keyDisplayText(\"tui.editor.cursorWordLeft\");\n\t\tconst cursorWordRight = keyDisplayText(\"tui.editor.cursorWordRight\");\n\t\tconst cursorLineStart = keyDisplayText(\"tui.editor.cursorLineStart\");\n\t\tconst cursorLineEnd = keyDisplayText(\"tui.editor.cursorLineEnd\");\n\t\tconst jumpForward = keyDisplayText(\"tui.editor.jumpForward\");\n\t\tconst jumpBackward = keyDisplayText(\"tui.editor.jumpBackward\");\n\t\tconst pageUp = keyDisplayText(\"tui.editor.pageUp\");\n\t\tconst pageDown = keyDisplayText(\"tui.editor.pageDown\");\n\n\t\t// Editing keybindings\n\t\tconst submit = keyDisplayText(\"tui.input.submit\");\n\t\tconst newLine = keyDisplayText(\"tui.input.newLine\");\n\t\tconst deleteWordBackward = keyDisplayText(\"tui.editor.deleteWordBackward\");\n\t\tconst deleteWordForward = keyDisplayText(\"tui.editor.deleteWordForward\");\n\t\tconst deleteToLineStart = keyDisplayText(\"tui.editor.deleteToLineStart\");\n\t\tconst deleteToLineEnd = keyDisplayText(\"tui.editor.deleteToLineEnd\");\n\t\tconst yank = keyDisplayText(\"tui.editor.yank\");\n\t\tconst yankPop = keyDisplayText(\"tui.editor.yankPop\");\n\t\tconst undo = keyDisplayText(\"tui.editor.undo\");\n\t\tconst tab = keyDisplayText(\"tui.input.tab\");\n\n\t\t// App keybindings\n\t\tconst interrupt = keyDisplayText(\"app.interrupt\");\n\t\tconst clear = keyDisplayText(\"app.clear\");\n\t\tconst exit = keyDisplayText(\"app.exit\");\n\t\tconst suspend = keyDisplayText(\"app.suspend\");\n\t\tconst cycleThinkingLevel = keyDisplayText(\"app.thinking.cycle\");\n\t\tconst cycleModelForward = keyDisplayText(\"app.model.cycleForward\");\n\t\tconst selectModel = keyDisplayText(\"app.model.select\");\n\t\tconst expandTools = keyDisplayText(\"app.tools.expand\");\n\t\tconst toggleThinking = keyDisplayText(\"app.thinking.toggle\");\n\t\tconst cycleTaskView = keyDisplayText(\"app.tasks.cycleView\");\n\t\tconst externalEditor = keyDisplayText(\"app.editor.external\");\n\t\tconst cycleModelBackward = keyDisplayText(\"app.model.cycleBackward\");\n\t\tconst followUp = keyDisplayText(\"app.message.followUp\");\n\t\tconst dequeue = keyDisplayText(\"app.message.dequeue\");\n\t\tconst pasteImage = keyDisplayText(\"app.clipboard.pasteImage\");\n\t\tconst viewForward = keyDisplayText(\"app.view.cycleForward\");\n\t\tconst unfoldOne = keyDisplayText(\"app.tools.unfoldOne\");\n\t\tconst foldOne = keyDisplayText(\"app.tools.foldOne\");\n\t\tconst viewBackward = keyDisplayText(\"app.view.cycleBackward\");\n\t\tconst voice = keyDisplayText(\"app.input.voiceTranscribe\");\n\t\tconst changeDirectory = keyDisplayText(\"app.session.changeDirectory\");\n\t\tconst openSettings = keyDisplayText(\"app.settings.open\");\n\t\tconst openHotkeys = keyDisplayText(\"app.hotkeys.open\");\n\t\tconst cycleMode = keyDisplayText(\"app.mode.cycle\");\n\t\tconst sessionTree = keyDisplayText(\"app.session.tree\");\n\t\tconst sessionResume = keyDisplayText(\"app.session.resume\");\n\n\t\tlet hotkeys = `\n**Navigation**\n| Key | Action |\n|-----|--------|\n| \\`${cursorUp}\\` / \\`${cursorDown}\\` / \\`${cursorLeft}\\` / \\`${cursorRight}\\` | Move cursor / browse history (Up when empty) |\n| \\`${cursorWordLeft}\\` / \\`${cursorWordRight}\\` | Move by word |\n| \\`${cursorLineStart}\\` | Start of line |\n| \\`${cursorLineEnd}\\` | End of line |\n| \\`${jumpForward}\\` | Jump forward to character |\n| \\`${jumpBackward}\\` | Jump backward to character |\n| \\`${pageUp}\\` / \\`${pageDown}\\` | Scroll by page |\n\n**Editing**\n| Key | Action |\n|-----|--------|\n| \\`${submit}\\` | Send message |\n| \\`${newLine}\\` | New line${process.platform === \"win32\" ? \" (Ctrl+Enter on Windows Terminal)\" : \"\"} |\n| \\`${deleteWordBackward}\\` | Delete word backwards |\n| \\`${deleteWordForward}\\` | Delete word forwards |\n| \\`${deleteToLineStart}\\` | Delete to start of line |\n| \\`${deleteToLineEnd}\\` | Delete to end of line |\n| \\`${yank}\\` | Paste the most-recently-deleted text |\n| \\`${yankPop}\\` | Cycle through the deleted text after pasting |\n| \\`${undo}\\` | Undo |\n\n**Flow** — the keys you hit without thinking\n| Key | Action |\n|-----|--------|\n| \\`${tab}\\` | Path completion / accept autocomplete |\n| \\`${interrupt}\\` | Cancel autocomplete / abort streaming |\n| \\`${clear}\\` | Clear editor (first) / exit (second) |\n| \\`${exit}\\` | Exit (when editor is empty) |\n| \\`${suspend}\\` | Suspend to background |\n| \\`${followUp}\\` | Queue follow-up message |\n| \\`${dequeue}\\` | Restore queued messages |\n| \\`${pasteImage}\\` | Paste image from clipboard |\n| \\`/\\` | Slash commands |\n| \\`!\\` | Run bash command |\n| \\`!!\\` | Run bash command (excluded from context) |\n\n**View** — what is on screen right now (\\`Ctrl\\`)\n| Key | Action |\n|-----|--------|\n| \\`${expandTools}\\` | Expand or collapse every tool block at once |\n| \\`${unfoldOne}\\` / \\`${foldOne}\\` | Open / re-fold one thing, newest first — a chain in radar, a tool body otherwise |\n| \\`${viewForward}\\` / \\`${viewBackward}\\` | Cycle tool output: radar → glance → full |\n| \\`${toggleThinking}\\` | Toggle thinking block visibility |\n| \\`${cycleTaskView}\\` | Cycle task panel view (tasks → subagents → teams) |\n| \\`${cycleModelForward}\\` / \\`${cycleModelBackward}\\` | Cycle models |\n| \\`${cycleThinkingLevel}\\` | Cycle thinking level |\n\n**Cockpit** — what the agent is, and where it works (\\`Alt\\`)\n| Key | Action |\n|-----|--------|\n| \\`${cycleMode}\\` | Cycle agent mode (ask → plan → build → debug) |\n| \\`${selectModel}\\` | Open model selector |\n| \\`${changeDirectory}\\` | Change working directory (\\`/cd\\`) |\n| \\`${sessionTree}\\` | Open session tree |\n| \\`${sessionResume}\\` | Resume a session from history |\n| \\`${openSettings}\\` | Open settings |\n| \\`${openHotkeys}\\` | Show this list |\n| \\`${externalEditor}\\` | Edit message in external editor |\n| \\`${voice}\\` | Record voice and transcribe |\n\nInside a picker, every \\`Ctrl\\` key still edits the query — the picker's own\nverbs are on \\`Alt\\`, and its hint line names them.\n`;\n\n\t\t// Add extension-registered shortcuts\n\t\tconst extensionRunner = this.ctx.session.extensionRunner;\n\t\tconst shortcuts = extensionRunner.getShortcuts(this.ctx.keybindings.getEffectiveConfig());\n\t\tif (shortcuts.size > 0) {\n\t\t\thotkeys += `\n**Extensions**\n| Key | Action |\n|-----|--------|\n`;\n\t\t\tfor (const [key, shortcut] of shortcuts) {\n\t\t\t\tconst description = shortcut.description ?? shortcut.extensionPath;\n\t\t\t\tconst keyDisplay = formatKeyText(key, { capitalize: true });\n\t\t\t\thotkeys += `| \\`${keyDisplay}\\` | ${description} |\\n`;\n\t\t\t}\n\t\t}\n\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ctx.chatContainer.addChild(new Text(theme.bold(theme.fg(\"accent\", \"Keyboard Shortcuts\")), 1, 0));\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(new Markdown(hotkeys.trim(), 1, 1, this.ctx.getMarkdownThemeWithSettings()));\n\t\tthis.ctx.chatContainer.addChild(new DynamicBorder());\n\t\tthis.ctx.ui.requestRender();\n\t}\n\n\tasync handleClear(): Promise<void> {\n\t\tthis.ctx.stopLoadingAnimation();\n\t\tthis.ctx.statusContainer.clear();\n\t\ttry {\n\t\t\tconst result = await this.ctx.runtimeHost.newSession();\n\t\t\tif (result.cancelled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.ctx.renderCurrentSessionState();\n\t\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.ctx.chatContainer.addChild(new Text(`${theme.fg(\"accent\", \"✓ New session started\")}`, 1, 1));\n\t\t\tthis.ctx.ui.requestRender();\n\t\t} catch (error: unknown) {\n\t\t\tawait this.ctx.handleFatalRuntimeError(\"Failed to create session\", error);\n\t\t}\n\t}\n\n\t/**\n\t * `/cd [path]` — move the whole session to another directory.\n\t *\n\t * Written for the case it exists to serve: you are done in one repo and want\n\t * to work in the next one without losing the process, its provider auth, or\n\t * its warmed model list. Bare `/cd` goes home, `/cd -` goes back to where you\n\t * came from, `~` and relative paths resolve as a shell would.\n\t *\n\t * The move starts a fresh session rooted in the target — see\n\t * `AgentSessionRuntime.changeDirectory` for why the whole runtime is rebuilt\n\t * rather than a cwd string reassigned.\n\t */\n\tasync handleChangeDirectory(text: string): Promise<void> {\n\t\tconst prefix = \"/cd\";\n\t\tconst rawArg = text.startsWith(prefix) ? text.slice(prefix.length).trim() : text.trim();\n\t\tconst previousCwd = this.ctx.sessionManager.getCwd();\n\n\t\tlet target: string;\n\t\tif (!rawArg || rawArg === \"~\") {\n\t\t\ttarget = os.homedir();\n\t\t} else if (rawArg === \"-\") {\n\t\t\tif (!this.previousCwd) {\n\t\t\t\tthis.ctx.showWarning(\"No previous directory to return to\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\ttarget = this.previousCwd;\n\t\t} else {\n\t\t\tconst expanded = rawArg.startsWith(\"~/\") ? path.join(os.homedir(), rawArg.slice(2)) : rawArg;\n\t\t\ttarget = path.resolve(previousCwd, expanded);\n\t\t}\n\n\t\tif (path.resolve(target) === path.resolve(previousCwd)) {\n\t\t\tthis.ctx.showStatus(`Already in ${target}`);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.ctx.stopLoadingAnimation();\n\t\tthis.ctx.statusContainer.clear();\n\t\ttry {\n\t\t\tconst result = await this.ctx.runtimeHost.changeDirectory(target);\n\t\t\tif (result.cancelled) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.previousCwd = previousCwd;\n\t\t\tthis.ctx.renderCurrentSessionState();\n\t\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\t\tthis.ctx.chatContainer.addChild(\n\t\t\t\tnew Text(\n\t\t\t\t\t`${theme.fg(\"accent\", \"✓ Working directory\")} ${theme.fg(\"muted\", result.cwd)}\\n` +\n\t\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\t\"dim\",\n\t\t\t\t\t\t\t`New session started here. ${keyDisplayText(\"app.session.resume\")} reopens the session you left in ${previousCwd}.`,\n\t\t\t\t\t\t),\n\t\t\t\t\t1,\n\t\t\t\t\t1,\n\t\t\t\t),\n\t\t\t);\n\t\t\tthis.ctx.ui.requestRender();\n\t\t} catch (error: unknown) {\n\t\t\tif (error instanceof ChangeDirectoryError) {\n\t\t\t\tthis.ctx.showError(error.message);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait this.ctx.handleFatalRuntimeError(`Failed to change directory to ${target}`, error);\n\t\t}\n\t}\n\n\t/** Directory completions for `/cd <prefix>`, plus the two shell shorthands. */\n\tgetChangeDirectoryCompletions(argumentPrefix: string): Array<{ value: string; label: string }> {\n\t\tconst cwd = this.ctx.sessionManager.getCwd();\n\t\tconst expanded = argumentPrefix.startsWith(\"~/\")\n\t\t\t? path.join(os.homedir(), argumentPrefix.slice(2))\n\t\t\t: argumentPrefix;\n\t\t// A prefix ending in a separator names the directory to list; otherwise the\n\t\t// last segment is a partial name to filter its parent by.\n\t\tconst endsWithSep = expanded.endsWith(\"/\") || expanded.endsWith(path.sep);\n\t\tconst base = endsWithSep ? expanded : path.dirname(expanded);\n\t\tconst partial = endsWithSep ? \"\" : path.basename(expanded);\n\t\tconst searchDir = path.isAbsolute(base) ? base : path.resolve(cwd, base || \".\");\n\n\t\tconst completions: Array<{ value: string; label: string }> = [];\n\t\tif (!argumentPrefix) {\n\t\t\tif (this.previousCwd) completions.push({ value: \"-\", label: `- (${this.previousCwd})` });\n\t\t\tcompletions.push({ value: \"~\", label: `~ (${os.homedir()})` });\n\t\t}\n\n\t\tlet entries: fs.Dirent[];\n\t\ttry {\n\t\t\tentries = fs.readdirSync(searchDir, { withFileTypes: true });\n\t\t} catch {\n\t\t\treturn completions;\n\t\t}\n\n\t\tfor (const entry of entries) {\n\t\t\tif (!entry.isDirectory()) continue;\n\t\t\tif (partial && !entry.name.startsWith(partial)) continue;\n\t\t\tif (!partial && entry.name.startsWith(\".\")) continue;\n\t\t\tconst joined =\n\t\t\t\tbase && !endsWithSep\n\t\t\t\t\t? path.join(path.dirname(argumentPrefix), entry.name)\n\t\t\t\t\t: `${argumentPrefix}${entry.name}`;\n\t\t\tcompletions.push({ value: `${joined}/`, label: `${entry.name}/` });\n\t\t}\n\t\treturn completions.slice(0, 50);\n\t}\n\n\thandleDebug(): void {\n\t\tconst width = this.ctx.ui.terminal.columns;\n\t\tconst height = this.ctx.ui.terminal.rows;\n\t\tconst allLines = this.ctx.ui.render(width);\n\n\t\tconst debugLogPath = getDebugLogPath();\n\t\tconst debugData = [\n\t\t\t`Debug output at ${new Date().toISOString()}`,\n\t\t\t`Terminal: ${width}x${height}`,\n\t\t\t`Total lines: ${allLines.length}`,\n\t\t\t\"\",\n\t\t\t\"=== All rendered lines with visible widths ===\",\n\t\t\t...allLines.map((line, idx) => {\n\t\t\t\tconst vw = visibleWidth(line);\n\t\t\t\tconst escaped = JSON.stringify(line);\n\t\t\t\treturn `[${idx}] (w=${vw}) ${escaped}`;\n\t\t\t}),\n\t\t\t\"\",\n\t\t\t\"=== Agent messages (JSONL) ===\",\n\t\t\t...this.ctx.session.messages.map((msg) => JSON.stringify(msg)),\n\t\t\t\"\",\n\t\t].join(\"\\n\");\n\n\t\tfs.mkdirSync(path.dirname(debugLogPath), { recursive: true });\n\t\tfs.writeFileSync(debugLogPath, debugData);\n\n\t\tthis.ctx.chatContainer.addChild(new Spacer(1));\n\t\tthis.ctx.chatContainer.addChild(\n\t\t\tnew Text(`${theme.fg(\"accent\", \"✓ Debug log written\")}\\n${theme.fg(\"muted\", debugLogPath)}`, 1, 1),\n\t\t);\n\t\tthis.ctx.ui.requestRender();\n\t}\n}\n"]}
|
|
@@ -12,7 +12,7 @@ import { getDebugLogPath, getShareViewerUrl } from "../../config.js";
|
|
|
12
12
|
import { loadAgentRegistry } from "../../core/agent-registry.js";
|
|
13
13
|
import { ChangeDirectoryError, SessionImportFileNotFoundError } from "../../core/agent-session-runtime.js";
|
|
14
14
|
import { MissingSessionCwdError } from "../../core/session-cwd.js";
|
|
15
|
-
import {
|
|
15
|
+
import { parseSessionColorSlot, SESSION_COLOR_NAME_LIST, SESSION_COLOR_SLOTS, sessionColorName, } from "../../core/session-identity.js";
|
|
16
16
|
import { getSubagentPool } from "../../core/subagent-pool-instance.js";
|
|
17
17
|
import { getChangelogPath, parseChangelog } from "../../utils/changelog.js";
|
|
18
18
|
import { copyToClipboard } from "../../utils/clipboard.js";
|
|
@@ -337,22 +337,30 @@ export class CommandExecutor {
|
|
|
337
337
|
this.ctx.ui.requestRender();
|
|
338
338
|
}
|
|
339
339
|
/**
|
|
340
|
-
* `/color <
|
|
341
|
-
*
|
|
342
|
-
*
|
|
340
|
+
* `/color <slot>` sets the session's chip colour directly, where a slot is a
|
|
341
|
+
* number, a name (`green`), or a name's initial (`g`). Bare `/color` is
|
|
342
|
+
* handled by the caller, which opens the swatch picker instead — the names
|
|
343
|
+
* describe a hue family rather than the exact colour a theme fills the slot
|
|
344
|
+
* with, so which one reads best here is still a thing you look at.
|
|
345
|
+
*
|
|
346
|
+
* The confirmation names the slot it landed on, because the aliases fold
|
|
347
|
+
* neighbouring hues together: `/color red` answering with "magenta" is how
|
|
348
|
+
* you learn the palette has no red of its own.
|
|
343
349
|
*/
|
|
344
350
|
handleColor(text) {
|
|
345
351
|
const arg = text.replace(/^\/color\s*/, "").trim();
|
|
346
352
|
if (!arg)
|
|
347
353
|
return false;
|
|
348
|
-
const slot =
|
|
349
|
-
if (
|
|
350
|
-
this.ctx.showWarning(`Usage: /color <1-${SESSION_COLOR_SLOTS}
|
|
354
|
+
const slot = parseSessionColorSlot(arg);
|
|
355
|
+
if (slot === undefined) {
|
|
356
|
+
this.ctx.showWarning(`Usage: /color <1-${SESSION_COLOR_SLOTS}> or /color <${SESSION_COLOR_NAME_LIST.join("|")}> ` +
|
|
357
|
+
"(first letter works too), or /color on its own to pick one");
|
|
351
358
|
return true;
|
|
352
359
|
}
|
|
353
360
|
this.ctx.session.setSessionColor(slot);
|
|
361
|
+
const name = sessionColorName(slot);
|
|
354
362
|
this.ctx.chatContainer.addChild(new Spacer(1));
|
|
355
|
-
this.ctx.chatContainer.addChild(new Text(`${theme.fg("dim", "Session color set:")} ${this.currentChip()}`, 1, 0));
|
|
363
|
+
this.ctx.chatContainer.addChild(new Text(`${theme.fg("dim", "Session color set:")} ${this.currentChip()}${name ? ` ${theme.fg("dim", name)}` : ""}`, 1, 0));
|
|
356
364
|
this.ctx.ui.requestRender();
|
|
357
365
|
return true;
|
|
358
366
|
}
|