2025-10-31 19:07:36 +00:00
import { render , useKeyboard , useRenderer , useTerminalDimensions } from "@opentui/solid"
import { Clipboard } from "@tui/util/clipboard"
import { TextAttributes } from "@opentui/core"
2025-11-09 01:17:46 +00:00
import { RouteProvider , useRoute } from "@tui/context/route"
2025-12-23 20:40:55 +00:00
import { Switch , Match , createEffect , untrack , ErrorBoundary , createSignal , onMount , batch , Show , on } from "solid-js"
2025-10-31 19:07:36 +00:00
import { Installation } from "@/installation"
2025-12-02 20:12:11 +00:00
import { Flag } from "@/flag/flag"
2025-10-31 19:07:36 +00:00
import { DialogProvider , useDialog } from "@tui/ui/dialog"
2025-11-21 05:21:06 +00:00
import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider"
2025-10-31 19:07:36 +00:00
import { SDKProvider , useSDK } from "@tui/context/sdk"
2025-11-09 01:17:46 +00:00
import { SyncProvider , useSync } from "@tui/context/sync"
2025-10-31 19:07:36 +00:00
import { LocalProvider , useLocal } from "@tui/context/local"
2025-12-06 23:21:32 +00:00
import { DialogModel , useConnected } from "@tui/component/dialog-model"
2025-12-08 17:31:22 +00:00
import { DialogMcp } from "@tui/component/dialog-mcp"
2025-10-31 19:07:36 +00:00
import { DialogStatus } from "@tui/component/dialog-status"
import { DialogThemeList } from "@tui/component/dialog-theme-list"
import { DialogHelp } from "./ui/dialog-help"
import { CommandProvider , useCommandDialog } from "@tui/component/dialog-command"
import { DialogAgent } from "@tui/component/dialog-agent"
import { DialogSessionList } from "@tui/component/dialog-session-list"
import { KeybindProvider } from "@tui/context/keybind"
import { ThemeProvider , useTheme } from "@tui/context/theme"
import { Home } from "@tui/routes/home"
import { Session } from "@tui/routes/session"
import { PromptHistoryProvider } from "./component/prompt/history"
2026-01-06 20:05:57 +00:00
import { FrecencyProvider } from "./component/prompt/frecency"
2025-12-23 15:58:00 +00:00
import { PromptStashProvider } from "./component/prompt/stash"
2025-10-31 19:07:36 +00:00
import { DialogAlert } from "./ui/dialog-alert"
2025-11-10 17:27:19 +00:00
import { ToastProvider , useToast } from "./ui/toast"
2025-11-01 16:13:10 +00:00
import { ExitProvider , useExit } from "./context/exit"
2025-10-31 19:07:36 +00:00
import { Session as SessionApi } from "@/session"
import { TuiEvent } from "./event"
2025-10-31 20:12:58 +00:00
import { KVProvider , useKV } from "./context/kv"
2025-11-09 01:17:46 +00:00
import { Provider } from "@/provider/provider"
import { ArgsProvider , useArgs , type Args } from "./context/args"
2025-11-30 05:49:45 +00:00
import open from "open"
2026-01-02 02:18:28 +00:00
import { writeHeapSnapshot } from "v8"
2025-12-04 17:12:58 +00:00
import { PromptRefProvider , usePromptRef } from "./context/prompt"
2025-10-31 19:07:36 +00:00
2025-11-01 17:54:01 +00:00
async function getTerminalBackgroundColor ( ) : Promise < "dark" | "light" > {
2025-11-04 04:36:01 +00:00
// can't set raw mode if not a TTY
if ( ! process . stdin . isTTY ) return "dark"
2025-11-01 17:54:01 +00:00
return new Promise ( ( resolve ) = > {
let timeout : NodeJS.Timeout
const cleanup = ( ) = > {
process . stdin . setRawMode ( false )
process . stdin . removeListener ( "data" , handler )
clearTimeout ( timeout )
}
const handler = ( data : Buffer ) = > {
const str = data . toString ( )
const match = str . match ( /\x1b]11;([^\x07\x1b]+)/ )
if ( match ) {
cleanup ( )
const color = match [ 1 ]
// Parse RGB values from color string
// Formats: rgb:RR/GG/BB or #RRGGBB or rgb(R,G,B)
let r = 0 ,
g = 0 ,
b = 0
if ( color . startsWith ( "rgb:" ) ) {
const parts = color . substring ( 4 ) . split ( "/" )
r = parseInt ( parts [ 0 ] , 16 ) >> 8 // Convert 16-bit to 8-bit
g = parseInt ( parts [ 1 ] , 16 ) >> 8 // Convert 16-bit to 8-bit
b = parseInt ( parts [ 2 ] , 16 ) >> 8 // Convert 16-bit to 8-bit
} else if ( color . startsWith ( "#" ) ) {
r = parseInt ( color . substring ( 1 , 3 ) , 16 )
g = parseInt ( color . substring ( 3 , 5 ) , 16 )
b = parseInt ( color . substring ( 5 , 7 ) , 16 )
} else if ( color . startsWith ( "rgb(" ) ) {
const parts = color . substring ( 4 , color . length - 1 ) . split ( "," )
r = parseInt ( parts [ 0 ] )
g = parseInt ( parts [ 1 ] )
b = parseInt ( parts [ 2 ] )
}
// Calculate luminance using relative luminance formula
const luminance = ( 0.299 * r + 0.587 * g + 0.114 * b ) / 255
// Determine if dark or light based on luminance threshold
resolve ( luminance > 0.5 ? "light" : "dark" )
}
}
process . stdin . setRawMode ( true )
process . stdin . on ( "data" , handler )
process . stdout . write ( "\x1b]11;?\x07" )
timeout = setTimeout ( ( ) = > {
cleanup ( )
resolve ( "dark" )
} , 1000 )
} )
}
2026-01-09 21:33:46 +00:00
import type { EventSource } from "./context/sdk"
export function tui ( input : {
url : string
args : Args
directory? : string
fetch? : typeof fetch
2026-01-31 19:42:36 +00:00
headers? : RequestInit [ "headers" ]
2026-01-09 21:33:46 +00:00
events? : EventSource
onExit ? : ( ) = > Promise < void >
} ) {
2025-10-31 19:07:36 +00:00
// promise to prevent immediate exit
2025-11-01 17:54:01 +00:00
return new Promise < void > ( async ( resolve ) = > {
const mode = await getTerminalBackgroundColor ( )
2025-10-31 19:07:36 +00:00
const onExit = async ( ) = > {
await input . onExit ? . ( )
resolve ( )
}
render (
( ) = > {
return (
2025-12-11 22:39:27 +00:00
< ErrorBoundary
fallback = { ( error , reset ) = > < ErrorComponent error = { error } reset = { reset } onExit = { onExit } mode = { mode } / > }
>
2025-11-09 01:17:46 +00:00
< ArgsProvider { ...input.args } >
< ExitProvider onExit = { onExit } >
< KVProvider >
< ToastProvider >
< RouteProvider >
2026-01-09 21:33:46 +00:00
< SDKProvider
url = { input . url }
directory = { input . directory }
fetch = { input . fetch }
2026-01-31 19:42:36 +00:00
headers = { input . headers }
2026-01-09 21:33:46 +00:00
events = { input . events }
>
2025-11-09 01:17:46 +00:00
< SyncProvider >
< ThemeProvider mode = { mode } >
< LocalProvider >
< KeybindProvider >
2025-12-23 15:58:00 +00:00
< PromptStashProvider >
< DialogProvider >
< CommandProvider >
2026-01-06 20:05:57 +00:00
< FrecencyProvider >
< PromptHistoryProvider >
< PromptRefProvider >
< App / >
< / PromptRefProvider >
< / PromptHistoryProvider >
< / FrecencyProvider >
2025-12-23 15:58:00 +00:00
< / CommandProvider >
< / DialogProvider >
< / PromptStashProvider >
2025-11-09 01:17:46 +00:00
< / KeybindProvider >
< / LocalProvider >
< / ThemeProvider >
< / SyncProvider >
< / SDKProvider >
< / RouteProvider >
< / ToastProvider >
< / KVProvider >
< / ExitProvider >
< / ArgsProvider >
2025-10-31 19:07:36 +00:00
< / ErrorBoundary >
)
} ,
{
targetFps : 60 ,
gatherStats : false ,
exitOnCtrlC : false ,
2025-12-10 14:01:41 +00:00
useKittyKeyboard : { } ,
2026-02-02 07:12:50 +00:00
autoFocus : false ,
2025-12-17 08:38:04 +00:00
consoleOptions : {
keyBindings : [ { name : "y" , ctrl : true , action : "copy-selection" } ] ,
onCopySelection : ( text ) = > {
Clipboard . copy ( text ) . catch ( ( error ) = > {
console . error ( ` Failed to copy console selection to clipboard: ${ error } ` )
} )
2025-12-17 08:40:22 +00:00
} ,
2025-12-17 08:38:04 +00:00
} ,
2025-10-31 19:07:36 +00:00
} ,
)
} )
}
function App() {
const route = useRoute ( )
const dimensions = useTerminalDimensions ( )
const renderer = useRenderer ( )
2026-02-02 07:12:50 +00:00
Clipboard . setRenderer ( renderer )
2025-10-31 19:07:36 +00:00
renderer . disableStdoutInterception ( )
const dialog = useDialog ( )
const local = useLocal ( )
2025-10-31 20:12:58 +00:00
const kv = useKV ( )
2025-10-31 19:07:36 +00:00
const command = useCommandDialog ( )
2025-12-21 19:57:55 +00:00
const sdk = useSDK ( )
2025-10-31 19:07:36 +00:00
const toast = useToast ( )
2025-11-01 17:54:01 +00:00
const { theme , mode , setMode } = useTheme ( )
2025-11-09 01:17:46 +00:00
const sync = useSync ( )
2025-11-01 16:13:10 +00:00
const exit = useExit ( )
2025-12-04 17:12:58 +00:00
const promptRef = usePromptRef ( )
2025-10-31 19:07:36 +00:00
2025-12-24 00:46:01 +00:00
// Wire up console copy-to-clipboard via opentui's onCopySelection callback
renderer . console . onCopySelection = async ( text : string ) = > {
if ( ! text || text . length === 0 ) return
await Clipboard . copy ( text )
. then ( ( ) = > toast . show ( { message : "Copied to clipboard" , variant : "info" } ) )
. catch ( toast . error )
renderer . clearSelection ( )
}
2025-12-18 04:30:01 +00:00
const [ terminalTitleEnabled , setTerminalTitleEnabled ] = createSignal ( kv . get ( "terminal_title_enabled" , true ) )
2025-12-23 20:40:55 +00:00
createEffect ( ( ) = > {
console . log ( JSON . stringify ( route . data ) )
2025-10-31 19:07:36 +00:00
} )
2025-12-23 20:40:55 +00:00
// Update terminal window title based on current route and session
2025-12-06 20:49:11 +00:00
createEffect ( ( ) = > {
2025-12-18 04:30:01 +00:00
if ( ! terminalTitleEnabled ( ) || Flag . OPENCODE_DISABLE_TERMINAL_TITLE ) return
2025-12-17 05:57:03 +00:00
2025-12-06 20:49:11 +00:00
if ( route . data . type === "home" ) {
2025-12-11 12:10:04 +00:00
renderer . setTerminalTitle ( "OpenCode" )
2025-12-06 20:49:11 +00:00
return
}
if ( route . data . type === "session" ) {
2025-12-23 20:40:55 +00:00
const session = sync . session . get ( route . data . sessionID )
if ( ! session || SessionApi . isDefaultTitle ( session . title ) ) {
renderer . setTerminalTitle ( "OpenCode" )
2025-12-06 20:49:11 +00:00
return
}
2025-12-23 20:40:55 +00:00
// Truncate title to 40 chars max
const title = session . title . length > 40 ? session . title . slice ( 0 , 37 ) + "..." : session . title
renderer . setTerminalTitle ( ` OC | ${ title } ` )
2025-12-06 20:49:11 +00:00
}
} )
2025-11-09 01:17:46 +00:00
const args = useArgs ( )
onMount ( ( ) = > {
batch ( ( ) = > {
if ( args . agent ) local . agent . set ( args . agent )
if ( args . model ) {
const { providerID , modelID } = Provider . parseModel ( args . model )
if ( ! providerID || ! modelID )
return toast . show ( {
variant : "warning" ,
message : ` Invalid model format: ${ args . model } ` ,
duration : 3000 ,
} )
local . model . set ( { providerID , modelID } , { recent : true } )
}
if ( args . sessionID ) {
route . navigate ( {
type : "session" ,
sessionID : args.sessionID ,
} )
}
} )
} )
2025-11-25 22:27:06 +00:00
let continued = false
2025-11-09 01:53:11 +00:00
createEffect ( ( ) = > {
2025-12-20 17:39:26 +00:00
// When using -c, session list is loaded in blocking phase, so we can navigate at "partial"
if ( continued || sync . status === "loading" || ! args . continue ) return
2025-12-15 01:50:54 +00:00
const match = sync . data . session
. toSorted ( ( a , b ) = > b . time . updated - a . time . updated )
. find ( ( x ) = > x . parentID === undefined ) ? . id
2025-11-25 22:27:06 +00:00
if ( match ) {
continued = true
route . navigate ( { type : "session" , sessionID : match } )
2025-11-09 01:53:11 +00:00
}
} )
2025-12-02 00:35:40 +00:00
createEffect (
on (
( ) = > sync . status === "complete" && sync . data . provider . length === 0 ,
( isEmpty , wasEmpty ) = > {
// only trigger when we transition into an empty-provider state
if ( ! isEmpty || wasEmpty ) return
dialog . replace ( ( ) = > < DialogProviderList / > )
} ,
) ,
)
2025-12-06 23:21:32 +00:00
const connected = useConnected ( )
2025-10-31 19:07:36 +00:00
command . register ( ( ) = > [
{
title : "Switch session" ,
value : "session.list" ,
keybind : "session_list" ,
category : "Session" ,
2025-12-06 23:21:32 +00:00
suggested : sync.data.session.length > 0 ,
2026-01-18 02:39:19 +00:00
slash : {
name : "sessions" ,
aliases : [ "resume" , "continue" ] ,
} ,
2025-10-31 19:07:36 +00:00
onSelect : ( ) = > {
dialog . replace ( ( ) = > < DialogSessionList / > )
} ,
} ,
{
title : "New session" ,
2025-12-06 23:21:32 +00:00
suggested : route.data.type === "session" ,
2025-10-31 19:07:36 +00:00
value : "session.new" ,
keybind : "session_new" ,
category : "Session" ,
2026-01-18 02:39:19 +00:00
slash : {
name : "new" ,
aliases : [ "clear" ] ,
} ,
2025-10-31 19:07:36 +00:00
onSelect : ( ) = > {
2025-12-04 17:12:58 +00:00
const current = promptRef . current
// Don't require focus - if there's any text, preserve it
const currentPrompt = current ? . current ? . input ? current.current : undefined
2025-10-31 19:07:36 +00:00
route . navigate ( {
type : "home" ,
2025-12-04 17:12:58 +00:00
initialPrompt : currentPrompt ,
2025-10-31 19:07:36 +00:00
} )
dialog . clear ( )
} ,
} ,
{
title : "Switch model" ,
value : "model.list" ,
keybind : "model_list" ,
2025-12-06 23:21:32 +00:00
suggested : true ,
2025-10-31 19:07:36 +00:00
category : "Agent" ,
2026-01-18 02:39:19 +00:00
slash : {
name : "models" ,
} ,
2025-10-31 19:07:36 +00:00
onSelect : ( ) = > {
dialog . replace ( ( ) = > < DialogModel / > )
} ,
} ,
2025-10-31 23:42:27 +00:00
{
title : "Model cycle" ,
value : "model.cycle_recent" ,
keybind : "model_cycle_recent" ,
category : "Agent" ,
2026-01-18 02:39:19 +00:00
hidden : true ,
2025-10-31 23:42:27 +00:00
onSelect : ( ) = > {
local . model . cycle ( 1 )
} ,
} ,
{
title : "Model cycle reverse" ,
value : "model.cycle_recent_reverse" ,
keybind : "model_cycle_recent_reverse" ,
category : "Agent" ,
2026-01-18 02:39:19 +00:00
hidden : true ,
2025-10-31 23:42:27 +00:00
onSelect : ( ) = > {
local . model . cycle ( - 1 )
} ,
} ,
2025-12-15 22:03:50 +00:00
{
title : "Favorite cycle" ,
value : "model.cycle_favorite" ,
keybind : "model_cycle_favorite" ,
category : "Agent" ,
2026-01-18 02:39:19 +00:00
hidden : true ,
2025-12-15 22:03:50 +00:00
onSelect : ( ) = > {
local . model . cycleFavorite ( 1 )
} ,
} ,
{
title : "Favorite cycle reverse" ,
value : "model.cycle_favorite_reverse" ,
keybind : "model_cycle_favorite_reverse" ,
category : "Agent" ,
2026-01-18 02:39:19 +00:00
hidden : true ,
2025-12-15 22:03:50 +00:00
onSelect : ( ) = > {
local . model . cycleFavorite ( - 1 )
} ,
} ,
2025-10-31 19:07:36 +00:00
{
title : "Switch agent" ,
value : "agent.list" ,
keybind : "agent_list" ,
category : "Agent" ,
2026-01-18 02:39:19 +00:00
slash : {
name : "agents" ,
} ,
2025-10-31 19:07:36 +00:00
onSelect : ( ) = > {
dialog . replace ( ( ) = > < DialogAgent / > )
} ,
} ,
2025-12-08 17:31:22 +00:00
{
title : "Toggle MCPs" ,
value : "mcp.list" ,
category : "Agent" ,
2026-01-18 02:39:19 +00:00
slash : {
name : "mcps" ,
} ,
2025-12-08 17:31:22 +00:00
onSelect : ( ) = > {
dialog . replace ( ( ) = > < DialogMcp / > )
} ,
} ,
2025-10-31 19:07:36 +00:00
{
title : "Agent cycle" ,
value : "agent.cycle" ,
keybind : "agent_cycle" ,
category : "Agent" ,
2026-01-18 02:39:19 +00:00
hidden : true ,
2025-10-31 19:07:36 +00:00
onSelect : ( ) = > {
local . agent . move ( 1 )
} ,
} ,
2025-12-30 18:33:50 +00:00
{
title : "Variant cycle" ,
value : "variant.cycle" ,
keybind : "variant_cycle" ,
category : "Agent" ,
2026-01-18 02:39:19 +00:00
hidden : true ,
2025-12-30 18:33:50 +00:00
onSelect : ( ) = > {
local . model . variant . cycle ( )
} ,
} ,
2025-10-31 19:07:36 +00:00
{
title : "Agent cycle reverse" ,
value : "agent.cycle.reverse" ,
keybind : "agent_cycle_reverse" ,
category : "Agent" ,
2026-01-18 02:39:19 +00:00
hidden : true ,
2025-10-31 19:07:36 +00:00
onSelect : ( ) = > {
local . agent . move ( - 1 )
} ,
} ,
2025-12-06 23:21:32 +00:00
{
title : "Connect provider" ,
value : "provider.connect" ,
suggested : ! connected ( ) ,
2026-01-18 02:39:19 +00:00
slash : {
name : "connect" ,
} ,
2025-12-06 23:21:32 +00:00
onSelect : ( ) = > {
dialog . replace ( ( ) = > < DialogProviderList / > )
} ,
category : "Provider" ,
} ,
2025-10-31 19:07:36 +00:00
{
title : "View status" ,
keybind : "status_view" ,
value : "opencode.status" ,
2026-01-18 02:39:19 +00:00
slash : {
name : "status" ,
} ,
2025-10-31 19:07:36 +00:00
onSelect : ( ) = > {
dialog . replace ( ( ) = > < DialogStatus / > )
} ,
category : "System" ,
} ,
{
title : "Switch theme" ,
value : "theme.switch" ,
2026-01-04 15:13:19 +00:00
keybind : "theme_list" ,
2026-01-18 02:39:19 +00:00
slash : {
name : "themes" ,
} ,
2025-10-31 19:07:36 +00:00
onSelect : ( ) = > {
dialog . replace ( ( ) = > < DialogThemeList / > )
} ,
category : "System" ,
} ,
2025-11-01 17:54:01 +00:00
{
2025-12-03 05:03:24 +00:00
title : "Toggle appearance" ,
2025-11-01 17:54:01 +00:00
value : "theme.switch_mode" ,
2025-12-03 05:03:24 +00:00
onSelect : ( dialog ) = > {
2025-11-01 17:54:01 +00:00
setMode ( mode ( ) === "dark" ? "light" : "dark" )
2025-12-03 05:03:24 +00:00
dialog . clear ( )
2025-11-01 17:54:01 +00:00
} ,
category : "System" ,
} ,
2025-10-31 19:07:36 +00:00
{
title : "Help" ,
value : "help.show" ,
2026-01-18 02:39:19 +00:00
slash : {
name : "help" ,
} ,
2025-10-31 19:07:36 +00:00
onSelect : ( ) = > {
dialog . replace ( ( ) = > < DialogHelp / > )
} ,
category : "System" ,
} ,
2025-11-30 05:49:45 +00:00
{
title : "Open docs" ,
value : "docs.open" ,
onSelect : ( ) = > {
open ( "https://opencode.ai/docs" ) . catch ( ( ) = > { } )
dialog . clear ( )
} ,
category : "System" ,
} ,
2025-11-01 16:13:10 +00:00
{
title : "Exit the app" ,
value : "app.exit" ,
2026-01-18 02:39:19 +00:00
slash : {
name : "exit" ,
aliases : [ "quit" , "q" ] ,
} ,
2025-11-17 22:13:41 +00:00
onSelect : ( ) = > exit ( ) ,
2025-11-01 16:13:10 +00:00
category : "System" ,
2025-11-01 17:54:01 +00:00
} ,
2025-11-06 02:13:35 +00:00
{
title : "Toggle debug panel" ,
2025-11-06 02:26:58 +00:00
category : "System" ,
2025-11-06 02:13:35 +00:00
value : "app.debug" ,
onSelect : ( dialog ) = > {
renderer . toggleDebugOverlay ( )
dialog . clear ( )
} ,
} ,
{
title : "Toggle console" ,
2025-11-06 02:26:58 +00:00
category : "System" ,
2025-12-24 00:46:01 +00:00
value : "app.console" ,
2025-11-06 02:13:35 +00:00
onSelect : ( dialog ) = > {
renderer . console . toggle ( )
dialog . clear ( )
} ,
} ,
2026-01-02 02:18:28 +00:00
{
title : "Write heap snapshot" ,
category : "System" ,
value : "app.heap_snapshot" ,
onSelect : ( dialog ) = > {
const path = writeHeapSnapshot ( )
toast . show ( {
variant : "info" ,
message : ` Heap snapshot written to ${ path } ` ,
duration : 5000 ,
} )
dialog . clear ( )
} ,
} ,
2025-11-22 18:48:23 +00:00
{
title : "Suspend terminal" ,
value : "terminal.suspend" ,
keybind : "terminal_suspend" ,
category : "System" ,
2026-01-18 02:39:19 +00:00
hidden : true ,
2025-11-22 18:48:23 +00:00
onSelect : ( ) = > {
process . once ( "SIGCONT" , ( ) = > {
renderer . resume ( )
} )
renderer . suspend ( )
// pid=0 means send the signal to all processes in the process group
process . kill ( 0 , "SIGTSTP" )
} ,
} ,
2025-12-18 04:30:01 +00:00
{
title : terminalTitleEnabled ( ) ? "Disable terminal title" : "Enable terminal title" ,
value : "terminal.title.toggle" ,
keybind : "terminal_title_toggle" ,
category : "System" ,
onSelect : ( dialog ) = > {
setTerminalTitleEnabled ( ( prev ) = > {
const next = ! prev
kv . set ( "terminal_title_enabled" , next )
if ( ! next ) renderer . setTerminalTitle ( "" )
return next
} )
dialog . clear ( )
} ,
} ,
2026-01-25 22:57:47 +00:00
{
title : kv.get ( "animations_enabled" , true ) ? "Disable animations" : "Enable animations" ,
value : "app.toggle.animations" ,
category : "System" ,
onSelect : ( dialog ) = > {
kv . set ( "animations_enabled" , ! kv . get ( "animations_enabled" , true ) )
dialog . clear ( )
} ,
} ,
2026-01-26 14:01:23 +00:00
{
title : kv.get ( "diff_wrap_mode" , "word" ) === "word" ? "Disable diff wrapping" : "Enable diff wrapping" ,
value : "app.toggle.diffwrap" ,
category : "System" ,
onSelect : ( dialog ) = > {
const current = kv . get ( "diff_wrap_mode" , "word" )
kv . set ( "diff_wrap_mode" , current === "word" ? "none" : "word" )
dialog . clear ( )
} ,
} ,
2025-10-31 19:07:36 +00:00
] )
createEffect ( ( ) = > {
2025-12-02 00:35:40 +00:00
const currentModel = local . model . current ( )
if ( ! currentModel ) return
if ( currentModel . providerID === "openrouter" && ! kv . get ( "openrouter_warning" , false ) ) {
2025-10-31 19:07:36 +00:00
untrack ( ( ) = > {
DialogAlert . show (
dialog ,
"Warning" ,
"While openrouter is a convenient way to access LLMs your request will often be routed to subpar providers that do not work well in our testing.\n\nFor reliable access to models check out OpenCode Zen\nhttps://opencode.ai/zen" ,
2025-10-31 20:12:58 +00:00
) . then ( ( ) = > kv . set ( "openrouter_warning" , true ) )
2025-10-31 19:07:36 +00:00
} )
}
} )
2025-12-21 19:57:55 +00:00
sdk . event . on ( TuiEvent . CommandExecute . type , ( evt ) = > {
2025-10-31 19:07:36 +00:00
command . trigger ( evt . properties . command )
} )
2025-12-21 19:57:55 +00:00
sdk . event . on ( TuiEvent . ToastShow . type , ( evt ) = > {
2025-10-31 19:07:36 +00:00
toast . show ( {
title : evt.properties.title ,
message : evt.properties.message ,
variant : evt.properties.variant ,
duration : evt.properties.duration ,
} )
} )
2026-01-02 22:57:21 +00:00
sdk . event . on ( TuiEvent . SessionSelect . type , ( evt ) = > {
route . navigate ( {
type : "session" ,
sessionID : evt.properties.sessionID ,
} )
} )
2025-12-21 19:57:55 +00:00
sdk . event . on ( SessionApi . Event . Deleted . type , ( evt ) = > {
2025-10-31 19:07:36 +00:00
if ( route . data . type === "session" && route . data . sessionID === evt . properties . info . id ) {
route . navigate ( { type : "home" } )
toast . show ( {
variant : "info" ,
message : "The current session was deleted" ,
} )
}
} )
2025-12-21 19:57:55 +00:00
sdk . event . on ( SessionApi . Event . Error . type , ( evt ) = > {
2025-11-04 23:29:27 +00:00
const error = evt . properties . error
2026-01-05 14:53:19 +00:00
if ( error && typeof error === "object" && error . name === "MessageAbortedError" ) return
2025-11-04 23:29:27 +00:00
const message = ( ( ) = > {
2025-12-26 16:21:33 +00:00
if ( ! error ) return "An error occurred"
2025-11-04 23:29:27 +00:00
if ( typeof error === "object" ) {
const data = error . data
if ( "message" in data && typeof data . message === "string" ) {
return data . message
}
}
return String ( error )
} ) ( )
toast . show ( {
variant : "error" ,
message ,
duration : 5000 ,
} )
} )
2025-12-21 19:57:55 +00:00
sdk . event . on ( Installation . Event . UpdateAvailable . type , ( evt ) = > {
2025-11-22 22:26:46 +00:00
toast . show ( {
variant : "info" ,
title : "Update Available" ,
message : ` OpenCode v ${ evt . properties . version } is available. Run 'opencode upgrade' to update manually. ` ,
duration : 10000 ,
} )
} )
2025-10-31 19:07:36 +00:00
return (
< box
width = { dimensions ( ) . width }
height = { dimensions ( ) . height }
backgroundColor = { theme . background }
onMouseUp = { async ( ) = > {
2025-12-02 20:12:11 +00:00
if ( Flag . OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT ) {
renderer . clearSelection ( )
return
}
2025-10-31 19:07:36 +00:00
const text = renderer . getSelection ( ) ? . getSelectedText ( )
if ( text && text . length > 0 ) {
await Clipboard . copy ( text )
2025-11-01 14:34:21 +00:00
. then ( ( ) = > toast . show ( { message : "Copied to clipboard" , variant : "info" } ) )
. catch ( toast . error )
2025-10-31 19:07:36 +00:00
renderer . clearSelection ( )
}
} }
>
2025-11-27 01:11:39 +00:00
< Switch >
< Match when = { route . data . type === "home" } >
< Home / >
< / Match >
< Match when = { route . data . type === "session" } >
< Session / >
< / Match >
< / Switch >
2025-10-31 19:07:36 +00:00
< / box >
)
}
2025-10-31 21:20:32 +00:00
2025-12-11 22:39:27 +00:00
function ErrorComponent ( props : {
error : Error
reset : ( ) = > void
onExit : ( ) = > Promise < void >
mode ? : "dark" | "light"
} ) {
2025-10-31 21:20:32 +00:00
const term = useTerminalDimensions ( )
2026-01-08 20:22:24 +00:00
const renderer = useRenderer ( )
const handleExit = async ( ) = > {
renderer . setTerminalTitle ( "" )
renderer . destroy ( )
props . onExit ( )
}
2025-10-31 21:20:32 +00:00
useKeyboard ( ( evt ) = > {
if ( evt . ctrl && evt . name === "c" ) {
2026-01-08 20:22:24 +00:00
handleExit ( )
2025-10-31 21:20:32 +00:00
}
} )
const [ copied , setCopied ] = createSignal ( false )
2026-01-02 21:02:52 +00:00
const issueURL = new URL ( "https://github.com/anomalyco/opencode/issues/new?template=bug-report.yml" )
2025-10-31 21:20:32 +00:00
2025-12-11 22:39:27 +00:00
// Choose safe fallback colors per mode since theme context may not be available
const isLight = props . mode === "light"
const colors = {
bg : isLight ? "#ffffff" : "#0a0a0a" ,
text : isLight ? "#1a1a1a" : "#eeeeee" ,
muted : isLight ? "#8a8a8a" : "#808080" ,
primary : isLight ? "#3b7dd8" : "#fab283" ,
}
2025-10-31 21:20:32 +00:00
if ( props . error . message ) {
issueURL . searchParams . set ( "title" , ` opentui: fatal: ${ props . error . message } ` )
}
if ( props . error . stack ) {
2025-10-31 21:53:08 +00:00
issueURL . searchParams . set (
"description" ,
"```\n" + props . error . stack . substring ( 0 , 6000 - issueURL . toString ( ) . length ) + "...\n```" ,
)
2025-10-31 21:20:32 +00:00
}
2025-11-12 16:40:48 +00:00
issueURL . searchParams . set ( "opencode-version" , Installation . VERSION )
2025-10-31 21:20:32 +00:00
const copyIssueURL = ( ) = > {
2025-10-31 21:53:08 +00:00
Clipboard . copy ( issueURL . toString ( ) ) . then ( ( ) = > {
setCopied ( true )
} )
2025-10-31 21:20:32 +00:00
}
return (
2025-12-11 22:39:27 +00:00
< box flexDirection = "column" gap = { 1 } backgroundColor = { colors . bg } >
2025-10-31 21:20:32 +00:00
< box flexDirection = "row" gap = { 1 } alignItems = "center" >
2025-12-11 22:39:27 +00:00
< text attributes = { TextAttributes . BOLD } fg = { colors . text } >
Please report an issue .
< / text >
< box onMouseUp = { copyIssueURL } backgroundColor = { colors . primary } padding = { 1 } >
< text attributes = { TextAttributes . BOLD } fg = { colors . bg } >
Copy issue URL ( exception info pre - filled )
< / text >
2025-10-31 21:20:32 +00:00
< / box >
2025-12-11 22:39:27 +00:00
{ copied ( ) && < text fg = { colors . muted } > Successfully copied < / text > }
2025-10-31 21:20:32 +00:00
< / box >
< box flexDirection = "row" gap = { 2 } alignItems = "center" >
2025-12-11 22:39:27 +00:00
< text fg = { colors . text } > A fatal error occurred ! < / text >
< box onMouseUp = { props . reset } backgroundColor = { colors . primary } padding = { 1 } >
< text fg = { colors . bg } > Reset TUI < / text >
2025-10-31 21:20:32 +00:00
< / box >
2026-01-08 20:22:24 +00:00
< box onMouseUp = { handleExit } backgroundColor = { colors . primary } padding = { 1 } >
2025-12-11 22:39:27 +00:00
< text fg = { colors . bg } > Exit < / text >
2025-10-31 21:20:32 +00:00
< / box >
< / box >
< scrollbox height = { Math . floor ( term ( ) . height * 0.7 ) } >
2025-12-11 22:39:27 +00:00
< text fg = { colors . muted } > { props . error . stack } < / text >
2025-10-31 21:20:32 +00:00
< / scrollbox >
2025-12-11 22:39:27 +00:00
< text fg = { colors . text } > { props . error . message } < / text >
2025-10-31 21:53:08 +00:00
< / box >
2025-10-31 21:20:32 +00:00
)
}