feat(feature): add terminal title renaming with session status

This commit is contained in:
YeonGyu-Kim
2025-12-03 13:41:39 +09:00
parent 9a11901590
commit 9a9351a082
3 changed files with 163 additions and 1 deletions

View File

@@ -0,0 +1 @@
export * from "./title"

View File

@@ -0,0 +1,62 @@
export type SessionStatus = "ready" | "processing" | "tool" | "error" | "idle"
const STATUS_ICONS: Record<SessionStatus, string> = {
ready: "",
processing: "◐",
tool: "⚡",
error: "✖",
idle: "○",
}
export interface TitleContext {
sessionId: string
sessionTitle?: string
directory?: string
status?: SessionStatus
currentTool?: string
customSuffix?: string
}
const DEFAULT_TITLE = "OpenCode"
const MAX_TITLE_LENGTH = 30
function truncate(str: string, maxLen: number): string {
if (str.length <= maxLen) return str
return str.slice(0, maxLen - 1) + "…"
}
export function formatTerminalTitle(ctx: TitleContext): string {
const title = ctx.sessionTitle || DEFAULT_TITLE
const truncatedTitle = truncate(title, MAX_TITLE_LENGTH)
const parts: string[] = ["[OpenCode]", truncatedTitle]
if (ctx.status) {
parts.push(STATUS_ICONS[ctx.status])
}
return parts.join(" ")
}
function isTmuxEnvironment(): boolean {
return !!process.env.TMUX || process.env.TERM_PROGRAM === "tmux"
}
export function setTerminalTitle(title: string): void {
// Use stderr to avoid race conditions with stdout buffer
// ANSI escape sequences work on stderr as well
process.stderr.write(`\x1b]0;${title}\x07`)
if (isTmuxEnvironment()) {
process.stderr.write(`\x1bk${title}\x1b\\`)
}
}
export function updateTerminalTitle(ctx: TitleContext): void {
const title = formatTerminalTitle(ctx)
setTerminalTitle(title)
}
export function resetTerminalTitle(): void {
setTerminalTitle(`[OpenCode] ${DEFAULT_TITLE}`)
}

View File

@@ -1,11 +1,18 @@
import type { Plugin } from "@opencode-ai/plugin" import type { Plugin } from "@opencode-ai/plugin"
import { builtinAgents } from "./agents" import { builtinAgents } from "./agents"
import { createTodoContinuationEnforcer, createContextWindowMonitorHook } from "./hooks" import { createTodoContinuationEnforcer, createContextWindowMonitorHook } from "./hooks"
import { updateTerminalTitle } from "./features/terminal"
const OhMyOpenCodePlugin: Plugin = async (ctx) => { const OhMyOpenCodePlugin: Plugin = async (ctx) => {
const todoContinuationEnforcer = createTodoContinuationEnforcer(ctx) const todoContinuationEnforcer = createTodoContinuationEnforcer(ctx)
const contextWindowMonitor = createContextWindowMonitorHook(ctx) const contextWindowMonitor = createContextWindowMonitorHook(ctx)
updateTerminalTitle({ sessionId: "main" })
let mainSessionID: string | undefined
let currentSessionID: string | undefined
let currentSessionTitle: string | undefined
return { return {
config: async (config) => { config: async (config) => {
config.agent = { config.agent = {
@@ -17,9 +24,101 @@ const OhMyOpenCodePlugin: Plugin = async (ctx) => {
event: async (input) => { event: async (input) => {
await todoContinuationEnforcer(input) await todoContinuationEnforcer(input)
await contextWindowMonitor.event(input) await contextWindowMonitor.event(input)
const { event } = input
const props = event.properties as Record<string, unknown> | undefined
if (event.type === "session.created") {
const sessionInfo = props?.info as { id?: string; title?: string; parentID?: string } | undefined
if (!sessionInfo?.parentID) {
mainSessionID = sessionInfo?.id
currentSessionID = sessionInfo?.id
currentSessionTitle = sessionInfo?.title
updateTerminalTitle({
sessionId: currentSessionID || "main",
status: "idle",
directory: ctx.directory,
sessionTitle: currentSessionTitle,
})
}
}
if (event.type === "session.updated") {
const sessionInfo = props?.info as { id?: string; title?: string; parentID?: string } | undefined
if (!sessionInfo?.parentID) {
currentSessionID = sessionInfo?.id
currentSessionTitle = sessionInfo?.title
updateTerminalTitle({
sessionId: currentSessionID || "main",
status: "processing",
directory: ctx.directory,
sessionTitle: currentSessionTitle,
})
}
}
if (event.type === "session.deleted") {
const sessionInfo = props?.info as { id?: string } | undefined
if (sessionInfo?.id === mainSessionID) {
mainSessionID = undefined
currentSessionID = undefined
currentSessionTitle = undefined
updateTerminalTitle({
sessionId: "main",
status: "idle",
})
}
}
if (event.type === "session.error") {
const sessionID = props?.sessionID as string | undefined
if (sessionID && sessionID === mainSessionID) {
updateTerminalTitle({
sessionId: sessionID,
status: "error",
directory: ctx.directory,
sessionTitle: currentSessionTitle,
})
}
}
if (event.type === "session.idle") {
const sessionID = props?.sessionID as string | undefined
if (sessionID && sessionID === mainSessionID) {
updateTerminalTitle({
sessionId: sessionID,
status: "idle",
directory: ctx.directory,
sessionTitle: currentSessionTitle,
})
}
}
}, },
"tool.execute.after": contextWindowMonitor["tool.execute.after"], "tool.execute.before": async (input, _output) => {
if (input.sessionID === mainSessionID) {
updateTerminalTitle({
sessionId: input.sessionID,
status: "tool",
currentTool: input.tool,
directory: ctx.directory,
sessionTitle: currentSessionTitle,
})
}
},
"tool.execute.after": async (input, output) => {
await contextWindowMonitor["tool.execute.after"](input, output)
if (input.sessionID === mainSessionID) {
updateTerminalTitle({
sessionId: input.sessionID,
status: "idle",
directory: ctx.directory,
sessionTitle: currentSessionTitle,
})
}
},
} }
} }