Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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