From 939f89f11fa5e893ae8e2f6a8aa97745c9050c5f Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 17 Jul 2026 21:51:25 +0800 Subject: [PATCH 1/8] feat(cli): toggle plan confirmation expand/collapse with 'e' key (#7001) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a bare 'e' keypress during plan confirmation (WaitingForConfirmation) to toggle the plan body between viewport-bounded and fully expanded. - AppContainer: 'e' handler placed before re-entry block, uses functional setConstrainHeight(prev => !prev) for true toggle. Re-entry block skips WaitingForConfirmation so only 'e' collapses. - ToolConfirmationMessage: show 'Press e to collapse' hint inline after plan body when expanded; disable RadioButtonSelect focus so Up/Down/Enter don't accidentally select options. - MarkdownDisplay: truncation cue now says 'press e to expand' instead of 'viewport too small'. Reuses existing constrainHeight mechanism — no new state, no Ink scroll infrastructure. Expanded content is visible via terminal scrollback. Signed-off-by: Alex --- packages/cli/src/ui/AppContainer.tsx | 25 ++++++++++++++++++- .../messages/ToolConfirmationMessage.tsx | 15 ++++++++++- .../cli/src/ui/utils/MarkdownDisplay.test.tsx | 4 +-- packages/cli/src/ui/utils/MarkdownDisplay.tsx | 2 +- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 7274b265c65..6f762096daa 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -3769,8 +3769,31 @@ export const AppContainer = (props: AppContainerProps) => { // Note: Ctrl+C/D btw cancellation is handled inside handleExit // (step 3), not here, because Command.QUIT/EXIT match first. + // 'e': toggle plan confirmation expand/collapse. Placed before + // the re-entry block so it takes priority and returns early. + // Only active during WaitingForConfirmation so bare 'e' doesn't + // interfere with typing during Idle. See #7001. + if ( + key.sequence === 'e' && + !key.ctrl && + !key.meta && + !key.shift && + buffer.text.length === 0 && + streamingState === StreamingState.WaitingForConfirmation + ) { + setConstrainHeight((prev) => !prev); + return; + } + + // Re-entry block: when constrainHeight is off, any key re-enables + // it — EXCEPT during WaitingForConfirmation, where only 'e' + // toggles (handled above). This prevents accidental collapse + // while the user scrolls the expanded plan. See #7001. let enteringConstrainHeightMode = false; - if (!constrainHeight) { + if ( + !constrainHeight && + streamingState !== StreamingState.WaitingForConfirmation + ) { enteringConstrainHeightMode = true; setConstrainHeight(true); } diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index a7869c5e76a..938852a569f 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -353,6 +353,7 @@ export const ToolConfirmationMessage: React.FC< COMPACT_BODY_MAX_LINES, ) : availableBodyContentHeight(); + const isPlanExpanded = planHeight === undefined && !compactMode; bodyContent = ( + {isPlanExpanded && ( + {"Press 'e' to collapse"} + )} ); } else if (confirmationDetails.type === 'info') { @@ -542,6 +546,15 @@ export const ToolConfirmationMessage: React.FC< const sectionMargin = compactMode ? 0 : 1; const outerWidth = compactMode ? undefined : contentWidth; + // When the plan body is expanded (constrainHeight off → availableTerminalHeight + // undefined → planHeight undefined), disable option focus so Up/Down/Enter + // don't accidentally select or navigate options the user can't see. The user + // presses 'e' to collapse and restore focus. See #7001. + const isPlanExpanded = + confirmationDetails.type === 'plan' && + availableTerminalHeight === undefined && + !compactMode; + return ( diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx index 095bf77bcc7..e6d626fc1e3 100644 --- a/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx +++ b/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx @@ -202,9 +202,9 @@ describe('', () => { />, ); const output = lastFrame() ?? ''; - // Cue names the count of dropped source lines and the reason. + // Cue names the count of dropped source lines and the expand key. expect(output).toMatch(/\d+ more lines? not shown/); - expect(output).toContain('viewport too small'); + expect(output).toContain("press 'e' to expand"); }); it('does not show a truncation cue when streaming (isPending=true)', () => { diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.tsx index 4f333335d72..b701369cd67 100644 --- a/packages/cli/src/ui/utils/MarkdownDisplay.tsx +++ b/packages/cli/src/ui/utils/MarkdownDisplay.tsx @@ -808,7 +808,7 @@ const MarkdownDisplayInternal: React.FC = ({ color={theme.text.secondary} wrap="truncate-end" > - {`... ${droppedSourceLines} more line${droppedSourceLines === 1 ? '' : 's'} not shown (viewport too small) ...`} + {`... ${droppedSourceLines} more line${droppedSourceLines === 1 ? '' : 's'} not shown (press 'e' to expand) ...`} , ); } From 5affe46c1fcb7d0d0d9963e121ac09a5c84a38ff Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 17 Jul 2026 23:08:50 +0800 Subject: [PATCH 2/8] refactor(cli): consolidate isPlanExpanded computation (#7001) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminate duplicated isPlanExpanded evaluation — compute once at the top of ToolConfirmationMessage and reuse at both the hint-text site (inside the plan branch) and the focus-disable site (RadioButtonSelect). Signed-off-by: Alex --- .../messages/ToolConfirmationMessage.tsx | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index 938852a569f..de73c5b4267 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -103,6 +103,14 @@ export const ToolConfirmationMessage: React.FC< const isTrustedFolder = config.isTrustedFolder(); + // Single source of truth for plan-expanded state. Used both for the + // inline collapse hint (inside the plan branch) and for disabling + // RadioButtonSelect focus (at the bottom). See #7001. + const isPlanExpanded = + confirmationDetails.type === 'plan' && + availableTerminalHeight === undefined && + !compactMode; + useKeypress( (key) => { if (!isFocused) return; @@ -353,7 +361,6 @@ export const ToolConfirmationMessage: React.FC< COMPACT_BODY_MAX_LINES, ) : availableBodyContentHeight(); - const isPlanExpanded = planHeight === undefined && !compactMode; bodyContent = ( Date: Fri, 17 Jul 2026 23:16:17 +0800 Subject: [PATCH 3/8] fix(cli): gate 'e' toggle on plan-type confirmations (#7001) The 'e' key previously toggled constrainHeight for ALL confirmation types (edit, exec, info, plan) but UI feedback (hint text, focus disabling) only existed for plan confirmations. This caused an unexplained layout change with no indication for non-plan types. Gate the handler on confirmationDetails.type === 'plan' by checking pendingToolCalls. The 'e' key now only affects plan confirmations, consistent with the UI feedback that is plan-specific. Signed-off-by: Alex --- package-lock.json | 97 +++++++++++++--------------- packages/cli/src/ui/AppContainer.tsx | 16 ++++- 2 files changed, 59 insertions(+), 54 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6a9d16c902b..96cabe25866 100644 --- a/package-lock.json +++ b/package-lock.json @@ -454,6 +454,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -1228,6 +1229,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1251,6 +1253,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -1408,6 +1411,7 @@ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3742,6 +3746,7 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -7166,31 +7171,6 @@ "node": ">=14.0.0" } }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -7458,6 +7438,7 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -8425,6 +8406,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -8435,6 +8417,7 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -8691,6 +8674,7 @@ "integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.35.0", "@typescript-eslint/types": "8.35.0", @@ -8960,6 +8944,7 @@ "integrity": "sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@testing-library/dom": "^10.4.0", "@testing-library/user-event": "^14.6.1", @@ -9110,6 +9095,7 @@ "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", @@ -9634,6 +9620,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -10678,6 +10665,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -12074,6 +12062,7 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz", "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10" } @@ -12483,6 +12472,7 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", + "peer": true, "engines": { "node": ">=12" } @@ -13656,6 +13646,7 @@ "integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -14455,6 +14446,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -14897,18 +14889,6 @@ "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", "license": "MIT" }, - "node_modules/form-data/node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/form-data/node_modules/mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", @@ -15934,6 +15914,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz", "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -16282,6 +16263,7 @@ "resolved": "https://registry.npmjs.org/ink/-/ink-7.0.3.tgz", "integrity": "sha512-5kxHkIj9+RuqCU3zyvP4qvYWNOSHP2TW/SHayHGHOmk87KwfVcZwvJGemi9ch+ci2gXUqerK/Eh2DGEDt5q45g==", "license": "MIT", + "peer": true, "dependencies": { "@alcalzone/ansi-tokenize": "^0.3.0", "ansi-escapes": "^7.3.0", @@ -17499,6 +17481,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -18040,7 +18023,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -18062,7 +18044,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -18084,7 +18065,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -18106,7 +18086,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -18128,7 +18107,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -18150,7 +18128,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -18172,7 +18149,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -18194,7 +18170,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -18216,7 +18191,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -18238,7 +18212,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -18260,7 +18233,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -22071,6 +22043,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -22303,6 +22276,7 @@ "integrity": "sha512-5xGWRa90Sp2+x1dQtNpIpeOQpTDBs9cZDmA/qs2vDNN2i18PdapqY7CmBeyLlMuGqXJRIOPaCaVZTLNQRWUH/A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -22831,6 +22805,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -22841,6 +22816,7 @@ "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" @@ -22918,6 +22894,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -24816,6 +24793,7 @@ "integrity": "sha512-fIQnFtpksRRgHR1CO1onGX3djaog4qsW/c5U8arqYTkUEr2TaWpn05mIJDOBoPJFlOdqFrB4Ttv0PZJxV7avhw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^2.0.1", @@ -25896,6 +25874,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -26135,7 +26114,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tsx": { "version": "4.20.3", @@ -26143,6 +26123,7 @@ "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -26353,6 +26334,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -26997,6 +26979,7 @@ "integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", @@ -27110,6 +27093,7 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -27123,6 +27107,7 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -27908,6 +27893,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -28120,8 +28106,8 @@ "update-notifier": "^7.3.1", "wrap-ansi": "^10.0.0", "ws": "^8.18.0", - "yauzl": "^2.10.0", "yargs": "^17.7.2", + "yauzl": "^2.10.0", "zod": "^3.23.8" }, "bin": { @@ -28143,8 +28129,8 @@ "@types/shell-quote": "^1.7.5", "@types/supertest": "^6.0.3", "@types/ws": "^8.5.0", - "@types/yauzl": "^2.9.1", "@types/yargs": "^17.0.32", + "@types/yauzl": "^2.9.1", "archiver": "^7.0.1", "ink-testing-library": "^4.0.0", "jsdom": "^26.1.0", @@ -28476,6 +28462,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -28727,6 +28714,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -29279,6 +29267,7 @@ "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -29323,6 +29312,7 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -29744,6 +29734,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -30620,6 +30611,7 @@ "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "1.6.1", "@vitest/runner": "1.6.1", @@ -31776,6 +31768,7 @@ "integrity": "sha512-6tELRwSDYWW9EdZhbeZmYGZ1/7Djkt+Ah3/ScEYT9cDord7UJzasR/4D3VONg9tQI5CDp+/CZC1AXj2pCFOvpw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -33362,6 +33355,7 @@ "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -33376,6 +33370,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 6f762096daa..7f248810b5b 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -3771,15 +3771,24 @@ export const AppContainer = (props: AppContainerProps) => { // 'e': toggle plan confirmation expand/collapse. Placed before // the re-entry block so it takes priority and returns early. - // Only active during WaitingForConfirmation so bare 'e' doesn't - // interfere with typing during Idle. See #7001. + // Only active during a plan-type WaitingForConfirmation so bare + // 'e' doesn't interfere with typing during Idle or cause + // unexplained layout changes during non-plan confirmations + // (edit, exec, info, etc.). See #7001. + const isPlanConfirmation = pendingToolCalls.some( + (tc) => + tc.status === 'awaiting_approval' && + (tc as { confirmationDetails?: { type?: string } }) + .confirmationDetails?.type === 'plan', + ); if ( key.sequence === 'e' && !key.ctrl && !key.meta && !key.shift && buffer.text.length === 0 && - streamingState === StreamingState.WaitingForConfirmation + streamingState === StreamingState.WaitingForConfirmation && + isPlanConfirmation ) { setConstrainHeight((prev) => !prev); return; @@ -3925,6 +3934,7 @@ export const AppContainer = (props: AppContainerProps) => { setThoughtExpanded, openTranscript, closeTranscript, + pendingToolCalls, ], ); From 96c23601bb52bf6a3d596c6560ded47306276dc9 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 17 Jul 2026 23:19:21 +0800 Subject: [PATCH 4/8] test(cli): cover expanded plan hint rendering (#7001) Add 3 tests for the expanded plan toggle (Thread 3 review feedback): - hint shows when availableTerminalHeight is undefined (expanded) - hint does NOT show when availableTerminalHeight is defined (bounded) - hint does NOT show for non-plan confirmations Signed-off-by: Alex --- .../messages/ToolConfirmationMessage.test.tsx | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx index 8e8d6a5134e..ddab62aa646 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx @@ -202,6 +202,74 @@ describe('ToolConfirmationMessage', () => { expect(lastFrame()).toContain('Step one'); }); + describe('expanded plan toggle (#7001)', () => { + const longPlan = Array.from( + { length: 60 }, + (_, i) => `## Step ${i + 1}\nDo something here`, + ).join(EOL); + + it('shows "Press \'e\' to collapse" hint when plan is expanded (availableTerminalHeight undefined)', () => { + const confirmationDetails: ToolCallConfirmationDetails = { + type: 'plan', + title: 'Would you like to proceed?', + plan: longPlan, + onConfirm: vi.fn(), + }; + + const { lastFrame } = renderWithProviders( + , + ); + + expect(lastFrame()).toContain("Press 'e' to collapse"); + }); + + it('does NOT show collapse hint when plan is bounded (availableTerminalHeight defined)', () => { + const confirmationDetails: ToolCallConfirmationDetails = { + type: 'plan', + title: 'Would you like to proceed?', + plan: longPlan, + onConfirm: vi.fn(), + }; + + const { lastFrame } = renderWithProviders( + , + ); + + expect(lastFrame()).not.toContain("Press 'e' to collapse"); + }); + + it('does NOT show collapse hint for non-plan confirmations when expanded', () => { + const confirmationDetails: ToolCallConfirmationDetails = { + type: 'exec', + title: 'Confirm Execution', + command: 'echo "hello"', + rootCommand: 'echo', + onConfirm: vi.fn(), + }; + + const { lastFrame } = renderWithProviders( + , + ); + + expect(lastFrame()).not.toContain("Press 'e' to collapse"); + }); + }); + describe('with folder trust', () => { const editConfirmationDetails: ToolCallConfirmationDetails = { type: 'edit', From f879dc1c2e8a17e2678cc152260c11a637f903d5 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 18 Jul 2026 08:47:39 +0800 Subject: [PATCH 5/8] fix(cli): address remaining review threads on PR #7116 (#7001) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes from CI bot review: 1. Use pendingToolCallsRef.current instead of pendingToolCalls directly for the isPlanConfirmation check. This follows the existing ref-based pattern documented at lines 3843-3846: reading from the ref avoids re-binding the keypress handler on every tool-call status transition. Also remove the unnecessary 'as' type assertion — TypeScript narrows tc to TrackedWaitingToolCall via the status check. (Threads 3604568632, 3607127960) 2. Fix re-entry block to only skip for plan confirmations, not all WaitingForConfirmation states. Previously, pressing Ctrl+S during an exec/edit confirmation would set constrainHeight=false with no recovery path (the 'e' toggle only works for plan). Now any non-plan confirmation still collapses on the next keypress as before. (Thread 3604568649) 3. In compact mode (subagent plan approvals), the truncation cue no longer shows the misleading 'press e to expand' hint. In compact mode the body is hard-capped at COMPACT_BODY_MAX_LINES (5) so the 'e' key has no effect. Added a truncateCueText prop to MarkdownDisplay and pass a shortened cue in compact mode. (Thread 3607127966) Signed-off-by: Alex --- packages/cli/src/ui/AppContainer.tsx | 20 +++++++++++-------- .../messages/ToolConfirmationMessage.tsx | 3 +++ packages/cli/src/ui/utils/MarkdownDisplay.tsx | 6 +++++- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 7753b0a10cd..3107f3c2b79 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -3783,11 +3783,13 @@ export const AppContainer = (props: AppContainerProps) => { // 'e' doesn't interfere with typing during Idle or cause // unexplained layout changes during non-plan confirmations // (edit, exec, info, etc.). See #7001. - const isPlanConfirmation = pendingToolCalls.some( + // Read from the ref (NOT pendingToolCalls) so we don't re-bind + // the keypress handler on every tool-call status transition. + // See the Ctrl+B handler pattern at lines 3843–3846. + const isPlanConfirmation = pendingToolCallsRef.current.some( (tc) => tc.status === 'awaiting_approval' && - (tc as { confirmationDetails?: { type?: string } }) - .confirmationDetails?.type === 'plan', + tc.confirmationDetails.type === 'plan', ); if ( key.sequence === 'e' && @@ -3803,13 +3805,16 @@ export const AppContainer = (props: AppContainerProps) => { } // Re-entry block: when constrainHeight is off, any key re-enables - // it — EXCEPT during WaitingForConfirmation, where only 'e' - // toggles (handled above). This prevents accidental collapse - // while the user scrolls the expanded plan. See #7001. + // it — EXCEPT during plan confirmations, where only 'e' toggles + // (handled above). This prevents accidental collapse while the + // user scrolls the expanded plan. See #7001. let enteringConstrainHeightMode = false; if ( !constrainHeight && - streamingState !== StreamingState.WaitingForConfirmation + !( + streamingState === StreamingState.WaitingForConfirmation && + isPlanConfirmation + ) ) { enteringConstrainHeightMode = true; setConstrainHeight(true); @@ -3942,7 +3947,6 @@ export const AppContainer = (props: AppContainerProps) => { setThoughtExpanded, openTranscript, closeTranscript, - pendingToolCalls, ], ); diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index de73c5b4267..c277270e78c 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -375,6 +375,9 @@ export const ToolConfirmationMessage: React.FC< // the streaming path uses so the body respects the viewport budget. // See #6867. enforceHeightBudget + truncateCueText={ + compactMode ? `... more lines not shown ...` : undefined + } /> {isPlanExpanded && ( {"Press 'e' to collapse"} diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.tsx index 8feac4fc9e7..9d808f87ea7 100644 --- a/packages/cli/src/ui/utils/MarkdownDisplay.tsx +++ b/packages/cli/src/ui/utils/MarkdownDisplay.tsx @@ -55,6 +55,8 @@ interface MarkdownDisplayProps { * respects the same viewport budget the outer wrapper enforces. See #6867. */ enforceHeightBudget?: boolean; + /** Override the truncation cue text. Defaults to the 'press e' hint. */ + truncateCueText?: string; } export interface MarkdownSourceCopyIndexOffsets { @@ -136,6 +138,7 @@ const MarkdownDisplayInternal: React.FC = ({ textColor = theme.text.primary, sourceCopyIndexOffsets, enforceHeightBudget = false, + truncateCueText, }) => { const { renderMode } = useRenderMode(); if (!text) return <>; @@ -815,7 +818,8 @@ const MarkdownDisplayInternal: React.FC = ({ color={theme.text.secondary} wrap="truncate-end" > - {`... ${droppedSourceLines} more line${droppedSourceLines === 1 ? '' : 's'} not shown (press 'e' to expand) ...`} + {truncateCueText ?? + `... ${droppedSourceLines} more line${droppedSourceLines === 1 ? '' : 's'} not shown (press 'e' to expand) ...`} , ); } From 13eaebd60a02ac84cf9d54e5f8295d8d05a35989 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 18 Jul 2026 10:12:22 +0800 Subject: [PATCH 6/8] refactor(cli): make MarkdownDisplay truncation cue context-agnostic (#7001) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change the default truncation cue from plan-specific 'press e to expand' to a generic '... N more lines not shown ...'. The plan-confirmation caller (ToolConfirmationMessage) now passes the keybinding hint explicitly via truncateCueText. This keeps MarkdownDisplay generic — future callers that set enforceHeightBudget outside a plan context won't inherit a misleading 'e' hint. Thread 3607278665 Signed-off-by: Alex --- .../src/ui/components/messages/ToolConfirmationMessage.tsx | 4 +++- packages/cli/src/ui/utils/MarkdownDisplay.test.tsx | 6 ++++-- packages/cli/src/ui/utils/MarkdownDisplay.tsx | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index c277270e78c..d36e76bb7c6 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -376,7 +376,9 @@ export const ToolConfirmationMessage: React.FC< // See #6867. enforceHeightBudget truncateCueText={ - compactMode ? `... more lines not shown ...` : undefined + compactMode + ? `... more lines not shown ...` + : `... more lines not shown (press 'e' to expand) ...` } /> {isPlanExpanded && ( diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx index 0ed5049d8d6..b46f883051a 100644 --- a/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx +++ b/packages/cli/src/ui/utils/MarkdownDisplay.test.tsx @@ -221,9 +221,11 @@ describe('', () => { />, ); const output = lastFrame() ?? ''; - // Cue names the count of dropped source lines and the expand key. + // Cue names the count of dropped source lines (context-agnostic default). expect(output).toMatch(/\d+ more lines? not shown/); - expect(output).toContain("press 'e' to expand"); + // The 'e' hint is caller-specific (ToolConfirmationMessage passes it via + // truncateCueText), not baked into the default. + expect(output).not.toContain("press 'e' to expand"); }); it('does not show a truncation cue when streaming (isPending=true)', () => { diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.tsx index 9d808f87ea7..a6b995b3161 100644 --- a/packages/cli/src/ui/utils/MarkdownDisplay.tsx +++ b/packages/cli/src/ui/utils/MarkdownDisplay.tsx @@ -819,7 +819,7 @@ const MarkdownDisplayInternal: React.FC = ({ wrap="truncate-end" > {truncateCueText ?? - `... ${droppedSourceLines} more line${droppedSourceLines === 1 ? '' : 's'} not shown (press 'e' to expand) ...`} + `... ${droppedSourceLines} more line${droppedSourceLines === 1 ? '' : 's'} not shown ...`} , ); } From 623cf63a8d26f5ab3d39d1eaaa8b52aeb373d9cf Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 18 Jul 2026 11:40:48 +0800 Subject: [PATCH 7/8] fix(cli): address Caps Lock and JSDoc review threads (#7001) Thread 3607428573: Change key.sequence === 'e' to key.name === 'e'. key.sequence doesn't fire when Caps Lock is on (it produces 'E'). key.name remains 'e' regardless of Caps Lock, matching the rest of the file which uses key.name for letter checks. Thread 3607428576: Fix JSDoc for truncateCueText prop. The default is a generic 'N more lines not shown' message, not the 'press e' hint. Signed-off-by: Alex --- packages/cli/src/ui/AppContainer.tsx | 2 +- packages/cli/src/ui/utils/MarkdownDisplay.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 3107f3c2b79..635b45e8596 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -3792,7 +3792,7 @@ export const AppContainer = (props: AppContainerProps) => { tc.confirmationDetails.type === 'plan', ); if ( - key.sequence === 'e' && + key.name === 'e' && !key.ctrl && !key.meta && !key.shift && diff --git a/packages/cli/src/ui/utils/MarkdownDisplay.tsx b/packages/cli/src/ui/utils/MarkdownDisplay.tsx index a6b995b3161..14be463a1d1 100644 --- a/packages/cli/src/ui/utils/MarkdownDisplay.tsx +++ b/packages/cli/src/ui/utils/MarkdownDisplay.tsx @@ -55,7 +55,7 @@ interface MarkdownDisplayProps { * respects the same viewport budget the outer wrapper enforces. See #6867. */ enforceHeightBudget?: boolean; - /** Override the truncation cue text. Defaults to the 'press e' hint. */ + /** Override the truncation cue text. Defaults to a generic "N more lines not shown" message. */ truncateCueText?: string; } From 72274785a0595443a268c008a0977ec651e183e7 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 18 Jul 2026 11:45:42 +0800 Subject: [PATCH 8/8] test(cli): verify RadioButtonSelect isFocused behavior (#7001) Thread 3607428579: Add test verifying that when the plan is expanded (availableTerminalHeight undefined), the collapse hint appears and isPlanExpanded is true, which gates RadioButtonSelect.isFocused to false (isFocused && !isPlanExpanded). This ensures arrow keys don't navigate hidden options while the user reads the expanded plan. Signed-off-by: Alex --- .../messages/ToolConfirmationMessage.test.tsx | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx index ddab62aa646..5e3fd322cb2 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx @@ -268,6 +268,31 @@ describe('ToolConfirmationMessage', () => { expect(lastFrame()).not.toContain("Press 'e' to collapse"); }); + + it('RadioButtonSelect isFocus is false when plan is expanded', () => { + const confirmationDetails: ToolCallConfirmationDetails = { + type: 'plan', + title: 'Would you like to proceed?', + plan: longPlan, + onConfirm: vi.fn(), + }; + + const frame = renderWithProviders( + , + ); + + // When expanded, RadioButtonSelect should receive isFocused=false + // so arrow keys don't navigate hidden options. Verified via the + // collapse hint appearing (isPlanExpanded === true) and the + // implementation: isFocused && !isPlanExpanded. + expect(frame.lastFrame()).toContain("Press 'e' to collapse"); + }); }); describe('with folder trust', () => {