@deepseek-harness-tui/dsh-tui 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -24
- package/assets/whale-girl/whale-girl-dark.png +0 -0
- package/assets/whale-girl/whale-girl-happy.png +0 -0
- package/assets/whale-girl/whale-girl.png +0 -0
- package/bin/dsh-tui.js +93 -6
- package/cordis.patch.yml +63 -22
- package/cordis.yml +3 -2
- package/guide/dsh-tui-guide/SKILL.md +37 -0
- package/guide/dsh-tui-guide/configuration.en.md +410 -0
- package/guide/dsh-tui-guide/configuration.md +349 -0
- package/guide/dsh-tui-guide/getting-started.en.md +398 -0
- package/guide/dsh-tui-guide/getting-started.md +397 -0
- package/guide/dsh-tui-guide/interaction.en.md +674 -0
- package/guide/dsh-tui-guide/interaction.md +747 -0
- package/guide/dsh-tui-guide/migrate.en.md +166 -0
- package/guide/dsh-tui-guide/migrate.md +132 -0
- package/guide/dsh-tui-guide/plugins.en.md +68 -0
- package/guide/dsh-tui-guide/plugins.md +56 -0
- package/guide/dsh-tui-guide/themes.en.md +192 -0
- package/guide/dsh-tui-guide/themes.md +185 -0
- package/guide/dsh-tui-guide/user-guide.en.md +608 -0
- package/guide/dsh-tui-guide/user-guide.md +586 -0
- package/guide/dsh-tui-guide/vscode.en.md +301 -0
- package/guide/dsh-tui-guide/vscode.md +272 -0
- package/lib/settings.json +1272 -0
- package/lib/types/adapter/channel/ui-policy.d.ts +10 -2
- package/lib/types/adapter/channel/ui-policy.d.ts.map +1 -1
- package/lib/types/adapter/channel/ui-policy.js +17 -0
- package/lib/types/adapter/kernel/replay.d.ts.map +1 -1
- package/lib/types/adapter/kernel/replay.js +2 -0
- package/lib/types/adapter/ports/channel-display.d.ts +12 -0
- package/lib/types/adapter/ports/channel-display.d.ts.map +1 -1
- package/lib/types/adapter/ports/channel-session.d.ts +38 -0
- package/lib/types/adapter/ports/channel-session.d.ts.map +1 -1
- package/lib/types/adapter/ports/channel-settings.d.ts +14 -1
- package/lib/types/adapter/ports/channel-settings.d.ts.map +1 -1
- package/lib/types/adapter/ports/channel-ui.d.ts +75 -6
- package/lib/types/adapter/ports/channel-ui.d.ts.map +1 -1
- package/lib/types/adapter/ports/channel-view.d.ts +86 -9
- package/lib/types/adapter/ports/channel-view.d.ts.map +1 -1
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +2 -0
- package/lib/types/components/BalanceReportRow.d.ts +10 -2
- package/lib/types/components/BalanceReportRow.d.ts.map +1 -1
- package/lib/types/components/BalanceReportRow.js +18 -4
- package/lib/types/components/Chat/JobCard.d.ts +9 -0
- package/lib/types/components/Chat/JobCard.d.ts.map +1 -1
- package/lib/types/components/Chat/JobCard.js +42 -15
- package/lib/types/components/Chat/SubagentMessage.js +6 -6
- package/lib/types/components/CompactionStatusRow.d.ts +24 -0
- package/lib/types/components/CompactionStatusRow.d.ts.map +1 -0
- package/lib/types/components/CompactionStatusRow.js +72 -0
- package/lib/types/components/GoalTodoPanel.js +2 -2
- package/lib/types/components/HelpMenu.d.ts +2 -2
- package/lib/types/components/HelpMenu.d.ts.map +1 -1
- package/lib/types/components/HelpMenu.js +5 -4
- package/lib/types/components/InlineMathParagraph.d.ts +22 -0
- package/lib/types/components/InlineMathParagraph.d.ts.map +1 -0
- package/lib/types/components/InlineMathParagraph.js +178 -0
- package/lib/types/components/JobsPanel.d.ts.map +1 -1
- package/lib/types/components/JobsPanel.js +80 -33
- package/lib/types/components/LogoV2.d.ts +41 -3
- package/lib/types/components/LogoV2.d.ts.map +1 -1
- package/lib/types/components/LogoV2.js +148 -24
- package/lib/types/components/Markdown.d.ts +10 -3
- package/lib/types/components/Markdown.d.ts.map +1 -1
- package/lib/types/components/Markdown.js +47 -14
- package/lib/types/components/MathBlock.d.ts +13 -0
- package/lib/types/components/MathBlock.d.ts.map +1 -0
- package/lib/types/components/MathBlock.js +136 -0
- package/lib/types/components/MessageList.d.ts +10 -1
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +16 -7
- package/lib/types/components/MigratePicker.d.ts +30 -0
- package/lib/types/components/MigratePicker.d.ts.map +1 -0
- package/lib/types/components/MigratePicker.js +38 -0
- package/lib/types/components/PresetPicker.d.ts +4 -0
- package/lib/types/components/PresetPicker.d.ts.map +1 -1
- package/lib/types/components/PresetPicker.js +5 -1
- package/lib/types/components/PromptInput.d.ts +17 -0
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +60 -8
- package/lib/types/components/SplitDiffView.d.ts.map +1 -1
- package/lib/types/components/SplitDiffView.js +2 -1
- package/lib/types/components/StarPrompt.d.ts +59 -0
- package/lib/types/components/StarPrompt.d.ts.map +1 -0
- package/lib/types/components/StarPrompt.js +326 -0
- package/lib/types/components/StreamingMarkdown.d.ts.map +1 -1
- package/lib/types/components/StreamingMarkdown.js +20 -3
- package/lib/types/components/SubagentCard.d.ts.map +1 -1
- package/lib/types/components/SubagentCard.js +8 -8
- package/lib/types/components/SubagentDetailScene.d.ts.map +1 -1
- package/lib/types/components/SubagentDetailScene.js +108 -10
- package/lib/types/components/TimelineRail.d.ts.map +1 -1
- package/lib/types/components/TimelineRail.js +9 -1
- package/lib/types/components/Whale.d.ts +17 -8
- package/lib/types/components/Whale.d.ts.map +1 -1
- package/lib/types/components/Whale.js +25 -15
- package/lib/types/components/WhaleGirl.d.ts +35 -0
- package/lib/types/components/WhaleGirl.d.ts.map +1 -0
- package/lib/types/components/WhaleGirl.js +50 -0
- package/lib/types/components/WorkingSpinner.d.ts +4 -1
- package/lib/types/components/WorkingSpinner.d.ts.map +1 -1
- package/lib/types/components/WorkingSpinner.js +2 -2
- package/lib/types/components/bigfont.d.ts +47 -18
- package/lib/types/components/bigfont.d.ts.map +1 -1
- package/lib/types/components/bigfont.js +55 -40
- package/lib/types/components/design-system/ThemeProvider.d.ts +6 -0
- package/lib/types/components/design-system/ThemeProvider.d.ts.map +1 -1
- package/lib/types/components/design-system/ThemeProvider.js +40 -1
- package/lib/types/components/maidPortrait.d.ts +44 -0
- package/lib/types/components/maidPortrait.d.ts.map +1 -0
- package/lib/types/components/maidPortrait.js +178 -0
- package/lib/types/components/mathPreview.d.ts +25 -0
- package/lib/types/components/mathPreview.d.ts.map +1 -0
- package/lib/types/components/mathPreview.js +48 -0
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantThinkingMessage.js +6 -5
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +3 -2
- package/lib/types/components/messages/TranscriptImages.d.ts.map +1 -1
- package/lib/types/components/messages/TranscriptImages.js +11 -1
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts +14 -2
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +80 -38
- package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
- package/lib/types/components/questions/PlanReviewPanel.js +3 -3
- package/lib/types/components/sessions/ForeignSessionRow.d.ts +34 -0
- package/lib/types/components/sessions/ForeignSessionRow.d.ts.map +1 -0
- package/lib/types/components/sessions/ForeignSessionRow.js +43 -0
- package/lib/types/components/sessions/SourceTabs.d.ts +52 -0
- package/lib/types/components/sessions/SourceTabs.d.ts.map +1 -0
- package/lib/types/components/sessions/SourceTabs.js +70 -0
- package/lib/types/components/splashEggs.d.ts +56 -0
- package/lib/types/components/splashEggs.d.ts.map +1 -0
- package/lib/types/components/splashEggs.js +67 -0
- package/lib/types/components/splashFonts.d.ts +119 -0
- package/lib/types/components/splashFonts.d.ts.map +1 -0
- package/lib/types/components/splashFonts.js +267 -0
- package/lib/types/components/splashLayout.d.ts +41 -0
- package/lib/types/components/splashLayout.d.ts.map +1 -0
- package/lib/types/components/splashLayout.js +40 -0
- package/lib/types/components/trajectory/MiniWake.d.ts +11 -1
- package/lib/types/components/trajectory/MiniWake.d.ts.map +1 -1
- package/lib/types/components/trajectory/MiniWake.js +9 -3
- package/lib/types/components/whaleGirlSprite.d.ts +17 -0
- package/lib/types/components/whaleGirlSprite.d.ts.map +1 -0
- package/lib/types/components/whaleGirlSprite.js +47 -0
- package/lib/types/deepseekPricing.d.ts +74 -0
- package/lib/types/deepseekPricing.d.ts.map +1 -1
- package/lib/types/deepseekPricing.js +110 -11
- package/lib/types/dsh-adapter/channel/action-readiness.d.ts +1 -1
- package/lib/types/dsh-adapter/channel/action-readiness.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/action-readiness.js +6 -1
- package/lib/types/dsh-adapter/channel/background-action.d.ts +1 -0
- package/lib/types/dsh-adapter/channel/background-action.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/background-action.js +1 -0
- package/lib/types/dsh-adapter/channel/binding-events.d.ts +8 -9
- package/lib/types/dsh-adapter/channel/binding-events.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/binding-events.js +69 -21
- package/lib/types/dsh-adapter/channel/compaction.d.ts +2 -1
- package/lib/types/dsh-adapter/channel/compaction.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/compaction.js +35 -2
- package/lib/types/dsh-adapter/channel/external-commands.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/external-commands.js +8 -2
- package/lib/types/dsh-adapter/channel/job-projection.d.ts +1 -0
- package/lib/types/dsh-adapter/channel/job-projection.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/job-projection.js +88 -3
- package/lib/types/dsh-adapter/channel/local-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/local-actions.js +6 -1
- package/lib/types/dsh-adapter/channel/mode-actions.d.ts +13 -0
- package/lib/types/dsh-adapter/channel/mode-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/mode-actions.js +48 -7
- package/lib/types/dsh-adapter/channel/mode-permission-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/mode-permission-actions.js +25 -9
- package/lib/types/dsh-adapter/channel/model-switch.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/model-switch.js +5 -1
- package/lib/types/dsh-adapter/channel/paths.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/paths.js +7 -1
- package/lib/types/dsh-adapter/channel/preferences.d.ts +1 -1
- package/lib/types/dsh-adapter/channel/preferences.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/preferences.js +31 -5
- package/lib/types/dsh-adapter/channel/projection.d.ts +1 -2
- package/lib/types/dsh-adapter/channel/projection.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/projection.js +127 -17
- package/lib/types/dsh-adapter/channel/question-record.d.ts +76 -0
- package/lib/types/dsh-adapter/channel/question-record.d.ts.map +1 -0
- package/lib/types/dsh-adapter/channel/question-record.js +107 -0
- package/lib/types/dsh-adapter/channel/session-lineage.d.ts +42 -0
- package/lib/types/dsh-adapter/channel/session-lineage.d.ts.map +1 -0
- package/lib/types/dsh-adapter/channel/session-lineage.js +51 -0
- package/lib/types/dsh-adapter/channel/session-live-adoption.d.ts +2 -0
- package/lib/types/dsh-adapter/channel/session-live-adoption.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-live-adoption.js +9 -3
- package/lib/types/dsh-adapter/channel/session-metadata.d.ts +2 -1
- package/lib/types/dsh-adapter/channel/session-metadata.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-metadata.js +39 -7
- package/lib/types/dsh-adapter/channel/session-reset.d.ts +1 -1
- package/lib/types/dsh-adapter/channel/session-reset.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-reset.js +7 -0
- package/lib/types/dsh-adapter/channel/session-resume.d.ts +1 -0
- package/lib/types/dsh-adapter/channel/session-resume.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-resume.js +4 -2
- package/lib/types/dsh-adapter/channel/settings-host.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/settings-host.js +8 -0
- package/lib/types/dsh-adapter/channel/state.d.ts +17 -2
- package/lib/types/dsh-adapter/channel/state.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/state.js +6 -1
- package/lib/types/dsh-adapter/channel/subagent-projection.d.ts +30 -35
- package/lib/types/dsh-adapter/channel/subagent-projection.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/subagent-projection.js +143 -4
- package/lib/types/dsh-adapter/channel/types.d.ts +1 -1
- package/lib/types/dsh-adapter/channel/types.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +35 -5
- package/lib/types/dsh-adapter/compat/messages.d.ts +6 -0
- package/lib/types/dsh-adapter/compat/messages.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/messages.js +9 -0
- package/lib/types/dsh-adapter/contract.d.ts +6 -5
- package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
- package/lib/types/dsh-adapter/contract.js +14 -6
- package/lib/types/dsh-adapter/index.d.ts +36 -1
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +22 -6
- package/lib/types/dsh-adapter/jobs.d.ts +82 -16
- package/lib/types/dsh-adapter/jobs.d.ts.map +1 -1
- package/lib/types/dsh-adapter/jobs.js +104 -9
- package/lib/types/dsh-adapter/migrate/adapters/claude-code.d.ts +3 -0
- package/lib/types/dsh-adapter/migrate/adapters/claude-code.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/adapters/claude-code.js +131 -0
- package/lib/types/dsh-adapter/migrate/adapters/claude-code.parse.d.ts +17 -0
- package/lib/types/dsh-adapter/migrate/adapters/claude-code.parse.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/adapters/claude-code.parse.js +329 -0
- package/lib/types/dsh-adapter/migrate/adapters/codex.d.ts +3 -0
- package/lib/types/dsh-adapter/migrate/adapters/codex.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/adapters/codex.js +102 -0
- package/lib/types/dsh-adapter/migrate/adapters/codex.parse.d.ts +16 -0
- package/lib/types/dsh-adapter/migrate/adapters/codex.parse.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/adapters/codex.parse.js +289 -0
- package/lib/types/dsh-adapter/migrate/adapters/grok-build.d.ts +3 -0
- package/lib/types/dsh-adapter/migrate/adapters/grok-build.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/adapters/grok-build.js +117 -0
- package/lib/types/dsh-adapter/migrate/adapters/grok-build.parse.d.ts +13 -0
- package/lib/types/dsh-adapter/migrate/adapters/grok-build.parse.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/adapters/grok-build.parse.js +178 -0
- package/lib/types/dsh-adapter/migrate/adapters/omp.d.ts +3 -0
- package/lib/types/dsh-adapter/migrate/adapters/omp.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/adapters/omp.js +133 -0
- package/lib/types/dsh-adapter/migrate/adapters/scan-fs.d.ts +60 -0
- package/lib/types/dsh-adapter/migrate/adapters/scan-fs.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/adapters/scan-fs.js +178 -0
- package/lib/types/dsh-adapter/migrate/adapters/scan.d.ts +8 -0
- package/lib/types/dsh-adapter/migrate/adapters/scan.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/adapters/scan.js +40 -0
- package/lib/types/dsh-adapter/migrate/adapters/zcode.d.ts +3 -0
- package/lib/types/dsh-adapter/migrate/adapters/zcode.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/adapters/zcode.js +93 -0
- package/lib/types/dsh-adapter/migrate/adapters/zcode.parse.d.ts +9 -0
- package/lib/types/dsh-adapter/migrate/adapters/zcode.parse.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/adapters/zcode.parse.js +89 -0
- package/lib/types/dsh-adapter/migrate/bin-path.d.ts +6 -0
- package/lib/types/dsh-adapter/migrate/bin-path.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/bin-path.js +30 -0
- package/lib/types/dsh-adapter/migrate/browse.d.ts +42 -0
- package/lib/types/dsh-adapter/migrate/browse.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/browse.js +90 -0
- package/lib/types/dsh-adapter/migrate/cli.d.ts +9 -0
- package/lib/types/dsh-adapter/migrate/cli.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/cli.js +106 -0
- package/lib/types/dsh-adapter/migrate/import-one.d.ts +89 -0
- package/lib/types/dsh-adapter/migrate/import-one.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/import-one.js +133 -0
- package/lib/types/dsh-adapter/migrate/index.d.ts +35 -0
- package/lib/types/dsh-adapter/migrate/index.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/index.js +107 -0
- package/lib/types/dsh-adapter/migrate/parse/injection.d.ts +40 -0
- package/lib/types/dsh-adapter/migrate/parse/injection.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/parse/injection.js +127 -0
- package/lib/types/dsh-adapter/migrate/parse/jsonl.d.ts +28 -0
- package/lib/types/dsh-adapter/migrate/parse/jsonl.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/parse/jsonl.js +40 -0
- package/lib/types/dsh-adapter/migrate/parse/role-turns.d.ts +28 -0
- package/lib/types/dsh-adapter/migrate/parse/role-turns.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/parse/role-turns.js +50 -0
- package/lib/types/dsh-adapter/migrate/parse/title.d.ts +18 -0
- package/lib/types/dsh-adapter/migrate/parse/title.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/parse/title.js +29 -0
- package/lib/types/dsh-adapter/migrate/parse/tools.d.ts +62 -0
- package/lib/types/dsh-adapter/migrate/parse/tools.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/parse/tools.js +110 -0
- package/lib/types/dsh-adapter/migrate/picker.d.ts +64 -0
- package/lib/types/dsh-adapter/migrate/picker.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/picker.js +86 -0
- package/lib/types/dsh-adapter/migrate/recent-agents.d.ts +40 -0
- package/lib/types/dsh-adapter/migrate/recent-agents.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/recent-agents.js +100 -0
- package/lib/types/dsh-adapter/migrate/sessionize.d.ts +39 -0
- package/lib/types/dsh-adapter/migrate/sessionize.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/sessionize.js +271 -0
- package/lib/types/dsh-adapter/migrate/types.d.ts +176 -0
- package/lib/types/dsh-adapter/migrate/types.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/types.js +11 -0
- package/lib/types/dsh-adapter/migrate/uuid.d.ts +7 -0
- package/lib/types/dsh-adapter/migrate/uuid.d.ts.map +1 -0
- package/lib/types/dsh-adapter/migrate/uuid.js +25 -0
- package/lib/types/dsh-adapter/oauth/bonus.d.ts +27 -0
- package/lib/types/dsh-adapter/oauth/bonus.d.ts.map +1 -0
- package/lib/types/dsh-adapter/oauth/bonus.js +69 -0
- package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/command.d.ts +2 -1
- package/lib/types/dsh-adapter/oauth/command.d.ts.map +1 -0
- package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/command.js +7 -4
- package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/credentials.d.ts +13 -10
- package/lib/types/dsh-adapter/oauth/credentials.d.ts.map +1 -0
- package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/credentials.js +61 -23
- package/lib/types/dsh-adapter/oauth/deepseek.d.ts +39 -0
- package/lib/types/dsh-adapter/oauth/deepseek.d.ts.map +1 -0
- package/lib/types/dsh-adapter/oauth/deepseek.js +189 -0
- package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/index.d.ts +24 -16
- package/lib/types/dsh-adapter/oauth/index.d.ts.map +1 -0
- package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/index.js +34 -20
- package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/interaction.d.ts +14 -12
- package/lib/types/dsh-adapter/oauth/interaction.d.ts.map +1 -0
- package/lib/types/dsh-adapter/oauth/interaction.js +283 -0
- package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/opener.d.ts +2 -1
- package/lib/types/dsh-adapter/oauth/opener.d.ts.map +1 -0
- package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/opener.js +1 -1
- package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/pi-ai.d.ts +10 -3
- package/lib/types/dsh-adapter/oauth/pi-ai.d.ts.map +1 -0
- package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/pi-ai.js +8 -3
- package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/profiles.d.ts +6 -3
- package/lib/types/dsh-adapter/oauth/profiles.d.ts.map +1 -0
- package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/profiles.js +11 -3
- package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/service.d.ts +14 -7
- package/lib/types/dsh-adapter/oauth/service.d.ts.map +1 -0
- package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/service.js +82 -17
- package/lib/types/dsh-adapter/plugin.d.ts +12 -3
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +215 -376
- package/lib/types/dsh-adapter/presets.d.ts +6 -4
- package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
- package/lib/types/dsh-adapter/presets.js +6 -4
- package/lib/types/dsh-adapter/providerWizard.d.ts +17 -0
- package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -1
- package/lib/types/dsh-adapter/providerWizard.js +260 -68
- package/lib/types/dsh-adapter/questions.d.ts +22 -15
- package/lib/types/dsh-adapter/questions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/questions.js +47 -45
- package/lib/types/dsh-adapter/sessions/digest.d.ts +11 -6
- package/lib/types/dsh-adapter/sessions/digest.d.ts.map +1 -1
- package/lib/types/dsh-adapter/sessions/digest.js +20 -13
- package/lib/types/dsh-adapter/sessions/frames.d.ts +3 -1
- package/lib/types/dsh-adapter/sessions/frames.d.ts.map +1 -1
- package/lib/types/dsh-adapter/sessions/frames.js +5 -4
- package/lib/types/dsh-adapter/sessions/list.d.ts +6 -1
- package/lib/types/dsh-adapter/sessions/list.d.ts.map +1 -1
- package/lib/types/dsh-adapter/sessions/list.js +188 -164
- package/lib/types/dsh-adapter/sessions/recovery.d.ts +16 -0
- package/lib/types/dsh-adapter/sessions/recovery.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessions/recovery.js +108 -0
- package/lib/types/dsh-adapter/sessions/snapshot.d.ts +7 -0
- package/lib/types/dsh-adapter/sessions/snapshot.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessions/snapshot.js +90 -0
- package/lib/types/dsh-adapter/sessions/store.d.ts +10 -4
- package/lib/types/dsh-adapter/sessions/store.d.ts.map +1 -1
- package/lib/types/dsh-adapter/sessions/store.js +62 -25
- package/lib/types/dsh-adapter/sideQuestion.d.ts +41 -2
- package/lib/types/dsh-adapter/sideQuestion.d.ts.map +1 -1
- package/lib/types/dsh-adapter/sideQuestion.js +98 -2
- package/lib/types/dsh-adapter/subagents.d.ts +29 -3
- package/lib/types/dsh-adapter/subagents.d.ts.map +1 -1
- package/lib/types/dsh-adapter/subagents.js +87 -3
- package/lib/types/herdr.d.ts +2 -0
- package/lib/types/herdr.d.ts.map +1 -1
- package/lib/types/herdr.js +9 -3
- package/lib/types/i18n.d.ts +598 -60
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +198 -31
- package/lib/types/ink/components/App.d.ts +2 -2
- package/lib/types/ink/components/App.d.ts.map +1 -1
- package/lib/types/ink/components/App.js +15 -2
- package/lib/types/ink/components/Image.d.ts +14 -1
- package/lib/types/ink/components/Image.d.ts.map +1 -1
- package/lib/types/ink/components/Image.js +2 -2
- package/lib/types/ink/events/input-event.js +3 -3
- package/lib/types/ink/hooks/use-terminal-images.d.ts +12 -0
- package/lib/types/ink/hooks/use-terminal-images.d.ts.map +1 -1
- package/lib/types/ink/hooks/use-terminal-images.js +13 -0
- package/lib/types/ink/ink.d.ts +6 -0
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +16 -1
- package/lib/types/ink/kitty-graphics.d.ts +44 -3
- package/lib/types/ink/kitty-graphics.d.ts.map +1 -1
- package/lib/types/ink/kitty-graphics.js +184 -23
- package/lib/types/ink/output.d.ts +24 -2
- package/lib/types/ink/output.d.ts.map +1 -1
- package/lib/types/ink/output.js +83 -5
- package/lib/types/ink/parse-keypress.d.ts +61 -1
- package/lib/types/ink/parse-keypress.d.ts.map +1 -1
- package/lib/types/ink/parse-keypress.js +764 -93
- package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
- package/lib/types/ink/render-node-to-output.js +8 -1
- package/lib/types/ink/screen.d.ts +24 -0
- package/lib/types/ink/screen.d.ts.map +1 -1
- package/lib/types/ink/screen.js +89 -0
- package/lib/types/ink/selection.d.ts +40 -18
- package/lib/types/ink/selection.d.ts.map +1 -1
- package/lib/types/ink/selection.js +142 -30
- package/lib/types/ink/sixel-codec.d.ts +12 -1
- package/lib/types/ink/sixel-codec.d.ts.map +1 -1
- package/lib/types/ink/sixel-codec.js +29 -5
- package/lib/types/ink/sixel-graphics.d.ts.map +1 -1
- package/lib/types/ink/sixel-graphics.js +5 -3
- package/lib/types/ink/styles.d.ts +9 -0
- package/lib/types/ink/styles.d.ts.map +1 -1
- package/lib/types/ink/terminal-image.d.ts +8 -0
- package/lib/types/ink/terminal-image.d.ts.map +1 -1
- package/lib/types/ink/terminal-querier.d.ts +32 -1
- package/lib/types/ink/terminal-querier.d.ts.map +1 -1
- package/lib/types/ink/terminal-querier.js +54 -2
- package/lib/types/ink/timeline-rail.d.ts +5 -0
- package/lib/types/ink/timeline-rail.d.ts.map +1 -1
- package/lib/types/ink/update-overflow-guard.d.ts +24 -0
- package/lib/types/ink/update-overflow-guard.d.ts.map +1 -1
- package/lib/types/ink/update-overflow-guard.js +25 -3
- package/lib/types/math/inline-layout.d.ts +29 -0
- package/lib/types/math/inline-layout.d.ts.map +1 -0
- package/lib/types/math/inline-layout.js +106 -0
- package/lib/types/math/layout.d.ts +58 -0
- package/lib/types/math/layout.d.ts.map +1 -0
- package/lib/types/math/layout.js +59 -0
- package/lib/types/math/renderer.d.ts +64 -0
- package/lib/types/math/renderer.d.ts.map +1 -0
- package/lib/types/math/renderer.js +256 -0
- package/lib/types/minimalUiMode.d.ts +19 -0
- package/lib/types/minimalUiMode.d.ts.map +1 -0
- package/lib/types/minimalUiMode.js +23 -0
- package/lib/types/oauth.d.ts +5 -10
- package/lib/types/oauth.d.ts.map +1 -1
- package/lib/types/oauth.js +5 -10
- package/lib/types/screens/Chat.d.ts +17 -1
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +627 -54
- package/lib/types/screens/SessionSupervisor.d.ts +2 -1
- package/lib/types/screens/SessionSupervisor.d.ts.map +1 -1
- package/lib/types/screens/SessionSupervisor.js +156 -19
- package/lib/types/screens/Settings.d.ts.map +1 -1
- package/lib/types/screens/Settings.js +49 -11
- package/lib/types/screens/StatusLine.d.ts +4 -0
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +57 -27
- package/lib/types/screens/chatOverlay.d.ts +5 -0
- package/lib/types/screens/chatOverlay.d.ts.map +1 -1
- package/lib/types/screens/chatOverlay.js +1 -0
- package/lib/types/screens/sessionSupervisor/ForeignSessionPanes.d.ts +47 -0
- package/lib/types/screens/sessionSupervisor/ForeignSessionPanes.d.ts.map +1 -0
- package/lib/types/screens/sessionSupervisor/ForeignSessionPanes.js +65 -0
- package/lib/types/screens/sessionSupervisor/model.d.ts +12 -0
- package/lib/types/screens/sessionSupervisor/model.d.ts.map +1 -1
- package/lib/types/screens/sessionSupervisor/model.js +26 -0
- package/lib/types/screens/sessionSupervisor/useForeignSessions.d.ts +107 -0
- package/lib/types/screens/sessionSupervisor/useForeignSessions.d.ts.map +1 -0
- package/lib/types/screens/sessionSupervisor/useForeignSessions.js +312 -0
- package/lib/types/screens/sessionSupervisor/useSessionSupervisor.d.ts +4 -1
- package/lib/types/screens/sessionSupervisor/useSessionSupervisor.d.ts.map +1 -1
- package/lib/types/screens/sessionSupervisor/useSessionSupervisor.js +97 -14
- package/lib/types/sessionHistory.d.ts +2 -2
- package/lib/types/sessionHistory.d.ts.map +1 -1
- package/lib/types/sessionHistory.js +31 -5
- package/lib/types/settings/definitions.d.ts +681 -0
- package/lib/types/settings/definitions.d.ts.map +1 -0
- package/lib/types/settings/definitions.js +511 -0
- package/lib/types/starAction.d.ts +44 -0
- package/lib/types/starAction.d.ts.map +1 -0
- package/lib/types/starAction.js +52 -0
- package/lib/types/terminal-utils/latex.d.ts +10 -0
- package/lib/types/terminal-utils/latex.d.ts.map +1 -0
- package/lib/types/terminal-utils/latex.js +1417 -0
- package/lib/types/terminal-utils/markdown.d.ts +3 -1
- package/lib/types/terminal-utils/markdown.d.ts.map +1 -1
- package/lib/types/terminal-utils/markdown.js +26 -1
- package/lib/types/terminal-utils/math.d.ts +66 -0
- package/lib/types/terminal-utils/math.d.ts.map +1 -0
- package/lib/types/terminal-utils/math.js +337 -0
- package/lib/types/tips.d.ts.map +1 -1
- package/lib/types/tips.js +16 -4
- package/lib/types/tuiDisplayPrefs.d.ts +67 -0
- package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
- package/lib/types/tuiDisplayPrefs.js +59 -0
- package/lib/types/ui.d.ts +1 -1
- package/lib/types/ui.d.ts.map +1 -1
- package/lib/types/ui.js +1 -1
- package/lib/types/update.js +2 -2
- package/lib/types/usageStats.d.ts +98 -0
- package/lib/types/usageStats.d.ts.map +1 -0
- package/lib/types/usageStats.js +169 -0
- package/lib/types/utils/bmp.d.ts +17 -0
- package/lib/types/utils/bmp.d.ts.map +1 -0
- package/lib/types/utils/bmp.js +154 -0
- package/lib/types/utils/clipboard.d.ts +14 -2
- package/lib/types/utils/clipboard.d.ts.map +1 -1
- package/lib/types/utils/clipboard.js +143 -8
- package/lib/types/utils/fold-long-lines.d.ts.map +1 -1
- package/lib/types/utils/fold-long-lines.js +2 -1
- package/lib/types/utils/keymap.d.ts +4 -1
- package/lib/types/utils/keymap.d.ts.map +1 -1
- package/lib/types/utils/keymap.js +8 -0
- package/lib/types/utils/modifiers.d.ts +4 -3
- package/lib/types/utils/modifiers.d.ts.map +1 -1
- package/lib/types/utils/modifiers.js +4 -3
- package/lib/types/utils/pastedImagePath.d.ts +8 -0
- package/lib/types/utils/pastedImagePath.d.ts.map +1 -1
- package/lib/types/utils/pastedImagePath.js +29 -5
- package/node_modules/@dsh-tui-vendor/mathjax-tex-svg/LICENSE +202 -0
- package/node_modules/@dsh-tui-vendor/mathjax-tex-svg/NOTICE +20 -0
- package/node_modules/@dsh-tui-vendor/mathjax-tex-svg/index.d.ts +16 -0
- package/node_modules/@dsh-tui-vendor/mathjax-tex-svg/lib/bundled-packages.json +4 -0
- package/node_modules/@dsh-tui-vendor/mathjax-tex-svg/lib/index.mjs +20 -0
- package/node_modules/@dsh-tui-vendor/mathjax-tex-svg/lib/index.mjs.LEGAL.txt +35 -0
- package/node_modules/@dsh-tui-vendor/mathjax-tex-svg/package.json +29 -0
- package/package.json +127 -102
- package/presets/liangshen/agent.cordis.yml +4 -1
- package/presets/liangshen/preset.yml +1 -1
- package/lib/types/minimalMode.d.ts +0 -13
- package/lib/types/minimalMode.d.ts.map +0 -1
- package/lib/types/minimalMode.js +0 -17
- package/node_modules/@deepseek-harness-tui/dsh-auth/LICENSE +0 -21
- package/node_modules/@deepseek-harness-tui/dsh-auth/README.md +0 -143
- package/node_modules/@deepseek-harness-tui/dsh-auth/cordis.patch.yml +0 -11
- package/node_modules/@deepseek-harness-tui/dsh-auth/dsh-plugin.json +0 -44
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.js +0 -193
- package/node_modules/@deepseek-harness-tui/dsh-auth/package.json +0 -69
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* then interprets sequences as keypresses.
|
|
6
6
|
*/
|
|
7
7
|
import { Buffer } from 'buffer';
|
|
8
|
-
import {
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { isCSIFinal, isCSIIntermediate, isCSIParam, PASTE_END, PASTE_START } from './termio/csi.js';
|
|
9
10
|
import { createTokenizer } from './termio/tokenize.js';
|
|
10
11
|
// eslint-disable-next-line no-control-regex
|
|
11
12
|
const META_KEY_CODE_RE = /^(?:\x1b)([a-zA-Z0-9])$/;
|
|
@@ -13,6 +14,11 @@ const META_KEY_CODE_RE = /^(?:\x1b)([a-zA-Z0-9])$/;
|
|
|
13
14
|
const FN_KEY_RE =
|
|
14
15
|
// eslint-disable-next-line no-control-regex
|
|
15
16
|
/^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/;
|
|
17
|
+
// Complete CSI framing, including sequences outside our keyboard vocabulary.
|
|
18
|
+
// Keep their identity so InputEvent's unknown-code guard suppresses protocol
|
|
19
|
+
// bytes rather than stripping ESC and inserting the parameters as text.
|
|
20
|
+
// eslint-disable-next-line no-control-regex
|
|
21
|
+
const COMPLETE_CSI_RE = /^\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]$/;
|
|
16
22
|
// CSI u (kitty keyboard protocol): ESC [ codepoint [; modifier] u
|
|
17
23
|
// Example: ESC[13;2u = Shift+Enter, ESC[27u = Escape (no modifiers)
|
|
18
24
|
// Modifier is optional - when absent, defaults to 1 (no modifiers)
|
|
@@ -44,6 +50,12 @@ const MODIFY_OTHER_KEYS_RE = /^\x1b\[27;(\d+);(\d+)~/;
|
|
|
44
50
|
const WIN32_INPUT_RE = /^\x1b\[([\d;]*)_$/;
|
|
45
51
|
const WIN32_INPUT_TAIL_RE = /\[\d*;\d*;\d*;[01](?:;\d*){0,2}_/g;
|
|
46
52
|
const WIN32_INPUT_TAILS_RE = /^(?:\[\d*;\d*;\d*;[01](?:;\d*){0,2}_)+$/;
|
|
53
|
+
const WIN32_INPUT_PREFIX_RE = /^\x1b\[[\d;]*$/;
|
|
54
|
+
const WIN32_INPUT_BODY_PREFIX_RE = /^\x1b\[\d*;\d*;\d*;[01]?(?:;\d*){0,2}$/;
|
|
55
|
+
// A record fits well within 64 bytes (six INPUT_RECORD integer fields).
|
|
56
|
+
// Bound both memory and the time ambiguous digits can be held after ESC.
|
|
57
|
+
const WIN32_INPUT_MAX_LENGTH = 64;
|
|
58
|
+
const WIN32_INPUT_GRACE_MS = 1000;
|
|
47
59
|
// Prefix of a fragmenting SGR mouse report (`[<btn;col;rowM/m`). ConPTY can
|
|
48
60
|
// split one report across multiple stdin reads; when App's 50ms escape timer
|
|
49
61
|
// fires between the fragments, the pieces stop being part of one buffered
|
|
@@ -74,6 +86,14 @@ const SGR_MOUSE_TAIL_PREFIX_RE = /^\[<\d+;\d+;\d+[Mm]/;
|
|
|
74
86
|
// the deadline bounds this ambiguity. It runs on every call so continuous
|
|
75
87
|
// input cannot starve it into a de-facto immortal hold.
|
|
76
88
|
const MOUSE_TAIL_HOLD_GRACE_MS = 1000;
|
|
89
|
+
// How long an unfinished terminal-reply prefix may be held for its
|
|
90
|
+
// continuation, and how many bytes of it may accumulate. Same caliber as the
|
|
91
|
+
// win32 record hold above: measured from FIRST capture (later flushes must not
|
|
92
|
+
// renew it), deadline checked on every parse call, and an over-limit or
|
|
93
|
+
// expired hold is released by tokenizer.reset() — protocol bytes are dropped
|
|
94
|
+
// rather than leaked into the prompt.
|
|
95
|
+
const TERMINAL_RESPONSE_TAIL_GRACE_MS = 1000;
|
|
96
|
+
const TERMINAL_RESPONSE_MAX_LENGTH = 64;
|
|
77
97
|
// dwControlKeyState modifier bits (others — NUMLOCK_ON 0x20, CAPSLOCK_ON
|
|
78
98
|
// 0x80, ENHANCED_KEY 0x100 — are state indicators, not pressed modifiers)
|
|
79
99
|
const WIN32_CS_ALT = 0x01 | 0x02;
|
|
@@ -165,7 +185,54 @@ const XTVERSION_RE = /^\x1bP>\|(.*?)(?:\x07|\x1b\\)$/s;
|
|
|
165
185
|
// Button 32=left-drag (0x20 | motion-bit). Plain 0/1/2 = left/mid/right click.
|
|
166
186
|
// eslint-disable-next-line no-control-regex
|
|
167
187
|
const SGR_MOUSE_RE = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/;
|
|
188
|
+
/**
|
|
189
|
+
* Terminal control sequences that must never survive into a paste payload.
|
|
190
|
+
*
|
|
191
|
+
* A desktop file DROP reaches the TUI as terminal bytes, not as text: Windows
|
|
192
|
+
* Terminal / OpenConsole hand a dropped path over as an OSC 8 hyperlink
|
|
193
|
+
* (`ESC ] 8 ; <params> ; file:///… ST`), and a `DECSET 9001` (win32-input-mode)
|
|
194
|
+
* host additionally decomposes that payload into per-character key records
|
|
195
|
+
* (see the decomposed-paste matcher below). The hyperlink itself is RESTORED
|
|
196
|
+
* to its local path before this hygiene runs (see `createPasteKey` and the
|
|
197
|
+
* OSC 8 block below); what is stripped here is the rest of the protocol
|
|
198
|
+
* shapes a paste payload can carry.
|
|
199
|
+
*
|
|
200
|
+
* Scope (DESIGN D3, narrowed by measurement): COMPLETE OSC sequences only.
|
|
201
|
+
* CSI sequences, bare ESC bytes and C0 control bytes are deliberately KEPT —
|
|
202
|
+
* a bracketed paste's payload is user data. PR #1142 pins that protocol-shaped
|
|
203
|
+
* literal text (`ESC[1;2;3;1A`) survives byte-for-byte
|
|
204
|
+
* (`scripts/verify-win32-input.tsx`), and the whole C0 band belongs to the
|
|
205
|
+
* COMPOSER: every single-line paste ingress flattens C0/C1 to a SPACE
|
|
206
|
+
* (`flattenPasteInline`), so a C0 byte dropped here does not sanitize
|
|
207
|
+
* anything — it deletes the space the composer owes
|
|
208
|
+
* (`scripts/verify-question-paste.tsx` 1a/1c pin that flattening, TAB/CR/LF
|
|
209
|
+
* and DEL included). The wide strip (CSI + residual ESC) proved mutually
|
|
210
|
+
* exclusive with the #1142 contract.
|
|
211
|
+
*/
|
|
212
|
+
// eslint-disable-next-line no-control-regex -- deliberate: paste payloads carry terminal sequences
|
|
213
|
+
const OSC_IN_PASTE = /\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/gu;
|
|
214
|
+
/**
|
|
215
|
+
* Strip the protocol frames one paste payload can carry — see
|
|
216
|
+
* {@link OSC_IN_PASTE} for the exact scope.
|
|
217
|
+
*
|
|
218
|
+
* This is the LAST step of the paste choke point, and it is deliberately the
|
|
219
|
+
* only one: no C0/C1 byte is removed here, because removal is lossy where the
|
|
220
|
+
* composer is not (it flattens those bytes to spaces). The registration that
|
|
221
|
+
* briefly added an inert-C0 strip here was the CI regression that
|
|
222
|
+
* `scripts/verify-question-paste.tsx` 1a/1c caught; `verify-paste-drop`
|
|
223
|
+
* re-pins the surviving side of that contract.
|
|
224
|
+
*/
|
|
225
|
+
function cleanPastePayload(content) {
|
|
226
|
+
return content.replace(OSC_IN_PASTE, '');
|
|
227
|
+
}
|
|
168
228
|
function createPasteKey(content) {
|
|
229
|
+
// Order is a contract (D6/D1): a complete OSC 8 drop frame is RESTORED to
|
|
230
|
+
// its local path BEFORE the payload hygiene — hygiene strips the very OSC
|
|
231
|
+
// frame the drop is encoded in. Every paste path goes through this ONE
|
|
232
|
+
// choke point (VT bracketed paste, the decomposed win32 paste, and both
|
|
233
|
+
// flush paths), so no caller has to remember the order.
|
|
234
|
+
const dropPath = osc8DropPath(content);
|
|
235
|
+
const text = dropPath === null ? cleanPastePayload(content) : pastePayloadForPath(dropPath);
|
|
169
236
|
return {
|
|
170
237
|
kind: 'key',
|
|
171
238
|
name: '',
|
|
@@ -175,11 +242,143 @@ function createPasteKey(content) {
|
|
|
175
242
|
shift: false,
|
|
176
243
|
option: false,
|
|
177
244
|
super: false,
|
|
178
|
-
sequence:
|
|
179
|
-
raw:
|
|
245
|
+
sequence: text,
|
|
246
|
+
raw: text,
|
|
180
247
|
isPasted: true,
|
|
181
248
|
};
|
|
182
249
|
}
|
|
250
|
+
// -- OSC 8 drop frames -------------------------------------------------------
|
|
251
|
+
//
|
|
252
|
+
// Windows hands a dropped file over as an OSC 8 hyperlink:
|
|
253
|
+
//
|
|
254
|
+
// ESC ] 8 ; <params> ; <URI> ( BEL | ST )
|
|
255
|
+
//
|
|
256
|
+
// `params` is the hyperlink attribute list (`id=16:42`, or empty for the form
|
|
257
|
+
// this TUI itself emits) and the URI is everything after the SECOND `;` — a
|
|
258
|
+
// URI may legally contain `;`, params may not.
|
|
259
|
+
//
|
|
260
|
+
// A frame is claimed in the fixed decision order (D6) by
|
|
261
|
+
// `claimProtocolSequence`, and — when the frame set was already reassembled
|
|
262
|
+
// into one paste body (win32 decomposed paste / VT bracketed paste) — by
|
|
263
|
+
// `createPasteKey` before the payload hygiene runs.
|
|
264
|
+
/** OSC 8 introducer: `ESC ] 8 ;`. */
|
|
265
|
+
const OSC8_FRAME_PREFIX = '\x1b]8;';
|
|
266
|
+
/**
|
|
267
|
+
* Longest OSC 8 frame the record accumulator will hold. A drop URI is a local
|
|
268
|
+
* path plus percent-encoding, and the composer's own pasted-path cap is 4096
|
|
269
|
+
* chars (`MAX_PASTED_PATH_CHARS`), so anything beyond this cannot become a
|
|
270
|
+
* usable path and must not extend a hold indefinitely.
|
|
271
|
+
*/
|
|
272
|
+
const OSC8_FRAME_MAX_LENGTH = 4096;
|
|
273
|
+
/**
|
|
274
|
+
* Read one OSC 8 frame at `offset`. Returns null when the bytes there are not
|
|
275
|
+
* an OSC 8 frame, or when the URI is followed by anything other than a frame
|
|
276
|
+
* terminator (BEL 0x07 / ST `ESC \`) or the start of the next frame.
|
|
277
|
+
*
|
|
278
|
+
* The "next frame" shape matters because the win32 record translator drops
|
|
279
|
+
* BEL outright (a synthesized Uc=7 record carries no key meaning), so a
|
|
280
|
+
* decomposed drop can lose its BEL terminator before the payload is
|
|
281
|
+
* reassembled. A lone frame with neither a terminator nor a successor stays
|
|
282
|
+
* unclaimed: the tail of a truncated stream must not be read as a path.
|
|
283
|
+
*/
|
|
284
|
+
function readOsc8Frame(payload, offset) {
|
|
285
|
+
if (!payload.startsWith(OSC8_FRAME_PREFIX, offset))
|
|
286
|
+
return null;
|
|
287
|
+
const paramsEnd = payload.indexOf(';', offset + OSC8_FRAME_PREFIX.length);
|
|
288
|
+
if (paramsEnd === -1)
|
|
289
|
+
return null;
|
|
290
|
+
const uriStart = paramsEnd + 1;
|
|
291
|
+
let uriEnd = uriStart;
|
|
292
|
+
while (uriEnd < payload.length && payload[uriEnd] !== '\u0007' && payload[uriEnd] !== '\x1b') {
|
|
293
|
+
uriEnd++;
|
|
294
|
+
}
|
|
295
|
+
const uri = payload.slice(uriStart, uriEnd);
|
|
296
|
+
if (payload[uriEnd] === '\u0007')
|
|
297
|
+
return { uri, end: uriEnd + 1 };
|
|
298
|
+
if (payload.startsWith('\x1b\\', uriEnd))
|
|
299
|
+
return { uri, end: uriEnd + 2 };
|
|
300
|
+
if (payload.startsWith(OSC8_FRAME_PREFIX, uriEnd))
|
|
301
|
+
return { uri, end: uriEnd };
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* The local path a payload restores to, or null when the payload is NOT a
|
|
306
|
+
* pure OSC 8 frame set or its URI is not a decodable LOCAL file URL. Called
|
|
307
|
+
* with one complete sequence (the decision chain) or with a whole reassembled
|
|
308
|
+
* paste body (the choke point).
|
|
309
|
+
*
|
|
310
|
+
* Fail-closed rules (DESIGN D4 / AC-4):
|
|
311
|
+
* - only `file://` URIs are restored — other schemes fall through to the
|
|
312
|
+
* normal response/prose handling;
|
|
313
|
+
* - a URI carrying whitespace is multiple tokens, not one path, and is
|
|
314
|
+
* refused (the same conservative contract as `parsePastedImagePath`);
|
|
315
|
+
* - more than one distinct URI is a multi-file drop, which v1 does not
|
|
316
|
+
* cover: fail closed rather than silently keep only the first file;
|
|
317
|
+
* - a remote authority (`file://server/share/…`) or a decode failure is
|
|
318
|
+
* never silently reinterpreted as a local path;
|
|
319
|
+
* - a payload with any trailing non-OSC-8 bytes is NOT a drop and is left to
|
|
320
|
+
* the hygiene path, so prose that merely quotes a link keeps its text.
|
|
321
|
+
*/
|
|
322
|
+
function osc8DropPath(payload) {
|
|
323
|
+
let offset = 0;
|
|
324
|
+
let uri;
|
|
325
|
+
while (offset < payload.length) {
|
|
326
|
+
const frame = readOsc8Frame(payload, offset);
|
|
327
|
+
if (frame === null || frame.end <= offset)
|
|
328
|
+
return null;
|
|
329
|
+
if (frame.uri !== '') {
|
|
330
|
+
if (uri !== undefined && uri !== frame.uri)
|
|
331
|
+
return null;
|
|
332
|
+
uri = frame.uri;
|
|
333
|
+
}
|
|
334
|
+
offset = frame.end;
|
|
335
|
+
}
|
|
336
|
+
if (uri === undefined || /\s/u.test(uri))
|
|
337
|
+
return null;
|
|
338
|
+
return localPathOfFileUri(uri);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* `file://` URI → local path (percent-escapes decoded by `fileURLToPath`), or
|
|
342
|
+
* null when the URI is not a local file URL or decoding fails.
|
|
343
|
+
*/
|
|
344
|
+
function localPathOfFileUri(uri) {
|
|
345
|
+
if (!/^file:\/\//iu.test(uri))
|
|
346
|
+
return null;
|
|
347
|
+
let url;
|
|
348
|
+
try {
|
|
349
|
+
url = new URL(uri);
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
// A non-empty authority is a remote share (UNC): refuse it instead of
|
|
355
|
+
// letting `fileURLToPath` reinterpret it as a local path on hosts that
|
|
356
|
+
// ignore the authority. `localhost` is this machine.
|
|
357
|
+
if (url.host !== '' && url.host !== 'localhost')
|
|
358
|
+
return null;
|
|
359
|
+
try {
|
|
360
|
+
return fileURLToPath(url);
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* The paste payload for a restored drop path. `parsePastedImagePath` accepts
|
|
368
|
+
* exactly one whitespace-free token, or the whole payload quoted — and a drop
|
|
369
|
+
* is ONE unambiguous file, so a path containing whitespace is handed over in
|
|
370
|
+
* the quoted single-token form the composer already documents
|
|
371
|
+
* (`@"my dir/a.ts"` is the mention spelling of the same shape). A path no
|
|
372
|
+
* quote can represent (a literal `"`) stays bare: the composer then inserts
|
|
373
|
+
* it verbatim, which is the pre-existing fail-closed behaviour for ambiguous
|
|
374
|
+
* tokens. Windows paths cannot contain `"`, so on this platform the quoted
|
|
375
|
+
* form is exactly the decoded path.
|
|
376
|
+
*/
|
|
377
|
+
function pastePayloadForPath(path) {
|
|
378
|
+
if (!/[\s"]/u.test(path))
|
|
379
|
+
return path;
|
|
380
|
+
return path.includes('"') ? path : `"${path}"`;
|
|
381
|
+
}
|
|
183
382
|
/** DECRPM status values (response to DECRQM) */
|
|
184
383
|
export const DECRPM_STATUS = {
|
|
185
384
|
NOT_RECOGNIZED: 0,
|
|
@@ -188,6 +387,23 @@ export const DECRPM_STATUS = {
|
|
|
188
387
|
PERMANENTLY_SET: 3,
|
|
189
388
|
PERMANENTLY_RESET: 4,
|
|
190
389
|
};
|
|
390
|
+
// Unfinished prefixes of every reply shape parseTerminalResponse() knows.
|
|
391
|
+
// MUST be kept in sync with the pattern list above: a reply type added there
|
|
392
|
+
// needs an alternative here too, or a ConPTY-split tail of that shape is
|
|
393
|
+
// released at the flush and leaks into the prompt as literal text.
|
|
394
|
+
// ESC [ ? … DA1 / kitty flags / DECXCPR / DECRPM
|
|
395
|
+
// ESC [ ? … $ DECRPM's `$y` intermediate byte
|
|
396
|
+
// ESC [ > … DA2
|
|
397
|
+
// ESC [ digits [; digits] DSR (row;col) and XTWINOPS pixel-size replies
|
|
398
|
+
// ESC [ … $ a `$`-intermediate CSI awaiting its final byte
|
|
399
|
+
// ESC P > | … XTVERSION (DCS)
|
|
400
|
+
// Only PREFIXES are matched; completeness is always decided by
|
|
401
|
+
// parseTerminalResponse(), never by this regex. Introducer-only prefixes
|
|
402
|
+
// (`ESC [`, `ESC P`) are deliberately excluded: they are indistinguishable
|
|
403
|
+
// from ordinary key input, and holding the bare `ESC [` swallowed
|
|
404
|
+
// `ESC[`+letter input in the #1073 review.
|
|
405
|
+
// eslint-disable-next-line no-control-regex
|
|
406
|
+
const TERMINAL_RESPONSE_PREFIX_RE = /^(?:\x1b\[\?[\d;]*\$?|\x1b\[>[\d;]*|\x1b\[[\d;]*\$|\x1b\[\d+(?:;\d+)*|\x1bP>\|)/;
|
|
191
407
|
/**
|
|
192
408
|
* Try to recognize a sequence token as a terminal response.
|
|
193
409
|
* Returns null if the sequence is not a known response pattern
|
|
@@ -266,6 +482,90 @@ function parseTerminalResponse(s) {
|
|
|
266
482
|
}
|
|
267
483
|
return null;
|
|
268
484
|
}
|
|
485
|
+
// Unfinished prefixes of the reply shapes parseTerminalResponse() accepts,
|
|
486
|
+
// paired with the response type they can still complete into. This is the
|
|
487
|
+
// shape half of the claim gate: a held reply fragment is only joined to its
|
|
488
|
+
// continuation while one of these types is actually expected by a pending
|
|
489
|
+
// query (terminalExpectedResponseTypes). The patterns are deliberately
|
|
490
|
+
// tighter than TERMINAL_RESPONSE_PREFIX_RE: `?` + two params can still grow
|
|
491
|
+
// into a DA1 but never into a DECRPM (`?mode;status$y` has exactly two
|
|
492
|
+
// params), so a DA1-shaped fragment is released as literal text as soon as
|
|
493
|
+
// only DECRPM is in flight. `parseTerminalResponse` remains the only
|
|
494
|
+
// authority on completeness.
|
|
495
|
+
// eslint-disable-next-line no-control-regex
|
|
496
|
+
const INCOMPLETE_RESPONSE_PREFIXES = [
|
|
497
|
+
// DA1: any `?`-parameter list (including the bare introducer).
|
|
498
|
+
['da1', /^\x1b\[\?[\d;]*$/],
|
|
499
|
+
// DECRPM: `?mode;status` before the `$` intermediate, then the same before
|
|
500
|
+
// `$` + final.
|
|
501
|
+
['decrpm', /^\x1b\[\?\d*(?:;\d*)?$/],
|
|
502
|
+
['decrpm', /^\x1b\[\?\d+;\d+\$$/],
|
|
503
|
+
// Kitty flags and DECXCPR: one `?` + at most two numeric params.
|
|
504
|
+
['kittyKeyboard', /^\x1b\[\?\d*$/],
|
|
505
|
+
['cursorPosition', /^\x1b\[\?\d*(?:;\d*)?$/],
|
|
506
|
+
// DA2: any `>`-parameter list.
|
|
507
|
+
['da2', /^\x1b\[>[\d;]*$/],
|
|
508
|
+
// XTWINOPS: `6;h;w t` / `4;h;w t`, up to the final byte.
|
|
509
|
+
['terminalPixelSize', /^\x1b\[[46](?:;\d*){0,2}$/],
|
|
510
|
+
// XTVERSION: `P>` can still gain `|` and a payload; ST's ESC may flush.
|
|
511
|
+
['xtversion', /^\x1bP>(?:\|[^\x1b]*(?:\x1b)?)?$/],
|
|
512
|
+
];
|
|
513
|
+
/**
|
|
514
|
+
* Response types an unfinished reply-shaped candidate could still complete
|
|
515
|
+
* into, judged from its introducer and parameter bytes alone. Empty means
|
|
516
|
+
* the bytes cannot become a known reply (either a final byte already ended a
|
|
517
|
+
* different sequence, or the shape is unknown) and must stay literal.
|
|
518
|
+
* @param candidate - the inbound bytes, with the ESC introducer synthesized.
|
|
519
|
+
* @returns the types a continuation could still produce.
|
|
520
|
+
*/
|
|
521
|
+
function possibleResponseTypes(candidate) {
|
|
522
|
+
const types = new Set();
|
|
523
|
+
for (const [type, pattern] of INCOMPLETE_RESPONSE_PREFIXES) {
|
|
524
|
+
if (pattern.test(candidate))
|
|
525
|
+
types.add(type);
|
|
526
|
+
}
|
|
527
|
+
return types;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Whether an unfinished candidate can still become a response the host is
|
|
531
|
+
* currently waiting for. Both halves are required: an in-flight query of the
|
|
532
|
+
* matching type, and a shape that can still produce it.
|
|
533
|
+
*/
|
|
534
|
+
function expectsResponseType(candidate, expected) {
|
|
535
|
+
for (const type of possibleResponseTypes(candidate)) {
|
|
536
|
+
if (expected.has(type))
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Find the shortest complete terminal response at the head of a post-flush
|
|
543
|
+
* tail whose introducer ESC was already flushed as a key. The caller
|
|
544
|
+
* synthesizes the ESC back; `parseTerminalResponse` decides completeness, so
|
|
545
|
+
* only a type the host expects is claimed and trailing bytes are left for
|
|
546
|
+
* ordinary parsing.
|
|
547
|
+
* @param tail - fragment without its ESC introducer.
|
|
548
|
+
* @param expected - response types of the queries still awaiting a reply.
|
|
549
|
+
* @returns the claimed response and how many bytes of `tail` it consumed, or
|
|
550
|
+
* undefined when no expected reply completes at the head.
|
|
551
|
+
*/
|
|
552
|
+
function claimExpectedResponseHead(tail, expected) {
|
|
553
|
+
const limit = Math.min(tail.length, TERMINAL_RESPONSE_MAX_LENGTH);
|
|
554
|
+
for (let consumed = 2; consumed <= limit; consumed++) {
|
|
555
|
+
const head = '\x1b' + tail.slice(0, consumed);
|
|
556
|
+
const response = parseTerminalResponse(head);
|
|
557
|
+
if (response) {
|
|
558
|
+
return expected.has(response.type)
|
|
559
|
+
? { response, consumed }
|
|
560
|
+
: undefined;
|
|
561
|
+
}
|
|
562
|
+
// A head that can no longer become an expected reply ends the scan:
|
|
563
|
+
// longer candidates extend a shape that is already closed or unknown.
|
|
564
|
+
if (!expectsResponseType(head, expected))
|
|
565
|
+
return undefined;
|
|
566
|
+
}
|
|
567
|
+
return undefined;
|
|
568
|
+
}
|
|
269
569
|
function splitNumericParams(params) {
|
|
270
570
|
if (!params)
|
|
271
571
|
return [];
|
|
@@ -517,6 +817,42 @@ function win32RecordChar(key) {
|
|
|
517
817
|
return key.sequence === key.name ? key.name : undefined;
|
|
518
818
|
}
|
|
519
819
|
}
|
|
820
|
+
/**
|
|
821
|
+
* Uc field of the raw win32 record behind `key`, or undefined when the key
|
|
822
|
+
* did not come from one (the decomposed stream also carries plain keys).
|
|
823
|
+
*/
|
|
824
|
+
function win32RecordUc(key) {
|
|
825
|
+
const match = WIN32_INPUT_RE.exec(key.raw ?? '');
|
|
826
|
+
if (!match)
|
|
827
|
+
return undefined;
|
|
828
|
+
const field = match[1].split(';')[2];
|
|
829
|
+
return field === undefined || field === '' ? 0 : parseInt(field, 10);
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Append one paste-body key to the decomposed-paste buffer. Classic conhost
|
|
833
|
+
* spells a pasted CRLF break as two records — CR (Uc=13) then LF (Uc=10) —
|
|
834
|
+
* and the LF must fold into the CR's newline instead of appending a second
|
|
835
|
+
* one (issue #1090). Only a CR record arms the fold, so LF-only text, a lone
|
|
836
|
+
* CR, and ordinary characters (including a real `_`) keep their bytes.
|
|
837
|
+
*/
|
|
838
|
+
function appendWin32PasteChar(state, key) {
|
|
839
|
+
const ch = win32RecordChar(key);
|
|
840
|
+
if (ch === undefined)
|
|
841
|
+
return;
|
|
842
|
+
if (ch !== '\n') {
|
|
843
|
+
state.lastWasCarriageReturn = false;
|
|
844
|
+
state.buffer += ch;
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
const uc = win32RecordUc(key);
|
|
848
|
+
if (uc === 10 && state.lastWasCarriageReturn) {
|
|
849
|
+
// LF record closing the CRLF pair: the CR already emitted the newline.
|
|
850
|
+
state.lastWasCarriageReturn = false;
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
state.lastWasCarriageReturn = uc === 13;
|
|
854
|
+
state.buffer += '\n';
|
|
855
|
+
}
|
|
520
856
|
/**
|
|
521
857
|
* Feed one translated win32 key through the decomposed-paste matcher.
|
|
522
858
|
* Returns the keys to emit (empty while holding a candidate prefix or
|
|
@@ -537,12 +873,14 @@ function feedWin32Paste(state, key) {
|
|
|
537
873
|
if (!state.active) {
|
|
538
874
|
state.active = true;
|
|
539
875
|
state.buffer = '';
|
|
876
|
+
state.lastWasCarriageReturn = false;
|
|
540
877
|
return [];
|
|
541
878
|
}
|
|
542
879
|
// End marker complete: the whole paste as a single event.
|
|
543
880
|
const paste = createPasteKey(state.buffer);
|
|
544
881
|
state.active = false;
|
|
545
882
|
state.buffer = '';
|
|
883
|
+
state.lastWasCarriageReturn = false;
|
|
546
884
|
return [paste];
|
|
547
885
|
}
|
|
548
886
|
return [];
|
|
@@ -556,13 +894,13 @@ function feedWin32Paste(state, key) {
|
|
|
556
894
|
state.matched = 0;
|
|
557
895
|
if (state.active) {
|
|
558
896
|
for (const k of held)
|
|
559
|
-
state
|
|
897
|
+
appendWin32PasteChar(state, k);
|
|
560
898
|
return feedWin32Paste(state, key);
|
|
561
899
|
}
|
|
562
900
|
return [...held, ...feedWin32Paste(state, key)];
|
|
563
901
|
}
|
|
564
902
|
if (state.active) {
|
|
565
|
-
state
|
|
903
|
+
appendWin32PasteChar(state, key);
|
|
566
904
|
return [];
|
|
567
905
|
}
|
|
568
906
|
return [key];
|
|
@@ -581,50 +919,85 @@ function synthesizedWin32Char(key) {
|
|
|
581
919
|
}
|
|
582
920
|
return win32RecordChar(key);
|
|
583
921
|
}
|
|
584
|
-
|
|
922
|
+
/**
|
|
923
|
+
* The ONE decision chain for a complete reassembled terminal sequence. The
|
|
924
|
+
* order is a contract (DESIGN D6) and must not be permuted:
|
|
925
|
+
*
|
|
926
|
+
* 1. OSC 8 drop frame — a dropped file's hyperlink. It must win over the
|
|
927
|
+
* OSC response shape in step 2, which also matches `ESC ] 8 ; …` and
|
|
928
|
+
* used to swallow the whole link as a bogus terminal reply (#1067).
|
|
929
|
+
* 2. terminal response — DECRPM/DA/OSC replies to our own queries
|
|
930
|
+
* (#1177's type-bound claiming).
|
|
931
|
+
* 3. mouse report — SGR (1006), with the X10 report as the legacy
|
|
932
|
+
* compatibility fallback.
|
|
933
|
+
*
|
|
934
|
+
* A sequence that falls through all three is a record body or a keypress, and
|
|
935
|
+
* only the caller can tell which (`parseReassembledWin32Protocol` for records,
|
|
936
|
+
* the token loop for VT input), so that dispatch stays with the caller.
|
|
937
|
+
*/
|
|
938
|
+
function claimProtocolSequence(sequence) {
|
|
939
|
+
const dropPath = osc8DropPath(sequence);
|
|
940
|
+
if (dropPath !== null) {
|
|
941
|
+
// Hand the local path to the ONE paste choke point (D2), so the existing
|
|
942
|
+
// image stage / `@` reference pipeline sees a normal paste.
|
|
943
|
+
return createPasteKey(pastePayloadForPath(dropPath));
|
|
944
|
+
}
|
|
585
945
|
const response = parseTerminalResponse(sequence);
|
|
586
946
|
if (response)
|
|
587
947
|
return { kind: 'response', sequence, response };
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
948
|
+
return parseMouseEvent(sequence) ?? parseX10MouseEvent(sequence);
|
|
949
|
+
}
|
|
950
|
+
function parseReassembledWin32Protocol(sequence) {
|
|
951
|
+
// Fixed decision order (D6) — see claimProtocolSequence.
|
|
952
|
+
const claimed = claimProtocolSequence(sequence);
|
|
953
|
+
if (claimed)
|
|
954
|
+
return claimed;
|
|
591
955
|
if (SGR_MOUSE_RE.test(sequence) || (sequence.length === 6 && sequence.startsWith('\x1b[M'))) {
|
|
592
956
|
return parseKeypress(sequence);
|
|
593
957
|
}
|
|
594
958
|
return null;
|
|
595
959
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
960
|
+
/** Release every key held for the current candidate sequence. */
|
|
961
|
+
function releaseWin32ProtocolHold(state) {
|
|
962
|
+
const held = state.held;
|
|
963
|
+
state.held = [];
|
|
964
|
+
state.sequence = '';
|
|
965
|
+
return held;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Resolve one accumulated protocol candidate. An array is the dispatch result
|
|
969
|
+
* (empty while the frame is still incomplete); null means the bytes are not a
|
|
970
|
+
* protocol shape we may hold, and the caller releases its keys.
|
|
971
|
+
*
|
|
972
|
+
* Order and bounds are the existing contract (#1142/#1177) with the OSC 8
|
|
973
|
+
* drop frame (D1) added FIRST: under win32-input-mode a drop can arrive as
|
|
974
|
+
* per-character synthesized records without bracketed-paste markers, so the
|
|
975
|
+
* hyperlink has to be reassembled here, ahead of any response/mouse claim.
|
|
976
|
+
* Holding past the introducer also keeps it away from
|
|
977
|
+
* `parseTerminalResponse`, whose OSC shape would otherwise take
|
|
978
|
+
* `ESC ] 8 ; …` as a bogus reply and swallow the dropped path (#1067).
|
|
979
|
+
* The OSC terminator is BEL or ST with its own length cap: a BEL record has
|
|
980
|
+
* no key meaning and is dropped by the translator, so a decomposed frame can
|
|
981
|
+
* lose its BEL — such a frame is released by the flush bound like any other
|
|
982
|
+
* abandoned sequence.
|
|
983
|
+
*/
|
|
984
|
+
function resolveWin32ProtocolCandidate(state, sequence, ch) {
|
|
614
985
|
if (sequence === '\x1b' || sequence === '\x1b[')
|
|
615
986
|
return [];
|
|
616
|
-
if (
|
|
617
|
-
const
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
987
|
+
if (sequence.startsWith('\x1b]')) {
|
|
988
|
+
const terminated = sequence.endsWith('\u0007') || sequence.endsWith('\x1b\\');
|
|
989
|
+
if (!terminated)
|
|
990
|
+
return sequence.length <= OSC8_FRAME_MAX_LENGTH ? [] : null;
|
|
991
|
+
const held = releaseWin32ProtocolHold(state);
|
|
992
|
+
const parsed = parseReassembledWin32Protocol(sequence);
|
|
993
|
+
return parsed ? [parsed] : held;
|
|
621
994
|
}
|
|
995
|
+
if (!sequence.startsWith('\x1b[') || sequence.length > 64)
|
|
996
|
+
return null;
|
|
622
997
|
if (sequence.startsWith('\x1b[M')) {
|
|
623
998
|
if (sequence.length < 6)
|
|
624
999
|
return [];
|
|
625
|
-
const held = state
|
|
626
|
-
state.held = [];
|
|
627
|
-
state.sequence = '';
|
|
1000
|
+
const held = releaseWin32ProtocolHold(state);
|
|
628
1001
|
const parsed = sequence.length === 6
|
|
629
1002
|
? parseReassembledWin32Protocol(sequence)
|
|
630
1003
|
: null;
|
|
@@ -634,16 +1007,29 @@ function feedWin32Protocol(state, key) {
|
|
|
634
1007
|
if ((code >= 0x20 && code <= 0x3f))
|
|
635
1008
|
return [];
|
|
636
1009
|
if (code >= 0x40 && code <= 0x7e) {
|
|
637
|
-
const held = state
|
|
638
|
-
state.held = [];
|
|
639
|
-
state.sequence = '';
|
|
1010
|
+
const held = releaseWin32ProtocolHold(state);
|
|
640
1011
|
const parsed = parseReassembledWin32Protocol(sequence);
|
|
641
1012
|
return parsed ? [parsed] : held;
|
|
642
1013
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
function feedWin32Protocol(state, key) {
|
|
1017
|
+
const ch = synthesizedWin32Char(key);
|
|
1018
|
+
if (state.held.length === 0) {
|
|
1019
|
+
if (ch !== '\x1b')
|
|
1020
|
+
return [key];
|
|
1021
|
+
state.held.push(key);
|
|
1022
|
+
state.sequence = ch;
|
|
1023
|
+
return [];
|
|
1024
|
+
}
|
|
1025
|
+
if (ch === undefined) {
|
|
1026
|
+
const held = releaseWin32ProtocolHold(state);
|
|
1027
|
+
return [...held, ...feedWin32Protocol(state, key)];
|
|
1028
|
+
}
|
|
1029
|
+
state.held.push(key);
|
|
1030
|
+
state.sequence += ch;
|
|
1031
|
+
return (resolveWin32ProtocolCandidate(state, state.sequence, ch) ??
|
|
1032
|
+
releaseWin32ProtocolHold(state));
|
|
647
1033
|
}
|
|
648
1034
|
function feedWin32Input(pasteState, protocolState, key) {
|
|
649
1035
|
const inputs = [];
|
|
@@ -683,25 +1069,149 @@ function inputToString(input) {
|
|
|
683
1069
|
* Tokenize and parse a chunk of terminal input into parsed keys, mouse
|
|
684
1070
|
* events, and terminal responses, maintaining paste-mode state.
|
|
685
1071
|
* @param prevState - the state returned by the previous call, or INITIAL_STATE.
|
|
686
|
-
* @param input - the input chunk; null
|
|
1072
|
+
* @param input - the input chunk; null applies the escape timeout (in-flight
|
|
1073
|
+
* win32 records keep their bounded recovery grace).
|
|
687
1074
|
* @returns the parsed inputs plus the state to pass to the next call.
|
|
688
1075
|
*/
|
|
689
1076
|
export function parseMultipleKeypresses(prevState, input = '') {
|
|
690
1077
|
const isFlush = input === null;
|
|
691
|
-
|
|
1078
|
+
let inputString = isFlush ? '' : inputToString(input);
|
|
692
1079
|
// Get or create tokenizer
|
|
693
1080
|
const tokenizer = prevState._tokenizer ?? createTokenizer({
|
|
694
1081
|
x10Mouse: true,
|
|
695
1082
|
splitInputControls: true,
|
|
696
1083
|
});
|
|
697
|
-
//
|
|
698
|
-
|
|
1084
|
+
// Keep record framing in the tokenizer, rather than converting a timed-out
|
|
1085
|
+
// prefix into a key and trying to scrub its text later (#827). The host
|
|
1086
|
+
// capability flag only says the platform MIGHT run the private mode; only a
|
|
1087
|
+
// decoded record proves it does, so the two stay separate: a frame whose
|
|
1088
|
+
// own shape is already record-specific holds on its own (that is how even
|
|
1089
|
+
// the first record survives a split), and anything else needs an open gate
|
|
1090
|
+
// plus a lit parser. A lone ESC therefore keeps its 50ms behavior on hosts
|
|
1091
|
+
// that ignore DECSET 9001, and bracketed-paste payloads remain literal.
|
|
1092
|
+
let win32InputMode = prevState.win32InputMode ?? false;
|
|
1093
|
+
let win32InputStartedAt = prevState.win32InputStartedAt;
|
|
1094
|
+
let win32EscFlushedAt = prevState.win32EscFlushedAt;
|
|
1095
|
+
let inPaste = prevState.mode === 'IN_PASTE';
|
|
1096
|
+
const now = Date.now();
|
|
1097
|
+
// Gate × evidence. The nullish default keeps direct callers that never
|
|
1098
|
+
// inject the flag on the pre-gate behavior.
|
|
1099
|
+
const win32HoldAllowed = (prevState.win32Capable ?? true) && win32InputMode;
|
|
1100
|
+
const isRecordPrefix = (value) => !inPaste && WIN32_INPUT_PREFIX_RE.test(value) &&
|
|
1101
|
+
(WIN32_INPUT_BODY_PREFIX_RE.test(value) || win32HoldAllowed);
|
|
1102
|
+
// Evidence is the live query lifecycle, not a recency window: the host
|
|
1103
|
+
// injects the response types of the queries still awaiting an answer, and
|
|
1104
|
+
// a reply is only ever claimed when its shape can complete into one of
|
|
1105
|
+
// them. Read-only here and re-read on every call, so a query that settles
|
|
1106
|
+
// (matched reply, sentinel drain, dispose) stops authorizing immediately.
|
|
1107
|
+
const expectedResponseTypes = new Set(prevState.terminalExpectedResponseTypes ?? []);
|
|
1108
|
+
const hasQueryEvidence = expectedResponseTypes.size > 0;
|
|
1109
|
+
// A buffer that is already reply-specific (TERMINAL_RESPONSE_PREFIX_RE) is
|
|
1110
|
+
// eligible to be held open for its continuation — but only with the
|
|
1111
|
+
// host-injected in-flight-query evidence (D1) AND a shape/type match (D2).
|
|
1112
|
+
// Record framing is judged first (D6): every numeric CSI prefix the record
|
|
1113
|
+
// branch can frame belongs to it, and this predicate never re-labels one.
|
|
1114
|
+
// Without evidence nothing is held, so a direct caller that never injects
|
|
1115
|
+
// it keeps the pre-gate behavior (D7 / AC-3). Paste payloads stay literal,
|
|
1116
|
+
// exactly like the record branch above.
|
|
1117
|
+
const isResponsePrefix = (value) => !isRecordPrefix(value) && !inPaste && TERMINAL_RESPONSE_PREFIX_RE.test(value);
|
|
1118
|
+
if (win32InputStartedAt !== undefined && now - win32InputStartedAt >= WIN32_INPUT_GRACE_MS) {
|
|
1119
|
+
tokenizer.reset();
|
|
1120
|
+
win32InputStartedAt = undefined;
|
|
1121
|
+
}
|
|
1122
|
+
if (win32EscFlushedAt !== undefined && now - win32EscFlushedAt >= WIN32_INPUT_GRACE_MS) {
|
|
1123
|
+
win32EscFlushedAt = undefined;
|
|
1124
|
+
}
|
|
1125
|
+
// Same deadline discipline for the reply-prefix hold: first capture only,
|
|
1126
|
+
// checked on every call (not just on flush, so continuous input cannot
|
|
1127
|
+
// starve it), and released by resetting the tokenizer — bytes dropped, the
|
|
1128
|
+
// same caliber as the win32 record hold, never leaked into the prompt.
|
|
1129
|
+
let terminalResponseHoldAt = prevState.terminalResponseHoldAt;
|
|
1130
|
+
if (terminalResponseHoldAt !== undefined &&
|
|
1131
|
+
now - terminalResponseHoldAt >= TERMINAL_RESPONSE_TAIL_GRACE_MS) {
|
|
1132
|
+
if (isResponsePrefix(tokenizer.buffer()))
|
|
1133
|
+
tokenizer.reset();
|
|
1134
|
+
terminalResponseHoldAt = undefined;
|
|
1135
|
+
}
|
|
1136
|
+
if (inputString && win32EscFlushedAt !== undefined) {
|
|
1137
|
+
// Only the immediate continuation of an actual ESC flush may acquire
|
|
1138
|
+
// a missing introducer. Never capture arbitrary '['-led user text.
|
|
1139
|
+
if (prevState.mode !== 'IN_PASTE' && tokenizer.buffer() === '' &&
|
|
1140
|
+
/^\[(?:[\d;]|$)/.test(inputString) &&
|
|
1141
|
+
(WIN32_INPUT_BODY_PREFIX_RE.test('\x1b' + inputString) || win32HoldAllowed))
|
|
1142
|
+
inputString = '\x1b' + inputString;
|
|
1143
|
+
win32EscFlushedAt = undefined;
|
|
1144
|
+
}
|
|
1145
|
+
const pending = tokenizer.buffer();
|
|
1146
|
+
if (isRecordPrefix(pending) && inputString) {
|
|
1147
|
+
const continuation = /^[\d;]*/.exec(inputString)[0];
|
|
1148
|
+
const final = inputString[continuation.length];
|
|
1149
|
+
const code = final?.charCodeAt(0);
|
|
1150
|
+
// A numeric prefix is not proof of Win32 framing: even four or more
|
|
1151
|
+
// parameters can belong to another CSI. Let the tokenizer consume valid
|
|
1152
|
+
// parameter/intermediate/final bytes; only an invalid continuation can
|
|
1153
|
+
// abandon the old frame. A bare ASCII letter may therefore end the CSI,
|
|
1154
|
+
// not start user text. Native Win32 typing supplies a fresh ESC record.
|
|
1155
|
+
if (code !== undefined &&
|
|
1156
|
+
!isCSIParam(code) && !isCSIIntermediate(code) && !isCSIFinal(code)) {
|
|
1157
|
+
tokenizer.reset();
|
|
1158
|
+
win32InputStartedAt = undefined;
|
|
1159
|
+
if (final === '\x1b')
|
|
1160
|
+
inputString = inputString.slice(continuation.length);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
// Two independent reasons to keep the tokenizer's buffer across a flush:
|
|
1164
|
+
// the #1142 record frame (win32), and an unfinished reply prefix when the
|
|
1165
|
+
// host reports a query in flight (D1 × D2). Each keeps its own bound (D6);
|
|
1166
|
+
// an over-limit buffer is dropped exactly like #1142 drops an oversized
|
|
1167
|
+
// record frame.
|
|
1168
|
+
const recordPrefix = isRecordPrefix(tokenizer.buffer());
|
|
1169
|
+
// Evidence is re-read on every call, never latched: once the host stops
|
|
1170
|
+
// reporting a query in flight — or the buffer's shape can no longer
|
|
1171
|
+
// complete into a response type it expects — the claim ends. A captured
|
|
1172
|
+
// hold is then dropped by the release branch below instead of being
|
|
1173
|
+
// flushed into the body (no evidence, no claim).
|
|
1174
|
+
const claimsResponsePrefix = hasQueryEvidence &&
|
|
1175
|
+
isResponsePrefix(tokenizer.buffer()) &&
|
|
1176
|
+
expectsResponseType(tokenizer.buffer(), expectedResponseTypes);
|
|
1177
|
+
let deferFlush = isFlush && (recordPrefix || claimsResponsePrefix);
|
|
1178
|
+
if (deferFlush &&
|
|
1179
|
+
tokenizer.buffer().length >
|
|
1180
|
+
(recordPrefix ? WIN32_INPUT_MAX_LENGTH : TERMINAL_RESPONSE_MAX_LENGTH)) {
|
|
1181
|
+
tokenizer.reset();
|
|
1182
|
+
deferFlush = false;
|
|
1183
|
+
}
|
|
1184
|
+
// First capture only: later flushes (App re-arms its timer while
|
|
1185
|
+
// `incomplete` is set) must not renew the deadline.
|
|
1186
|
+
if (deferFlush && claimsResponsePrefix)
|
|
1187
|
+
terminalResponseHoldAt ??= now;
|
|
1188
|
+
// A captured hold can lose its authorization before the flush: the host
|
|
1189
|
+
// re-injects the pending set on every call, so a query that settles (or a
|
|
1190
|
+
// shape that evolves out of the expected types) turns claimsResponsePrefix
|
|
1191
|
+
// false. Falling back to tokenizer.flush() there would hand the buffered
|
|
1192
|
+
// reply prefix to parseKeypress() as a key/literal text. Drop it like the
|
|
1193
|
+
// expiry path instead and end the hold. Both the captured timestamp and
|
|
1194
|
+
// the reply shape are required: bytes that were never held keep the
|
|
1195
|
+
// pre-gate release semantics (AC-3).
|
|
1196
|
+
const releaseResponseHold = isFlush &&
|
|
1197
|
+
terminalResponseHoldAt !== undefined &&
|
|
1198
|
+
isResponsePrefix(tokenizer.buffer()) &&
|
|
1199
|
+
!claimsResponsePrefix;
|
|
1200
|
+
if (releaseResponseHold) {
|
|
1201
|
+
tokenizer.reset();
|
|
1202
|
+
terminalResponseHoldAt = undefined;
|
|
1203
|
+
}
|
|
1204
|
+
const tokens = isFlush
|
|
1205
|
+
? deferFlush || releaseResponseHold ? [] : tokenizer.flush()
|
|
1206
|
+
: tokenizer.feed(inputString);
|
|
1207
|
+
if (isFlush && !inPaste && tokens.some(token => token.value === '\x1b')) {
|
|
1208
|
+
win32EscFlushedAt = now;
|
|
1209
|
+
}
|
|
699
1210
|
// Convert tokens to parsed keys, handling paste mode
|
|
700
1211
|
const keys = [];
|
|
701
|
-
let inPaste = prevState.mode === 'IN_PASTE';
|
|
702
1212
|
let pasteBuffer = prevState.pasteBuffer;
|
|
703
1213
|
// Surrogate-pair scratch for win32-input-mode records. Threaded through a
|
|
704
|
-
// local object so prevState is never mutated —
|
|
1214
|
+
// local object so prevState is never mutated — callers can seed the parser
|
|
705
1215
|
// with the shared INITIAL_STATE singleton, and a pending high surrogate
|
|
706
1216
|
// leaking into it would survive into fresh parser instances.
|
|
707
1217
|
const win32Ctx = {
|
|
@@ -716,6 +1226,7 @@ export function parseMultipleKeypresses(prevState, input = '') {
|
|
|
716
1226
|
matched: 0,
|
|
717
1227
|
held: [],
|
|
718
1228
|
buffer: '',
|
|
1229
|
+
lastWasCarriageReturn: false,
|
|
719
1230
|
};
|
|
720
1231
|
const win32Protocol = prevState.win32Protocol ?? {
|
|
721
1232
|
held: [],
|
|
@@ -728,6 +1239,37 @@ export function parseMultipleKeypresses(prevState, input = '') {
|
|
|
728
1239
|
// App's 50ms flush timer, so a per-call flag would still let the SECOND
|
|
729
1240
|
// quiet flush kill a press split by >~100ms (observed over SSH).
|
|
730
1241
|
let mouseTailHoldAt = prevState.mouseTailHoldAt;
|
|
1242
|
+
const terminalResponseTailAfterEscFlushAt = prevState.terminalResponseTailAfterEscFlushAt;
|
|
1243
|
+
// Fragment of a reply whose introducer ESC was already flushed as a key,
|
|
1244
|
+
// held across calls until the rest of its shape arrives (see
|
|
1245
|
+
// KeyParseState.terminalResponseReattachTail).
|
|
1246
|
+
let terminalResponseReattachTail = prevState.terminalResponseReattachTail;
|
|
1247
|
+
// Re-attach (D3b, handed over from #796): a lone Escape was genuinely
|
|
1248
|
+
// flushed, so a text token inside the window may be the rest of a reply
|
|
1249
|
+
// that lost its introducer. Claim it only inside the bounded window AND
|
|
1250
|
+
// with a query in flight whose expected response type the shape can still
|
|
1251
|
+
// complete into — the missing gate CodeRabbit flagged on #796.
|
|
1252
|
+
// parseTerminalResponse() remains the completeness authority; bytes whose
|
|
1253
|
+
// shape matches no expected type stay literal, and without injected
|
|
1254
|
+
// evidence this branch never fires (AC-3).
|
|
1255
|
+
// Bounded by TIME, not by "the next call": a burst can interleave a chunk
|
|
1256
|
+
// between the flush and the tail (another reply answering an earlier query,
|
|
1257
|
+
// or a keystroke), and the real-ConPTY dry run leaked the tail when that
|
|
1258
|
+
// chunk closed the window early.
|
|
1259
|
+
const mayRecoverTerminalResponseTail = hasQueryEvidence &&
|
|
1260
|
+
terminalResponseTailAfterEscFlushAt !== undefined &&
|
|
1261
|
+
Date.now() - terminalResponseTailAfterEscFlushAt <= TERMINAL_RESPONSE_TAIL_GRACE_MS;
|
|
1262
|
+
// Carry that stamp under the same discipline while it is still in-window,
|
|
1263
|
+
// and drop it once it expires (or the evidence is gone) so no stale window
|
|
1264
|
+
// survives — the #1142 record window above carries its own stamp the same way.
|
|
1265
|
+
const carryTailAfterEscFlush = mayRecoverTerminalResponseTail
|
|
1266
|
+
? terminalResponseTailAfterEscFlushAt
|
|
1267
|
+
: undefined;
|
|
1268
|
+
// A closed window also drops the fragment it was holding: those bytes
|
|
1269
|
+
// cannot be joined to a reply any more, and protocol-shaped bytes are
|
|
1270
|
+
// dropped rather than shown.
|
|
1271
|
+
if (carryTailAfterEscFlush === undefined)
|
|
1272
|
+
terminalResponseReattachTail = undefined;
|
|
731
1273
|
// Hard deadline, checked at the top of EVERY call — not only on flush.
|
|
732
1274
|
// Continuous input keeps cancelling and re-arming App's 50ms flush timer,
|
|
733
1275
|
// so a flush-only check can starve while the user types: a hold past its
|
|
@@ -750,6 +1292,35 @@ export function parseMultipleKeypresses(prevState, input = '') {
|
|
|
750
1292
|
for (let qi = 0; qi < tokenQueue.length; qi++) {
|
|
751
1293
|
const token = tokenQueue[qi];
|
|
752
1294
|
if (token.type === 'sequence') {
|
|
1295
|
+
// Once the DCS introducer ESC has timed out, the tokenizer emits an
|
|
1296
|
+
// XTVERSION tail as text but its ST/BEL terminator as a sequence. Join
|
|
1297
|
+
// that terminator before treating it as an unrelated key. A lone ST
|
|
1298
|
+
// introducer ESC may itself time out before the trailing backslash.
|
|
1299
|
+
const heldTail = terminalResponseReattachTail;
|
|
1300
|
+
if (heldTail !== undefined &&
|
|
1301
|
+
heldTail.startsWith('P>|') &&
|
|
1302
|
+
mayRecoverTerminalResponseTail &&
|
|
1303
|
+
expectedResponseTypes.has('xtversion')) {
|
|
1304
|
+
if (token.value === '\x1b' && heldTail.length + 3 <= TERMINAL_RESPONSE_MAX_LENGTH) {
|
|
1305
|
+
terminalResponseReattachTail = heldTail + '\x1b';
|
|
1306
|
+
win32EscFlushedAt = undefined;
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
if (token.value === '\x1b\\' || token.value === '\x07') {
|
|
1310
|
+
const sequence = '\x1b' + heldTail + token.value;
|
|
1311
|
+
const response = sequence.length <= TERMINAL_RESPONSE_MAX_LENGTH
|
|
1312
|
+
? parseTerminalResponse(sequence)
|
|
1313
|
+
: null;
|
|
1314
|
+
if (response?.type === 'xtversion') {
|
|
1315
|
+
terminalResponseReattachTail = undefined;
|
|
1316
|
+
keys.push({ kind: 'response', sequence, response });
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
// An unrelated sequence between the held fragment and its terminator
|
|
1322
|
+
// proves the reply died; do not merge it with later input.
|
|
1323
|
+
terminalResponseReattachTail = undefined;
|
|
753
1324
|
if (token.value === PASTE_START) {
|
|
754
1325
|
inPaste = true;
|
|
755
1326
|
pasteBuffer = '';
|
|
@@ -779,6 +1350,7 @@ export function parseMultipleKeypresses(prevState, input = '') {
|
|
|
779
1350
|
else {
|
|
780
1351
|
const win32 = parseWin32KeyEvent(token.value, win32Ctx);
|
|
781
1352
|
if (win32 !== undefined) {
|
|
1353
|
+
win32InputMode = true;
|
|
782
1354
|
// A fresh protocol record proves a held SGR prefix's report died:
|
|
783
1355
|
// report bytes are contiguous on the wire, so nothing may
|
|
784
1356
|
// interleave between a report's fragments.
|
|
@@ -797,48 +1369,41 @@ export function parseMultipleKeypresses(prevState, input = '') {
|
|
|
797
1369
|
}
|
|
798
1370
|
}
|
|
799
1371
|
else {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1372
|
+
// Fixed decision order (D6) for one complete token — see
|
|
1373
|
+
// claimProtocolSequence: OSC 8 drop → terminal reply → mouse.
|
|
1374
|
+
// SGR mouse comes first (1006); X10 (legacy 1000/1002 without SGR)
|
|
1375
|
+
// is the compatibility fallback. Wheel falls through to
|
|
1376
|
+
// parseKeypress, which turns it into a wheel key WITH the pointer
|
|
1377
|
+
// coordinates for position-based routing.
|
|
1378
|
+
const claimed = claimProtocolSequence(token.value);
|
|
1379
|
+
if (claimed !== null) {
|
|
1380
|
+
// Any claim — drop, terminal reply or complete mouse report — is
|
|
1381
|
+
// a protocol boundary: a held prefix belongs to an older, dead
|
|
1382
|
+
// report. Discard it BEFORE it can merge the next fragment into
|
|
1383
|
+
// a phantom event.
|
|
803
1384
|
mouseTailHold = undefined;
|
|
804
1385
|
mouseTailHoldAt = undefined;
|
|
805
|
-
keys.push(
|
|
1386
|
+
keys.push(claimed);
|
|
1387
|
+
}
|
|
1388
|
+
else if (SGR_MOUSE_PREFIX_RE.test(token.value.replace(/^\x1b/, ''))) {
|
|
1389
|
+
// Flush-truncated SGR mouse report: the tokenizer's flush
|
|
1390
|
+
// emitted the buffered prefix (ESC still attached) as a
|
|
1391
|
+
// sequence token. It is protocol bytes mid-report, not a key —
|
|
1392
|
+
// strip the ESC, hold for the continuation (the text-token
|
|
1393
|
+
// branch above completes it), and never let it fall through
|
|
1394
|
+
// to parseKeypress, where it would leak into the prompt.
|
|
1395
|
+
// A fresh prefix REPLACES any stale hold instead of appending:
|
|
1396
|
+
// the new report's arrival proves the old one's tail never
|
|
1397
|
+
// came (`[<0;18` + `[<64;…` concatenated parses as garbage).
|
|
1398
|
+
mouseTailHold = token.value.replace(/^\x1b/, '');
|
|
1399
|
+
mouseTailHoldAt = Date.now();
|
|
806
1400
|
}
|
|
807
1401
|
else {
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
parseX10MouseEvent(token.value);
|
|
814
|
-
if (mouse) {
|
|
815
|
-
// A complete report arrived — any held prefix belongs to an
|
|
816
|
-
// older, dead report. Discard it BEFORE it can merge the next
|
|
817
|
-
// fragment into a phantom event.
|
|
818
|
-
mouseTailHold = undefined;
|
|
819
|
-
mouseTailHoldAt = undefined;
|
|
820
|
-
keys.push(mouse);
|
|
821
|
-
}
|
|
822
|
-
else if (SGR_MOUSE_PREFIX_RE.test(token.value.replace(/^\x1b/, ''))) {
|
|
823
|
-
// Flush-truncated SGR mouse report: the tokenizer's flush
|
|
824
|
-
// emitted the buffered prefix (ESC still attached) as a
|
|
825
|
-
// sequence token. It is protocol bytes mid-report, not a key —
|
|
826
|
-
// strip the ESC, hold for the continuation (the text-token
|
|
827
|
-
// branch above completes it), and never let it fall through
|
|
828
|
-
// to parseKeypress, where it would leak into the prompt.
|
|
829
|
-
// A fresh prefix REPLACES any stale hold instead of appending:
|
|
830
|
-
// the new report's arrival proves the old one's tail never
|
|
831
|
-
// came (`[<0;18` + `[<64;…` concatenated parses as garbage).
|
|
832
|
-
mouseTailHold = token.value.replace(/^\x1b/, '');
|
|
833
|
-
mouseTailHoldAt = Date.now();
|
|
834
|
-
}
|
|
835
|
-
else {
|
|
836
|
-
// Ordinary key sequence (arrows, function keys, …) — still an
|
|
837
|
-
// ESC protocol start, so a held prefix's report is dead.
|
|
838
|
-
mouseTailHold = undefined;
|
|
839
|
-
mouseTailHoldAt = undefined;
|
|
840
|
-
keys.push(parseKeypress(token.value));
|
|
841
|
-
}
|
|
1402
|
+
// Ordinary key sequence (arrows, function keys, …) — still an
|
|
1403
|
+
// ESC protocol start, so a held prefix's report is dead.
|
|
1404
|
+
mouseTailHold = undefined;
|
|
1405
|
+
mouseTailHoldAt = undefined;
|
|
1406
|
+
keys.push(parseKeypress(token.value));
|
|
842
1407
|
}
|
|
843
1408
|
}
|
|
844
1409
|
}
|
|
@@ -852,11 +1417,14 @@ export function parseMultipleKeypresses(prevState, input = '') {
|
|
|
852
1417
|
// a held prefix's report is dead. Discard before recovering.
|
|
853
1418
|
mouseTailHold = undefined;
|
|
854
1419
|
mouseTailHoldAt = undefined;
|
|
1420
|
+
terminalResponseReattachTail = undefined;
|
|
855
1421
|
// A delayed win32-input-mode continuation can arrive after App's
|
|
856
1422
|
// escape timer has already flushed its ESC prefix. Recover complete
|
|
857
1423
|
// record tails so their protocol bytes do not leak into the prompt.
|
|
858
1424
|
for (const tail of token.value.match(WIN32_INPUT_TAIL_RE) ?? []) {
|
|
859
1425
|
const win32 = parseWin32KeyEvent('\x1b' + tail, win32Ctx);
|
|
1426
|
+
if (win32 !== undefined)
|
|
1427
|
+
win32InputMode = true;
|
|
860
1428
|
if (win32 !== undefined && win32 !== null) {
|
|
861
1429
|
for (let i = 0; i < win32.repeat; i++) {
|
|
862
1430
|
keys.push(...feedWin32Input(win32Paste, win32Protocol, win32.key));
|
|
@@ -883,6 +1451,7 @@ export function parseMultipleKeypresses(prevState, input = '') {
|
|
|
883
1451
|
// protocol bytes into the prompt as an ordinary key.
|
|
884
1452
|
mouseTailHold = undefined;
|
|
885
1453
|
mouseTailHoldAt = undefined;
|
|
1454
|
+
terminalResponseReattachTail = undefined;
|
|
886
1455
|
const resynthesized = '\x1b' + token.value;
|
|
887
1456
|
const mouse = parseMouseEvent(resynthesized);
|
|
888
1457
|
keys.push(mouse ?? parseKeypress(resynthesized));
|
|
@@ -898,6 +1467,7 @@ export function parseMultipleKeypresses(prevState, input = '') {
|
|
|
898
1467
|
// protocol prefix, a win32 tail, or ordinary typing. The prefix
|
|
899
1468
|
// regex (no $ anchor) matches the report at the head; the suffix
|
|
900
1469
|
// is whatever follows.
|
|
1470
|
+
terminalResponseReattachTail = undefined;
|
|
901
1471
|
const combined = mouseTailHold + token.value;
|
|
902
1472
|
const m = combined.match(SGR_MOUSE_TAIL_PREFIX_RE);
|
|
903
1473
|
const reportEnd = m[0].length;
|
|
@@ -927,6 +1497,7 @@ export function parseMultipleKeypresses(prevState, input = '') {
|
|
|
927
1497
|
// discards the hold once its grace expires. The regex demands `<` +
|
|
928
1498
|
// digits, which no realistic typed text produces as a single text
|
|
929
1499
|
// token.
|
|
1500
|
+
terminalResponseReattachTail = undefined;
|
|
930
1501
|
if (mouseTailHold === undefined)
|
|
931
1502
|
mouseTailHoldAt = Date.now();
|
|
932
1503
|
mouseTailHold = (mouseTailHold ?? '') + token.value;
|
|
@@ -934,14 +1505,74 @@ export function parseMultipleKeypresses(prevState, input = '') {
|
|
|
934
1505
|
else {
|
|
935
1506
|
// Ordinary typing while a hold is pending: text that can never
|
|
936
1507
|
// continue an SGR report proves the held report died. Discard the
|
|
937
|
-
// stale hold so it cannot merge the NEXT fragment into a phantom
|
|
938
|
-
// event, then pass the text through untouched.
|
|
1508
|
+
// stale hold so it cannot merge the NEXT fragment into a phantom event.
|
|
939
1509
|
mouseTailHold = undefined;
|
|
940
1510
|
mouseTailHoldAt = undefined;
|
|
941
|
-
|
|
1511
|
+
// #796 re-attach + post-flush reassembly: put the flushed introducer
|
|
1512
|
+
// back, then let the one reply-shape authority decide. A fragment
|
|
1513
|
+
// held from an earlier call is joined with this token first, so a
|
|
1514
|
+
// tail that is itself split again (`[?61;4;6` then `;14;21;22c`) is
|
|
1515
|
+
// reassembled instead of falling into the prompt in pieces. A claim
|
|
1516
|
+
// needs BOTH a pending query of the matching expected type and a
|
|
1517
|
+
// shape that can still complete into it; anything else keeps the
|
|
1518
|
+
// token literal (AC-3) and drops a stale fragment rather than
|
|
1519
|
+
// leaking protocol bytes.
|
|
1520
|
+
const candidate = (terminalResponseReattachTail ?? '') + token.value;
|
|
1521
|
+
const claimed = mayRecoverTerminalResponseTail
|
|
1522
|
+
? claimExpectedResponseHead(candidate, expectedResponseTypes)
|
|
1523
|
+
: undefined;
|
|
1524
|
+
if (claimed) {
|
|
1525
|
+
terminalResponseReattachTail = undefined;
|
|
1526
|
+
keys.push({
|
|
1527
|
+
kind: 'response',
|
|
1528
|
+
sequence: '\x1b' + candidate.slice(0, claimed.consumed),
|
|
1529
|
+
response: claimed.response,
|
|
1530
|
+
});
|
|
1531
|
+
// Trailing bytes beyond the completed reply are ordinary input
|
|
1532
|
+
// (the terminal batched the tail with the next keystrokes).
|
|
1533
|
+
const suffix = candidate.slice(claimed.consumed);
|
|
1534
|
+
if (suffix)
|
|
1535
|
+
tokenQueue.splice(qi + 1, 0, { type: 'text', value: suffix });
|
|
1536
|
+
}
|
|
1537
|
+
else if (mayRecoverTerminalResponseTail &&
|
|
1538
|
+
candidate.length <= TERMINAL_RESPONSE_MAX_LENGTH &&
|
|
1539
|
+
expectsResponseType('\x1b' + candidate, expectedResponseTypes)) {
|
|
1540
|
+
// Still an open expected reply prefix — hold it for the next call.
|
|
1541
|
+
terminalResponseReattachTail = candidate;
|
|
1542
|
+
}
|
|
1543
|
+
else {
|
|
1544
|
+
// The held fragment (if any) cannot be completed; drop it and give
|
|
1545
|
+
// the fresh bytes their own chance to start a reply prefix.
|
|
1546
|
+
terminalResponseReattachTail = undefined;
|
|
1547
|
+
if (mayRecoverTerminalResponseTail &&
|
|
1548
|
+
token.value.length <= TERMINAL_RESPONSE_MAX_LENGTH &&
|
|
1549
|
+
expectsResponseType('\x1b' + token.value, expectedResponseTypes)) {
|
|
1550
|
+
terminalResponseReattachTail = token.value;
|
|
1551
|
+
}
|
|
1552
|
+
else {
|
|
1553
|
+
keys.push(parseKeypress(token.value));
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
942
1556
|
}
|
|
943
1557
|
}
|
|
944
1558
|
}
|
|
1559
|
+
// Inspect the trailing buffer AFTER processing tokens: this read may have
|
|
1560
|
+
// entered a literal paste, or decoded the first win32 record. Emitting a
|
|
1561
|
+
// token ended the old sequence, so a new prefix gets its own deadline.
|
|
1562
|
+
if (tokens.length > 0)
|
|
1563
|
+
win32InputStartedAt = undefined;
|
|
1564
|
+
if (isRecordPrefix(tokenizer.buffer())) {
|
|
1565
|
+
if (tokenizer.buffer().length > WIN32_INPUT_MAX_LENGTH) {
|
|
1566
|
+
tokenizer.reset();
|
|
1567
|
+
win32InputStartedAt = undefined;
|
|
1568
|
+
}
|
|
1569
|
+
else {
|
|
1570
|
+
win32InputStartedAt ??= now;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
else {
|
|
1574
|
+
win32InputStartedAt = undefined;
|
|
1575
|
+
}
|
|
945
1576
|
// If flushing and still in paste mode, emit what we have
|
|
946
1577
|
if (isFlush && inPaste && pasteBuffer) {
|
|
947
1578
|
keys.push(createPasteKey(pasteBuffer));
|
|
@@ -949,11 +1580,12 @@ export function parseMultipleKeypresses(prevState, input = '') {
|
|
|
949
1580
|
pasteBuffer = '';
|
|
950
1581
|
}
|
|
951
1582
|
// Flush handling for the decomposed win32 paste: mid-paste (active) the
|
|
952
|
-
//
|
|
1583
|
+
// quiet timeout means the paste stream ended — finalize with whatever
|
|
953
1584
|
// was collected (mirrors the VT IN_PASTE flush above; a truncated end
|
|
954
1585
|
// marker must not strand the matcher and eat all future typing). Outside
|
|
955
1586
|
// a paste, release any held marker-prefix keys (e.g. a lone Escape).
|
|
956
|
-
|
|
1587
|
+
// A deferred record flush must not split these higher-level matchers.
|
|
1588
|
+
if (isFlush && !deferFlush && win32Paste.active) {
|
|
957
1589
|
let content = win32Paste.buffer;
|
|
958
1590
|
for (const k of win32Paste.held)
|
|
959
1591
|
content += win32RecordChar(k) ?? '';
|
|
@@ -962,16 +1594,18 @@ export function parseMultipleKeypresses(prevState, input = '') {
|
|
|
962
1594
|
win32Paste.buffer = '';
|
|
963
1595
|
win32Paste.held = [];
|
|
964
1596
|
win32Paste.matched = 0;
|
|
1597
|
+
win32Paste.lastWasCarriageReturn = false;
|
|
965
1598
|
}
|
|
966
|
-
else if (isFlush && win32Paste.held.length > 0) {
|
|
1599
|
+
else if (isFlush && !deferFlush && win32Paste.held.length > 0) {
|
|
967
1600
|
keys.push(...win32Paste.held);
|
|
968
1601
|
win32Paste.held = [];
|
|
969
1602
|
win32Paste.matched = 0;
|
|
1603
|
+
win32Paste.lastWasCarriageReturn = false;
|
|
970
1604
|
}
|
|
971
1605
|
// A quiet timeout ends a synthesized protocol candidate. Incomplete mouse
|
|
972
1606
|
// reports are terminal input and must not become prompt text; other held
|
|
973
1607
|
// input (a lone Escape or an unknown CSI sequence) remains ordinary keys.
|
|
974
|
-
if (isFlush && win32Protocol.held.length > 0) {
|
|
1608
|
+
if (isFlush && !deferFlush && win32Protocol.held.length > 0) {
|
|
975
1609
|
const isMouseCandidate = win32Protocol.sequence.startsWith('\x1b[<') ||
|
|
976
1610
|
win32Protocol.sequence.startsWith('\x1b[M');
|
|
977
1611
|
if (!isMouseCandidate)
|
|
@@ -986,6 +1620,24 @@ export function parseMultipleKeypresses(prevState, input = '') {
|
|
|
986
1620
|
// as text are exactly the leak this hold exists to prevent. Partial
|
|
987
1621
|
// recovery of the coords is not worth one more branch — a mouse event
|
|
988
1622
|
// with guessed terminators would dispatch phantom clicks.
|
|
1623
|
+
// The reply-prefix hold lives only while the tokenizer still retains a
|
|
1624
|
+
// reply-shaped prefix: completion, release or reset all end it, so the
|
|
1625
|
+
// NEXT hold captures its own first-capture deadline.
|
|
1626
|
+
if (terminalResponseHoldAt !== undefined && !isResponsePrefix(tokenizer.buffer())) {
|
|
1627
|
+
terminalResponseHoldAt = undefined;
|
|
1628
|
+
}
|
|
1629
|
+
// A REAL lone-Escape flush — an Escape emitted as its own key, not the ESC
|
|
1630
|
+
// introducer of a buffered sequence. Only this opens the #796 re-attach
|
|
1631
|
+
// window; the window alone is not evidence (D1), and a direct caller that
|
|
1632
|
+
// never injects the provenance keeps the pre-change behavior.
|
|
1633
|
+
const escapeFlushedNow = isFlush &&
|
|
1634
|
+
keys.some(key => key.kind === 'key' &&
|
|
1635
|
+
key.name === 'escape' &&
|
|
1636
|
+
key.sequence === '\x1b');
|
|
1637
|
+
// A fresh lone-Escape flush opens a NEW window: a fragment held against
|
|
1638
|
+
// the old one cannot be part of a reply whose introducer just arrived.
|
|
1639
|
+
if (escapeFlushedNow)
|
|
1640
|
+
terminalResponseReattachTail = undefined;
|
|
989
1641
|
// Build new state
|
|
990
1642
|
const newState = {
|
|
991
1643
|
mode: inPaste ? 'IN_PASTE' : 'NORMAL',
|
|
@@ -1003,12 +1655,28 @@ export function parseMultipleKeypresses(prevState, input = '') {
|
|
|
1003
1655
|
? '\x1b'
|
|
1004
1656
|
: ''),
|
|
1005
1657
|
pasteBuffer,
|
|
1658
|
+
// The host gate rides along: App replaces its state with this object on
|
|
1659
|
+
// every read, and the hold must stay closed for the whole session.
|
|
1660
|
+
win32Capable: prevState.win32Capable,
|
|
1661
|
+
// Same host-injected contract for the query evidence: read-only here,
|
|
1662
|
+
// re-injected by App on every input call, so it is never stale.
|
|
1663
|
+
terminalExpectedResponseTypes: prevState.terminalExpectedResponseTypes,
|
|
1664
|
+
win32InputMode,
|
|
1665
|
+
win32InputStartedAt,
|
|
1666
|
+
win32EscFlushedAt,
|
|
1006
1667
|
win32HighSurrogate: win32Ctx.high,
|
|
1007
1668
|
win32AltHighSurrogate: win32Ctx.altHigh,
|
|
1008
1669
|
win32Paste,
|
|
1009
1670
|
win32Protocol,
|
|
1010
1671
|
mouseTailHold,
|
|
1011
1672
|
mouseTailHoldAt,
|
|
1673
|
+
terminalResponseHoldAt,
|
|
1674
|
+
terminalResponseReattachTail,
|
|
1675
|
+
// A fresh lone-Escape flush re-arms the window; otherwise the carried
|
|
1676
|
+
// stamp keeps it open (see carryTailAfterEscFlush above).
|
|
1677
|
+
terminalResponseTailAfterEscFlushAt: escapeFlushedNow
|
|
1678
|
+
? Date.now()
|
|
1679
|
+
: carryTailAfterEscFlush,
|
|
1012
1680
|
_tokenizer: tokenizer,
|
|
1013
1681
|
};
|
|
1014
1682
|
return [keys, newState];
|
|
@@ -1508,6 +2176,9 @@ function parseKeypress(s = '') {
|
|
|
1508
2176
|
case '\u001b[1;5C':
|
|
1509
2177
|
return createNavKey(s, 'right', true);
|
|
1510
2178
|
}
|
|
2179
|
+
if (!key.name && !key.code && COMPLETE_CSI_RE.test(s)) {
|
|
2180
|
+
key.code = s.slice(1);
|
|
2181
|
+
}
|
|
1511
2182
|
return key;
|
|
1512
2183
|
}
|
|
1513
2184
|
function createNavKey(s, name, ctrl) {
|