mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 18:47:40 +00:00
feat(cli): set the title of one terminal pane (#30485)
* feat(cli): set the title of one terminal pane Adds `orca terminal set-pane-title --terminal <handle> --title <text>`; `--title ""` clears it. The runtime resolves the live handle to its saved leaf and asks the desktop window to name that pane through a new optional notifier hook (`ui:setPaneTitle`). Mounted panes apply it through the same local setter the GUI uses; parked panes get it in the saved layout their next mount reads. The tab's own name is never touched. Rebuilt on current main from #24755. Co-authored-by: stevelliu <stevelliu@tencent.com> * fix(renderer): a main-owned layout takes a CLI pane title before the window shows it * fix(renderer): a CLI pane title reaches a never-mounted tab with a rootless layout --------- Co-authored-by: stevelliu <stevelliu@tencent.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -38,6 +38,7 @@ export {
|
||||
formatTerminalList,
|
||||
formatTerminalRead,
|
||||
formatTerminalRename,
|
||||
formatTerminalSetPaneTitle,
|
||||
formatTerminalSend,
|
||||
formatTerminalShow,
|
||||
formatTerminalSplit,
|
||||
|
||||
@@ -115,6 +115,7 @@ export const HANDLER_GROUPS: readonly HandlerGroup[] = [
|
||||
'terminal wait',
|
||||
'terminal stop',
|
||||
'terminal rename',
|
||||
'terminal set-pane-title',
|
||||
'terminal create',
|
||||
'terminal switch',
|
||||
'terminal close',
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { RuntimeClient, RuntimeClientError } from '../runtime-client'
|
||||
import { parseArgs } from '../args'
|
||||
import { printHelp } from '../help'
|
||||
import { COMMAND_SPECS } from '../specs'
|
||||
import { TERMINAL_HANDLERS } from './terminal'
|
||||
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
describe('terminal set-pane-title', () => {
|
||||
it.each(['REVIEWER', ''])('sends an explicit title %j to the pane method', async (title) => {
|
||||
const parsed = parseArgs([
|
||||
'terminal',
|
||||
'set-pane-title',
|
||||
'--terminal',
|
||||
'term-1',
|
||||
'--title',
|
||||
title
|
||||
])
|
||||
const client = new RuntimeClient()
|
||||
const call = vi.spyOn(client, 'call').mockResolvedValue({
|
||||
id: 'request',
|
||||
ok: true,
|
||||
result: {
|
||||
paneTitle: { handle: 'term-1', tabId: 'tab-1', leafId: 'leaf-1', title: title || null }
|
||||
},
|
||||
_meta: { runtimeId: 'runtime-1' }
|
||||
})
|
||||
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
await TERMINAL_HANDLERS['terminal set-pane-title']({
|
||||
flags: parsed.flags,
|
||||
client,
|
||||
cwd: '/tmp',
|
||||
json: true
|
||||
})
|
||||
expect(call).toHaveBeenCalledExactlyOnceWith('terminal.setPaneTitle', {
|
||||
terminal: 'term-1',
|
||||
title
|
||||
})
|
||||
expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toMatchObject({
|
||||
result: { paneTitle: { title: title || null } }
|
||||
})
|
||||
})
|
||||
|
||||
it.each([{ args: [] }, { args: ['--title'] }])(
|
||||
'refuses a missing title before resolving a terminal (%j)',
|
||||
async ({ args }) => {
|
||||
const client = new RuntimeClient()
|
||||
const call = vi.spyOn(client, 'call')
|
||||
const parsed = parseArgs(['terminal', 'set-pane-title', ...args])
|
||||
await expect(
|
||||
TERMINAL_HANDLERS['terminal set-pane-title']({
|
||||
flags: parsed.flags,
|
||||
client,
|
||||
cwd: '/tmp',
|
||||
json: true
|
||||
})
|
||||
).rejects.toMatchObject({ code: 'invalid_argument' })
|
||||
expect(call).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
it('propagates an older host refusal without falling back to tab rename', async () => {
|
||||
const client = new RuntimeClient()
|
||||
const call = vi
|
||||
.spyOn(client, 'call')
|
||||
.mockRejectedValue(new RuntimeClientError('method_not_found', 'Unknown method'))
|
||||
await expect(
|
||||
TERMINAL_HANDLERS['terminal set-pane-title']({
|
||||
flags: new Map([
|
||||
['terminal', 'term-1'],
|
||||
['title', 'REVIEWER']
|
||||
]),
|
||||
client,
|
||||
cwd: '/tmp',
|
||||
json: true
|
||||
})
|
||||
).rejects.toMatchObject({ code: 'method_not_found' })
|
||||
expect(call).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('documents a required pane-scoped title and explicit clearing', () => {
|
||||
const spec = COMMAND_SPECS.find((entry) => entry.path.join(' ') === 'terminal set-pane-title')
|
||||
expect(spec?.usage).toContain('--title <text>')
|
||||
expect(spec?.usage).not.toContain('[--title')
|
||||
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
printHelp(COMMAND_SPECS, ['terminal', 'set-pane-title'])
|
||||
const help = log.mock.calls.flat().join('\n')
|
||||
expect(help).toContain('Custom title for one pane (pass "" to reset)')
|
||||
expect(help).not.toContain('omit or pass')
|
||||
})
|
||||
})
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
RuntimeTerminalListResult,
|
||||
RuntimeTerminalRead,
|
||||
RuntimeTerminalRename,
|
||||
RuntimeTerminalSetPaneTitle,
|
||||
RuntimeTerminalShow,
|
||||
RuntimeTerminalSplit,
|
||||
RuntimeTerminalWait
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
formatTerminalList,
|
||||
formatTerminalRead,
|
||||
formatTerminalRename,
|
||||
formatTerminalSetPaneTitle,
|
||||
formatTerminalShow,
|
||||
formatTerminalSplit,
|
||||
formatTerminalWait,
|
||||
@@ -24,7 +26,8 @@ import {
|
||||
import {
|
||||
getOptionalPositiveIntegerFlag,
|
||||
getOptionalStringFlag,
|
||||
getRequiredStringFlag
|
||||
getRequiredStringFlag,
|
||||
getRequiredStringFlagAllowingEmpty
|
||||
} from '../flags'
|
||||
import {
|
||||
annotateOmittedHostScope,
|
||||
@@ -152,6 +155,14 @@ export const TERMINAL_HANDLERS: Record<string, CommandHandler> = {
|
||||
})
|
||||
printResult(result, json, formatTerminalRename)
|
||||
},
|
||||
'terminal set-pane-title': async ({ flags, client, cwd, json }) => {
|
||||
const title = getRequiredStringFlagAllowingEmpty(flags, 'title')
|
||||
const result = await client.call<{ paneTitle: RuntimeTerminalSetPaneTitle }>(
|
||||
'terminal.setPaneTitle',
|
||||
{ terminal: await getTerminalHandle(flags, cwd, client), title }
|
||||
)
|
||||
printResult(result, json, formatTerminalSetPaneTitle)
|
||||
},
|
||||
'terminal create': async ({ flags, client, cwd, json }) => {
|
||||
if (client.isRemote && !flags.has('worktree')) {
|
||||
throw new RuntimeClientError(
|
||||
|
||||
@@ -88,6 +88,10 @@ function formatCommandFlagHelp(flag: string, commandPath: string[]): string {
|
||||
if (command === 'terminal close' && flag === 'tab') {
|
||||
return '--tab Close the whole tab and wait for durable persistence'
|
||||
}
|
||||
// Why: the shared --title help says "terminal tab" — correct for `rename`, wrong for one pane.
|
||||
if (command === 'terminal set-pane-title' && flag === 'title') {
|
||||
return '--title <text> Custom title for one pane (pass "" to reset)'
|
||||
}
|
||||
if (command === 'linear issue' && flag === 'id') {
|
||||
return '--id <id> Linear issue key, id, or URL'
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ export const ROOT_HELP_TEXT_PRIMARY = [
|
||||
' terminal wait Wait for a terminal condition (exit, tui-idle)',
|
||||
' terminal create Create a terminal session in a worktree',
|
||||
' terminal rename Set or clear the title of a terminal tab',
|
||||
' terminal set-pane-title Set or clear the title of one terminal pane',
|
||||
' terminal split Split an existing terminal pane',
|
||||
' terminal switch Bring a terminal tab to the foreground',
|
||||
' terminal focus Alias for terminal switch',
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SERVE_COMMAND_SPECS } from './serve'
|
||||
import { REPO_COMMAND_SPECS } from './repo'
|
||||
import { TERMINAL_SEND_COMMAND_SPEC } from './terminal-send'
|
||||
import { TERMINAL_CLOSE_COMMAND_SPEC } from './terminal-close'
|
||||
import { TERMINAL_SET_PANE_TITLE_COMMAND_SPEC } from './terminal-set-pane-title'
|
||||
|
||||
export const CORE_COMMAND_SPECS: CommandSpec[] = [
|
||||
{
|
||||
@@ -257,6 +258,7 @@ export const CORE_COMMAND_SPECS: CommandSpec[] = [
|
||||
'orca terminal rename --terminal term_abc123 --json'
|
||||
]
|
||||
},
|
||||
TERMINAL_SET_PANE_TITLE_COMMAND_SPEC,
|
||||
{
|
||||
path: ['terminal', 'split'],
|
||||
summary: 'Split an existing terminal pane',
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { CommandSpec } from '../args'
|
||||
import { GLOBAL_FLAGS } from '../args'
|
||||
|
||||
export const TERMINAL_SET_PANE_TITLE_COMMAND_SPEC: CommandSpec = {
|
||||
path: ['terminal', 'set-pane-title'],
|
||||
summary: 'Set or clear the title of one terminal pane',
|
||||
usage: 'orca terminal set-pane-title [--terminal <handle>] --title <text> [--json]',
|
||||
allowedFlags: [...GLOBAL_FLAGS, 'terminal', 'title'],
|
||||
notes: [
|
||||
'Sets the title of ONE pane. Split panes share a tab, so terminal rename cannot address one of them; use this command for a single pane.',
|
||||
'Requires the running Orca desktop; headless pane title changes are not supported.',
|
||||
'Pass --title "" to clear the pane back to its automatic title.'
|
||||
],
|
||||
examples: [
|
||||
'orca terminal set-pane-title --terminal term_abc123 --title "RUNNER"',
|
||||
'orca terminal set-pane-title --terminal term_abc123 --title "" --json'
|
||||
]
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
RuntimeTerminalRead,
|
||||
RuntimeTerminalRename,
|
||||
RuntimeTerminalSend,
|
||||
RuntimeTerminalSetPaneTitle,
|
||||
RuntimeTerminalShow,
|
||||
RuntimeTerminalSplit,
|
||||
RuntimeTerminalWait
|
||||
@@ -228,6 +229,14 @@ export function formatTerminalRename(result: { rename: RuntimeTerminalRename }):
|
||||
: `Cleared title for terminal ${result.rename.handle}.`
|
||||
}
|
||||
|
||||
export function formatTerminalSetPaneTitle(result: {
|
||||
paneTitle: RuntimeTerminalSetPaneTitle
|
||||
}): string {
|
||||
return result.paneTitle.title
|
||||
? `Set title of pane ${result.paneTitle.leafId} to "${result.paneTitle.title}".`
|
||||
: `Cleared title of pane ${result.paneTitle.leafId}.`
|
||||
}
|
||||
|
||||
export function formatTerminalCreate(result: { terminal: RuntimeTerminalCreate }): string {
|
||||
const titleNote = result.terminal.title ? ` (title: "${result.terminal.title}")` : ''
|
||||
const surfaceNote = result.terminal.surface ? ` [${result.terminal.surface}]` : ''
|
||||
|
||||
Reference in New Issue
Block a user