Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/tui-capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,20 @@ and image layout changes also retain the existing reconstruction behavior. A
reconstruction clears earlier shell scrollback and can reset the host's scroll
position; ordinary updates keep native scrolling and selection behavior.

During a run, regular mode lets a row scroll into native history only when later
updates cannot change it. These rows include finished steps, earlier blocks of a
streaming reply, and the welcome banner. The welcome banner omits live account
and runtime status in a conversation; account notices that need action appear
above the Composer. A running tool, a block that is still streaming (such as a
growing table or list), and the last tool step stay on screen until they are
final. If they do not fit, the screen shows their latest rows under a
`↑ N more lines above · still updating` line, and the full rows enter history in
order once final. Long turns extend their visible steps instead of re-folding or
dropping earlier steps, so ordinary progress never needs reconstruction. To keep
long sessions responsive, MCode stops retaining final rows that are far above
the screen; they remain in the terminal's own scrollback. A later reconstruction,
such as after a resize, redraws only the retained recent rows.

Rewind and Fork history-loading hints disappear as soon as their lists are ready.
Returning from a cancelled operation must not leave a stale loading message in the
Composer. Rewind displays its completed result after a successful operation.
Expand Down
5 changes: 3 additions & 2 deletions packages/tui/src/tui/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export function createTuiApp(options: CreateTuiAppOptions): TuiApp {
const transcriptView = new TranscriptView(transcriptVisibility, {
displayModes: new TranscriptPresentationController(),
workspaceDir: options.workspaceDir,
appendOnly: () => renderer.mode === 'regular',
});
const editor = createEditor(tui, workspaceRoots, options.runtime);
let draftLifecycle: TuiDraftLifecycle | undefined;
Expand Down Expand Up @@ -400,8 +401,8 @@ export function createTuiApp(options: CreateTuiAppOptions): TuiApp {
if (!userCell) return; // runtime-owned or retry-continuation turns
// A second Esc in the settle window can reach the runtime-owned branch
// with the same turnId; track restored turns so a repeat concatenates
// nothing. (Cell status cannot key this: abort-time markTurn also
// cancels still-pending user cells before the first restore.)
// nothing. (Track by turn rather than by cell status, which the runtime
// echo and the restore path both update.)
if (restoredAbortTurnIds.has(turnId)) return;
const hasIrreversibleActivity = cells.some((cell) => {
if (cell.id === `user:${turnId}`) return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,14 @@ export class TuiLiveTurnProjection {
const turnCells = this.transcript.snapshot().filter((cell) => cell.turnId === turnId);
for (const cell of turnCells) {
if (cell.status !== 'pending' && cell.status !== 'running') continue;
this.transcript.upsert({ id: cell.id, status, updatedAtMs: timestamp });
// A submitted prompt was delivered even when its run stops. Its cancelled
// marker means "returned to the Composer" and is set only by that restore
// path; marking it here would rewrite a prompt already in native history.
const cellStatus =
status === 'cancelled' && cell.kind === 'user' && cell.userPresentation !== 'pending-steer'
? 'succeeded'
: status;
this.transcript.upsert({ id: cell.id, status: cellStatus, updatedAtMs: timestamp });
}
const hasAssistant = turnCells.some(
(cell) => cell.kind === 'assistant' || cell.kind === 'assistant-preamble',
Expand Down
14 changes: 8 additions & 6 deletions packages/tui/src/tui/engine/LOCAL_CHANGES.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@
},
{
"path": "components/markdown.ts",
"currentSha256": "59eaf8bc4ef1c3a900892248fc3ab9a1483d18d84e63a888711c289f2bdf6e8d",
"changeIds": ["L005", "L007", "L013", "L029", "L026"],
"currentSha256": "1d32aa3553e99178db5a96130f9a18c1add235d5d70100a8cd2d47c4189ddd1b",
"changeIds": ["L005", "L007", "L013", "L029", "L026", "L045"],
"reason": "Keep strict TypeScript fixes and product Markdown hooks, plus Pi's post-baseline wrapped-table style restoration. 链接标签使用独立样式上下文,防止外层正文或标题颜色覆盖链接颜色。 稳定 Markdown 块缓存",
"behaviorImpact": "MCode keeps its compact code presentation without reimplementing Pi Markdown or leaking wrapped link styles into table borders and adjacent cells. 链接保留嵌套强调与行内代码,并在结束后恢复外层样式。 保留两个尾部块并对引用回退全文。"
},
Expand Down Expand Up @@ -163,7 +163,7 @@
},
{
"path": "tui-main-screen.ts",
"currentSha256": "f57eab79552763ae29bc39e2ced4c7905eb44fa163f538ba3ff6000136e0f493",
"currentSha256": "f38671720867d2baa633a0e8ca826bd36474edefc281efe6b6e6a2f81e7d3ddb",
"changeIds": [
"L005",
"L017",
Expand All @@ -174,15 +174,16 @@
"L038",
"L039",
"L041",
"L044"
"L044",
"L046"
],
"upstreamCommit": "6c4f360264397c59801f6da2bdac13e3b1fcbe91",
"reason": "Keep strict TypeScript fixes and stream full and differential renders through Pi's bounded terminal writer. 缩放期间仅重绘可见尾部 常规模式在差分比较前剥离行首 OSC 133 zone 标记。 内容收缩或历史内容变化触发回退重绘时仅更新可见区域。 Preserve ordered asynchronous terminal output under POSIX TTY backpressure.",
"behaviorImpact": "Regular viewport redraws erase rows in place so hosts that save an erased screen to scrollback do not retain stale transcript or footer rows. Visible text-only shrink with unchanged historical text temporarily pads the active screen to preserve host scrolling and the input position; later output reuses this space. Historical text replacement or removal still reconstructs the session to avoid stale or duplicate history. Rebuilding clears pre-launch shell scrollback. Genuine resize retains delayed history replay; redundant same-size notifications are ignored. Slow terminal output does not block input; pending frames coalesce and terminal handoffs drain ordered output."
},
{
"path": "tui.ts",
"currentSha256": "6bfb2095855d00532bfc96a4be57e0c974e0d8d06137bbba1e865986483e7005",
"currentSha256": "a661a3e326e1f10c9567b99c23fec5269849856a54417439597801f75ec077f8",
"changeIds": [
"L005",
"L015",
Expand All @@ -192,7 +193,8 @@
"L031",
"L040",
"L041",
"L044"
"L044",
"L046"
],
"reason": "Keep strict TypeScript fixes, expose Pi's existing immediate scheduler as a non-destructive product interaction contract, dispatch the input left over after terminal color sequences are removed, and coalesce synchronous submission renders. 提供 resize hook 及焦点输入过滤 独立面板声明键盘分页归属。 Preserve ordered asynchronous terminal output under POSIX TTY backpressure.",
"behaviorImpact": "Urgent product interactions render immediately without resetting differential state or clearing native scrollback, and a coalesced color answer no longer discards the keystrokes sharing its chunk. A synchronous submission frame dismisses the previous interrupted footer without a second input render. 焦点先交给 viewport listener,不进入编辑器。 handlesViewportKeys 为 true 时,fullscreen 分页交给焦点面板;默认仍由外层视口处理。 Slow terminal output does not block input; pending frames coalesce and terminal handoffs drain ordered output."
Expand Down
16 changes: 16 additions & 0 deletions packages/tui/src/tui/engine/LOCAL_CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ Remove `L024` when the selected Pi baseline natively matches legacy-terminal `Ct
- Minimal difference: when a shorter document would move the viewport origin backwards, or changed visible text is already in scrollback, clear and replay the complete current projection, except for addressable text-only shrink covered by L038. Compare changed historical rows without terminal sequences so style-only updates preserve scrollback. Other updates retain differential rendering and genuine resize retains the existing delayed history replay.
- Tradeoff: structural reconstruction clears native scrollback, including shell history from before TUI startup. Initial short chat documents retain natural document placement. L038 keeps freed visible rows temporarily blank instead of reconstructing unchanged history.
- Evidence: local-delta tests assert every visible row and the complete history, while real Tasks and feature lifecycle tests cover short/long content, background growth, paging, resize, nested panels and return to chat. Queue lifecycle tests replay bracketed CJK paste, Alt+Enter, auto-drain, and history refresh through Ghostty; equal-height and growing historical edits are also covered by xterm. Virtual terminals do not establish native Windows Terminal or iTerm2 touchpad acceptance.
- Product boundary: the regular chat layout keeps rows that may still change out of native history (see L045), so this reconstruction remains for resize and for content that changes after being reported final.
- Removal condition: the selected Pi baseline provides equivalent complete viewport and unique-history behavior.

## L036: Unframed multiline paste chunks
Expand Down Expand Up @@ -193,3 +194,18 @@ Remove `L024` when the selected Pi baseline natively matches legacy-terminal `Ct
- Evidence: terminal-output regressions cover held/partial writes, input dispatch, control ordering, transient and permanent failures, frame coalescing, final transcript preservation, keyboard input draining and editor handoff/shutdown races; application tests cover shutdown/suspend drains. A local macOS PTY with 10,000 synthetic history rows and a paused consumer no longer blocks the input loop while reconstructing history.
- Boundary: history reconstruction still transmits the complete ordered document. A stalled connection can delay visible output; it no longer synchronously stalls the JavaScript event loop. Windows and non-TTY output retain their existing writer. Native remote SSH and native Windows acceptance remain separate.
- Removal condition: the upstream terminal writer provides ordered asynchronous POSIX TTY writes and its render scheduler observes output backpressure.

## L045: Report committed Markdown lines

- Product contract: in Regular mode a streaming reply enters native scrollback only through rows that appended text cannot change, so a table, list or other block that is still growing never rewrites history.
- Minimal difference: `components/markdown.ts` exposes `getStableLineCount()`, the number of leading lines from the latest render that come from the blocks already committed by the L026 streaming cache. Rendering output is unchanged.
- Evidence: `tui-app.test.ts` regular-mode native history cases cover widening tables, lists that become loose, long prose that streams into history during a turn, and exact history after the run.
- Removal condition: the selected Pi baseline exposes an equivalent committed-prefix contract for streaming Markdown.

## L046: Discard final rows already in native history

- Product contract: a long Regular-mode session keeps frame cost bounded without rewriting native scrollback.
- Minimal difference: `Component.takeDiscardedRows()` lets the root report leading rows it dropped since the last frame. When those rows are all above the previous viewport, `tui-main-screen.ts` drops them from its retained lines and rebases its viewport and cursor rows before diffing. Other cases keep the existing differential and reconstruction paths.
- Product boundary: the MCode Transcript only drops whole units of final rows far above the screen, so the remaining output is an exact suffix of the previous output. A later reconstruction, such as a resize, replays only the retained rows.
- Evidence: `tui-engine-local-deltas.test.ts` retained-document cases assert no scrollback erase and exact native history after trimming (failing without the rebase), and exact history without stale rows when a root reports too few or too many discarded rows.
- Removal condition: the selected Pi baseline supports discarding a committed document prefix.
9 changes: 9 additions & 0 deletions packages/tui/src/tui/engine/components/markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,15 @@ export class Markdown implements Component {
this.cachedLines = undefined;
}

/**
* Number of leading lines from the latest render() that appending text cannot
* change. They come from blocks the streaming cache has already committed.
*/
getStableLineCount(): number {
if (!this.cachedLines || this.cachedLines.length === 0) return 0;
return Math.min(this.cachedLines.length, this.paddingY + this.stableLines.length);
}

invalidate(): void {
this.stableSource = "";
this.stableLines = [];
Expand Down
20 changes: 20 additions & 0 deletions packages/tui/src/tui/engine/tui-main-screen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,26 @@ export class TuiMainScreen extends TuiBase implements TUI {
// Render all components to get new lines. Strip OSC 133 zone sentinels before the
// differential compare so they never enter previousLines or any terminal write.
let newLines = this.render(width).map((line) => line.replace(OSC133_ZONE_PREFIX, ""));
// A root may drop leading rows that already sit in native scrollback. Rebase the
// retained state so later diffs address the shorter document without a replay.
const discardedRows = this.children.length === 1
? Math.max(0, Math.floor(this.children[0]?.takeDiscardedRows?.() ?? 0))
: 0;
// A miscount needs no separate check: the history comparison below sees the
// misaligned rows and reconstructs the session instead of splicing them.
if (
discardedRows > 0 && discardedRows <= prevViewportTop &&
!widthChanged && !heightChanged && !this.historyReplayPending
) {
this.previousLines = this.previousLines.slice(discardedRows);
this.previousViewportTop -= discardedRows;
prevViewportTop -= discardedRows;
viewportTop -= discardedRows;
this.cursorRow = Math.max(0, this.cursorRow - discardedRows);
this.hardwareCursorRow -= discardedRows;
hardwareCursorRow -= discardedRows;
this.maxLinesRendered = Math.max(0, this.maxLinesRendered - discardedRows);
}
const viewportLayouts = this.children.map((component) => ({
component,
key: component.getViewportLayoutKey?.(),
Expand Down
9 changes: 9 additions & 0 deletions packages/tui/src/tui/engine/tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ export interface Component {
*/
getViewportLayoutKey?(): string | undefined;

/**
* Regular mode only: report and reset how many leading rows the component dropped
* since the renderer last asked. Each dropped row must equal the leading row of the
* previous output and already be in native scrollback. The renderer rebases its state
* instead of rewriting history, which bounds the retained document. If the remaining
* history does not line up, its ordinary history comparison reconstructs the session.
*/
takeDiscardedRows?(): number;

/**
* Optional handler for keyboard input when component has focus
*/
Expand Down
Loading
Loading