Merge pull request #335 from deepseek-harness/worktree-windows-runtime
feat(windows): extend runtime and test portability
This commit is contained in:
@@ -10,6 +10,8 @@ This package owns interactive terminal presentation and input only. It injects `
|
||||
|
||||
The TUI rebuilds resumed history from the active session surface, renders Markdown responses and reasoning, applies each tool's `presentCall` / `presentResult` intent to terminal, diff, or generic cards, keeps the latest `todo/write` plan above the editor, and presents `ctx.userInteraction` questions in a wide bottom-left keyboard panel with progress, numbered options, and aligned descriptions. The latest logged session title becomes the header subtitle, with `welcome` before a title exists, and the terminal window title becomes `<session title> — <configured title>`. A durable `llm/retry` event retracts the failed step's live chunks and renders the scheduled retry count, delay, and failure in the transcript; success, exhaustion, and cancellation then settle through ordinary session events. The footer totals each logged model step's usage once, including failed attempts, while treating committed-message usage as a fallback for logs without a usage chunk. Its idle view compares token-meter pressure with `ctx.llm.resolveModelContext()` for the current route, displays `context unknown` when the adapter has no capacity metadata, and also shows tool-card mode and the current model with reasoning state; while the agent runs, an elapsed working indicator and `esc interrupt` replace that summary. Surface replacement events rebuild the transcript so compacted history does not reappear.
|
||||
|
||||
An embedding may provide `TuiRuntime.formatCwd` when its logical workspace label differs from the session's host directory. The override changes only the footer label; tools continue to use the session `cwd`.
|
||||
|
||||
Before model output, session events, tool presenters, questions, configuration, or diagnostics reach pi-tui's ANSI-aware renderers or the terminal title, the TUI renders C0 and C1 controls other than line feeds as visible `\xNN` text. Those sources cannot add terminal control sequences; the TUI and pi-tui retain ownership of terminal rendering and styling.
|
||||
|
||||
While the agent is running, ordinary editor submissions call `agent.steer()`; otherwise they call `agent.send()`. A slash at the start of the submitted line enters `ctx.commands` instead: known commands execute directly, unknown commands produce a warning, and neither path reaches the model. The TUI registers `/help`, `/model`, `/clear`, `/cancel`, `/reasoning`, `/tools`, `/redraw`, and `/exit` as agent-scoped definitions; every other effective command joins autocomplete and `/help` dynamically. Ctrl+C or Escape cancels a running turn. Tool cards collapse long bodies into a configurable head/tail preview; Ctrl+O toggles every card between its preview and full output. Ctrl+R toggles reasoning, Ctrl+L redraws, and Ctrl+D exits while idle.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { homedir } from 'node:os'
|
||||
import { relative, resolve, sep } from 'node:path'
|
||||
import { isAbsolute, relative, resolve, sep } from 'node:path'
|
||||
import {
|
||||
CombinedAutocompleteProvider,
|
||||
Container,
|
||||
@@ -170,6 +170,12 @@ export interface TuiRuntime {
|
||||
terminal: Terminal
|
||||
/** Exit hook used by terminal shutdown or a target-agent startup failure. */
|
||||
exit(code: number): void
|
||||
/**
|
||||
* Override the footer's logical working-directory label without changing the session directory used by tools.
|
||||
* @param cwd - Operational working directory from the session header.
|
||||
* @returns Unescaped label; the TUI makes terminal controls visible.
|
||||
*/
|
||||
formatCwd?: (cwd: string | undefined) => string
|
||||
/** Monotonic-enough wall clock for elapsed status rendering. Defaults to `Date.now`. */
|
||||
now?(): number
|
||||
}
|
||||
@@ -697,8 +703,10 @@ function formatCwd(cwd: string | undefined): string {
|
||||
const home = homedir()
|
||||
const rel = relative(resolve(home), resolve(cwd))
|
||||
if (rel === '') return '~'
|
||||
if (rel !== '..' && !rel.startsWith(`..${sep}`)) return displayText(`~${sep}${rel}`)
|
||||
return displayText(cwd)
|
||||
/* v8 ignore next -- Windows cross-drive coverage; POSIX relative() cannot return an absolute path. */
|
||||
if (isAbsolute(rel)) return cwd
|
||||
if (rel !== '..' && !rel.startsWith(`..${sep}`)) return `~${sep}${rel}`
|
||||
return cwd
|
||||
}
|
||||
|
||||
interface SessionTokenTotals {
|
||||
@@ -742,6 +750,7 @@ class FooterComponent implements Component {
|
||||
private readonly toolsExpanded: () => boolean,
|
||||
private readonly showReasoning: () => boolean,
|
||||
private readonly tokens: () => { input: number; output: number },
|
||||
private readonly cwdFormatter: TuiRuntime['formatCwd'],
|
||||
private readonly currentModel: () => string | undefined,
|
||||
private readonly contextPercent: () => number | undefined,
|
||||
private readonly runningSeconds: () => number,
|
||||
@@ -765,6 +774,9 @@ class FooterComponent implements Component {
|
||||
const context = contextPercent === undefined ? 'context unknown' : `${contextPercent}% context`
|
||||
const fullRight = `${context} tools:${this.toolsExpanded() ? 'expanded' : 'compact'} ${modelState}`
|
||||
const compactRight = `${context} ${modelState}`
|
||||
const formattedCwd = displayText(
|
||||
this.cwdFormatter?.(this.agent.session.header.cwd) ?? formatCwd(this.agent.session.header.cwd),
|
||||
)
|
||||
if (visibleWidth(counters) + visibleWidth(compactRight) + 1 > width) {
|
||||
const compact = truncateToWidth(compactRight, width, '')
|
||||
return [`${' '.repeat(Math.max(0, width - visibleWidth(compact)))}${this.palette.dim(compact)}`]
|
||||
@@ -773,7 +785,7 @@ class FooterComponent implements Component {
|
||||
const right = visibleWidth(fullRight) <= rightAvailable ? fullRight : compactRight
|
||||
const rightClipped = truncateToWidth(right, rightAvailable, '')
|
||||
const cwdAvailable = Math.max(0, width - visibleWidth(counters) - visibleWidth(rightClipped) - 3)
|
||||
const cwd = truncateToWidth(formatCwd(this.agent.session.header.cwd), cwdAvailable, '')
|
||||
const cwd = truncateToWidth(formattedCwd, cwdAvailable, '')
|
||||
const left = [cwd, counters].filter(Boolean).join(' ')
|
||||
const gap = ' '.repeat(Math.max(0, width - visibleWidth(left) - visibleWidth(rightClipped)))
|
||||
return [`${this.palette.dim(left)}${gap}${this.palette.dim(rightClipped)}`]
|
||||
@@ -1082,6 +1094,7 @@ export function createTuiChat(
|
||||
() => toolsExpanded,
|
||||
() => showReasoning,
|
||||
() => tokens,
|
||||
runtime.formatCwd,
|
||||
() => target.current?.model,
|
||||
() => contextWindow === undefined
|
||||
? undefined
|
||||
|
||||
@@ -12,7 +12,7 @@ import SessionStore, { SessionId, type Session } from '@deepseek-ai/dsh-session'
|
||||
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
|
||||
import type { ToolDefinition } from '@deepseek-ai/dsh-tools'
|
||||
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
|
||||
import { createTuiChat, type Config } from '../src/index.ts'
|
||||
import { createTuiChat, type Config, type TuiRuntime } from '../src/index.ts'
|
||||
|
||||
interface FakeAgent extends Agent {
|
||||
status: AgentStatus
|
||||
@@ -28,6 +28,7 @@ export interface TuiHarnessOptions {
|
||||
configureContext?: (ctx: Context) => Promise<void>
|
||||
beforeMount?: (session: Session) => void
|
||||
cwd?: string | null
|
||||
formatCwd?: TuiRuntime['formatCwd']
|
||||
agentOptions?: AgentOptions
|
||||
contextWindow?: number
|
||||
contextTokens?: number
|
||||
@@ -144,7 +145,12 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
|
||||
welcome: 'Coding agent ready.',
|
||||
sessionId,
|
||||
color: false,
|
||||
}, options.config), { terminal, exit, now: options.now ?? (() => 0) })
|
||||
}, options.config), {
|
||||
terminal,
|
||||
exit,
|
||||
now: options.now ?? (() => 0),
|
||||
...(options.formatCwd === undefined ? {} : { formatCwd: options.formatCwd }),
|
||||
})
|
||||
return { ctx, session, agent, terminal, exit, controller }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import type { Terminal } from '@earendil-works/pi-tui'
|
||||
@@ -168,6 +168,10 @@ describe('TUI config', () => {
|
||||
describe('pi-tui chat lifecycle and transcript', () => {
|
||||
it('uses the latest log-backed title for the header subtitle and terminal window', async () => {
|
||||
const result = await setup({
|
||||
// A fixed short cwd keeps the footer's token counters inside the 88-column
|
||||
// fake terminal regardless of where the checkout lives; cwd rendering has
|
||||
// its own dedicated variants test below.
|
||||
cwd: '/workspace',
|
||||
beforeMount(session) {
|
||||
session.append('session/title', {
|
||||
title: 'Restored session title',
|
||||
@@ -413,6 +417,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
|
||||
it('renders the ANSI palette and every markdown/content style', async () => {
|
||||
const result = await setup({
|
||||
cwd: '/workspace',
|
||||
config: { color: true },
|
||||
beforeMount(session) {
|
||||
session.append('user/message', {
|
||||
@@ -496,9 +501,21 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
expect(unsetResult.terminal.output).toContain('cwd unset')
|
||||
await dispose(unsetResult)
|
||||
|
||||
const homeParent = resolve(home, '..')
|
||||
const parentResult = await setup({ cwd: homeParent })
|
||||
expect(parentResult.terminal.output).toContain(homeParent)
|
||||
await dispose(parentResult)
|
||||
|
||||
const outsideResult = await setup({ cwd: '/opt' })
|
||||
expect(outsideResult.terminal.output).toContain('/opt')
|
||||
await dispose(outsideResult)
|
||||
|
||||
const logicalResult = await setup({
|
||||
cwd: '/w',
|
||||
formatCwd: cwd => `logical:${cwd}\x1b`,
|
||||
})
|
||||
expect(logicalResult.terminal.output).toContain('logical:/w\\x1b')
|
||||
await dispose(logicalResult)
|
||||
})
|
||||
|
||||
it('sends, steers, handles commands, global keys, and disposed-agent input', async () => {
|
||||
@@ -1174,8 +1191,9 @@ describe('TUI user-interaction dialogs', () => {
|
||||
result.terminal.send('x')
|
||||
result.terminal.send(' ')
|
||||
result.terminal.send('\r')
|
||||
await tick()
|
||||
expect(result.terminal.output).toContain('Select at least one option')
|
||||
await vi.waitFor(() => {
|
||||
expect(result.terminal.output).toContain('Select at least one option')
|
||||
})
|
||||
result.terminal.send('c')
|
||||
await tick()
|
||||
result.terminal.send('\x1b')
|
||||
|
||||
Reference in New Issue
Block a user