Major upgrades (15 items): 1. System prompt overhaul: tool usage guide, local model guidance, error recovery, safety guidelines, read/mutating tool categorization (12 tests) 2. Multiline input: Shift+Enter for newlines, bracketed paste support, Enter submits single-line, Ctrl+Enter forces submit on multiline 3. /undo command: removes last user turn + assistant/tool messages, preserves filesystem changes, shows count (7 tests) 4. Thinking/reasoning token support: capture `reasoning_content` from DeepSeek/QwQ-style models, render as dimmed collapsible block 5. @ mention improvements: fuzzy path matching (character-order matching), 5-minute cache TTL for file list refresh 6. readFile binary guard: 10MB size limit, extension-based binary detection, null-byte heuristic, CRLF line-ending fix 7. writeFile atomic write: temp file + rename pattern, explicit ENOENT check 8. bash string accumulation: O(n²) → O(n) with array chunks 9. estimateTokens regex speedup: per-character loop → regex bulk counting 10. handleCompletedMessage: `any` → `Record<string, unknown>` 11. Session restore: persist `allowedTools` in SessionRecord, restore on resume 12. Context window parallel detection: Promise.allSettled for Ollama + LM Studio 13. Streaming text accumulation: O(n²) → O(n) with text chunks array 14. /dashboard & context bar already implemented (no changes needed) 15. Cost estimation already implemented (no changes needed) Tests: 322 passing across 42 files, typecheck clean, build 289.56 KB
54 lines
1.9 KiB
TypeScript
54 lines
1.9 KiB
TypeScript
import { createPatch } from "diff";
|
|
import { randomUUID } from "node:crypto";
|
|
import { mkdir, readFile as fsReadFile, rename, unlink, writeFile as fsWriteFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { z } from "zod";
|
|
import { resolveWithinCwd } from "./pathGuard.js";
|
|
import type { ToolDef } from "./types.js";
|
|
|
|
const schema = z.object({
|
|
path: z.string().describe("File path to write, relative to the working directory or absolute."),
|
|
content: z.string().describe("Full file content to write."),
|
|
});
|
|
|
|
async function readExisting(resolved: string): Promise<string | null> {
|
|
try {
|
|
return await fsReadFile(resolved, "utf-8");
|
|
} catch (err: any) {
|
|
if (err?.code === "ENOENT") return null;
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
export const writeFileTool: ToolDef<z.infer<typeof schema>> = {
|
|
name: "write_file",
|
|
description: "Create or overwrite a file with the given content. Use for new files or full rewrites. For small changes to an existing file, prefer edit_file instead.",
|
|
schema,
|
|
mutating: true,
|
|
preview: async ({ path: filePath, content }, ctx) => {
|
|
let resolved: string;
|
|
try {
|
|
resolved = resolveWithinCwd(ctx.cwd, filePath);
|
|
} catch (err) {
|
|
return (err as Error).message;
|
|
}
|
|
const existing = await readExisting(resolved);
|
|
if (existing === null) {
|
|
return `Create new file ${resolved} (${content.length} chars)`;
|
|
}
|
|
return createPatch(resolved, existing, content, "", "");
|
|
},
|
|
handler: async ({ path: filePath, content }, ctx) => {
|
|
const resolved = resolveWithinCwd(ctx.cwd, filePath);
|
|
await mkdir(path.dirname(resolved), { recursive: true });
|
|
const tmpPath = resolved + ".tmp-" + randomUUID();
|
|
try {
|
|
await fsWriteFile(tmpPath, content, "utf-8");
|
|
await rename(tmpPath, resolved);
|
|
} catch (err) {
|
|
try { await unlink(tmpPath); } catch {}
|
|
throw err;
|
|
}
|
|
return { path: resolved, bytesWritten: Buffer.byteLength(content, "utf-8") };
|
|
},
|
|
}; |