feat(terminal): 支持配置 shell 与 shellArgs,终端标题使用 shell 名 (#125)
- 支持通过 cordis.patch.yml 指定终端 shell - 支持 shellArgs 带参启动终端 shell - 终端标题改用 shell 名,内部使用 uuid 管理
This commit is contained in:
@@ -201,6 +201,9 @@ export const api = {
|
||||
id,
|
||||
...(reason !== undefined ? { reason } : {}),
|
||||
})),
|
||||
/** The effective terminal shell and its display name (plugin-global). */
|
||||
shellGet: () =>
|
||||
call<{ shell: string; name: string }>('shell.get', {}),
|
||||
/** Read the side card preferences (plugin-global, no session scope). */
|
||||
settingsGet: () =>
|
||||
call<{ value?: unknown; revision?: number; externalDisable?: boolean }>('settings.get', {}),
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
import type { Context } from '../../context-types.ts'
|
||||
import type { BetterSidebarService } from '../service.ts'
|
||||
import { builtinTabs } from './tabs.tsx'
|
||||
import { builtinTabs, type BuiltinTabOptions } from './tabs.tsx'
|
||||
import { builtinViewers } from './viewers.tsx'
|
||||
|
||||
/**
|
||||
@@ -17,9 +17,13 @@ import { builtinViewers } from './viewers.tsx'
|
||||
* disposal). The `ctx` is threaded into tab descriptors that need it
|
||||
* (EditorHost reads `ctx.betterSidebar` for file-viewer matching).
|
||||
*/
|
||||
export function registerBuiltins(ctx: Context, service: BetterSidebarService): () => void {
|
||||
export function registerBuiltins(
|
||||
ctx: Context,
|
||||
service: BetterSidebarService,
|
||||
options: BuiltinTabOptions = {},
|
||||
): () => void {
|
||||
const disposers: (() => void)[] = []
|
||||
for (const tab of builtinTabs(ctx)) {
|
||||
for (const tab of builtinTabs(ctx, options)) {
|
||||
disposers.push(service.registerTab(tab))
|
||||
}
|
||||
for (const viewer of builtinViewers()) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* (editor / git / terminal / browser / subagent / diff) through
|
||||
* the same {@link BetterSidebarService} external plugins use — eating its
|
||||
* own dogfood. The terminal descriptor owns its quota (`TERMINAL_LIMIT`)
|
||||
* and mints `terminal:<n>` ids through `createTab`; the browser mints
|
||||
* and mints `terminal:<uuid>` ids through `createTab`; the browser mints
|
||||
* `browser:<n>` the same way (no quota). The editor IS the files window
|
||||
* (the old standalone explorer merged into it).
|
||||
*/
|
||||
@@ -52,6 +52,20 @@ interface TerminalViewProps {
|
||||
/** How many UI-owned terminals may be open at once (agent-owned ones are uncapped). */
|
||||
export const TERMINAL_LIMIT = 3
|
||||
|
||||
/** Optional per-registration builtin behavior (currently terminal title). */
|
||||
export interface BuiltinTabOptions {
|
||||
/** Returns the display title for newly opened terminal tabs. */
|
||||
terminalTitle?: () => string
|
||||
}
|
||||
|
||||
/** A client-side uuid for terminal tab identity (not shown in the UI). */
|
||||
function terminalUuid(): string {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
return `t${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`
|
||||
}
|
||||
|
||||
/** Count UI-owned terminals (agent:` tabs excluded — they are the model's). */
|
||||
function uiTerminalCount(state: SidebarState): number {
|
||||
return allLeaves(state.splits)
|
||||
@@ -60,7 +74,7 @@ function uiTerminalCount(state: SidebarState): number {
|
||||
}
|
||||
|
||||
/** The 6 built-in tab descriptors. */
|
||||
export function builtinTabs(ctx: Context): readonly TabDescriptor[] {
|
||||
export function builtinTabs(ctx: Context, options: BuiltinTabOptions = {}): readonly TabDescriptor[] {
|
||||
return [
|
||||
{
|
||||
id: 'editor',
|
||||
@@ -192,10 +206,12 @@ export function builtinTabs(ctx: Context): readonly TabDescriptor[] {
|
||||
if (count >= TERMINAL_LIMIT) return null
|
||||
return {
|
||||
tab: {
|
||||
id: `terminal:${state.nextTerminal}`,
|
||||
id: `terminal:${terminalUuid()}`,
|
||||
type: 'terminal',
|
||||
title: `${t('terminal')} ${state.nextTerminal}`,
|
||||
title: options.terminalTitle?.() ?? t('terminal'),
|
||||
},
|
||||
// Keep the legacy counter advancing for compatibility with older
|
||||
// persisted states; new ids no longer use it.
|
||||
patch: { nextTerminal: state.nextTerminal + 1 },
|
||||
}
|
||||
},
|
||||
|
||||
+22
-2
@@ -11,7 +11,7 @@
|
||||
import { createElement } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import type { Context } from '../context-types.ts'
|
||||
import { createSidebarStore } from './state.ts'
|
||||
import { allLeaves, createSidebarStore, isAgentTabId } from './state.ts'
|
||||
import { createBetterSidebarService, matchUrlTarget } from './service.ts'
|
||||
import { resetChunks } from './chunk-loader.ts'
|
||||
import { registerBuiltins } from './builtins/index.ts'
|
||||
@@ -66,11 +66,31 @@ export function apply(ctx: Context): void {
|
||||
// are ready by the time the sidebar renders.
|
||||
const service = createBetterSidebarService(sidebarStore)
|
||||
ctx.provide('betterSidebar', service)
|
||||
// Terminal tab titles use the host's effective shell name (e.g. bash/zsh)
|
||||
// instead of "Terminal 1". Start with a safe fallback and replace it as
|
||||
// soon as the host shell info resolves. Tabs created before the response
|
||||
// arrives keep the fallback title, so also retitle any already-open UI
|
||||
// terminal tabs that still carry it.
|
||||
const fallbackTitle = t('terminal')
|
||||
let terminalTitle = fallbackTitle
|
||||
void api.shellGet().then(({ name }) => {
|
||||
terminalTitle = name
|
||||
const snapshot = service.getSnapshot()
|
||||
if (snapshot.state === undefined) return
|
||||
const tabs = allLeaves(snapshot.state.splits)
|
||||
.concat(allLeaves(snapshot.state.bottomSplits))
|
||||
.flatMap(leaf => leaf.tabs)
|
||||
for (const tab of tabs) {
|
||||
if (tab.type === 'terminal' && !isAgentTabId(tab.id) && tab.title === fallbackTitle) {
|
||||
service.updateTab(tab.id, { title: name })
|
||||
}
|
||||
}
|
||||
}).catch(() => { /* keep fallback */ })
|
||||
// Register the plugin's own built-in tabs and viewers through the same
|
||||
// service (eating our own dogfood). The disposer unregisters them on
|
||||
// fiber disposal (HMR-safe).
|
||||
ctx.effect(
|
||||
() => registerBuiltins(ctx, service),
|
||||
() => registerBuiltins(ctx, service, { terminalTitle: () => terminalTitle }),
|
||||
'dsh-better-sidebar: register built-in tabs and viewers',
|
||||
)
|
||||
// A failure anywhere in the client lifecycle must never take the app down
|
||||
|
||||
Reference in New Issue
Block a user