fix(tui): set mini terminal title on Linux (#38261)
This commit is contained in:
parent
ced3d5e02a
commit
0e08b7330f
5 changed files with 52 additions and 0 deletions
|
|
@ -82,6 +82,7 @@ export type Lifecycle = {
|
||||||
footer: FooterApi
|
footer: FooterApi
|
||||||
onResize(fn: () => void): () => void
|
onResize(fn: () => void): () => void
|
||||||
refreshTheme(): void
|
refreshTheme(): void
|
||||||
|
setTitle(title?: string): void
|
||||||
resetForReplay(input: { sessionTitle?: string; sessionID?: string; history: RunPrompt[] }): Promise<void>
|
resetForReplay(input: { sessionTitle?: string; sessionID?: string; history: RunPrompt[] }): Promise<void>
|
||||||
close(input: { showExit: boolean; sessionTitle?: string; sessionID?: string; history?: RunPrompt[] }): Promise<void>
|
close(input: { showExit: boolean; sessionTitle?: string; sessionID?: string; history?: RunPrompt[] }): Promise<void>
|
||||||
}
|
}
|
||||||
|
|
@ -184,6 +185,12 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
||||||
consoleMode: "disabled",
|
consoleMode: "disabled",
|
||||||
clearOnShutdown: false,
|
clearOnShutdown: false,
|
||||||
})
|
})
|
||||||
|
const setTitle = (title?: string) => {
|
||||||
|
if (input.host.platform !== "linux") return
|
||||||
|
if (!title || isDefaultTitle(title)) return renderer.setTerminalTitle("OpenCode")
|
||||||
|
renderer.setTerminalTitle(`OC | ${title.length > 40 ? title.slice(0, 37) + "..." : title}`)
|
||||||
|
}
|
||||||
|
setTitle(input.sessionTitle)
|
||||||
const theme = await resolveRunTheme(renderer, tuiConfig.theme, mono)
|
const theme = await resolveRunTheme(renderer, tuiConfig.theme, mono)
|
||||||
renderer.setBackgroundColor(theme.background)
|
renderer.setBackgroundColor(theme.background)
|
||||||
const state: SplashState = {
|
const state: SplashState = {
|
||||||
|
|
@ -338,6 +345,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
||||||
footer.close()
|
footer.close()
|
||||||
await footer.idle().catch(() => {})
|
await footer.idle().catch(() => {})
|
||||||
footer.destroy()
|
footer.destroy()
|
||||||
|
if (input.host.platform === "linux") renderer.setTerminalTitle("")
|
||||||
shutdown(renderer)
|
shutdown(renderer)
|
||||||
if (!wroteExit) {
|
if (!wroteExit) {
|
||||||
input.host.stdout.write("\n")
|
input.host.stdout.write("\n")
|
||||||
|
|
@ -350,6 +358,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
|
||||||
refreshTheme() {
|
refreshTheme() {
|
||||||
footer.refreshTheme()
|
footer.refreshTheme()
|
||||||
},
|
},
|
||||||
|
setTitle,
|
||||||
onResize(fn) {
|
onResize(fn) {
|
||||||
let width = renderer.terminalWidth
|
let width = renderer.terminalWidth
|
||||||
let height = renderer.terminalHeight
|
let height = renderer.terminalHeight
|
||||||
|
|
|
||||||
|
|
@ -443,6 +443,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||||
if (footer.isClosed || runtimeController.signal.aborted) return
|
if (footer.isClosed || runtimeController.signal.aborted) return
|
||||||
state.sessionID = next.sessionID
|
state.sessionID = next.sessionID
|
||||||
state.sessionTitle = next.sessionTitle ?? state.sessionTitle
|
state.sessionTitle = next.sessionTitle ?? state.sessionTitle
|
||||||
|
shell.setTitle(state.sessionTitle)
|
||||||
state.agent = next.agent
|
state.agent = next.agent
|
||||||
state.location = next.location
|
state.location = next.location
|
||||||
state.model = next.model
|
state.model = next.model
|
||||||
|
|
@ -741,6 +742,10 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||||
replayLimit: input.replayLimit,
|
replayLimit: input.replayLimit,
|
||||||
footer,
|
footer,
|
||||||
onCommit: rememberLocal,
|
onCommit: rememberLocal,
|
||||||
|
onSessionTitle: (title) => {
|
||||||
|
state.sessionTitle = title
|
||||||
|
shell.setTitle(title)
|
||||||
|
},
|
||||||
trace: log,
|
trace: log,
|
||||||
onCatalogRefresh: requestCatalogRefresh,
|
onCatalogRefresh: requestCatalogRefresh,
|
||||||
contextLimit: (model) =>
|
contextLimit: (model) =>
|
||||||
|
|
@ -887,6 +892,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
|
||||||
state.shown = false
|
state.shown = false
|
||||||
state.sessionID = created.sessionID
|
state.sessionID = created.sessionID
|
||||||
state.sessionTitle = created.sessionTitle
|
state.sessionTitle = created.sessionTitle
|
||||||
|
shell.setTitle(state.sessionTitle)
|
||||||
state.agent = created.agent ?? state.agent
|
state.agent = created.agent ?? state.agent
|
||||||
state.location = created.location
|
state.location = created.location
|
||||||
state.model = created.model
|
state.model = created.model
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ type StreamInput = {
|
||||||
replayLimit?: number
|
replayLimit?: number
|
||||||
footer: FooterApi
|
footer: FooterApi
|
||||||
onCommit?: (commit: StreamCommit) => void
|
onCommit?: (commit: StreamCommit) => void
|
||||||
|
onSessionTitle?: (title: string) => void
|
||||||
trace?: Trace
|
trace?: Trace
|
||||||
signal?: AbortSignal
|
signal?: AbortSignal
|
||||||
onCatalogRefresh?: (signal?: AbortSignal) => unknown | Promise<unknown>
|
onCatalogRefresh?: (signal?: AbortSignal) => unknown | Promise<unknown>
|
||||||
|
|
@ -828,6 +829,10 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
||||||
}
|
}
|
||||||
input.trace?.write("recv.event", event)
|
input.trace?.write("recv.event", event)
|
||||||
subagents.main(client, event, attempt.signal)
|
subagents.main(client, event, attempt.signal)
|
||||||
|
if (event.type === "session.renamed") {
|
||||||
|
input.onSessionTitle?.(event.data.title)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (event.type === "session.input.admitted") {
|
if (event.type === "session.input.admitted") {
|
||||||
if (event.data.input.type !== "user") return
|
if (event.data.input.type !== "user") return
|
||||||
mergePending({
|
mergePending({
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,7 @@ describe("run interactive runtime", () => {
|
||||||
footer: api,
|
footer: api,
|
||||||
onResize: () => () => {},
|
onResize: () => () => {},
|
||||||
refreshTheme: () => {},
|
refreshTheme: () => {},
|
||||||
|
setTitle: () => {},
|
||||||
resetForReplay: () => Promise.resolve(),
|
resetForReplay: () => Promise.resolve(),
|
||||||
close: () => Promise.resolve(),
|
close: () => Promise.resolve(),
|
||||||
}
|
}
|
||||||
|
|
@ -180,6 +181,7 @@ describe("run interactive runtime", () => {
|
||||||
footer: api,
|
footer: api,
|
||||||
onResize: () => () => {},
|
onResize: () => () => {},
|
||||||
refreshTheme: () => {},
|
refreshTheme: () => {},
|
||||||
|
setTitle: () => {},
|
||||||
resetForReplay: () => Promise.resolve(),
|
resetForReplay: () => Promise.resolve(),
|
||||||
close: () => Promise.resolve(),
|
close: () => Promise.resolve(),
|
||||||
}
|
}
|
||||||
|
|
@ -199,6 +201,7 @@ describe("run interactive runtime", () => {
|
||||||
const lifecycleStarted = defer<void>()
|
const lifecycleStarted = defer<void>()
|
||||||
const painted = defer<void>()
|
const painted = defer<void>()
|
||||||
const events: FooterEvent[] = []
|
const events: FooterEvent[] = []
|
||||||
|
const titles: Array<string | undefined> = []
|
||||||
const api = footer(events)
|
const api = footer(events)
|
||||||
api.idle = () => painted.promise
|
api.idle = () => painted.promise
|
||||||
const event = api.event
|
const event = api.event
|
||||||
|
|
@ -265,6 +268,7 @@ describe("run interactive runtime", () => {
|
||||||
footer: api,
|
footer: api,
|
||||||
onResize: () => () => {},
|
onResize: () => () => {},
|
||||||
refreshTheme: () => {},
|
refreshTheme: () => {},
|
||||||
|
setTitle: (title) => titles.push(title),
|
||||||
resetForReplay: () => Promise.resolve(),
|
resetForReplay: () => Promise.resolve(),
|
||||||
close: () => Promise.resolve(),
|
close: () => Promise.resolve(),
|
||||||
}
|
}
|
||||||
|
|
@ -282,6 +286,7 @@ describe("run interactive runtime", () => {
|
||||||
history: [{ text: "previous prompt", parts: [] }],
|
history: [{ text: "previous prompt", parts: [] }],
|
||||||
})
|
})
|
||||||
expect(events).toContainEqual({ type: "agent", agent: "review" })
|
expect(events).toContainEqual({ type: "agent", agent: "review" })
|
||||||
|
expect(titles).toEqual(["Resume"])
|
||||||
expect(events).toContainEqual({
|
expect(events).toContainEqual({
|
||||||
type: "model",
|
type: "model",
|
||||||
model: "Little Frank · OpenAI · high",
|
model: "Little Frank · OpenAI · high",
|
||||||
|
|
@ -344,6 +349,7 @@ describe("run interactive runtime", () => {
|
||||||
footer: api,
|
footer: api,
|
||||||
onResize: () => () => {},
|
onResize: () => () => {},
|
||||||
refreshTheme: () => {},
|
refreshTheme: () => {},
|
||||||
|
setTitle: () => {},
|
||||||
resetForReplay: () => Promise.resolve(),
|
resetForReplay: () => Promise.resolve(),
|
||||||
close: async (input) => {
|
close: async (input) => {
|
||||||
closedTitle = input.sessionTitle
|
closedTitle = input.sessionTitle
|
||||||
|
|
@ -423,6 +429,7 @@ describe("run interactive runtime", () => {
|
||||||
footer: api,
|
footer: api,
|
||||||
onResize: () => () => {},
|
onResize: () => () => {},
|
||||||
refreshTheme: () => {},
|
refreshTheme: () => {},
|
||||||
|
setTitle: () => {},
|
||||||
resetForReplay: () => Promise.resolve(),
|
resetForReplay: () => Promise.resolve(),
|
||||||
close: () => Promise.resolve(),
|
close: () => Promise.resolve(),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,31 @@ afterEach(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("V2 mini transport", () => {
|
describe("V2 mini transport", () => {
|
||||||
|
test("reports session title changes", async () => {
|
||||||
|
const events = feed()
|
||||||
|
events.push(connected())
|
||||||
|
const titles: string[] = []
|
||||||
|
const transport = await createSessionTransport({
|
||||||
|
sdk: sdk({ streams: [events] }),
|
||||||
|
sessionID: "ses_1",
|
||||||
|
thinking: false,
|
||||||
|
footer: footer().api,
|
||||||
|
onSessionTitle: (title) => titles.push(title),
|
||||||
|
})
|
||||||
|
|
||||||
|
events.push({
|
||||||
|
id: "evt_renamed",
|
||||||
|
created: 1,
|
||||||
|
type: "session.renamed",
|
||||||
|
durable: durable("ses_1", 1),
|
||||||
|
data: { sessionID: "ses_1", title: "Greeting" },
|
||||||
|
})
|
||||||
|
|
||||||
|
while (titles.length === 0) await Bun.sleep(0)
|
||||||
|
expect(titles).toEqual(["Greeting"])
|
||||||
|
await transport.close()
|
||||||
|
})
|
||||||
|
|
||||||
test("formats footer usage with compact tokens and context percentage", async () => {
|
test("formats footer usage with compact tokens and context percentage", async () => {
|
||||||
const events = feed()
|
const events = feed()
|
||||||
events.push(connected())
|
events.push(connected())
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue