-
Notifications
You must be signed in to change notification settings - Fork 3k
feat(ui): let the user read the full plan from the exit_plan_mode confirmation #7060
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,6 +6,9 @@ | |||||||||||||||||
|
|
||||||||||||||||||
| import type React from 'react'; | ||||||||||||||||||
| import { useEffect, useState } from 'react'; | ||||||||||||||||||
| import { promises as fs } from 'node:fs'; | ||||||||||||||||||
| import * as os from 'node:os'; | ||||||||||||||||||
| import * as path from 'node:path'; | ||||||||||||||||||
| import { Box, Text } from 'ink'; | ||||||||||||||||||
| import { DiffRenderer } from './DiffRenderer.js'; | ||||||||||||||||||
| import { RenderInline } from '../../utils/InlineMarkdownRenderer.js'; | ||||||||||||||||||
|
|
@@ -26,6 +29,7 @@ import type { RadioSelectItem } from '../shared/RadioButtonSelect.js'; | |||||||||||||||||
| import { RadioButtonSelect } from '../shared/RadioButtonSelect.js'; | ||||||||||||||||||
| import { MaxSizedBox } from '../shared/MaxSizedBox.js'; | ||||||||||||||||||
| import { useKeypress } from '../../hooks/useKeypress.js'; | ||||||||||||||||||
| import { useLaunchEditor } from '../../hooks/useLaunchEditor.js'; | ||||||||||||||||||
| import { useSettings } from '../../contexts/SettingsContext.js'; | ||||||||||||||||||
| import { theme } from '../../semantic-colors.js'; | ||||||||||||||||||
| import { t } from '../../../i18n/index.js'; | ||||||||||||||||||
|
|
@@ -103,11 +107,42 @@ export const ToolConfirmationMessage: React.FC< | |||||||||||||||||
|
|
||||||||||||||||||
| const isTrustedFolder = config.isTrustedFolder(); | ||||||||||||||||||
|
|
||||||||||||||||||
| const launchEditor = useLaunchEditor(); | ||||||||||||||||||
| const [planViewError, setPlanViewError] = useState<string | null>(null); | ||||||||||||||||||
|
|
||||||||||||||||||
| // #7001: a long plan can exceed the confirmation dialog's height budget and | ||||||||||||||||||
| // get truncated (with a "... N more lines not shown ..." cue since #6882), | ||||||||||||||||||
| // yet the user is asked to approve it. `o` writes the FULL plan to a temp | ||||||||||||||||||
| // file and opens it in the configured editor so the decision is informed; | ||||||||||||||||||
| // the dialog stays open (nothing is confirmed) while the user reads. | ||||||||||||||||||
| const openFullPlanInEditor = () => { | ||||||||||||||||||
| if (confirmationDetails.type !== 'plan') return; | ||||||||||||||||||
| setPlanViewError(null); | ||||||||||||||||||
| const openPlan = async () => { | ||||||||||||||||||
| const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'qwen-plan-')); | ||||||||||||||||||
| const planPath = path.join(dir, 'plan.md'); | ||||||||||||||||||
| await fs.writeFile(planPath, confirmationDetails.plan); | ||||||||||||||||||
| await launchEditor(planPath); | ||||||||||||||||||
| }; | ||||||||||||||||||
| void openPlan().catch((err: unknown) => { | ||||||||||||||||||
| setPlanViewError(err instanceof Error ? err.message : String(err)); | ||||||||||||||||||
| }); | ||||||||||||||||||
|
Comment on lines
+127
to
+129
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The
Suggested change
— qwen3.7-max via Qwen Code /review |
||||||||||||||||||
| }; | ||||||||||||||||||
|
|
||||||||||||||||||
| useKeypress( | ||||||||||||||||||
| (key) => { | ||||||||||||||||||
| if (!isFocused) return; | ||||||||||||||||||
| if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { | ||||||||||||||||||
| handleConfirm(ToolConfirmationOutcome.Cancel); | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
| if ( | ||||||||||||||||||
| key.name === 'o' && | ||||||||||||||||||
| !key.ctrl && | ||||||||||||||||||
| !key.meta && | ||||||||||||||||||
| confirmationDetails.type === 'plan' | ||||||||||||||||||
| ) { | ||||||||||||||||||
| openFullPlanInEditor(); | ||||||||||||||||||
| } | ||||||||||||||||||
| }, | ||||||||||||||||||
| { isActive: isFocused }, | ||||||||||||||||||
|
|
@@ -347,12 +382,15 @@ export const ToolConfirmationMessage: React.FC< | |||||||||||||||||
| value: ToolConfirmationOutcome.Cancel, | ||||||||||||||||||
| }); | ||||||||||||||||||
|
|
||||||||||||||||||
| const planHeight = compactMode | ||||||||||||||||||
| // Reserve one row for the "o open full plan" hint below the plan body. | ||||||||||||||||||
| const rawPlanHeight = compactMode | ||||||||||||||||||
| ? Math.min( | ||||||||||||||||||
| availableBodyContentHeight() ?? COMPACT_BODY_MAX_LINES, | ||||||||||||||||||
| COMPACT_BODY_MAX_LINES, | ||||||||||||||||||
| ) | ||||||||||||||||||
| : availableBodyContentHeight(); | ||||||||||||||||||
| const planHeight = | ||||||||||||||||||
| rawPlanHeight === undefined ? undefined : Math.max(rawPlanHeight - 1, 1); | ||||||||||||||||||
| bodyContent = ( | ||||||||||||||||||
| <Box flexDirection="column" paddingX={1} marginLeft={1}> | ||||||||||||||||||
| <MarkdownDisplay | ||||||||||||||||||
|
|
@@ -368,6 +406,17 @@ export const ToolConfirmationMessage: React.FC< | |||||||||||||||||
| // See #6867. | ||||||||||||||||||
| enforceHeightBudget | ||||||||||||||||||
| /> | ||||||||||||||||||
| {/* The plan can be height-truncated above, and the user is about to | ||||||||||||||||||
| approve it — always offer a way to read the WHOLE thing. See #7001. */} | ||||||||||||||||||
| {planViewError ? ( | ||||||||||||||||||
| <Text color={theme.status.error} wrap="truncate"> | ||||||||||||||||||
| {planViewError} | ||||||||||||||||||
| </Text> | ||||||||||||||||||
| ) : ( | ||||||||||||||||||
| <Text color={theme.text.secondary} wrap="truncate"> | ||||||||||||||||||
| {t('o open full plan in editor')} | ||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The i18n key
Suggested change
Register the key in — qwen3.7-max via Qwen Code /review |
||||||||||||||||||
| </Text> | ||||||||||||||||||
| )} | ||||||||||||||||||
| </Box> | ||||||||||||||||||
| ); | ||||||||||||||||||
| } else if (confirmationDetails.type === 'info') { | ||||||||||||||||||
|
|
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Critical]
openFullPlanInEditorcreates a temp directory viafs.mkdtempon everyopress but never removes it — not after the editor exits, not on error, not on dialog dismissal. The established codebase pattern intext-buffer.ts(lines 2524, 2559, 2626) wraps identicalmkdtempusage intry/finallywithfs.rmSync(tmpDir, { recursive: true, force: true }). — Failure scenario: eachopress orphans aqwen-plan-XXXXXX/plan.mddirectory containing the full plan text (potentially sensitive conversation content) underos.tmpdir(). On shared systems other processes can discover and read these files; on tmpfs-backed/tmpthey consume RAM until reboot.— qwen3.7-max via Qwen Code /review