// // util.js — small helpers shared across the extension. // const LOG_PREFIX = "[claude]"; /** Log only when the extension is running in development mode. */ function log(...args) { if (nova.inDevMode()) { console.log(LOG_PREFIX, ...args); } } function warn(...args) { console.warn(LOG_PREFIX, ...args); } /** Read a config key, preferring the workspace value when it is set. */ function conf(key, fallback) { let value = nova.workspace.config.get(key); if (value === null || value === undefined || value === "") { value = nova.config.get(key); } if (value === null || value === undefined || value === "") { return fallback; } return value; } // The CLI reports "default" for the mode its own flag calls "manual". const PERMISSION_MODE_LABELS = { default: "Ask each time", manual: "Ask each time", acceptEdits: "Accept edits", plan: "Plan only", auto: "Auto", dontAsk: "Never ask", bypassPermissions: "Bypass (dangerous)", }; function modeLabel(mode) { if (!mode) return ""; return PERMISSION_MODE_LABELS[mode] || mode; } /** Collapse whitespace and cut to `max` characters with an ellipsis. */ function oneLine(text, max = 80) { if (!text) return ""; const flat = String(text).replace(/\s+/g, " ").trim(); return flat.length > max ? flat.slice(0, max - 1) + "…" : flat; } /** Strip ANSI escapes — the CLI may include them in human-readable reasons. */ const ANSI_PATTERN = new RegExp("\\[[0-9;]*[A-Za-z]", "g"); function stripAnsi(text) { if (!text) return ""; return String(text).replace(ANSI_PATTERN, ""); } /** Path relative to the workspace, when it is inside one. */ function relativize(path) { if (!path) return ""; if (nova.workspace.path && path.startsWith(nova.workspace.path)) { const rel = path.slice(nova.workspace.path.length).replace(/^\//, ""); return rel || nova.path.basename(path); } return path; } function basename(path) { if (!path) return ""; return nova.path.basename(path); } function formatCost(usd) { if (typeof usd !== "number" || Number.isNaN(usd)) return null; if (usd === 0) return "$0.00"; if (usd < 0.01) return "<$0.01"; return "$" + usd.toFixed(2); } function formatTokens(n) { if (typeof n !== "number" || Number.isNaN(n)) return null; if (n < 1000) return String(n); if (n < 1000000) return (n / 1000).toFixed(n < 10000 ? 1 : 0) + "k"; return (n / 1000000).toFixed(1) + "M"; } function formatDuration(ms) { if (typeof ms !== "number" || Number.isNaN(ms)) return null; const s = ms / 1000; if (s < 60) return s.toFixed(1) + "s"; const m = Math.floor(s / 60); return `${m}m ${Math.round(s - m * 60)}s`; } /** * Trailing-edge throttle: coalesces bursts into one call every `delay` ms. * The returned function has `.flush()` to run a pending call immediately. */ function throttle(fn, delay) { let timer = null; let pending = false; let lastArgs = null; const invoke = (args) => { fn(...(args || [])); timer = setTimeout(() => { timer = null; if (pending) { pending = false; invoke(lastArgs); } }, delay); }; const throttled = (...args) => { lastArgs = args; if (timer === null) { invoke(args); } else { pending = true; } }; throttled.flush = () => { if (!pending) return; pending = false; if (timer !== null) { clearTimeout(timer); timer = null; } fn(...(lastArgs || [])); }; throttled.cancel = () => { pending = false; if (timer !== null) { clearTimeout(timer); timer = null; } }; return throttled; } /** Minimal event emitter — keeps subscription ordering obvious. */ class Signal { constructor() { this.handlers = []; } on(handler) { this.handlers.push(handler); return { dispose: () => { const i = this.handlers.indexOf(handler); if (i >= 0) this.handlers.splice(i, 1); }, }; } emit(...args) { for (const handler of this.handlers.slice()) { try { handler(...args); } catch (err) { warn("signal handler failed:", err); } } } } /** * Run a command to completion and collect its output. * Resolves with { status, stdout, stderr } — it never rejects on a non-zero exit. */ function runCommand(command, args, options = {}) { return new Promise((resolve) => { let process; try { process = new Process(command, { args: args, cwd: options.cwd || nova.workspace.path || nova.extension.globalStoragePath, env: options.env || {}, stdio: "pipe", shell: options.shell === true, }); } catch (err) { resolve({ status: -1, stdout: "", stderr: String(err) }); return; } const out = []; const err = []; let settled = false; const settle = (status, extraErr) => { if (settled) return; settled = true; resolve({ status: status, stdout: out.join(""), stderr: err.join("") + (extraErr || ""), }); }; process.onStdout((line) => out.push(line)); process.onStderr((line) => err.push(line)); process.onDidExit((status) => settle(status)); if (options.timeout) { setTimeout(() => { if (settled) return; try { process.terminate(); } catch (_) { /* already gone */ } settle(-1, "\n[timed out]"); }, options.timeout); } try { process.start(); } catch (e) { settle(-1, String(e)); } }); } /** * A writer for a process's stdin. * Nova exposes stdin as a WritableStream that also has a convenience `write()`; * fall back to a stream writer when it does not. */ function processWriter(process) { const stdin = process.stdin; if (!stdin) { throw new Error("Process was started without a writable stdin."); } if (typeof stdin.write === "function") { return { write: (text) => stdin.write(text), close: () => { try { if (typeof stdin.close === "function") stdin.close(); } catch (_) { /* already closed */ } }, }; } const writer = stdin.getWriter(); return { write: (text) => writer.write(text), close: () => { try { writer.close(); } catch (_) { /* already closed */ } }, }; } module.exports = { log, warn, conf, processWriter, modeLabel, oneLine, stripAnsi, relativize, basename, formatCost, formatTokens, formatDuration, throttle, Signal, runCommand, };