Merge origin/master into codex/code-mode-complete-result-card

This commit is contained in:
Tianyi Cui
2026-07-23 17:59:58 +08:00
273 changed files with 10384 additions and 4555 deletions

View File

@@ -0,0 +1,165 @@
/**
* Public interactive-extension contract for one mounted TUI front door.
*
* Plugins receive terminal-specific rendering primitives without access to
* the live pi-tui tree, focus controller, overlay handles, or terminal
* lifecycle. Registrations and open overlays remain owned by the calling
* Cordis fiber.
* @module @deepseek-ai/dsh-tui/extension
*/
/** Terminal component shape accepted from a trusted TUI extension. */
export interface TuiComponent {
/**
* Render this component for the supplied viewport width.
* @param width - Available terminal columns.
* @returns terminal lines owned by this component.
*/
render(width: number): string[]
/**
* Handle one terminal input sequence while this component owns focus.
* @param data - Raw terminal input sequence.
*/
handleInput?(data: string): void
/** Receive key-release events instead of having them filtered by the host. */
wantsKeyRelease?: boolean
/** Drop cached rendering derived from theme, size, or component state. */
invalidate(): void
}
/** Optional focus state forwarded by the host to a component. */
export interface TuiFocusable {
/** Whether the component currently owns terminal focus. */
focused: boolean
}
/** Read-only semantic color roles supplied by the mounted TUI. */
export interface TuiTheme {
/** Render ordinary foreground text. */
readonly text: (value: string) => string
/** Render secondary information. */
readonly muted: (value: string) => string
/** Render low-emphasis hints. */
readonly dim: (value: string) => string
/** Render the active accent role. */
readonly accent: (value: string) => string
/** Render a successful outcome. */
readonly success: (value: string) => string
/** Render a warning. */
readonly warning: (value: string) => string
/** Render an error. */
readonly error: (value: string) => string
/** Apply the host's bold role. */
readonly bold: (value: string) => string
}
/** Current terminal viewport exposed without the mutable Terminal object. */
export interface TuiViewport {
/** Terminal columns. */
readonly columns: number
/** Terminal rows. */
readonly rows: number
}
/** Supported overlay anchor points. */
export type TuiOverlayAnchor =
| 'center'
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right'
| 'top-center'
| 'bottom-center'
| 'left-center'
| 'right-center'
/** Terminal-edge spacing for an overlay. */
export interface TuiOverlayMargin {
/** Rows reserved above the overlay. */
readonly top?: number
/** Columns reserved to the right of the overlay. */
readonly right?: number
/** Rows reserved below the overlay. */
readonly bottom?: number
/** Columns reserved to the left of the overlay. */
readonly left?: number
}
/** Position and size constraints retained under TUI host ownership. */
export interface TuiOverlayOptions {
/** Width in columns or as a percentage of terminal width. */
readonly width?: number | `${number}%`
/** Minimum width in columns. */
readonly minWidth?: number
/** Maximum height in rows or as a percentage of terminal height. */
readonly maxHeight?: number | `${number}%`
/** Overlay anchor; defaults to the terminal center. */
readonly anchor?: TuiOverlayAnchor
/** Terminal-edge spacing. */
readonly margin?: number | TuiOverlayMargin
}
/** Capabilities available while an overlay component is queued or visible. */
export interface TuiOverlayHost {
/**
* Aborts when the request, caller fiber, overlay session, or TUI closes.
* Extension work started for the overlay must cooperate with this signal.
*/
readonly signal: AbortSignal
/** Current viewport; a fresh immutable value is returned on every read. */
readonly viewport: TuiViewport
/** Semantic styles that follow terminal color-scheme changes. */
readonly theme: TuiTheme
/**
* Escape control characters in untrusted display text.
* @param value - text crossing into terminal presentation.
* @returns a printable representation that cannot emit terminal controls.
*/
display(value: string): string
/** Invalidate the component and schedule one contained terminal redraw. */
invalidate(): void
/** Close this overlay normally; repeated calls are no-ops. */
close(): void
}
/** One effect-owned request to create an interactive overlay. */
export interface TuiOverlayRequest {
/**
* Construct the component when this request reaches the front of the modal
* queue. A throw closes the session with `reason: "error"`.
*/
readonly create: (host: TuiOverlayHost) => TuiComponent & Partial<TuiFocusable>
/** Host-owned position and size constraints. */
readonly options?: TuiOverlayOptions
/** Optional request cancellation in addition to caller and TUI ownership. */
readonly signal?: AbortSignal
}
/** Stable reason an overlay stopped being queued or visible. */
export type TuiOverlayCloseReason =
| 'closed'
| 'aborted'
| 'owner-disposed'
| 'tui-disposed'
| 'error'
/** Settled overlay outcome; component failures retain their original value. */
export type TuiOverlayOutcome =
| { readonly reason: Exclude<TuiOverlayCloseReason, 'error'> }
| { readonly reason: 'error'; readonly error: unknown }
/** Live state of an overlay operation. */
export type TuiOverlayState = 'queued' | 'active' | 'closed'
/** Handle returned to the extension that opened an overlay. */
export interface TuiOverlaySession {
/** Current queue/display state. */
readonly state: TuiOverlayState
/** Settles exactly once after the overlay leaves the queue or display. */
readonly closed: Promise<TuiOverlayOutcome>
/**
* Close the overlay normally and await its settled outcome.
* @returns the same immutable value exposed through {@link closed}.
*/
close(): Promise<TuiOverlayOutcome>
}

View File

@@ -31,13 +31,12 @@ import {
type EditorTheme,
type Focusable,
type MarkdownTheme,
type OverlayHandle,
type SelectListTheme,
type SlashCommand,
type Terminal,
type TerminalColorScheme,
} from '@earendil-works/pi-tui'
import type { Context } from 'cordis'
import { Service, type Context, type Fiber } from 'cordis'
import z from 'schemastery'
import {
installAgentLlmTarget,
@@ -91,6 +90,62 @@ import {
type AskUserQuestionItem,
type AskUserQuestionRequest,
} from '@deepseek-ai/dsh-user-interaction'
import {
TuiExtensionServiceImpl,
TuiOverlayManager,
} from './overlay-manager.ts'
import type {
TuiOverlayRequest,
TuiOverlaySession,
TuiTheme,
} from './extension.ts'
export type {
TuiComponent,
TuiFocusable,
TuiOverlayAnchor,
TuiOverlayCloseReason,
TuiOverlayHost,
TuiOverlayMargin,
TuiOverlayOptions,
TuiOverlayOutcome,
TuiOverlayRequest,
TuiOverlaySession,
TuiOverlayState,
TuiTheme,
TuiViewport,
} from './extension.ts'
declare module 'cordis' {
interface Context {
/** Terminal-only interaction service, available only while a TUI is mounted. */
tui: TuiExtensionService
}
}
/**
* Optional terminal-local interaction service provided by one mounted TUI.
*
* The concrete provider retains pi-tui, focus, and terminal lifecycle state.
* Plugins receive only effect-owned overlay sessions.
*/
export abstract class TuiExtensionService extends Service {
/** Exact agent driven by this terminal instance. */
abstract readonly agent: Agent
/**
* Queue an interactive overlay owned by the calling plugin fiber.
*
* The TUI displays one overlay at a time in FIFO order. Disposing the caller
* removes a queued overlay or closes an active one before plugin teardown
* settles. This live presentation is neither logged nor replayed.
*
* @param request - component factory, layout constraints, and cancellation.
* @returns the effect-owned overlay session.
* @throws when the TUI has begun shutting down.
*/
abstract openOverlay(request: TuiOverlayRequest): TuiOverlaySession
}
export const name = 'ui-tui'
export const inject = ['agents', 'commands', 'userInteraction', 'tools', 'llm', 'systemPrompt', 'tokenMeter']
@@ -1291,7 +1346,7 @@ interface PendingQuestion {
resolve(answer: AskUserQuestionAnswer): void
reject(error: unknown): void
onAbort: () => void
overlay: OverlayHandle | undefined
overlay: TuiOverlaySession | undefined
}
/** Add session candidates to pi-tui's existing command/file provider. */
@@ -1512,7 +1567,8 @@ export function createTuiChat(
const commandControllers = new Set<AbortController>()
const referenceControllers = new Set<AbortController>()
let activeQuestion: PendingQuestion | undefined
let modelOverlay: OverlayHandle | undefined
let modelOverlay: TuiOverlaySession | undefined
let tuiServiceFiber: Fiber | undefined
const target: AgentLlmTargetRef = { current: initialTarget(agent), assembled: undefined }
let contextWindow: number | undefined
let contextResolution: Promise<
@@ -1570,6 +1626,41 @@ export function createTuiChat(
requestRender()
}
const extensionTheme: TuiTheme = Object.freeze({
text: (value: string) => palette.text(value),
muted: (value: string) => palette.muted(value),
dim: (value: string) => palette.dim(value),
accent: (value: string) => palette.accent(value),
success: (value: string) => palette.success(value),
warning: (value: string) => palette.warning(value),
error: (value: string) => palette.error(value),
bold: (value: string) => palette.bold(value),
})
const overlayManager = new TuiOverlayManager({
viewport: () => Object.freeze({
columns: runtime.terminal.columns,
rows: runtime.terminal.rows,
}),
theme: () => extensionTheme,
display: displayText,
show: (component, options) => ui.showOverlay(component, options === undefined
? undefined
: {
...options,
...typeof options.margin === 'object'
? { margin: { ...options.margin } }
: {},
}),
invalidate: requestRender,
reportError: (error) => {
const message = errorChain(error)
ctx.logger.warn(`ui-tui: overlay failed: ${message}`)
/* v8 ignore next -- shutdown removes overlays before the terminal stops */
if (disposed) return
appendNotice(`TUI overlay failed: ${message}`, 'error')
},
})
const disposeTargetListeners = installAgentLlmTarget(agent.ctx, target)
const resolveContextWindow = (selected: AgentLlmTarget | undefined): void => {
@@ -1609,29 +1700,29 @@ export function createTuiChat(
appendNotice(`Current model: ${current}\nNo models are advertised by registered providers.`, 'warning')
return
}
modelOverlay?.hide()
modelOverlay = undefined
const close = (): void => {
modelOverlay?.hide()
modelOverlay = undefined
requestRender()
}
const dialog = new ModelDialog(
choices,
target.current,
resolved.maxModelOptions,
palette,
(selected) => {
close()
selectModel(selected)
void modelOverlay?.close()
const session = overlayManager.open({
create: () => new ModelDialog(
choices,
target.current,
resolved.maxModelOptions,
palette,
(selected) => {
void session.close()
selectModel(selected)
},
() => { void session.close() },
),
options: {
width: resolved.modelDialogWidth,
maxHeight: resolved.modelDialogMaxHeight,
anchor: 'center',
margin: 1,
},
close,
)
modelOverlay = ui.showOverlay(dialog, {
width: resolved.modelDialogWidth,
maxHeight: resolved.modelDialogMaxHeight,
anchor: 'center',
margin: 1,
})
modelOverlay = session
void session.closed.then(() => {
if (modelOverlay === session) modelOverlay = undefined
})
requestRender()
}
@@ -1934,7 +2025,7 @@ export function createTuiChat(
}
const rejectQuestion = (pending: PendingQuestion): void => {
pending.overlay?.hide()
void pending.overlay?.close()
pending.overlay = undefined
removeAbortListener(pending)
pending.reject(new UserInteractionError(
@@ -1957,31 +2048,48 @@ export function createTuiChat(
startNextQuestion()
return
}
const dialog = new QuestionDialog(
question,
pending.index + 1,
pending.request.questions.length,
pending.request.questions.length - pending.answers.length,
resolved.maxQuestionOptions,
palette,
(selection) => {
pending.overlay?.hide()
pending.overlay = undefined
pending.answers.push({ id: question.id, ...selection })
pending.index += 1
show()
const session = overlayManager.open({
...pending.request.signal === undefined ? {} : { signal: pending.request.signal },
create: () => new QuestionDialog(
question,
pending.index + 1,
pending.request.questions.length,
pending.request.questions.length - pending.answers.length,
resolved.maxQuestionOptions,
palette,
(selection) => {
pending.overlay = undefined
void session.close()
pending.answers.push({ id: question.id, ...selection })
pending.index += 1
show()
},
() => {
activeQuestion = undefined
rejectQuestion(pending)
startNextQuestion()
},
),
options: {
width: resolved.questionDialogWidth,
maxHeight: resolved.questionDialogMaxHeight,
anchor: 'bottom-left',
margin: { bottom: 1 },
},
() => {
activeQuestion = undefined
rejectQuestion(pending)
startNextQuestion()
},
)
pending.overlay = ui.showOverlay(dialog, {
width: resolved.questionDialogWidth,
maxHeight: resolved.questionDialogMaxHeight,
anchor: 'bottom-left',
margin: { bottom: 1 },
})
pending.overlay = session
void session.closed.then((result) => {
if (pending.overlay !== session) return
pending.overlay = undefined
/* v8 ignore next 2 -- close, abort, and shutdown settle the owner before this callback */
if (result.reason !== 'error') return
activeQuestion = undefined
removeAbortListener(pending)
pending.reject(new UserInteractionError(
`ask_user_question TUI failed: ${errorChain(result.error)}`,
'ASK_ABORTED',
))
startNextQuestion()
})
requestRender()
}
@@ -2052,20 +2160,23 @@ export function createTuiChat(
const shutdown = (exitProcess: boolean): Promise<void> => {
shuttingDown ??= (async () => {
disposed = true
overlayManager.beginShutdown()
contextResolution = undefined
clearStatus()
modelOverlay?.hide()
modelOverlay = undefined
for (const controller of commandControllers) controller.abort(new Error('TUI disposed'))
commandControllers.clear()
for (const controller of referenceControllers) controller.abort(new Error('TUI disposed'))
referenceControllers.clear()
await tuiServiceFiber?.dispose()
tuiServiceFiber = undefined
if (activeQuestion !== undefined) {
const pending = activeQuestion
activeQuestion = undefined
rejectQuestion(pending)
}
for (const pending of questionQueue.splice(0)) rejectQuestion(pending)
await overlayManager.dispose()
modelOverlay = undefined
disposeUserInteraction()
await runtime.terminal.drainInput(100, 20)
ui.stop()
@@ -2511,7 +2622,7 @@ export function createTuiChat(
}
const removeInputListener = ui.addInputListener((data) => {
if (activeQuestion !== undefined || modelOverlay !== undefined) return undefined
if (overlayManager.hasActiveOverlay()) return undefined
if (matchesKey(data, Key.ctrl('o'))) {
toggleTools()
return { consume: true }
@@ -2655,6 +2766,9 @@ export function createTuiChat(
ui.stop()
throw error
}
tuiServiceFiber = ctx.inject([], (serviceCtx) => {
new TuiExtensionServiceImpl(serviceCtx, agent, overlayManager)
})
startBannerReveal()
return {

View File

@@ -0,0 +1,369 @@
/**
* Private bridge between the public TUI extension contract and pi-tui.
*
* The manager serializes modal ownership, guards extension callbacks, and
* settles every queued or active operation before terminal teardown.
* @module @deepseek-ai/dsh-tui/overlay-manager
*/
import { Service, type Context } from 'cordis'
import type { Agent } from '@deepseek-ai/dsh-agent'
import type { TuiExtensionService } from './index.ts'
import type {
Component,
Focusable,
OverlayHandle,
} from '@earendil-works/pi-tui'
import type {
TuiComponent,
TuiFocusable,
TuiOverlayCloseReason,
TuiOverlayHost,
TuiOverlayOutcome,
TuiOverlayOptions,
TuiOverlayRequest,
TuiOverlaySession,
TuiOverlayState,
TuiTheme,
TuiViewport,
} from './extension.ts'
/** pi-tui operations retained by the front door instead of exposed to plugins. */
export interface TuiOverlayDriver {
/** Current terminal viewport. */
viewport(): TuiViewport
/** Current semantic theme facade. */
theme(): TuiTheme
/** Escape text at the terminal display boundary. */
display(value: string): string
/** Mount one guarded component and return its private pi-tui handle. */
show(component: Component, options: TuiOverlayOptions | undefined): OverlayHandle
/** Invalidate the mounted UI and request a render. */
invalidate(): void
/** Report a contained extension failure. */
reportError(error: unknown): void
}
interface OverlayEntry {
readonly request: TuiOverlayRequest
readonly controller: AbortController
readonly signal: AbortSignal
readonly closed: Promise<TuiOverlayOutcome>
readonly resolveClosed: (outcome: TuiOverlayOutcome) => void
readonly session: TuiOverlaySession
state: TuiOverlayState
component?: GuardedOverlayComponent
handle?: OverlayHandle
removeRequestAbort?: () => void
outcome?: TuiOverlayOutcome
failing?: boolean
}
/** Turn a close reason into its immutable public outcome. */
function outcome(reason: Exclude<TuiOverlayCloseReason, 'error'>): TuiOverlayOutcome {
return Object.freeze({ reason })
}
/** Retain only supported layout fields before a queued request returns to its caller. */
function retainOptions(options: TuiOverlayOptions): TuiOverlayOptions {
return Object.freeze({
...options.width === undefined ? {} : { width: options.width },
...options.minWidth === undefined ? {} : { minWidth: options.minWidth },
...options.maxHeight === undefined ? {} : { maxHeight: options.maxHeight },
...options.anchor === undefined ? {} : { anchor: options.anchor },
...options.margin === undefined
? {}
: {
margin: typeof options.margin === 'object'
? Object.freeze({ ...options.margin })
: options.margin,
},
})
}
/** Guard plugin component methods while preserving focus and key-release state. */
class GuardedOverlayComponent implements Component, Focusable {
constructor(
private readonly component: TuiComponent & Partial<TuiFocusable>,
private readonly fail: (error: unknown) => void,
) {}
get focused(): boolean {
try {
return this.component.focused ?? false
} catch (error) {
this.fail(error)
return false
}
}
set focused(value: boolean) {
try {
if ('focused' in this.component) this.component.focused = value
} catch (error) {
this.fail(error)
}
}
get wantsKeyRelease(): boolean {
try {
return this.component.wantsKeyRelease ?? false
} catch (error) {
this.fail(error)
return false
}
}
render(width: number): string[] {
try {
return this.component.render(width)
} catch (error) {
this.fail(error)
return []
}
}
handleInput(data: string): void {
try {
this.component.handleInput?.(data)
} catch (error) {
this.fail(error)
}
}
invalidate(): boolean {
try {
this.component.invalidate()
return true
} catch (error) {
this.fail(error)
return false
}
}
}
/** FIFO modal owner for one mounted TUI. */
export class TuiOverlayManager {
private readonly queue: OverlayEntry[] = []
private active: OverlayEntry | undefined
private accepting = true
private disposeTask: Promise<void> | undefined
constructor(private readonly driver: TuiOverlayDriver) {}
/**
* Whether one extension or built-in overlay currently owns terminal focus.
* @returns `true` while an overlay is active.
*/
hasActiveOverlay(): boolean {
return this.active !== undefined
}
/** Reject new work while the TUI unloads dependent extension fibers. */
beginShutdown(): void {
this.accepting = false
}
/**
* Queue one overlay without assigning Cordis ownership.
* @param request - component factory, constraints, and request signal.
* @returns an internal session that can close with an ownership reason.
*/
open(request: TuiOverlayRequest): TuiOverlaySession & {
closeWith(reason: Exclude<TuiOverlayCloseReason, 'error'>): Promise<TuiOverlayOutcome>
} {
if (!this.accepting) throw new Error('TUI is shutting down')
const requestSignal = request.signal
const retainedRequest: TuiOverlayRequest = Object.freeze({
create: request.create,
...request.options === undefined ? {} : { options: retainOptions(request.options) },
...requestSignal === undefined ? {} : { signal: requestSignal },
})
const controller = new AbortController()
const signal = requestSignal === undefined
? controller.signal
: AbortSignal.any([requestSignal, controller.signal])
const deferred = Promise.withResolvers<TuiOverlayOutcome>()
const session: TuiOverlaySession & {
closeWith(reason: Exclude<TuiOverlayCloseReason, 'error'>): Promise<TuiOverlayOutcome>
} = {
get state(): TuiOverlayState {
return entry.state
},
closed: deferred.promise,
close: () => this.close(entry, outcome('closed')),
closeWith: (reason: Exclude<TuiOverlayCloseReason, 'error'>) =>
this.close(entry, outcome(reason)),
}
const entry: OverlayEntry = {
request: retainedRequest,
controller,
signal,
closed: deferred.promise,
resolveClosed: deferred.resolve,
session,
state: 'queued',
}
if (requestSignal?.aborted === true) {
void this.close(entry, outcome('aborted'))
return session
}
if (requestSignal !== undefined) {
const onAbort = (): void => { void this.close(entry, outcome('aborted')) }
requestSignal.addEventListener('abort', onAbort, { once: true })
entry.removeRequestAbort = () => { requestSignal.removeEventListener('abort', onAbort) }
}
this.queue.push(entry)
this.activateNext()
return session
}
/** Stop accepting work and settle every active or queued overlay. */
dispose(): Promise<void> {
if (this.disposeTask !== undefined) return this.disposeTask
this.beginShutdown()
const entries = [
...this.active === undefined ? [] : [this.active],
...this.queue,
]
return this.disposeTask = Promise.all(
entries.map(entry => this.close(entry, outcome('tui-disposed'))),
).then(() => {})
}
private activateNext(): void {
if (!this.accepting || this.active !== undefined) return
const entry = this.queue.shift()
if (entry === undefined) return
this.active = entry
entry.state = 'active'
const host = this.host(entry)
let component: TuiComponent & Partial<TuiFocusable>
try {
component = entry.request.create(host)
} catch (error) {
this.fail(entry, error)
return
}
if (this.active !== entry) return
const guarded = new GuardedOverlayComponent(component, (error) => {
this.fail(entry, error)
})
entry.component = guarded
try {
const handle = this.driver.show(guarded, entry.request.options)
if (this.active !== entry) {
this.hide(handle)
return
}
entry.handle = handle
this.driver.invalidate()
} catch (error) {
this.fail(entry, error)
}
}
private host(entry: OverlayEntry): TuiOverlayHost {
const driver = this.driver
return Object.freeze({
get signal(): AbortSignal {
return entry.signal
},
get viewport(): TuiViewport {
return Object.freeze({ ...driver.viewport() })
},
get theme(): TuiTheme {
return driver.theme()
},
display: (value: string) => this.driver.display(value),
invalidate: () => {
if (this.active !== entry || entry.component === undefined || entry.failing === true) return
if (!entry.component.invalidate() || this.active !== entry) return
try {
this.driver.invalidate()
} catch (error) {
this.fail(entry, error)
}
},
close: () => { void this.close(entry, outcome('closed')) },
})
}
private fail(entry: OverlayEntry, error: unknown): void {
if (entry.state === 'closed' || entry.failing === true) return
entry.failing = true
this.report(error)
queueMicrotask(() => {
void this.close(entry, Object.freeze({ reason: 'error', error }))
})
}
private report(error: unknown): void {
try {
this.driver.reportError(error)
} catch {
// Error reporting is a containment boundary, never a second failure path.
}
}
private hide(handle: OverlayHandle): void {
try {
handle.hide()
} catch (error) {
this.report(error)
}
}
private close(entry: OverlayEntry, result: TuiOverlayOutcome): Promise<TuiOverlayOutcome> {
if (entry.outcome !== undefined) return entry.closed
entry.outcome = result
entry.state = 'closed'
entry.removeRequestAbort?.()
delete entry.removeRequestAbort
if (!entry.controller.signal.aborted) entry.controller.abort(result)
const queuedIndex = this.queue.indexOf(entry)
if (queuedIndex >= 0) this.queue.splice(queuedIndex, 1)
if (this.active === entry) {
this.active = undefined
if (entry.handle !== undefined) this.hide(entry.handle)
delete entry.handle
}
delete entry.component
entry.resolveClosed(result)
try {
this.driver.invalidate()
} catch (error) {
this.report(error)
}
queueMicrotask(() => { this.activateNext() })
return entry.closed
}
}
/** Cordis service whose method effects bind to the calling plugin fiber. */
export class TuiExtensionServiceImpl extends Service implements TuiExtensionService {
constructor(
ctx: Context,
readonly agent: Agent,
private readonly overlays: TuiOverlayManager,
) {
super(ctx, 'tui')
}
/** @inheritdoc */
openOverlay(request: TuiOverlayRequest): TuiOverlaySession {
let operation: ReturnType<TuiOverlayManager['open']> | undefined
const disposeOwner = this.ctx.effect(
() => () => operation?.closeWith('owner-disposed'),
'tui.openOverlay()',
)
try {
operation = this.overlays.open(request)
} catch (error) {
void disposeOwner()
throw error
}
void operation.closed.then(() => { void disposeOwner() })
return operation
}
}