From 685eacc4dba43be195f636ebb7b4c20fa9e95189 Mon Sep 17 00:00:00 2001 From: gvanrossum-ms Date: Mon, 25 Nov 2024 15:26:05 -0800 Subject: [PATCH] [spelunker] Add @files; add @purgeFile (call from @import); colorize output (#432) - Colorize output from most commands and operations - New command: `@files` lists all files found in the chunks; `--filter` limits output to files containing a substring. - New command: `@purgeFile` removes all mention of a file from the indexes. - Update `@import` to call the guts of `@purgeFile` before re-importing a file, to avoid duplicates. --------- Co-authored-by: Guido van Rossum --- ts/examples/spelunker/package.json | 1 + ts/examples/spelunker/src/chunkyIndex.ts | 27 +- ts/examples/spelunker/src/pythonImporter.ts | 49 +++- ts/examples/spelunker/src/queryInterface.ts | 277 ++++++++++++++++---- ts/pnpm-lock.yaml | 3 + 5 files changed, 284 insertions(+), 73 deletions(-) diff --git a/ts/examples/spelunker/package.json b/ts/examples/spelunker/package.json index 4703fea56..be0c03118 100644 --- a/ts/examples/spelunker/package.json +++ b/ts/examples/spelunker/package.json @@ -28,6 +28,7 @@ }, "dependencies": { "aiclient": "workspace:*", + "chalk": "^5.3.0", "code-processor": "workspace:*", "dotenv": "^16.3.1", "interactive-app": "workspace:*", diff --git a/ts/examples/spelunker/src/chunkyIndex.ts b/ts/examples/spelunker/src/chunkyIndex.ts index 7a49076c7..ac23e2178 100644 --- a/ts/examples/spelunker/src/chunkyIndex.ts +++ b/ts/examples/spelunker/src/chunkyIndex.ts @@ -18,10 +18,7 @@ export type IndexType = | "topics" | "goals" | "dependencies"; -export type NamedIndex = { - name: IndexType; - index: knowLib.TextIndex; -}; +export type NamedIndex = [IndexType, knowLib.TextIndex]; // A bundle of object stores and indexes etc. export class ChunkyIndex { @@ -31,7 +28,7 @@ export class ChunkyIndex { queryMaker: TypeChatJsonTranslator; answerMaker: TypeChatJsonTranslator; - // The rest are asynchronously initialized by initialize(). + // The rest are asynchronously initialized by reInitialize(rootDir). rootDir!: string; chunkFolder!: ObjectFolder; summariesIndex!: knowLib.TextIndex; @@ -85,22 +82,22 @@ export class ChunkyIndex { } } - getIndexByName(name: IndexType): knowLib.TextIndex { - for (const pair of this.allIndexes()) { - if (pair.name === name) { - return pair.index; + getIndexByName(indexName: IndexType): knowLib.TextIndex { + for (const [name, index] of this.allIndexes()) { + if (name === indexName) { + return index; } } - throw new Error(`Unknown index: ${name}`); + throw new Error(`Unknown index: ${indexName}`); } allIndexes(): NamedIndex[] { return [ - { name: "summaries", index: this.summariesIndex }, - { name: "keywords", index: this.keywordsIndex }, - { name: "topics", index: this.topicsIndex }, - { name: "goals", index: this.goalsIndex }, - { name: "dependencies", index: this.dependenciesIndex }, + ["summaries", this.summariesIndex], + ["keywords", this.keywordsIndex], + ["topics", this.topicsIndex], + ["goals", this.goalsIndex], + ["dependencies", this.dependenciesIndex], ]; } } diff --git a/ts/examples/spelunker/src/pythonImporter.ts b/ts/examples/spelunker/src/pythonImporter.ts index adeb90eda..1a4a673b2 100644 --- a/ts/examples/spelunker/src/pythonImporter.ts +++ b/ts/examples/spelunker/src/pythonImporter.ts @@ -28,6 +28,7 @@ TypeScript, of course). */ +import chalk, { ChalkInstance } from "chalk"; import * as fs from "fs"; import * as knowLib from "knowledge-processor"; import { asyncArray } from "typeagent"; @@ -42,8 +43,14 @@ import { chunkifyPythonFiles, ErrorItem, } from "./pythonChunker.js"; +import { purgeNormalizedFile } from "./queryInterface.js"; -function log(io: iapp.InteractiveIo | undefined, message: string): void { +function log( + io: iapp.InteractiveIo | undefined, + message: string, + color: ChalkInstance, +): void { + message = color(message); if (io) { io.writer.writeLine(message); } else { @@ -57,7 +64,7 @@ export async function importAllFiles( io: iapp.InteractiveIo | undefined, verbose: boolean, ): Promise { - log(io, `[Importing ${files.length} files]`); + log(io, `[Importing ${files.length} files]`, chalk.grey); const t0 = Date.now(); await importPythonFiles(files, chunkyIndex, io, verbose); @@ -66,6 +73,7 @@ export async function importAllFiles( log( io, `[Imported ${files.length} files in ${((t1 - t0) * 0.001).toFixed(3)} seconds]`, + chalk.grey, ); } @@ -80,6 +88,11 @@ async function importPythonFiles( fs.existsSync(file) ? fs.realpathSync(file) : file, ); + // Purge previous occurrences of these files. + for (const fileName of filenames) { + await purgeNormalizedFile(io, chunkyIndex, fileName, verbose); + } + // Chunkify Python files using a helper program. (TODO: Make generic over languages) const t0 = Date.now(); const results = await chunkifyPythonFiles(filenames); @@ -88,6 +101,7 @@ async function importPythonFiles( log( io, `[Some over-long files were split into multiple partial files]`, + chalk.yellow, ); } @@ -115,19 +129,24 @@ async function importPythonFiles( `[Chunked ${results.length} files ` + `(${numLines} lines, ${numBlobs} blobs, ${numChunks} chunks, ${numErrors} errors) ` + `in ${((t1 - t0) * 0.001).toFixed(3)} seconds]`, + chalk.gray, ); const chunkingErrors = results.filter( (result): result is ErrorItem => "error" in result, ); for (const error of chunkingErrors) { - log(io, `[Error: ${error.error}; Output: ${error.output ?? ""}]`); + log( + io, + `[Error: ${error.error}; Output: ${error.output ?? ""}]`, + chalk.redBright, + ); } const chunkedFiles = results.filter( (result): result is ChunkedFile => "chunks" in result, ); - log(io, `[Documenting ${chunkedFiles.length} files]`); + log(io, `[Documenting ${chunkedFiles.length} files]`, chalk.grey); const tt0 = Date.now(); const documentedFiles: FileDocumentation[] = []; @@ -141,7 +160,9 @@ async function importPythonFiles( let docs: FileDocumentation; nChunks += chunkedFile.chunks.length; try { - docs = await chunkyIndex.fileDocumenter.document( + docs = await exponentialBackoff( + io, + chunkyIndex.fileDocumenter.document, chunkedFile.chunks, ); } catch (error) { @@ -149,6 +170,7 @@ async function importPythonFiles( log( io, ` [Error documenting ${chunkedFile.fileName} in ${((t1 - t0) * 0.001).toFixed(3)} seconds: ${error}]`, + chalk.redBright, ); return; } @@ -157,6 +179,7 @@ async function importPythonFiles( log( io, ` [Documented ${chunkedFile.chunks.length} chunks in ${((t1 - t0) * 0.001).toFixed(3)} seconds for ${chunkedFile.fileName}]`, + chalk.grey, ); documentedFiles.push(docs); }, @@ -166,13 +189,14 @@ async function importPythonFiles( log( io, `[Documented ${documentedFiles.length} files (${nChunks} chunks) in ${((tt1 - tt0) * 0.001).toFixed(3)} seconds]`, + chalk.grey, ); const nonEmptyFiles = chunkedFiles.filter( (cf) => cf.chunks.filter((c) => c.docs).length, ); - log(io, `[Embedding ${nonEmptyFiles.length} files]`); + log(io, `[Embedding ${nonEmptyFiles.length} files]`, chalk.grey); if (nonEmptyFiles.length) { const ttt0 = Date.now(); @@ -186,6 +210,7 @@ async function importPythonFiles( log( io, `[Embedded ${documentedFiles.length} files in ${((ttt1 - ttt0) * 0.001).toFixed(3)} seconds]`, + chalk.grey, ); } } @@ -198,7 +223,7 @@ export async function embedChunkedFile( ): Promise { const chunks: Chunk[] = chunkedFile.chunks; if (chunks.length === 0) { - log(io, `[Skipping empty file ${chunkedFile.fileName}]`); + log(io, `[Skipping empty file ${chunkedFile.fileName}]`, chalk.yellow); return; } const t0 = Date.now(); @@ -209,6 +234,7 @@ export async function embedChunkedFile( log( io, ` [Embedded ${chunks.length} chunks in ${((t1 - t0) * 0.001).toFixed(3)} seconds for ${chunkedFile.fileName}]`, + chalk.grey, ); } @@ -271,6 +297,7 @@ async function embedChunk( io, ` [Embedded ${chunk.id} (${lineCount} lines @ ${chunk.blobs[0].start}) ` + `in ${((t1 - t0) * 0.001).toFixed(3)} seconds for ${chunk.fileName}]`, + chalk.gray, ); } } @@ -297,10 +324,14 @@ async function exponentialBackoff( return await callable(...args); } catch (error) { if (timeout > 1000) { - log(io, `[Error: ${error}; giving up]`); + log(io, `[Error: ${error}; giving up]`, chalk.redBright); throw error; } - log(io, `[Error: ${error}; retrying in ${timeout} ms]`); + log( + io, + `[Error: ${error}; retrying in ${timeout} ms]`, + chalk.redBright, + ); await new Promise((resolve) => setTimeout(resolve, timeout)); timeout *= 2; } diff --git a/ts/examples/spelunker/src/queryInterface.ts b/ts/examples/spelunker/src/queryInterface.ts index 7fc2c54b8..103f3d713 100644 --- a/ts/examples/spelunker/src/queryInterface.ts +++ b/ts/examples/spelunker/src/queryInterface.ts @@ -3,7 +3,9 @@ // User interface for querying the index. +import chalk, { ChalkInstance } from "chalk"; import * as fs from "fs"; +import * as util from "util"; import * as iapp from "interactive-app"; import * as knowLib from "knowledge-processor"; @@ -21,6 +23,45 @@ type QueryOptions = { verbose: boolean; }; +function writeColor( + io: iapp.InteractiveIo | undefined, + color: ChalkInstance, + message: string, +): void { + message = color(message); + if (io) { + io.writer.writeLine(message); + } else { + console.log(message); + } +} + +function writeNote(io: iapp.InteractiveIo | undefined, message: string): void { + writeColor(io, chalk.gray, message); +} + +function writeMain(io: iapp.InteractiveIo | undefined, message: string): void { + writeColor(io, chalk.white, message); +} + +function writeWarning( + io: iapp.InteractiveIo | undefined, + message: string, +): void { + writeColor(io, chalk.yellow, message); +} + +function writeError(io: iapp.InteractiveIo | undefined, message: string): void { + writeColor(io, chalk.redBright, message); +} + +function writeHeading( + io: iapp.InteractiveIo | undefined, + message: string, +): void { + writeColor(io, chalk.green, message); +} + export async function interactiveQueryLoop( chunkyIndex: ChunkyIndex, verbose = false, @@ -35,6 +76,8 @@ export async function interactiveQueryLoop( topics, goals, dependencies, + files, + purgeFile, }; iapp.addStandardHandlers(handlers); @@ -74,7 +117,7 @@ export async function interactiveQueryLoop( .filter((line) => line.length > 0 && line[0] !== "#") : []; if (!files.length) { - io.writer.writeLine("[No files to import (use --? for help)]"); + writeError(io, "[No files to import (use --? for help)]"); return; } await importAllFiles( @@ -95,7 +138,7 @@ export async function interactiveQueryLoop( force: true, }); await chunkyIndex.reInitialize(chunkyIndex.rootDir); - io.writer.writeLine("[All memory and all indexes cleared]"); + writeNote(io, "[All memory and all indexes cleared]"); // Actually the embeddings cache isn't. But we shouldn't have to care. } @@ -110,13 +153,14 @@ export async function interactiveQueryLoop( const chunk = await chunkyIndex.chunkFolder.get(chunkId); if (chunk) { const chunkDocs = chunk.docs?.chunkDocs ?? []; - io.writer.writeLine(`\nCHUNK ID: ${chunkId}`); + writeNote(io, `\nCHUNK ID: ${chunkId}`); for (const chunkDoc of chunkDocs) { - for (const pair of chunkyIndex.allIndexes()) { - if (pair.name == "summaries") { + for (const [name, _] of chunkyIndex.allIndexes()) { + if (name == "summaries") { if (chunkDoc.summary) { - io.writer.writeLine("SUMMARY:"); - io.writer.writeLine( + writeNote(io, "SUMMARY:"); + writeMain( + io, // Indent by two wordWrap(chunkDoc.summary).replace( /^/gm, @@ -124,23 +168,24 @@ export async function interactiveQueryLoop( ), ); } else { - io.writer.writeLine("SUMMARY: None"); + writeWarning(io, "SUMMARY: None"); } } else { const docItem: string[] | undefined = - chunkDoc[pair.name]; + chunkDoc[name]; if (docItem?.length) { - io.writer.writeLine( - `${pair.name.toUpperCase()}: ${docItem.join(", ")}`, + writeNote( + io, + `${name.toUpperCase()}: ${docItem.join(", ")}`, ); } } } } - io.writer.writeLine("CODE:"); + writeNote(io, "CODE:"); writeChunkLines(chunk, io, 100); } else { - io.writer.writeLine(`[Chunk ID ${chunkId} not found]`); + writeWarning(io, `[Chunk ID ${chunkId} not found]`); } } } @@ -289,6 +334,83 @@ export async function interactiveQueryLoop( await _reportIndex(args, io, "dependencies"); } + function filesDef(): iapp.CommandMetadata { + return { + description: "Show all recorded file names.", + options: { + filter: { + description: "Only show files containing this string", + type: "string", + }, + }, + }; + } + handlers.files.metadata = filesDef(); + async function files( + args: string[] | iapp.NamedArgs, + io: iapp.InteractiveIo, + ): Promise { + const namedArgs = iapp.parseNamedArguments(args, filesDef()); + const filter = namedArgs.filter; + const filesPopularity: Map = new Map(); + for await (const chunk of chunkyIndex.chunkFolder.allObjects()) { + filesPopularity.set( + chunk.fileName, + (filesPopularity.get(chunk.fileName) ?? 0) + 1, + ); + } + if (!filesPopularity.size) { + writeWarning(io, "[No files]"); + } else { + const sortedFiles = Array.from(filesPopularity) + .filter(([file, _]) => !filter || file.includes(filter)) + .sort(); + writeNote( + io, + `Found ${sortedFiles.length} ${filter ? "matching" : "total"} files.`, + ); + for (const [file, count] of sortedFiles) { + writeMain( + io, + `${chalk.blue(count.toFixed(0).padStart(7))} ${chalk.green(file)}`, + ); + } + } + } + + function purgeFileDef(): iapp.CommandMetadata { + return { + description: "Purge all mentions of a file.", + args: { + fileName: { + description: "File to purge", + type: "string", + }, + }, + options: { + verbose: { + description: "More verbose output", + type: "boolean", + }, + }, + }; + } + handlers.purgeFile.metadata = purgeFileDef(); + async function purgeFile( + args: string[] | iapp.NamedArgs, + io: iapp.InteractiveIo, + ): Promise { + const namedArgs = iapp.parseNamedArguments(args, purgeFileDef()); + const file = namedArgs.fileName as string; + const fileName = fs.existsSync(file) ? fs.realpathSync(file) : file; + await purgeNormalizedFile( + io, + chunkyIndex, + fileName, + namedArgs.verbose ?? verbose, + ); + } + async function _reportIndex( args: string[] | iapp.NamedArgs, io: iapp.InteractiveIo, @@ -297,7 +419,7 @@ export async function interactiveQueryLoop( const namedArgs = iapp.parseNamedArguments(args, keywordsDef()); const index = chunkyIndex.getIndexByName(indexName); if (namedArgs.debug) { - io.writer.writeLine(`[Debug: ${indexName}]`); + writeNote(io, `[Debug: ${indexName}]`); await _debugIndex(io, index, indexName, verbose); return; } @@ -338,10 +460,10 @@ export async function interactiveQueryLoop( } if (!hits.length) { - io.writer.writeLine(`No ${indexName}.`); + writeWarning(io, `No ${indexName}.`); // E.g., "No keywords." return; } else { - io.writer.writeLine(`Found ${hits.length} ${indexName}.`); + writeNote(io, `Found ${hits.length} ${indexName}.`); // TFIDF = TF(t) * IDF(t) = 1 * log(N / (1 + nt)) // - t is a term (in other contexts, a term occurring in a given chunk) @@ -358,8 +480,9 @@ export async function interactiveQueryLoop( return a.item.value.localeCompare(b.item.value); }); for (const hit of hits) { - io.writer.writeLine( - `${hit.score.toFixed(3).padStart(7)}: ${hit.item.value} :: ${(hit.item.sourceIds ?? []).join(", ")}`, + writeMain( + io, + `${hit.score.toFixed(3).padStart(7)}: ${chalk.green(hit.item.value)} :: ${(hit.item.sourceIds ?? []).join(", ")}`, ); } } @@ -373,25 +496,27 @@ export async function interactiveQueryLoop( ): Promise { const allTexts = Array.from(index.text()); for (const text of allTexts) { - if (verbose) io.writer.writeLine(`Text: ${text}`); + if (verbose) writeNote(io, `Text: ${text}`); const hits = await index.nearestNeighborsPairs( text, allTexts.length, ); if (verbose) { for (const hit of hits) { - io.writer.writeLine( + writeNote( + io, `${hit.score.toFixed(3).padStart(7)} ${hit.item.value}`, ); } } if (hits.length < 2) { - io.writer.writeLine(`No hit for ${text}`); + writeWarning(io, `No hit for ${text}`); } else { const end = hits.length - 1; - io.writer.writeLine( - `hits[0].item.value}: ${hits[1].item.value} (${hits[1].score.toFixed(3)}) -- ` + - `${hits[end].item.value} (${hits[end].score.toFixed(3)})`, + writeMain( + io, + `${chalk.green(hits[0].item.value)}: ${chalk.blue(hits[1].item.value)} (${hits[1].score.toFixed(3)}) -- ` + + `${chalk.blue(hits[end].item.value)} (${hits[end].score.toFixed(3)})`, ); } } @@ -411,6 +536,52 @@ export async function interactiveQueryLoop( }); } +export async function purgeNormalizedFile( + io: iapp.InteractiveIo | undefined, + chunkyIndex: ChunkyIndex, + fileName: string, + verbose: boolean, +): Promise { + // Step 1: find chunks to remove. + let toDelete: Set = new Set(); + for await (const chunk of chunkyIndex.chunkFolder.allObjects()) { + if (chunk.fileName === fileName) { + toDelete.add(chunk.id); + if (verbose) writeNote(io, `[Purging chunk ${chunk.id}]`); + } + } + if (!toDelete.size) { + writeNote(io, `[No chunks to purge for file ${fileName}]`); + return; + } + + // Step 2: remove chunks. + writeNote( + io, + `[Purging ${toDelete.size} existing chunks for file ${fileName}]`, + ); + for (const id of toDelete) { + if (verbose) writeNote(io, `[Purging chunk ${id}]`); + await chunkyIndex.chunkFolder.remove(id); + } + + // Step 3: remove chunk ids from indexes. + const deletions: ChunkId[] = Array.from(toDelete); + for (const [name, index] of chunkyIndex.allIndexes()) { + let updates = 0; + for await (const textBlock of index.entries()) { + if (textBlock?.sourceIds?.some((id) => deletions.includes(id))) { + if (verbose) { + writeNote(io, `[Purging ${name} entry ${textBlock.value}]`); + } + await index.remove(textBlock.value, deletions); + updates++; + } + } + writeNote(io, `[Purged ${updates} ${name}]`); // name is plural, e.g. "keywords". + } +} + async function processQuery( input: string, chunkyIndex: ChunkyIndex, @@ -464,12 +635,16 @@ async function proposeQueries( makeQueryMakerPrompt(input), ); if (!result.success) { - io.writer.writeLine(`[Error: ${result.message}]`); + writeError(io, `[Error: ${result.message}]`); return undefined; } const specs = result.data; - if (queryOptions.verbose) - io.writer.writeLine(JSON.stringify(specs, null, 2)); + if (queryOptions.verbose) { + // Use util.inspect() to colorize JSON; writeColor() doesn't do that. + io.writer.writeLine( + util.inspect(specs, { depth: null, colors: true, compact: false }), + ); + } return specs; } @@ -482,12 +657,10 @@ async function runIndexQueries( const chunkIdScores: Map> = new Map(); // Record score of each chunk id. const totalNumChunks = await chunkyIndex.chunkFolder.size(); // Nominator in IDF calculation. - for (const namedIndex of chunkyIndex.allIndexes()) { - const indexName = namedIndex.name; - const index = namedIndex.index; + for (const [indexName, index] of chunkyIndex.allIndexes()) { const spec: QuerySpec | undefined = (proposedQueries as any)[indexName]; if (!spec) { - io.writer.writeLine(`[No query for ${indexName}]`); + writeWarning(io, `[No query for ${indexName}]`); continue; } const hits = await index.nearestNeighborsPairs( @@ -496,7 +669,7 @@ async function runIndexQueries( spec.minScore ?? queryOptions.minScore, ); if (!hits.length) { - io.writer.writeLine(`[No hits for ${indexName}]`); + writeWarning(io, `[No hits for ${indexName}]`); continue; } @@ -527,11 +700,13 @@ async function runIndexQueries( // Verbose logging. if (queryOptions.verbose) { - io.writer.writeLine( + writeNote( + io, `\nFound ${hits.length} ${indexName} for '${spec.query}':`, ); for (const hit of hits) { - io.writer.writeLine( + writeNote( + io, `${hit.score.toFixed(3).padStart(7)}: ${hit.item.value} -- ${hit.item.sourceIds?.join(", ")}`, ); } @@ -540,7 +715,8 @@ async function runIndexQueries( // Regular logging. const numChunks = new Set(hits.flatMap((h) => h.item.sourceIds)).size; const end = hits.length - 1; - io.writer.writeLine( + writeNote( + io, `[${indexName}: query '${spec.query}'; ${hits.length} hits; ` + `scores ${hits[0].score.toFixed(3)}--${hits[end].score.toFixed(3)}; ` + `${numChunks} unique chunk ids]`, @@ -548,7 +724,7 @@ async function runIndexQueries( } if (proposedQueries.unknownText) { - io.writer.writeLine(`[Unknown text: ${proposedQueries.unknownText}]`); + writeWarning(io, `[Unknown text: ${proposedQueries.unknownText}]`); } return chunkIdScores; @@ -566,9 +742,7 @@ async function generateAnswer( chunkIdScores.values(), ); - io.writer.writeLine( - `\n[Overall ${scoredChunkIds.length} unique chunk ids]`, - ); + writeNote(io, `\n[Overall ${scoredChunkIds.length} unique chunk ids]`); scoredChunkIds.sort((a, b) => b.score - a.score); scoredChunkIds.splice(20); // Arbitrary number. (TODO: Make it an option.) @@ -581,13 +755,13 @@ async function generateAnswer( if (maybeChunk) chunks.push(maybeChunk); } - io.writer.writeLine(`[Sending ${chunks.length} chunks to answerMaker]`); + writeNote(io, `[Sending ${chunks.length} chunks to answerMaker]`); // Step 3c: Make the request and check for success. const request = JSON.stringify(chunks); if (queryOptions.verbose) { - io.writer.writeLine(`Request: ${JSON.stringify(chunks, null, 2)}`); + writeNote(io, `Request: ${JSON.stringify(chunks, null, 2)}`); } const answerResult = await chunkyIndex.answerMaker.translate( @@ -596,12 +770,13 @@ async function generateAnswer( ); if (!answerResult.success) { - io.writer.writeLine(`[Error: ${answerResult.message}]`); + writeError(io, `[Error: ${answerResult.message}]`); return undefined; } if (queryOptions.verbose) { - io.writer.writeLine( + writeNote( + io, `AnswerResult: ${JSON.stringify(answerResult.data, null, 2)}`, ); } @@ -610,13 +785,16 @@ async function generateAnswer( } function reportQuery(answer: AnswerSpecs, io: iapp.InteractiveIo): void { - io.writer.writeLine( + writeHeading( + io, `\nAnswer (confidence ${answer.confidence.toFixed(3).replace(/0+$/, "")}):`, ); - io.writer.writeLine(wordWrap(answer.answer)); - if (answer.message) io.writer.writeLine(`Message: ${answer.message}`); + writeMain(io, wordWrap(answer.answer)); + if (answer.message) + writeWarning(io, "\n" + wordWrap(`Message: ${answer.message}`)); if (answer.references.length) { - io.writer.writeLine( + writeNote( + io, `\nReferences (${answer.references.length}): ${answer.references.join(",").replace(/,/g, ", ")}`, ); } @@ -698,10 +876,11 @@ function writeChunkLines( outer: for (const blob of chunk.blobs) { for (let i = 0; i < blob.lines.length; i++) { if (lineBudget-- <= 0) { - io.writer.writeLine(" ..."); + writeNote(io, " ..."); break outer; } - io.writer.writeLine( + writeMain( + io, `${(1 + blob.start + i).toString().padStart(6)}: ${blob.lines[i].trimEnd()}`, ); } diff --git a/ts/pnpm-lock.yaml b/ts/pnpm-lock.yaml index 1b7144fbc..525564272 100644 --- a/ts/pnpm-lock.yaml +++ b/ts/pnpm-lock.yaml @@ -220,6 +220,9 @@ importers: aiclient: specifier: workspace:* version: link:../../packages/aiclient + chalk: + specifier: ^5.3.0 + version: 5.3.0 code-processor: specifier: workspace:* version: link:../../packages/codeProcessor