Resolve merge conflicts

This commit is contained in:
Kevin van Dijk
2026-02-23 20:06:20 +01:00
298 changed files with 15340 additions and 5719 deletions
+3 -3
View File
@@ -6,6 +6,7 @@ import { Agent } from "../../agent/agent"
import { Provider } from "../../provider/provider"
import path from "path"
import fs from "fs/promises"
import { Filesystem } from "../../util/filesystem"
import matter from "gray-matter"
import { Instance } from "../../project/instance"
import { EOL } from "os"
@@ -202,8 +203,7 @@ const AgentCreateCommand = cmd({
await fs.mkdir(targetPath, { recursive: true })
const file = Bun.file(filePath)
if (await file.exists()) {
if (await Filesystem.exists(filePath)) {
if (isFullyNonInteractive) {
console.error(`Error: Agent file already exists: ${filePath}`)
process.exit(1)
@@ -212,7 +212,7 @@ const AgentCreateCommand = cmd({
throw new UI.CancelledError()
}
await Bun.write(filePath, content)
await Filesystem.write(filePath, content)
if (isFullyNonInteractive) {
console.log(filePath)
+45
View File
@@ -159,6 +159,38 @@ async function handlePluginAuth(plugin: { auth: PluginAuth }, provider: string):
return false
}
/**
* Build a deduplicated list of plugin-registered auth providers that are not
* already present in models.dev, respecting enabled/disabled provider lists.
* Pure function with no side effects; safe to test without mocking.
*/
export function resolvePluginProviders(input: {
hooks: Hooks[]
existingProviders: Record<string, unknown>
disabled: Set<string>
enabled?: Set<string>
providerNames: Record<string, string | undefined>
}): Array<{ id: string; name: string }> {
const seen = new Set<string>()
const result: Array<{ id: string; name: string }> = []
for (const hook of input.hooks) {
if (!hook.auth) continue
const id = hook.auth.provider
if (seen.has(id)) continue
seen.add(id)
if (Object.hasOwn(input.existingProviders, id)) continue
if (input.disabled.has(id)) continue
if (input.enabled && !input.enabled.has(id)) continue
result.push({
id,
name: input.providerNames[id] ?? id,
})
}
return result
}
export const AuthCommand = cmd({
command: "auth",
describe: "manage credentials",
@@ -287,6 +319,14 @@ export const AuthLoginCommand = cmd({
vercel: 7,
}
// kilocode_change end
const pluginProviders = resolvePluginProviders({
hooks: await Plugin.list(),
existingProviders: providers,
disabled,
enabled,
providerNames: Object.fromEntries(Object.entries(config.provider ?? {}).map(([id, p]) => [id, p.name])),
})
let provider = await prompts.autocomplete({
message: "Select provider",
maxItems: 8,
@@ -309,6 +349,11 @@ export const AuthLoginCommand = cmd({
}[x.id],
})),
),
...pluginProviders.map((x) => ({
label: x.name,
value: x.id,
hint: "plugin",
})),
{
value: "other",
label: "Other",
+1 -1
View File
@@ -18,7 +18,7 @@ export const ExportCommand = cmd({
handler: async (args) => {
await bootstrap(process.cwd(), async () => {
let sessionID = args.sessionID
process.stderr.write(`Exporting session: ${sessionID ?? "latest"}`)
process.stderr.write(`Exporting session: ${sessionID ?? "latest"}\n`)
if (!sessionID) {
UI.empty()
+127 -39
View File
@@ -1,5 +1,6 @@
import path from "path"
import { exec } from "child_process"
import { Filesystem } from "../../util/filesystem"
import * as prompts from "@clack/prompts"
import { map, pipe, sortBy, values } from "remeda"
import { Octokit } from "@octokit/rest"
@@ -173,6 +174,18 @@ export function extractResponseText(parts: MessageV2.Part[]): string | null {
throw new Error("Failed to parse response: no parts returned")
}
/**
* Formats a PROMPT_TOO_LARGE error message with details about files in the prompt.
* Content is base64 encoded, so we calculate original size by multiplying by 0.75.
*/
export function formatPromptTooLargeError(files: { filename: string; content: string }[]): string {
const fileDetails =
files.length > 0
? `\n\nFiles in prompt:\n${files.map((f) => ` - ${f.filename} (${((f.content.length * 0.75) / 1024).toFixed(0)} KB)`).join("\n")}`
: ""
return `PROMPT_TOO_LARGE: The prompt exceeds the model's context limit.${fileDetails}`
}
export const GithubCommand = cmd({
command: "github",
describe: "manage GitHub agent",
@@ -368,7 +381,7 @@ export const GithubInstallCommand = cmd({
const envStr = providerEnvStr || kiloGatewayEnv ? `\n env:${providerEnvStr}${kiloGatewayEnv}` : ""
await Bun.write(
await Filesystem.write(
path.join(app.root, WORKFLOW_FILE),
`name: kilo
@@ -549,8 +562,12 @@ export const GithubRunCommand = cmd({
const branch = await checkoutNewBranch(branchPrefix)
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
const response = await chat(userPrompt, promptFiles)
const { dirty, uncommittedChanges } = await branchIsDirty(head)
if (dirty) {
const { dirty, uncommittedChanges, switched } = await branchIsDirty(head, branch)
if (switched) {
// Agent switched branches (likely created its own branch/PR)
console.log("Agent managed its own branch, skipping infrastructure push/PR")
console.log("Response:", response)
} else if (dirty) {
const summary = await summarize(response)
// workflow_dispatch has an actor for co-author attribution, schedule does not
await pushToNewBranch(summary, branch, uncommittedChanges, isScheduleEvent)
@@ -561,7 +578,11 @@ export const GithubRunCommand = cmd({
summary,
`${response}\n\nTriggered by ${triggerType}${footer({ image: true })}`,
)
console.log(`Created PR #${pr}`)
if (pr) {
console.log(`Created PR #${pr}`)
} else {
console.log("Skipped PR creation (no new commits)")
}
} else {
console.log("Response:", response)
}
@@ -576,8 +597,11 @@ export const GithubRunCommand = cmd({
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
const dataPrompt = buildPromptDataForPR(prData)
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
const { dirty, uncommittedChanges } = await branchIsDirty(head)
if (dirty) {
const { dirty, uncommittedChanges, switched } = await branchIsDirty(head, prData.headRefName)
if (switched) {
console.log("Agent managed its own branch, skipping infrastructure push")
}
if (dirty && !switched) {
const summary = await summarize(response)
await pushToLocalBranch(summary, uncommittedChanges)
}
@@ -587,12 +611,15 @@ export const GithubRunCommand = cmd({
}
// Fork PR
else {
await checkoutForkBranch(prData)
const forkBranch = await checkoutForkBranch(prData)
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
const dataPrompt = buildPromptDataForPR(prData)
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
const { dirty, uncommittedChanges } = await branchIsDirty(head)
if (dirty) {
const { dirty, uncommittedChanges, switched } = await branchIsDirty(head, forkBranch)
if (switched) {
console.log("Agent managed its own branch, skipping infrastructure push")
}
if (dirty && !switched) {
const summary = await summarize(response)
await pushToForkBranch(summary, prData, uncommittedChanges)
}
@@ -608,8 +635,13 @@ export const GithubRunCommand = cmd({
const issueData = await fetchIssue()
const dataPrompt = buildPromptDataForIssue(issueData)
const response = await chat(`${userPrompt}\n\n${dataPrompt}`, promptFiles)
const { dirty, uncommittedChanges } = await branchIsDirty(head)
if (dirty) {
const { dirty, uncommittedChanges, switched } = await branchIsDirty(head, branch)
if (switched) {
// Agent switched branches (likely created its own branch/PR).
// Don't push the stale infrastructure branch — just comment.
await createComment(`${response}${footer({ image: true })}`)
await removeReaction(commentType)
} else if (dirty) {
const summary = await summarize(response)
await pushToNewBranch(summary, branch, uncommittedChanges, false)
const pr = await createPR(
@@ -618,7 +650,11 @@ export const GithubRunCommand = cmd({
summary,
`${response}\n\nCloses #${issueId}${footer({ image: true })}`,
)
await createComment(`Created PR #${pr}${footer({ image: true })}`)
if (pr) {
await createComment(`Created PR #${pr}${footer({ image: true })}`)
} else {
await createComment(`${response}${footer({ image: true })}`)
}
await removeReaction(commentType)
} else {
await createComment(`${response}${footer({ image: true })}`)
@@ -811,6 +847,7 @@ export const GithubRunCommand = cmd({
replacement,
})
}
return { userPrompt: prompt, promptFiles: imgData }
}
@@ -918,10 +955,15 @@ export const GithubRunCommand = cmd({
// result should always be assistant just satisfying type checker
if (result.info.role === "assistant" && result.info.error) {
console.error("Agent error:", result.info.error)
throw new Error(
`${result.info.error.name}: ${"message" in result.info.error ? result.info.error.message : ""}`,
)
const err = result.info.error
console.error("Agent error:", err)
if (err.name === "ContextOverflowError") {
throw new Error(formatPromptTooLargeError(files))
}
const errorMsg = err.data?.message || ""
throw new Error(`${err.name}: ${errorMsg}`)
}
const text = extractResponseText(result.parts)
@@ -947,10 +989,15 @@ export const GithubRunCommand = cmd({
})
if (summary.info.role === "assistant" && summary.info.error) {
console.error("Summary agent error:", summary.info.error)
throw new Error(
`${summary.info.error.name}: ${"message" in summary.info.error ? summary.info.error.message : ""}`,
)
const err = summary.info.error
console.error("Summary agent error:", err)
if (err.name === "ContextOverflowError") {
throw new Error(formatPromptTooLargeError(files))
}
const errorMsg = err.data?.message || ""
throw new Error(`${err.name}: ${errorMsg}`)
}
const summaryText = extractResponseText(summary.parts)
@@ -1056,6 +1103,7 @@ export const GithubRunCommand = cmd({
await $`git remote add fork https://github.com/${pr.headRepository.nameWithOwner}.git`
await $`git fetch fork --depth=${depth} ${remoteBranch}`
await $`git checkout -b ${localBranch} fork/${remoteBranch}`
return localBranch
}
function generateBranchName(type: "issue" | "pr" | "schedule" | "dispatch") {
@@ -1113,23 +1161,44 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
await $`git push fork HEAD:${remoteBranch}`
}
async function branchIsDirty(originalHead: string) {
async function branchIsDirty(originalHead: string, expectedBranch: string) {
console.log("Checking if branch is dirty...")
// Detect if the agent switched branches during chat (e.g. created
// its own branch, committed, and possibly pushed/created a PR).
const current = (await $`git rev-parse --abbrev-ref HEAD`).stdout.toString().trim()
if (current !== expectedBranch) {
console.log(`Branch changed during chat: expected ${expectedBranch}, now on ${current}`)
return { dirty: true, uncommittedChanges: false, switched: true }
}
const ret = await $`git status --porcelain`
const status = ret.stdout.toString().trim()
if (status.length > 0) {
return {
dirty: true,
uncommittedChanges: true,
}
return { dirty: true, uncommittedChanges: true, switched: false }
}
const head = await $`git rev-parse HEAD`
const head = (await $`git rev-parse HEAD`).stdout.toString().trim()
return {
dirty: head.stdout.toString().trim() !== originalHead,
dirty: head !== originalHead,
uncommittedChanges: false,
switched: false,
}
}
// Verify commits exist between base ref and a branch using rev-list.
// Falls back to fetching from origin when local refs are missing
// (common in shallow clones from actions/checkout).
async function hasNewCommits(base: string, head: string) {
const result = await $`git rev-list --count ${base}..${head}`.nothrow()
if (result.exitCode !== 0) {
console.log(`rev-list failed, fetching origin/${base}...`)
await $`git fetch origin ${base} --depth=1`.nothrow()
const retry = await $`git rev-list --count origin/${base}..${head}`.nothrow()
if (retry.exitCode !== 0) return true // assume dirty if we can't tell
return parseInt(retry.stdout.toString().trim()) > 0
}
return parseInt(result.stdout.toString().trim()) > 0
}
async function assertPermissions() {
// Only called for non-schedule events, so actor is defined
console.log(`Asserting permissions for user ${actor}...`)
@@ -1249,7 +1318,7 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
})
}
async function createPR(base: string, branch: string, title: string, body: string) {
async function createPR(base: string, branch: string, title: string, body: string): Promise<number | null> {
console.log("Creating pull request...")
// Check if an open PR already exists for this head→base combination
@@ -1274,17 +1343,36 @@ Co-authored-by: ${actor} <${actor}@users.noreply.github.com>"`
console.log(`Failed to check for existing PR: ${e}`)
}
const pr = await withRetry(() =>
octoRest.rest.pulls.create({
owner,
repo,
head: branch,
base,
title,
body,
}),
)
return pr.data.number
// Verify there are commits between base and head before creating the PR.
// In shallow clones, the branch can appear dirty but share the same
// commit as the base, causing a 422 from GitHub.
if (!(await hasNewCommits(base, branch))) {
console.log(`No commits between ${base} and ${branch}, skipping PR creation`)
return null
}
try {
const pr = await withRetry(() =>
octoRest.rest.pulls.create({
owner,
repo,
head: branch,
base,
title,
body,
}),
)
return pr.data.number
} catch (e: unknown) {
// Handle "No commits between X and Y" validation error from GitHub.
// This can happen when the branch was pushed but has no new commits
// relative to the base (e.g. shallow clone edge cases).
if (e instanceof Error && e.message.includes("No commits between")) {
console.log(`GitHub rejected PR: ${e.message}`)
return null
}
throw e
}
}
async function withRetry<T>(fn: () => Promise<T>, retries = 1, delayMs = 5000): Promise<T> {
+2 -2
View File
@@ -8,6 +8,7 @@ import { SessionTable, MessageTable, PartTable } from "../../session/session.sql
import { Instance } from "../../project/instance"
import { ShareNext } from "../../share/share-next"
import { EOL } from "os"
import { Filesystem } from "../../util/filesystem"
/** Discriminated union returned by the ShareNext API (GET /api/share/:id/data) */
export type ShareData =
@@ -116,8 +117,7 @@ export const ImportCommand = cmd({
exportData = transformed
} else {
const file = Bun.file(args.file)
exportData = await file.json().catch(() => {})
exportData = await Filesystem.readJson<NonNullable<typeof exportData>>(args.file).catch(() => undefined)
if (!exportData) {
process.stdout.write(`File not found: ${args.file}`)
process.stdout.write(EOL)
+5 -6
View File
@@ -13,6 +13,7 @@ import { Installation } from "../../installation"
import path from "path"
import { Global } from "../../global"
import { modify, applyEdits } from "jsonc-parser"
import { Filesystem } from "../../util/filesystem"
import { Bus } from "../../bus"
function getAuthStatusIcon(status: MCP.AuthStatus): string {
@@ -388,7 +389,7 @@ async function resolveConfigPath(baseDir: string, global = false) {
}
for (const candidate of candidates) {
if (await Bun.file(candidate).exists()) {
if (await Filesystem.exists(candidate)) {
return candidate
}
}
@@ -398,11 +399,9 @@ async function resolveConfigPath(baseDir: string, global = false) {
}
async function addMcpToConfig(name: string, mcpConfig: Config.Mcp, configPath: string) {
const file = Bun.file(configPath)
let text = "{}"
if (await file.exists()) {
text = await file.text()
if (await Filesystem.exists(configPath)) {
text = await Filesystem.readText(configPath)
}
// Use jsonc-parser to modify while preserving comments
@@ -411,7 +410,7 @@ async function addMcpToConfig(name: string, mcpConfig: Config.Mcp, configPath: s
})
const result = applyEdits(text, edits)
await Bun.write(configPath, result)
await Filesystem.write(configPath, result)
return configPath
}
+22 -15
View File
@@ -6,6 +6,7 @@ import { cmd } from "./cmd"
import { Flag } from "../../flag/flag"
import { bootstrap } from "../bootstrap"
import { EOL } from "os"
import { Filesystem } from "../../util/filesystem"
import { createOpencodeClient, type Message, type OpencodeClient, type ToolPart } from "@kilocode/sdk/v2"
import { Server } from "../../server/server"
import { Provider } from "../../provider/provider"
@@ -167,12 +168,17 @@ function websearch(info: ToolProps<typeof WebSearchTool>) {
}
function task(info: ToolProps<typeof TaskTool>) {
const agent = Locale.titlecase(info.input.subagent_type)
const desc = info.input.description
const started = info.part.state.status === "running"
const input = info.part.state.input
const status = info.part.state.status
const subagent =
typeof input.subagent_type === "string" && input.subagent_type.trim().length > 0 ? input.subagent_type : "unknown"
const agent = Locale.titlecase(subagent)
const desc =
typeof input.description === "string" && input.description.trim().length > 0 ? input.description : undefined
const icon = status === "error" ? "✗" : status === "running" ? "•" : "✓"
const name = desc ?? `${agent} Task`
inline({
icon: started ? "•" : "✓",
icon,
title: name,
description: desc ? `${agent} Agent` : undefined,
})
@@ -324,19 +330,12 @@ export const RunCommand = cmd({
for (const filePath of list) {
const resolvedPath = path.resolve(process.cwd(), filePath)
const file = Bun.file(resolvedPath)
const stats = await file.stat().catch(() => {})
if (!stats) {
UI.error(`File not found: ${filePath}`)
process.exit(1)
}
if (!(await file.exists())) {
if (!(await Filesystem.exists(resolvedPath))) {
UI.error(`File not found: ${filePath}`)
process.exit(1)
}
const stat = await file.stat()
const mime = stat.isDirectory() ? "application/x-directory" : "text/plain"
const mime = (await Filesystem.isDir(resolvedPath)) ? "application/x-directory" : "text/plain"
files.push({
type: "file",
@@ -466,9 +465,17 @@ export const RunCommand = cmd({
const part = event.properties.part
if (part.sessionID !== sessionID) continue
if (part.type === "tool" && part.state.status === "completed") {
if (part.type === "tool" && (part.state.status === "completed" || part.state.status === "error")) {
if (emit("tool_use", { part })) continue
tool(part)
if (part.state.status === "completed") {
tool(part)
continue
}
inline({
icon: "✗",
title: `${part.tool} failed`,
})
UI.error(part.state.error)
}
if (
+33 -17
View File
@@ -5,6 +5,7 @@ import { bootstrap } from "../bootstrap"
import { UI } from "../ui"
import { Locale } from "../../util/locale"
import { Flag } from "../../flag/flag"
import { Filesystem } from "../../util/filesystem"
import { EOL } from "os"
import path from "path"
@@ -17,18 +18,18 @@ function pagerCmd(): string[] {
// user could have less installed via other options
const lessOnPath = Bun.which("less")
if (lessOnPath) {
if (Bun.file(lessOnPath).size) return [lessOnPath, ...lessOptions]
if (Filesystem.stat(lessOnPath)?.size) return [lessOnPath, ...lessOptions]
}
if (Flag.KILO_GIT_BASH_PATH) {
const less = path.join(Flag.KILO_GIT_BASH_PATH, "..", "..", "usr", "bin", "less.exe")
if (Bun.file(less).size) return [less, ...lessOptions]
if (Filesystem.stat(less)?.size) return [less, ...lessOptions]
}
const git = Bun.which("git")
if (git) {
const less = path.join(git, "..", "..", "usr", "bin", "less.exe")
if (Bun.file(less).size) return [less, ...lessOptions]
if (Filesystem.stat(less)?.size) return [less, ...lessOptions]
}
// Fall back to Windows built-in more (via cmd.exe)
@@ -38,10 +39,34 @@ function pagerCmd(): string[] {
export const SessionCommand = cmd({
command: "session",
describe: "manage sessions",
builder: (yargs: Argv) => yargs.command(SessionListCommand).demandCommand(),
builder: (yargs: Argv) => yargs.command(SessionListCommand).command(SessionDeleteCommand).demandCommand(),
async handler() {},
})
export const SessionDeleteCommand = cmd({
command: "delete <sessionID>",
describe: "delete a session",
builder: (yargs: Argv) => {
return yargs.positional("sessionID", {
describe: "session ID to delete",
type: "string",
demandOption: true,
})
},
handler: async (args) => {
await bootstrap(process.cwd(), async () => {
try {
await Session.get(args.sessionID)
} catch {
UI.error(`Session not found: ${args.sessionID}`)
process.exit(1)
}
await Session.remove(args.sessionID)
UI.println(UI.Style.TEXT_SUCCESS_BOLD + `Session ${args.sessionID} deleted` + UI.Style.TEXT_NORMAL)
})
},
})
export const SessionListCommand = cmd({
command: "list",
describe: "list sessions",
@@ -61,26 +86,17 @@ export const SessionListCommand = cmd({
},
handler: async (args) => {
await bootstrap(process.cwd(), async () => {
const sessions = []
for await (const session of Session.list()) {
if (!session.parentID) {
sessions.push(session)
}
}
const sessions = [...Session.list({ roots: true, limit: args.maxCount })]
sessions.sort((a, b) => b.time.updated - a.time.updated)
const limitedSessions = args.maxCount ? sessions.slice(0, args.maxCount) : sessions
if (limitedSessions.length === 0) {
if (sessions.length === 0) {
return
}
let output: string
if (args.format === "json") {
output = formatSessionJSON(limitedSessions)
output = formatSessionJSON(sessions)
} else {
output = formatSessionTable(limitedSessions)
output = formatSessionTable(sessions)
}
const shouldPaginate = process.stdout.isTTY && !args.maxCount && args.format === "table"
@@ -1,9 +1,10 @@
import path from "path"
import { Global } from "@/global"
import { Filesystem } from "@/util/filesystem"
import { onMount } from "solid-js"
import { createStore } from "solid-js/store"
import { createSimpleContext } from "../../context/helper"
import { appendFile } from "fs/promises"
import { appendFile, writeFile } from "fs/promises"
function calculateFrecency(entry?: { frequency: number; lastOpen: number }): number {
if (!entry) return 0
@@ -17,9 +18,9 @@ const MAX_FRECENCY_ENTRIES = 1000
export const { use: useFrecency, provider: FrecencyProvider } = createSimpleContext({
name: "Frecency",
init: () => {
const frecencyFile = Bun.file(path.join(Global.Path.state, "frecency.jsonl"))
const frecencyPath = path.join(Global.Path.state, "frecency.jsonl")
onMount(async () => {
const text = await frecencyFile.text().catch(() => "")
const text = await Filesystem.readText(frecencyPath).catch(() => "")
const lines = text
.split("\n")
.filter(Boolean)
@@ -53,7 +54,7 @@ export const { use: useFrecency, provider: FrecencyProvider } = createSimpleCont
if (sorted.length > 0) {
const content = sorted.map((entry) => JSON.stringify(entry)).join("\n") + "\n"
Bun.write(frecencyFile, content).catch(() => {})
writeFile(frecencyPath, content).catch(() => {})
}
})
@@ -68,7 +69,7 @@ export const { use: useFrecency, provider: FrecencyProvider } = createSimpleCont
lastOpen: Date.now(),
}
setStore("data", absolutePath, newEntry)
appendFile(frecencyFile.name!, JSON.stringify({ path: absolutePath, ...newEntry }) + "\n").catch(() => {})
appendFile(frecencyPath, JSON.stringify({ path: absolutePath, ...newEntry }) + "\n").catch(() => {})
if (Object.keys(store.data).length > MAX_FRECENCY_ENTRIES) {
const sorted = Object.entries(store.data)
@@ -76,7 +77,7 @@ export const { use: useFrecency, provider: FrecencyProvider } = createSimpleCont
.slice(0, MAX_FRECENCY_ENTRIES)
setStore("data", Object.fromEntries(sorted))
const content = sorted.map(([path, entry]) => JSON.stringify({ path, ...entry })).join("\n") + "\n"
Bun.write(frecencyFile, content).catch(() => {})
writeFile(frecencyPath, content).catch(() => {})
}
}
@@ -1,5 +1,6 @@
import path from "path"
import { Global } from "@/global"
import { Filesystem } from "@/util/filesystem"
import { onMount } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { clone } from "remeda"
@@ -30,9 +31,9 @@ const MAX_HISTORY_ENTRIES = 50
export const { use: usePromptHistory, provider: PromptHistoryProvider } = createSimpleContext({
name: "PromptHistory",
init: () => {
const historyFile = Bun.file(path.join(Global.Path.state, "prompt-history.jsonl"))
const historyPath = path.join(Global.Path.state, "prompt-history.jsonl")
onMount(async () => {
const text = await historyFile.text().catch(() => "")
const text = await Filesystem.readText(historyPath).catch(() => "")
const lines = text
.split("\n")
.filter(Boolean)
@@ -51,7 +52,7 @@ export const { use: usePromptHistory, provider: PromptHistoryProvider } = create
// Rewrite file with only valid entries to self-heal corruption
if (lines.length > 0) {
const content = lines.map((line) => JSON.stringify(line)).join("\n") + "\n"
writeFile(historyFile.name!, content).catch(() => {})
writeFile(historyPath, content).catch(() => {})
}
})
@@ -97,11 +98,11 @@ export const { use: usePromptHistory, provider: PromptHistoryProvider } = create
if (trimmed) {
const content = store.history.map((line) => JSON.stringify(line)).join("\n") + "\n"
writeFile(historyFile.name!, content).catch(() => {})
writeFile(historyPath, content).catch(() => {})
return
}
appendFile(historyFile.name!, JSON.stringify(entry) + "\n").catch(() => {})
appendFile(historyPath, JSON.stringify(entry) + "\n").catch(() => {})
},
}
},
@@ -1,6 +1,8 @@
import { BoxRenderable, TextareaRenderable, MouseEvent, PasteEvent, t, dim, fg } from "@opentui/core"
import { createEffect, createMemo, type JSX, onMount, createSignal, onCleanup, on, Show, Switch, Match } from "solid-js"
import "opentui-spinner/solid"
import path from "path"
import { Filesystem } from "@/util/filesystem"
import { useLocal } from "@tui/context/local"
import { useTheme } from "@tui/context/theme"
import { EmptyBorder } from "@tui/component/border"
@@ -696,7 +698,7 @@ export function Prompt(props: PromptProps) {
async function pasteImage(file: { filename?: string; content: string; mime: string }) {
const currentOffset = input.visualCursor.offset
const extmarkStart = currentOffset
const count = store.prompt.parts.filter((x) => x.type === "file").length
const count = store.prompt.parts.filter((x) => x.type === "file" && x.mime.startsWith("image/")).length
const virtualText = `[Image ${count + 1}]`
const extmarkEnd = extmarkStart + virtualText.length
const textToInsert = virtualText + " "
@@ -948,26 +950,26 @@ export function Prompt(props: PromptProps) {
const isUrl = /^(https?):\/\//.test(filepath)
if (!isUrl) {
try {
const file = Bun.file(filepath)
const mime = Filesystem.mimeType(filepath)
const filename = path.basename(filepath)
// Handle SVG as raw text content, not as base64 image
if (file.type === "image/svg+xml") {
if (mime === "image/svg+xml") {
event.preventDefault()
const content = await file.text().catch(() => {})
const content = await Filesystem.readText(filepath).catch(() => {})
if (content) {
pasteText(content, `[SVG: ${file.name ?? "image"}]`)
pasteText(content, `[SVG: ${filename ?? "image"}]`)
return
}
}
if (file.type.startsWith("image/")) {
if (mime.startsWith("image/")) {
event.preventDefault()
const content = await file
.arrayBuffer()
const content = await Filesystem.readArrayBuffer(filepath)
.then((buffer) => Buffer.from(buffer).toString("base64"))
.catch(() => {})
if (content) {
await pasteImage({
filename: file.name,
mime: file.type,
filename,
mime,
content,
})
return
@@ -1,5 +1,6 @@
import path from "path"
import { Global } from "@/global"
import { Filesystem } from "@/util/filesystem"
import { onMount } from "solid-js"
import { createStore, produce } from "solid-js/store"
import { clone } from "remeda"
@@ -18,9 +19,9 @@ const MAX_STASH_ENTRIES = 50
export const { use: usePromptStash, provider: PromptStashProvider } = createSimpleContext({
name: "PromptStash",
init: () => {
const stashFile = Bun.file(path.join(Global.Path.state, "prompt-stash.jsonl"))
const stashPath = path.join(Global.Path.state, "prompt-stash.jsonl")
onMount(async () => {
const text = await stashFile.text().catch(() => "")
const text = await Filesystem.readText(stashPath).catch(() => "")
const lines = text
.split("\n")
.filter(Boolean)
@@ -39,7 +40,7 @@ export const { use: usePromptStash, provider: PromptStashProvider } = createSimp
// Rewrite file with only valid entries to self-heal corruption
if (lines.length > 0) {
const content = lines.map((line) => JSON.stringify(line)).join("\n") + "\n"
writeFile(stashFile.name!, content).catch(() => {})
writeFile(stashPath, content).catch(() => {})
}
})
@@ -66,11 +67,11 @@ export const { use: usePromptStash, provider: PromptStashProvider } = createSimp
if (trimmed) {
const content = store.entries.map((line) => JSON.stringify(line)).join("\n") + "\n"
writeFile(stashFile.name!, content).catch(() => {})
writeFile(stashPath, content).catch(() => {})
return
}
appendFile(stashFile.name!, JSON.stringify(stash) + "\n").catch(() => {})
appendFile(stashPath, JSON.stringify(stash) + "\n").catch(() => {})
},
pop() {
if (store.entries.length === 0) return undefined
@@ -82,7 +83,7 @@ export const { use: usePromptStash, provider: PromptStashProvider } = createSimp
)
const content =
store.entries.length > 0 ? store.entries.map((line) => JSON.stringify(line)).join("\n") + "\n" : ""
writeFile(stashFile.name!, content).catch(() => {})
writeFile(stashPath, content).catch(() => {})
return entry
},
remove(index: number) {
@@ -94,7 +95,7 @@ export const { use: usePromptStash, provider: PromptStashProvider } = createSimp
)
const content =
store.entries.length > 0 ? store.entries.map((line) => JSON.stringify(line)).join("\n") + "\n" : ""
writeFile(stashFile.name!, content).catch(() => {})
writeFile(stashPath, content).catch(() => {})
},
}
},
@@ -34,7 +34,6 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({
renderer.setTerminalTitle("")
renderer.destroy()
win32FlushInputBuffer()
await input.onExit?.()
if (reason) {
const formatted = FormatError(reason) ?? FormatUnknownError(reason)
if (formatted) {
@@ -43,7 +42,7 @@ export const { use: useExit, provider: ExitProvider } = createSimpleContext({
}
const text = store.get()
if (text) process.stdout.write(text + "\n")
process.exit(0)
await input.onExit?.()
},
{
message: store,
@@ -1,4 +1,5 @@
import { Global } from "@/global"
import { Filesystem } from "@/util/filesystem"
import { createSignal, type Setter } from "solid-js"
import { createStore } from "solid-js/store"
import { createSimpleContext } from "./helper"
@@ -9,10 +10,9 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
init: () => {
const [ready, setReady] = createSignal(false)
const [store, setStore] = createStore<Record<string, any>>()
const file = Bun.file(path.join(Global.Path.state, "kv.json"))
const filePath = path.join(Global.Path.state, "kv.json")
file
.json()
Filesystem.readJson(filePath)
.then((x) => {
setStore(x)
})
@@ -44,7 +44,7 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
},
set(key: string, value: any) {
setStore(key, value)
Bun.write(file, JSON.stringify(store, null, 2))
Filesystem.writeJson(filePath, store)
},
}
return result
@@ -12,6 +12,7 @@ import { Provider } from "@/provider/provider"
import { useArgs } from "./args"
import { useSDK } from "./sdk"
import { RGBA } from "@opentui/core"
import { Filesystem } from "@/util/filesystem"
export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
name: "Local",
@@ -119,7 +120,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
variant: {},
})
const file = Bun.file(path.join(Global.Path.state, "model.json"))
const filePath = path.join(Global.Path.state, "model.json")
const state = {
pending: false,
}
@@ -130,19 +131,15 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
return
}
state.pending = false
Bun.write(
file,
JSON.stringify({
recent: modelStore.recent,
favorite: modelStore.favorite,
variant: modelStore.variant,
}),
)
Filesystem.writeJson(filePath, {
recent: modelStore.recent,
favorite: modelStore.favorite,
variant: modelStore.variant,
})
}
file
.json()
.then((x) => {
Filesystem.readJson(filePath)
.then((x: any) => {
if (Array.isArray(x.recent)) setModelStore("recent", x.recent)
if (Array.isArray(x.favorite)) setModelStore("favorite", x.favorite)
if (typeof x.variant === "object" && x.variant !== null) setModelStore("variant", x.variant)
@@ -3,6 +3,7 @@ import path from "path"
import { createEffect, createMemo, onMount } from "solid-js"
import { useSync } from "@tui/context/sync"
import { createSimpleContext } from "./helper"
import { Glob } from "../../../../util/glob"
import aura from "./theme/aura.json" with { type: "json" }
import ayu from "./theme/ayu.json" with { type: "json" }
import catppuccin from "./theme/catppuccin.json" with { type: "json" }
@@ -393,7 +394,6 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
},
})
const CUSTOM_THEME_GLOB = new Bun.Glob("themes/*.json")
async function getCustomThemes() {
const directories = [
Global.Path.config,
@@ -407,14 +407,14 @@ async function getCustomThemes() {
const result: Record<string, ThemeJson> = {}
for (const dir of directories) {
for await (const item of CUSTOM_THEME_GLOB.scan({
absolute: true,
followSymlinks: true,
dot: true,
for (const item of await Glob.scan("themes/*.json", {
cwd: dir,
absolute: true,
dot: true,
symlink: true,
})) {
const name = path.basename(item, ".json")
result[name] = await Bun.file(item).json()
result[name] = await Filesystem.readJson(item)
}
}
return result
@@ -64,12 +64,16 @@ function EditBody(props: { request: PermissionRequest }) {
return (
<box flexDirection="column" gap={1}>
<box flexDirection="row" gap={1} paddingLeft={1}>
<text fg={theme.textMuted}>{"→"}</text>
<text fg={theme.textMuted}>Edit {normalizePath(filepath())}</text>
</box>
<Show when={diff()}>
<scrollbox height="100%">
<scrollbox
height="100%"
verticalScrollbarOptions={{
trackOptions: {
backgroundColor: theme.background,
foregroundColor: theme.borderActive,
},
}}
>
<diff
diff={diff()}
view={view()}
@@ -91,6 +95,11 @@ function EditBody(props: { request: PermissionRequest }) {
/>
</scrollbox>
</Show>
<Show when={!diff()}>
<box paddingLeft={1}>
<text fg={theme.textMuted}>No diff provided</text>
</box>
</Show>
</box>
)
}
@@ -196,76 +205,233 @@ export function PermissionPrompt(props: { request: PermissionRequest }) {
</Match>
<Match when={store.stage === "permission"}>
{(() => {
const info = () => {
const permission = props.request.permission
const data = input()
if (permission === "edit") {
const raw = props.request.metadata?.filepath
const filepath = typeof raw === "string" ? raw : ""
return {
icon: "→",
title: `Edit ${normalizePath(filepath)}`,
body: <EditBody request={props.request} />,
}
}
if (permission === "read") {
const raw = data.filePath
const filePath = typeof raw === "string" ? raw : ""
return {
icon: "→",
title: `Read ${normalizePath(filePath)}`,
body: (
<Show when={filePath}>
<box paddingLeft={1}>
<text fg={theme.textMuted}>{"Path: " + normalizePath(filePath)}</text>
</box>
</Show>
),
}
}
if (permission === "glob") {
const pattern = typeof data.pattern === "string" ? data.pattern : ""
return {
icon: "✱",
title: `Glob "${pattern}"`,
body: (
<Show when={pattern}>
<box paddingLeft={1}>
<text fg={theme.textMuted}>{"Pattern: " + pattern}</text>
</box>
</Show>
),
}
}
if (permission === "grep") {
const pattern = typeof data.pattern === "string" ? data.pattern : ""
return {
icon: "✱",
title: `Grep "${pattern}"`,
body: (
<Show when={pattern}>
<box paddingLeft={1}>
<text fg={theme.textMuted}>{"Pattern: " + pattern}</text>
</box>
</Show>
),
}
}
if (permission === "list") {
const raw = data.path
const dir = typeof raw === "string" ? raw : ""
return {
icon: "→",
title: `List ${normalizePath(dir)}`,
body: (
<Show when={dir}>
<box paddingLeft={1}>
<text fg={theme.textMuted}>{"Path: " + normalizePath(dir)}</text>
</box>
</Show>
),
}
}
if (permission === "bash") {
const title =
typeof data.description === "string" && data.description ? data.description : "Shell command"
const command = typeof data.command === "string" ? data.command : ""
return {
icon: "#",
title,
body: (
<Show when={command}>
<box paddingLeft={1}>
<text fg={theme.text}>{"$ " + command}</text>
</box>
</Show>
),
}
}
if (permission === "task") {
const type = typeof data.subagent_type === "string" ? data.subagent_type : "Unknown"
const desc = typeof data.description === "string" ? data.description : ""
return {
icon: "#",
title: `${Locale.titlecase(type)} Task`,
body: (
<Show when={desc}>
<box paddingLeft={1}>
<text fg={theme.text}>{"◉ " + desc}</text>
</box>
</Show>
),
}
}
if (permission === "webfetch") {
const url = typeof data.url === "string" ? data.url : ""
return {
icon: "%",
title: `WebFetch ${url}`,
body: (
<Show when={url}>
<box paddingLeft={1}>
<text fg={theme.textMuted}>{"URL: " + url}</text>
</box>
</Show>
),
}
}
if (permission === "websearch") {
const query = typeof data.query === "string" ? data.query : ""
return {
icon: "◈",
title: `Exa Web Search "${query}"`,
body: (
<Show when={query}>
<box paddingLeft={1}>
<text fg={theme.textMuted}>{"Query: " + query}</text>
</box>
</Show>
),
}
}
if (permission === "codesearch") {
const query = typeof data.query === "string" ? data.query : ""
return {
icon: "◇",
title: `Exa Code Search "${query}"`,
body: (
<Show when={query}>
<box paddingLeft={1}>
<text fg={theme.textMuted}>{"Query: " + query}</text>
</box>
</Show>
),
}
}
if (permission === "external_directory") {
const meta = props.request.metadata ?? {}
const parent = typeof meta["parentDir"] === "string" ? meta["parentDir"] : undefined
const filepath = typeof meta["filepath"] === "string" ? meta["filepath"] : undefined
const pattern = props.request.patterns?.[0]
const derived =
typeof pattern === "string" ? (pattern.includes("*") ? path.dirname(pattern) : pattern) : undefined
const raw = parent ?? filepath ?? derived
const dir = normalizePath(raw)
const patterns = (props.request.patterns ?? []).filter((p): p is string => typeof p === "string")
return {
icon: "←",
title: `Access external directory ${dir}`,
body: (
<Show when={patterns.length > 0}>
<box paddingLeft={1} gap={1}>
<text fg={theme.textMuted}>Patterns</text>
<box>
<For each={patterns}>{(p) => <text fg={theme.text}>{"- " + p}</text>}</For>
</box>
</box>
</Show>
),
}
}
if (permission === "doom_loop") {
return {
icon: "⟳",
title: "Continue after repeated failures",
body: (
<box paddingLeft={1}>
<text fg={theme.textMuted}>This keeps the session running despite repeated failures.</text>
</box>
),
}
}
return {
icon: "⚙",
title: `Call tool ${permission}`,
body: (
<box paddingLeft={1}>
<text fg={theme.textMuted}>{"Tool: " + permission}</text>
</box>
),
}
}
const current = info()
const header = () => (
<box flexDirection="column" gap={0}>
<box flexDirection="row" gap={1} flexShrink={0}>
<text fg={theme.warning}>{"△"}</text>
<text fg={theme.text}>Permission required</text>
</box>
<box flexDirection="row" gap={1} paddingLeft={2} flexShrink={0}>
<text fg={theme.textMuted} flexShrink={0}>
{current.icon}
</text>
<text fg={theme.text}>{current.title}</text>
</box>
</box>
)
const body = (
<Prompt
title="Permission required"
body={
<Switch>
<Match when={props.request.permission === "edit"}>
<EditBody request={props.request} />
</Match>
<Match when={props.request.permission === "read"}>
<TextBody icon="→" title={`Read ` + normalizePath(input().filePath as string)} />
</Match>
<Match when={props.request.permission === "glob"}>
<TextBody icon="✱" title={`Glob "` + (input().pattern ?? "") + `"`} />
</Match>
<Match when={props.request.permission === "grep"}>
<TextBody icon="✱" title={`Grep "` + (input().pattern ?? "") + `"`} />
</Match>
<Match when={props.request.permission === "list"}>
<TextBody icon="→" title={`List ` + normalizePath(input().path as string)} />
</Match>
<Match when={props.request.permission === "bash"}>
<TextBody
icon="#"
title={(input().description as string) ?? ""}
description={("$ " + input().command) as string}
/>
</Match>
<Match when={props.request.permission === "task"}>
<TextBody
icon="#"
title={`${Locale.titlecase((input().subagent_type as string) ?? "Unknown")} Task`}
description={"◉ " + input().description}
/>
</Match>
<Match when={props.request.permission === "webfetch"}>
<TextBody icon="%" title={`WebFetch ` + (input().url ?? "")} />
</Match>
<Match when={props.request.permission === "websearch"}>
<TextBody icon="◈" title={`Exa Web Search "` + (input().query ?? "") + `"`} />
</Match>
<Match when={props.request.permission === "codesearch"}>
<TextBody icon="◇" title={`Exa Code Search "` + (input().query ?? "") + `"`} />
</Match>
<Match when={props.request.permission === "external_directory"}>
{(() => {
const meta = props.request.metadata ?? {}
const parent = typeof meta["parentDir"] === "string" ? meta["parentDir"] : undefined
const filepath = typeof meta["filepath"] === "string" ? meta["filepath"] : undefined
const pattern = props.request.patterns?.[0]
const derived =
typeof pattern === "string"
? pattern.includes("*")
? path.dirname(pattern)
: pattern
: undefined
const raw = parent ?? filepath ?? derived
const dir = normalizePath(raw)
return <TextBody icon="←" title={`Access external directory ` + dir} />
})()}
</Match>
<Match when={props.request.permission === "doom_loop"}>
<TextBody icon="⟳" title="Continue after repeated failures" />
</Match>
<Match when={true}>
<TextBody icon="⚙" title={`Call tool ` + props.request.permission} />
</Match>
</Switch>
}
header={header()}
body={current.body}
options={{ once: "Allow once", always: "Allow always", reject: "Reject" }}
escapeKey="reject"
fullscreen
@@ -375,6 +541,7 @@ function RejectPrompt(props: { onConfirm: (message: string) => void; onCancel: (
function Prompt<const T extends Record<string, string>>(props: {
title: string
header?: JSX.Element
body: JSX.Element
options: T
escapeKey?: keyof T
@@ -448,10 +615,19 @@ function Prompt<const T extends Record<string, string>>(props: {
})}
>
<box gap={1} paddingLeft={1} paddingRight={3} paddingTop={1} paddingBottom={1} flexGrow={1}>
<box flexDirection="row" gap={1} paddingLeft={1} flexShrink={0}>
<text fg={theme.warning}>{"△"}</text>
<text fg={theme.text}>{props.title}</text>
</box>
<Show
when={props.header}
fallback={
<box flexDirection="row" gap={1} paddingLeft={1} flexShrink={0}>
<text fg={theme.warning}>{"△"}</text>
<text fg={theme.text}>{props.title}</text>
</box>
}
>
<box paddingLeft={1} flexShrink={0}>
{props.header}
</box>
</Show>
{props.body}
</box>
<box
@@ -80,7 +80,15 @@ export function Sidebar(props: { sessionID: string; overlay?: boolean }) {
paddingRight={2}
position={props.overlay ? "absolute" : "relative"}
>
<scrollbox flexGrow={1}>
<scrollbox
flexGrow={1}
verticalScrollbarOptions={{
trackOptions: {
backgroundColor: theme.background,
foregroundColor: theme.borderActive,
},
}}
>
<box flexShrink={0} gap={1} paddingRight={1}>
<box paddingRight={1}>
<text fg={theme.text}>
+4 -1
View File
@@ -3,10 +3,12 @@ import { tui } from "./app"
import { Rpc } from "@/util/rpc"
import { type rpc } from "./worker"
import path from "path"
import { fileURLToPath } from "url"
import { UI } from "@/cli/ui"
import { iife } from "@/util/iife"
import { Log } from "@/util/log"
import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
import { Filesystem } from "@/util/filesystem"
import type { Event } from "@kilocode/sdk/v2"
import type { EventSource } from "./context/sdk"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
@@ -99,7 +101,7 @@ export const TuiThreadCommand = cmd({
const distWorker = new URL("./cli/cmd/tui/worker.js", import.meta.url)
const workerPath = await iife(async () => {
if (typeof KILO_WORKER_PATH !== "undefined") return KILO_WORKER_PATH
if (await Bun.file(distWorker).exists()) return distWorker
if (await Filesystem.exists(fileURLToPath(distWorker))) return distWorker
return localWorker
})
try {
@@ -191,5 +193,6 @@ export const TuiThreadCommand = cmd({
} finally {
unguard?.()
}
process.exit(0)
},
})
@@ -4,6 +4,7 @@ import clipboardy from "clipboardy"
import { lazy } from "../../../../util/lazy.js"
import { tmpdir } from "os"
import path from "path"
import { Filesystem } from "../../../../util/filesystem"
/**
* Writes text to clipboard via OSC 52 escape sequence.
@@ -34,9 +35,8 @@ export namespace Clipboard {
await $`osascript -e 'set imageData to the clipboard as "PNGf"' -e 'set fileRef to open for access POSIX file "${tmpfile}" with write permission' -e 'set eof fileRef to 0' -e 'write imageData to fileRef' -e 'close access fileRef'`
.nothrow()
.quiet()
const file = Bun.file(tmpfile)
const buffer = await file.arrayBuffer()
return { data: Buffer.from(buffer).toString("base64"), mime: "image/png" }
const buffer = await Filesystem.readBytes(tmpfile)
return { data: buffer.toString("base64"), mime: "image/png" }
} catch {
} finally {
await $`rm -f "${tmpfile}"`.nothrow().quiet()
@@ -3,6 +3,7 @@ import { rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { CliRenderer } from "@opentui/core"
import { Filesystem } from "@/util/filesystem"
export namespace Editor {
export async function open(opts: { value: string; renderer: CliRenderer }): Promise<string | undefined> {
@@ -12,7 +13,7 @@ export namespace Editor {
const filepath = join(tmpdir(), `${Date.now()}.md`)
await using _ = defer(async () => rm(filepath, { force: true }))
await Bun.write(filepath, opts.value)
await Filesystem.write(filepath, opts.value)
opts.renderer.suspend()
opts.renderer.currentRenderBuffer.clear()
const parts = editor.split(" ")
@@ -23,7 +24,7 @@ export namespace Editor {
stderr: "inherit",
})
await proc.exited
const content = await Bun.file(filepath).text()
const content = await Filesystem.readText(filepath)
opts.renderer.currentRenderBuffer.clear()
opts.renderer.resume()
opts.renderer.requestRender()
+6 -1
View File
@@ -137,7 +137,12 @@ export const rpc = {
async shutdown() {
Log.Default.info("worker shutting down")
if (eventStream.abort) eventStream.abort.abort()
await Instance.disposeAll()
await Promise.race([
Instance.disposeAll(),
new Promise((resolve) => {
setTimeout(resolve, 5000)
}),
])
if (server) server.stop(true)
},
}
+4 -5
View File
@@ -7,6 +7,7 @@ import { $ } from "bun"
import fs from "fs/promises"
import path from "path"
import os from "os"
import { Filesystem } from "../../util/filesystem"
interface UninstallArgs {
keepConfig: boolean
@@ -268,9 +269,7 @@ async function getShellConfigFile(): Promise<string | null> {
.catch(() => false)
if (!exists) continue
const content = await Bun.file(file)
.text()
.catch(() => "")
const content = await Filesystem.readText(file).catch(() => "")
// kilocode_change start - detect both opencode and kilo markers
if (
content.includes("# opencode") ||
@@ -287,7 +286,7 @@ async function getShellConfigFile(): Promise<string | null> {
}
async function cleanShellConfig(file: string) {
const content = await Bun.file(file).text()
const content = await Filesystem.readText(file)
const lines = content.split("\n")
const filtered: string[] = []
@@ -325,7 +324,7 @@ async function cleanShellConfig(file: string) {
}
const output = filtered.join("\n") + "\n"
await Bun.write(file, output)
await Filesystem.write(file, output)
}
async function getDirectorySize(dir: string): Promise<number> {
+3
View File
@@ -106,6 +106,9 @@ export namespace UI {
}
export function error(message: string) {
if (message.startsWith("Error: ")) {
message = message.slice("Error: ".length)
}
println(Style.TEXT_DANGER_BOLD + "Error: " + Style.TEXT_NORMAL + message)
}