cc-haha/desktop/src/api/terminal.ts
程序员阿江(Relakkes) 35f9f0d0f8 feat: make project terminals open where users work
Desktop terminal access should behave like an IDE: active project sessions open a bottom panel in the session working directory while keeping a full terminal tab available for dedicated use. The panel has constrained resizing and cleanup so session tab state remains isolated, and terminal guidance points users to the bundled claude-haha command for extension setup.

Constraint: Desktop bundles the user-facing CLI as claude-haha while claude-sidecar remains internal
Rejected: Always opening a standalone terminal tab | loses the current project context and diverges from common IDE behavior
Rejected: Exposing claude-sidecar in terminal guidance | it is an internal launcher, not the supportable user command
Confidence: high
Scope-risk: moderate
Directive: Keep bottom terminals keyed by session id and pass session workDir/projectPath into spawned terminals
Tested: bun run check:desktop
Tested: git diff --check
Tested: Computer Use E2E against built macOS app during implementation
Not-tested: bun run quality:pr is blocked by existing branch policy requiring allow-cli-core-change approval
2026-05-05 19:05:36 +08:00

59 lines
1.6 KiB
TypeScript

import { isTauriRuntime } from '../lib/desktopRuntime'
export type TerminalSpawnResult = {
session_id: number
shell: string
cwd: string
}
export type TerminalOutputPayload = {
session_id: number
data: string
}
export type TerminalExitPayload = {
session_id: number
code: number
signal?: string | null
}
type Unlisten = () => void
async function invoke<T>(command: string, args?: Record<string, unknown>): Promise<T> {
if (!isTauriRuntime()) {
throw new Error('Terminal is available in the desktop app runtime.')
}
const api = await import('@tauri-apps/api/core')
return api.invoke<T>(command, args)
}
export const terminalApi = {
isAvailable: isTauriRuntime,
spawn(input: { cols: number; rows: number; cwd?: string }) {
return invoke<TerminalSpawnResult>('terminal_spawn', input)
},
write(sessionId: number, data: string) {
return invoke<void>('terminal_write', { sessionId, data })
},
resize(sessionId: number, cols: number, rows: number) {
return invoke<void>('terminal_resize', { sessionId, cols, rows })
},
kill(sessionId: number) {
return invoke<void>('terminal_kill', { sessionId })
},
async onOutput(handler: (payload: TerminalOutputPayload) => void): Promise<Unlisten> {
const events = await import('@tauri-apps/api/event')
return events.listen<TerminalOutputPayload>('terminal-output', (event) => handler(event.payload))
},
async onExit(handler: (payload: TerminalExitPayload) => void): Promise<Unlisten> {
const events = await import('@tauri-apps/api/event')
return events.listen<TerminalExitPayload>('terminal-exit', (event) => handler(event.payload))
},
}