|
|
@@ -2,9 +2,19 @@ import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentu
|
|
|
import { Clipboard } from "@tui/util/clipboard"
|
|
|
import { TextAttributes } from "@opentui/core"
|
|
|
import { RouteProvider, useRoute } from "@tui/context/route"
|
|
|
-import { Switch, Match, createEffect, untrack, ErrorBoundary, createSignal, onMount, batch, Show, on } from "solid-js"
|
|
|
+import {
|
|
|
+ Switch,
|
|
|
+ Match,
|
|
|
+ createEffect,
|
|
|
+ untrack,
|
|
|
+ ErrorBoundary,
|
|
|
+ createSignal,
|
|
|
+ onMount,
|
|
|
+ onCleanup,
|
|
|
+ batch,
|
|
|
+ on,
|
|
|
+} from "solid-js"
|
|
|
import { Installation } from "@/installation"
|
|
|
-import { Global } from "@/global"
|
|
|
import { Flag } from "@/flag/flag"
|
|
|
import { DialogProvider, useDialog } from "@tui/ui/dialog"
|
|
|
import { DialogProvider as DialogProviderList } from "@tui/component/dialog-provider"
|
|
|
@@ -35,6 +45,7 @@ import { Provider } from "@/provider/provider"
|
|
|
import { ArgsProvider, useArgs, type Args } from "./context/args"
|
|
|
import open from "open"
|
|
|
import { PromptRefProvider, usePromptRef } from "./context/prompt"
|
|
|
+import { iife } from "@/util/iife"
|
|
|
|
|
|
async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
|
|
|
// can't set raw mode if not a TTY
|
|
|
@@ -181,29 +192,82 @@ function App() {
|
|
|
|
|
|
const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true))
|
|
|
|
|
|
- createEffect(() => {
|
|
|
- console.log(JSON.stringify(route.data))
|
|
|
+ // Update terminal window title based on current route and session
|
|
|
+ // Braille spinner animation frames for when agent is running (space + single character for consistent width with "OC")
|
|
|
+ const spinnerFrames = [" ⠋", " ⠙", " ⠹", " ⠸", " ⠼", " ⠴", " ⠦", " ⠧", " ⠇", " ⠏"]
|
|
|
+ // Permission request animation frames (flashing triangle with leading space)
|
|
|
+ const permissionFrames = [" ◭", " "]
|
|
|
+ let spinnerInterval: ReturnType<typeof setInterval> | undefined
|
|
|
+ let spinnerIndex = 0
|
|
|
+ let currentTitle = ""
|
|
|
+ let currentAnimationType: "spinner" | "permission" | undefined
|
|
|
+
|
|
|
+ // Cleanup interval on component unmount
|
|
|
+ onCleanup(() => {
|
|
|
+ if (spinnerInterval) {
|
|
|
+ clearInterval(spinnerInterval)
|
|
|
+ spinnerInterval = undefined
|
|
|
+ }
|
|
|
})
|
|
|
|
|
|
- // Update terminal window title based on current route and session
|
|
|
createEffect(() => {
|
|
|
if (!terminalTitleEnabled() || Flag.OPENCODE_DISABLE_TERMINAL_TITLE) return
|
|
|
|
|
|
if (route.data.type === "home") {
|
|
|
+ if (spinnerInterval) {
|
|
|
+ clearInterval(spinnerInterval)
|
|
|
+ spinnerInterval = undefined
|
|
|
+ currentAnimationType = undefined
|
|
|
+ }
|
|
|
renderer.setTerminalTitle("OpenCode")
|
|
|
return
|
|
|
}
|
|
|
|
|
|
if (route.data.type === "session") {
|
|
|
- const session = sync.session.get(route.data.sessionID)
|
|
|
- if (!session || SessionApi.isDefaultTitle(session.title)) {
|
|
|
- renderer.setTerminalTitle("OpenCode")
|
|
|
+ const sessionID = route.data.sessionID
|
|
|
+ const session = sync.session.get(sessionID)
|
|
|
+ const status = sync.data.session_status[sessionID]
|
|
|
+ const isBusy = status?.type === "busy"
|
|
|
+ const permissions = sync.data.permission[sessionID] ?? []
|
|
|
+ const hasPermissionRequest = permissions.length > 0
|
|
|
+ const hasTitle = session && !SessionApi.isDefaultTitle(session.title)
|
|
|
+
|
|
|
+ // Truncate title to 40 chars max, fallback to "OpenCode" if no title yet
|
|
|
+ currentTitle = iife(() => {
|
|
|
+ if (!hasTitle) return "OpenCode"
|
|
|
+ if (session.title.length > 40) return session.title.slice(0, 37) + "..."
|
|
|
+ return session.title
|
|
|
+ })
|
|
|
+
|
|
|
+ // Determine which animation to show (permission takes priority)
|
|
|
+ const targetAnimation = hasPermissionRequest ? "permission" : isBusy ? "spinner" : undefined
|
|
|
+ const frames = hasPermissionRequest ? permissionFrames : spinnerFrames
|
|
|
+
|
|
|
+ if (!targetAnimation) {
|
|
|
+ // Stop animation and show static title
|
|
|
+ if (spinnerInterval) {
|
|
|
+ clearInterval(spinnerInterval)
|
|
|
+ spinnerInterval = undefined
|
|
|
+ currentAnimationType = undefined
|
|
|
+ }
|
|
|
+ renderer.setTerminalTitle(hasTitle ? `OC | ${currentTitle}` : "OpenCode")
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- // Truncate title to 40 chars max
|
|
|
- const title = session.title.length > 40 ? session.title.slice(0, 37) + "..." : session.title
|
|
|
- renderer.setTerminalTitle(`OC | ${title}`)
|
|
|
+ // Start or switch animation
|
|
|
+ if (!spinnerInterval || currentAnimationType !== targetAnimation) {
|
|
|
+ if (spinnerInterval) clearInterval(spinnerInterval)
|
|
|
+ spinnerIndex = 0
|
|
|
+ currentAnimationType = targetAnimation
|
|
|
+ renderer.setTerminalTitle(`${frames[spinnerIndex]} | ${currentTitle}`)
|
|
|
+ spinnerInterval = setInterval(
|
|
|
+ () => {
|
|
|
+ spinnerIndex = (spinnerIndex + 1) % frames.length
|
|
|
+ renderer.setTerminalTitle(`${frames[spinnerIndex]} | ${currentTitle}`)
|
|
|
+ },
|
|
|
+ hasPermissionRequest ? 400 : 80,
|
|
|
+ )
|
|
|
+ }
|
|
|
}
|
|
|
})
|
|
|
|
|
|
@@ -525,7 +589,7 @@ function App() {
|
|
|
sdk.event.on(SessionApi.Event.Error.type, (evt) => {
|
|
|
const error = evt.properties.error
|
|
|
const message = (() => {
|
|
|
- if (!error) return "An error occured"
|
|
|
+ if (!error) return "An error occurred"
|
|
|
|
|
|
if (typeof error === "object") {
|
|
|
const data = error.data
|