feat(feature): add terminal title renaming with session status
This commit is contained in:
1
src/features/terminal/index.ts
Normal file
1
src/features/terminal/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./title"
|
||||||
62
src/features/terminal/title.ts
Normal file
62
src/features/terminal/title.ts
Normal 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}`)
|
||||||
|
}
|
||||||
101
src/index.ts
101
src/index.ts
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user