Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
32f2a6e
fix(cli): draw OpenTUI tool confirmations inline as ink does
chiga0 Sep 11, 2026
85ec425
fix(cli): let a plain-text tool update replace OpenTUI's structured p…
chiga0 Sep 11, 2026
cc230a0
fix(cli): port ink's question dialog to OpenTUI in full
chiga0 Sep 11, 2026
074229c
fix(cli): keep a keystroke burst intact in OpenTUI's auth inputs
chiga0 Sep 11, 2026
ce46b45
fix(cli): draw ink's inline tool arguments row in OpenTUI
chiga0 Sep 12, 2026
19e2c25
docs: correct the row-budget claim the viewport fix overturned
chiga0 Sep 14, 2026
fc28b2d
docs: retire the two card gaps this change closed
chiga0 Sep 14, 2026
4ef811e
fix(cli): keep shell output within ink's history retention
chiga0 Sep 14, 2026
a39f6a2
fix(cli): keep OpenTUI's conversation inside the terminal
chiga0 Sep 14, 2026
c097497
docs: record the shell-output compaction this branch already ships
chiga0 Sep 14, 2026
b642795
Merge remote-tracking branch 'origin/main' into fix/opentui-parity-fo…
chiga0 Sep 14, 2026
7bee45a
docs: attribute the startup update-check notice to the runtime
chiga0 Sep 14, 2026
277f06d
fix(cli): freeze the waiting row's spinner while a confirmation is open
chiga0 Sep 14, 2026
8184e33
fix(cli): cancel a pending tab swap when a second answer lands inside…
chiga0 Sep 14, 2026
e11b9c4
fix(cli): drop the footer while a gated server holds the dialog slot
chiga0 Sep 14, 2026
f47fd98
fix(cli): hold ink's pending glyph on a call queued behind another ap…
chiga0 Sep 14, 2026
dfa63a5
Merge remote-tracking branch 'origin/main' into fix/opentui-parity-fo…
chiga0 Sep 14, 2026
772a23f
docs: record the inline-confirmation row reserves as named debt
chiga0 Sep 14, 2026
0cd2ce2
fix(cli): list mid-turn queued prompts above the composer
chiga0 Sep 14, 2026
f85567f
docs: correct the mutation counts recorded for the queued prompt rows
chiga0 Sep 14, 2026
85c100a
docs: state the queued row hint's cut-off and its assertion layers ex…
chiga0 Sep 14, 2026
2deca65
fix(cli): record the model dialog's outcome like ink does
chiga0 Sep 14, 2026
87ccc13
fix(cli): let OpenTUI dialog fields move the caret like ink's
chiga0 Sep 14, 2026
a842a44
docs: correct the cursor-cell frames recorded for the dialog caret
chiga0 Sep 14, 2026
aff5481
fix(cli): point the awaiting arrow at the call the queue can answer
chiga0 Sep 15, 2026
843d481
fix(cli): keep a keystroke burst intact across the dialog fields and …
chiga0 Sep 15, 2026
a2e4b8e
docs: record the keystroke-burst decisions and correct what the sweep…
chiga0 Sep 15, 2026
2e94387
fix(cli): read the loading-phrase setting for the waiting row
chiga0 Sep 15, 2026
eea432e
fix(cli): sanitize a queued prompt before the waiting row shows it
chiga0 Sep 15, 2026
7430c4f
refactor(cli): drop the @-mention card's unreachable arguments row
chiga0 Sep 15, 2026
a0fb2de
test(cli): stop handing React a null key in the dialog harnesses
chiga0 Sep 15, 2026
f1ec59c
docs(cli): correct what two source comments claim
chiga0 Sep 15, 2026
b702297
docs: record the pointer divergence and the pins review measured as l…
chiga0 Sep 15, 2026
77d3b0c
Merge remote-tracking branch 'origin/main' into fix/opentui-parity-fo…
chiga0 Sep 15, 2026
80c5614
fix(cli): keep a click on the transcript out of the composer's focus
chiga0 Sep 15, 2026
2ccebd8
fix(cli): stop the bare End key at its own line in dialog fields
chiga0 Sep 15, 2026
f87161c
fix(cli): fit the question dialog's chip row to the room it has
chiga0 Sep 15, 2026
9ee896b
fix(cli): erase a word for the byte a legacy terminal sends for it
chiga0 Sep 15, 2026
e2b325c
docs: correct what a resumed session brings back for a slash command
chiga0 Sep 15, 2026
76c07ac
fix(cli): keep a pasted newline inside the row the caret sits on
chiga0 Sep 15, 2026
28a457a
Merge remote-tracking branch 'origin/main' into fix/opentui-parity-fo…
chiga0 Sep 15, 2026
e2219b0
Merge remote-tracking branch 'origin/main' into fix/opentui-parity-fo…
chiga0 Sep 16, 2026
5a94b7b
fix(cli): hold a settled field closed and follow the tail its owner a…
chiga0 Sep 16, 2026
da4255e
fix(cli): mirror the question dialog's answers and lock a settled row
chiga0 Sep 16, 2026
e5f471b
fix(cli): pause the loading row's timer while a call is parked
chiga0 Sep 16, 2026
affcf80
docs(design): record the answer lock and the parked timer, correct th…
chiga0 Sep 16, 2026
32433ec
docs(design): correct the ctrl+C actor set and the unported key families
chiga0 Sep 16, 2026
912c9d2
test(cli): pin the wiring and placement the review measured as free
chiga0 Sep 16, 2026
2ccf2d1
test(cli): witness the ctrl+E routing the review measured as free
chiga0 Sep 16, 2026
5508926
fix(cli): bound the ask-user answer row and re-arm a wizard field aft…
chiga0 Sep 16, 2026
f337335
fix(cli): settle the answer row on the verdict that records it
chiga0 Sep 16, 2026
0d0abf2
refactor(cli): collapse the answer lock to the armed timer alone
chiga0 Sep 16, 2026
ae91c5f
docs: record the machine leg of the round-three fixes
chiga0 Sep 16, 2026
29d1d5d
Merge remote-tracking branch 'origin/main' into fix/opentui-parity-fo…
chiga0 Sep 16, 2026
f4a8f7d
docs: record the matrix re-run after merging main
chiga0 Sep 16, 2026
fef97a3
fix(cli): scope the answer row's re-arm, lock, width and caret cell
chiga0 Sep 17, 2026
f69b0be
docs: attribute the answer lock to the branches Decision 37 put it on
chiga0 Sep 17, 2026
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
286 changes: 256 additions & 30 deletions docs/design/2026-09-10-opentui-parity-defect-sweep.md

Large diffs are not rendered by default.

204 changes: 182 additions & 22 deletions docs/design/2026-09-10-opentui-parity-defect-sweep.zh-CN.md

Large diffs are not rendered by default.

47 changes: 31 additions & 16 deletions packages/cli/src/ui/components/messages/ToolMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1157,7 +1157,35 @@ export const TOOL_ARGS_INLINE_MAX_LINES = 2;

/**
* One-line JSON for the `ui.showToolCallArgs` row, or undefined when there is
* nothing worth adding.
* nothing worth adding. Serializes `args`, then defers to
* {@link formatInlineToolArgsJson} for the row itself.
*/
export function formatInlineToolArgs(
args: Record<string, unknown> | undefined,
description: string,
uncapped: boolean,
rowWidth?: number,
): string | undefined {
if (!args || Object.keys(args).length === 0) {
return undefined;
}

let json: string;
try {
json = JSON.stringify(args);
} catch {
// Circular or otherwise unserializable args — the header line is all we
// can honestly show.
return undefined;
}

return formatInlineToolArgsJson(json, description, uncapped, rowWidth);
}

/**
* The `ui.showToolCallArgs` row over an already-serialized `json`, so a renderer
* that carries the call's arguments as text (OpenTUI's `tool-args` event) draws
* the same row as this one rather than keeping a second copy of the policy.
*
* Skipped when `description` already IS the args JSON: MCP invocations return
* `safeJsonStringify(params)` from `getDescription()`, so rendering both would
Expand All @@ -1179,25 +1207,12 @@ export const TOOL_ARGS_INLINE_MAX_LINES = 2;
* component). When given, the row is bounded to `TOOL_ARGS_INLINE_MAX_LINES`
* wrapped rows rather than by character count alone — see that constant.
*/
export function formatInlineToolArgs(
args: Record<string, unknown> | undefined,
export function formatInlineToolArgsJson(
json: string,
description: string,
uncapped: boolean,
rowWidth?: number,
): string | undefined {
if (!args || Object.keys(args).length === 0) {
return undefined;
}

let json: string;
try {
json = JSON.stringify(args);
} catch {
// Circular or otherwise unserializable args — the header line is all we
// can honestly show.
return undefined;
}

const trimmedDescription = description.trim();
if (trimmedDescription.startsWith('{')) {
try {
Expand Down
21 changes: 21 additions & 0 deletions packages/cli/src/ui/opentui/dialogs-auth.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,16 @@ async function typeText(text: string): Promise<void> {
}
}

/** Every character in one act, as a burst out of a single pty read arrives. */
async function typeBatched(text: string): Promise<void> {
const handler = lastKeyboardHandler();
await act(async () => {
for (const char of text) {
handler(baseKeyEvent({ name: char, sequence: char }));
}
});
}

async function pressEsc(): Promise<boolean> {
const handler = mocks.state.inputHandlers.at(-1);
if (!handler) throw new Error('no raw input handler registered');
Expand Down Expand Up @@ -445,6 +455,17 @@ describe('bracketed-paste into dialog inputs (#57)', () => {
await press('return'); // baseUrl → apiKey
}

it('keeps every character of a burst that shares one batch', async () => {
await runToApiKeyStep();
await typeBatched('sk-burst-key');
expect(
screen.getByText((_, element) => element?.textContent === 'sk-burst-key'),
).toBeTruthy();
// the burst is what the wizard carries forward, not its last character
await press('return'); // apiKey → models
expect(screen.getByText(/Enter model IDs directly/)).toBeTruthy();
});

it('inserts a paste into the API-key input and prevents default', async () => {
await runToApiKeyStep();
const event = await pasteText('sk-pasted-key');
Expand Down
19 changes: 15 additions & 4 deletions packages/cli/src/ui/opentui/dialogs-auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
* - documentation/TOS links render as plain text (no OSC 8 in dialogs).
*/

import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useKeyboard, usePaste, useRenderer } from '@opentui/react';
import type { PasteEvent } from '@opentui/core';
import { decodePasteBytes } from '@opentui/core';
Expand Down Expand Up @@ -239,18 +239,29 @@ function useLineInputKeys(
onChange: (next: string) => void,
onSubmit: () => void,
) {
// Key events can land in one React batch, where the value captured by the
// render that registered the handler is already stale by the second
// keystroke. The mirror is written synchronously so each event appends to
// what the previous one produced, and re-synced on render so a value set
// from anywhere else is not lost.
const latest = useRef(value);
latest.current = value;
const change = (next: string) => {
latest.current = next;
onChange(next);
};
useKeyboard((key) => {
const o = toOriginalKey(key);
if (o.name === 'return' || o.name === 'enter') {
onSubmit();
Comment thread
chiga0 marked this conversation as resolved.
Resolved
return;
}
if (o.name === 'backspace' || o.name === 'delete') {
onChange(value.slice(0, -1));
change(latest.current.slice(0, -1));
return;
}
if (isPrintableKeyInput(key)) {
onChange(value + key.sequence);
change(latest.current + key.sequence);
}
});
// Bracketed pastes arrive as one PasteEvent with no keypress per character
Expand All @@ -262,7 +273,7 @@ function useLineInputKeys(
const text = normalizePastedText(decodePasteBytes(event.bytes));
if (!text) return;
event.preventDefault();
onChange(value + text);
change(latest.current + text);
});
}

Expand Down
Loading
Loading