Merge remote-tracking branch 'origin/master' into fix/human-transcript-projection

# Conflicts:
#	.agents/notes/implemented/feature/2026-07-17-dedicated-full-screen-tui-front-door.i18n.yaml
#	docs/cordis-catalog/services.md
#	packages/host/apiproxy/README.i18n.yaml
#	packages/ui/tui/README.i18n.yaml
This commit is contained in:
Hypatia May
2026-07-30 19:57:49 +08:00
270 changed files with 4387 additions and 3144 deletions

View File

@@ -19,7 +19,7 @@ import {
type SlashCommand,
type TerminalColorScheme,
} from '@earendil-works/pi-tui'
import { Service, type Context, type Fiber } from 'cordis'
import { Service, type Context, type Fiber, type FiberState } from 'cordis'
import {
assembleContextFor,
installAgentLlmTarget,
@@ -57,6 +57,7 @@ import {
TuiExtensionServiceImpl,
TuiOverlayManager,
} from './extension/overlay-manager.ts'
import {
parseTuiPromptTemplate,
renderTuiPromptTemplate,
@@ -172,6 +173,9 @@ export type {
TuiViewport,
} from './extension/types.ts'
/** First terminal Cordis state: FAILED, DISPOSED, and UNLOADING are unusable. */
const FIBER_FAILED = 3 as FiberState.FAILED
declare module 'cordis' {
interface Context {
/** Terminal-only interaction service, available only while a TUI is mounted. */
@@ -184,8 +188,6 @@ declare module 'cordis' {
tuiGoodbyeMessage: string | undefined
/** Skill the launcher wants auto-invoked as the fresh session's first turn; absent leaves it to the user. */
tuiInitialSkill: string | undefined
/** Launcher-owned session-store root the app bundle defaults to; absent keeps the bundle's project-local default. */
launcherSessionsRoot: string | undefined
}
}
@@ -230,16 +232,6 @@ export const TUI_GOODBYE_MESSAGE_KEY = 'tuiGoodbyeMessage'
*/
export const INITIAL_SKILL_KEY = 'tuiInitialSkill'
/**
* Context key a launcher sets before any Loader entry mounts
* (`ctx.provide(SESSIONS_ROOT_KEY, root)`) to supply its session-store root as
* the app bundle's default persistence root. Shared-store policy (one store
* across every cwd) belongs to the launcher — the dsh CLI resolves it under the
* Harness home — never to a plugin; a bundle without this slot keeps its own
* project-local default, and an explicit `persistenceRoot` config still wins.
*/
export const SESSIONS_ROOT_KEY = 'launcherSessionsRoot'
/**
* Optional terminal-local interaction service provided by one mounted TUI.
*
@@ -315,7 +307,6 @@ export function createTuiChat(
const sessionId = SessionId(config.sessionId ?? 'main')
const agent = ctx.agents.get(sessionId)
if (agent === undefined) throw new Error(`ui-tui: session "${sessionId}" is not running`)
const sessionQuery = ctx.get('sessionQuery')
const resolved = resolveTuiConfig(config)
const palette = createPalette(resolved.theme.color)
const mdTheme = markdownTheme(palette)
@@ -877,7 +868,14 @@ export function createTuiChat(
resolved,
palette,
overlayManager,
sessionQuery,
// Optional and independently mounted. Cordis transiently leaves this sibling
// non-ACTIVE during command callbacks, so the non-strict read is intentional;
// terminal fiber states still exclude failed, closing, and closed providers.
sessionQuery: () => {
const implementation = ctx.reflect._getImpl('sessionQuery', false)
if (implementation === undefined || implementation.fiber.state >= FIBER_FAILED) return undefined
return ctx.get('sessionQuery', false)
},
ui,
editor,
appendNotice,
@@ -1689,9 +1687,35 @@ export function mountTui(ctx: Context, config: Config, runtime: TuiRuntime): voi
if (existing !== undefined) start(existing)
}
const ROOT_DISPOSE_TIMEOUT_MS = 5_000
/**
* Dispose the whole application before process exit, with a bounded fallback.
* @param ctx - The TUI plugin context whose root owns sibling resources.
* @param code - Process status to report.
* @param exit - Exit boundary, replaceable by tests.
*/
export function disposeRootAndExit(
ctx: Context,
code: number,
exit: (status: number) => void = (status) => { process.exit(status) },
): void {
let exited = false
const exitOnce = (): void => {
if (exited) return
exited = true
exit(code)
}
const timeout = setTimeout(exitOnce, ROOT_DISPOSE_TIMEOUT_MS)
void ctx.root.fiber.dispose().then(
() => { clearTimeout(timeout); exitOnce() },
() => { clearTimeout(timeout); exitOnce() },
)
}
/** Cordis entry point using the process terminal; explicit TUI composition requires a TTY pair. */
/* v8 ignore start -- production process wiring; fake-terminal tests cover mountTui/createTuiChat,
and the tui-agent PTY smoke covers the real entry */
and apps/cli PTY smokes cover the real entry */
export function apply(ctx: Context, config: Config): void {
if (!process.stdin.isTTY || !process.stdout.isTTY) {
throw new Error('ui-tui: both stdin and stdout must be TTYs; use the one-shot @deepseek-ai/dsh-cli-demo app for pipes')
@@ -1711,7 +1735,7 @@ export function apply(ctx: Context, config: Config): void {
initialSkill === undefined ? {} : { initialSkill },
), {
terminal: new ProcessTerminal(),
exit: code => process.exit(code),
exit: (code) => { disposeRootAndExit(ctx, code) },
...resumeHost === undefined ? {} : { handoffResume: (sessionId, cwd) => resumeHost.handoff(sessionId, cwd) },
...goodbyeMessage === undefined ? {} : { goodbyeMessage },
})