Skip to content
95 changes: 0 additions & 95 deletions bun.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/opencode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"typecheck": "tsgo --noEmit",
"test": "bun test",
"build": "bun run script/build.ts",
"build:lite": "bun run script/build-lite.ts",
"dev": "bun run --conditions=browser ./src/index.ts",
"random": "echo 'Random script updated at $(date)' && echo 'Change queued successfully' && echo 'Another change made' && echo 'Yet another change' && echo 'One more change' && echo 'Final change' && echo 'Another final change' && echo 'Yet another final change'",
"clean": "echo 'Cleaning up...' && rm -rf node_modules dist",
Expand Down
59 changes: 59 additions & 0 deletions packages/opencode/script/build-lite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env bun
import path from "path"
import fs from "fs"
import { $ } from "bun"

const ROOT = path.resolve(import.meta.dirname, "..")
const DIST = path.join(ROOT, "dist", "oclite-darwin-arm64")
const BIN = path.join(DIST, "bin")
const HOME_BIN = path.join(process.env.HOME!, "bin")

async function build() {
console.log("Building oclite...")

// Clean
await fs.promises.rm(DIST, { recursive: true, force: true })
await fs.promises.mkdir(BIN, { recursive: true })

// Build
const result = await Bun.build({
entrypoints: [path.join(ROOT, "src/cli/lite/index.ts")],
outdir: BIN,
target: "bun",
minify: true,
sourcemap: "none",
naming: "oclite",
})

if (!result.success) {
console.error("Build failed:")
for (const log of result.logs) {
console.error(log)
}
process.exit(1)
}

// Make executable
await $`chmod +x ${path.join(BIN, "oclite")}`

// Copy to ~/bin
await fs.promises.mkdir(HOME_BIN, { recursive: true })
await fs.promises.copyFile(path.join(BIN, "oclite"), path.join(HOME_BIN, "oclite"))

// Sign for macOS
await $`codesign --force --deep --sign - ${path.join(HOME_BIN, "oclite")}`.quiet().nothrow()
await $`xattr -cr ${path.join(HOME_BIN, "oclite")}`.quiet().nothrow()

// Get size
const stat = await fs.promises.stat(path.join(HOME_BIN, "oclite"))
const sizeMB = (stat.size / 1024 / 1024).toFixed(2)

console.log(`✓ Built oclite (${sizeMB} MB)`)
console.log(`✓ Installed to ~/bin/oclite`)
console.log("\nRun with: ~/bin/oclite")
}

build().catch((err) => {
console.error(err)
process.exit(1)
})
7 changes: 5 additions & 2 deletions packages/opencode/src/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,8 +324,11 @@ export namespace Agent {
for await (const part of result.fullStream) {
if (part.type === "error") throw part.error
}
} catch (e: any) {
if (e?.name === "AbortError" || (e instanceof DOMException && e.name === "AbortError")) {
} catch (e) {
if (typeof e === "object" && e !== null && "name" in e && e.name === "AbortError") {
throw e
}
if (e instanceof DOMException && e.name === "AbortError") {
throw e
}
throw e
Expand Down
83 changes: 83 additions & 0 deletions packages/opencode/src/cli/cmd/tui/util/icons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* Task state icons and color system for terminal UI.
* Provides visual feedback for task status, agent roles, and UI elements.
*/

export namespace Icons {
/**
* ANSI color codes for terminal output
*/
export const colors = {
green: "\x1b[32m",
yellow: "\x1b[33m",
cyan: "\x1b[36m",
dim: "\x1b[2m",
red: "\x1b[31m",
magenta: "\x1b[35m",
reset: "\x1b[0m",
}

/**
* Task status icons with their default colors
*/
export const icons = {
taskComplete: "✓",
taskPending: "□",
taskRunning: "●",
taskProgress: "◇",
agentRole: "🟪",
}

/**
* Status colors lookup table (module-level for performance)
*/
const statusColors: Record<string, string> = {
completed: colors.green,
pending: colors.dim,
running: colors.green,
progress: colors.yellow,
}

/**
* Status icons lookup table (module-level for performance)
*/
const statusIcons: Record<string, string> = {
completed: icons.taskComplete,
pending: icons.taskPending,
running: icons.taskRunning,
progress: icons.taskProgress,
}

/**
* Returns a colored task status icon based on the given status
* @param status - The task status: 'completed', 'pending', 'running', or 'progress'
* @returns The colored icon as a string, or pending icon as safe fallback
*/
export function taskIcon(status: "completed" | "pending" | "running" | "progress"): string {
const color = statusColors[status]
const icon = statusIcons[status]

if (!color || !icon) return `${colors.dim}${icons.taskPending}${colors.reset}`
return `${color}${icon}${colors.reset}`
}

/**
* Returns a colored agent name badge in cyan
* @param name - The agent name
* @returns The colored agent name badge, or empty string if name is empty
*/
export function agentBadge(name: string): string {
if (!name) return ""
return `${colors.cyan}${name}${colors.reset}`
}

/**
* Returns a colored role badge with a purple icon prefix
* @param role - The role name
* @returns The colored role badge with icon, or empty string if role is empty
*/
export function roleBadge(role: string): string {
if (!role) return ""
return `${colors.magenta}${icons.agentRole}${colors.reset} ${role}`
}
}
137 changes: 137 additions & 0 deletions packages/opencode/src/cli/lite/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/usr/bin/env bun
import { cursor, clear, fg, style, write, screen } from "./terminal"
import { parseKey, LineEditor } from "./input"
import { Spinner } from "./spinner"
import { chat } from "./session"

const PROMPT = `${fg.cyan}>${style.reset} `

async function main() {
// Check TTY
if (!process.stdin.isTTY) {
console.error("oclite requires a TTY")
process.exit(1)
}

// Setup
write(screen.alt)
write(clear.screen)
write(cursor.home)

// Header
write(`${fg.brightCyan}${style.bold}oclite${style.reset} ${fg.gray}v0.1.0${style.reset}\n`)
write(`${fg.gray}Type /help for commands, Ctrl+C to exit${style.reset}\n\n`)

// Input setup
const editor = new LineEditor()
process.stdin.setRawMode(true)
process.stdin.resume()

// Render prompt
editor.render(PROMPT)

// Handle input
process.stdin.on("data", async (data: Buffer) => {
const key = parseKey(data)

// Ctrl+C to exit
if (key.ctrl && key.name === "c") {
cleanup()
process.exit(0)
}

const result = editor.handle(key)

if (result !== null) {
write("\n")

if (result.startsWith("/")) {
handleCommand(result)
} else if (result.trim()) {
await handleMessage(result)
}

editor.render(PROMPT)
} else {
editor.render(PROMPT)
}
})

// Cleanup on exit
function cleanup() {
write(cursor.show)
write(screen.main)
process.stdin.setRawMode(false)
}

process.on("exit", cleanup)
process.on("SIGINT", () => {
cleanup()
process.exit(0)
})
}

function handleCommand(cmd: string) {
const command = cmd.slice(1).toLowerCase().trim()

if (command === "help") {
write(`${fg.yellow}Commands:${style.reset}\n`)
write(` /help - Show this help\n`)
write(` /clear - Clear screen\n`)
write(` /quit - Exit oclite\n`)
write("\n")
return
}

if (command === "clear") {
write(clear.screen)
write(cursor.home)
write(`${fg.brightCyan}${style.bold}oclite${style.reset} ${fg.gray}v0.1.0${style.reset}\n\n`)
return
}

if (command === "quit" || command === "exit") {
process.exit(0)
}

write(`${fg.red}Unknown command: ${cmd}${style.reset}\n\n`)
}

async function handleMessage(message: string) {
const spinner = new Spinner("Thinking")
spinner.start()

let first = true
try {
for await (const chunk of chat(message)) {
if (first) {
spinner.stop(true)
first = false
}

if (chunk.type === "text" && chunk.content) {
write(chunk.content)
}

if (chunk.type === "tool_start" && chunk.tool) {
write(`\n${fg.yellow}▶ ${chunk.tool}${style.reset}\n`)
}

if (chunk.type === "tool_end" && chunk.tool) {
write(`${fg.green}✓ ${chunk.tool}${style.reset}\n`)
}

if (chunk.type === "error" && chunk.content) {
write(`\n${fg.red}Error: ${chunk.content}${style.reset}\n`)
}
}
} catch (err) {
if (first) spinner.stop(false)
const msg = err instanceof Error ? err.message : String(err)
write(`\n${fg.red}Error: ${msg}${style.reset}\n`)
}

write("\n")
}

main().catch(console.error)
Loading