nastech-tui 0.0.1
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/.prettierrc +11 -0
- package/README.md +346 -0
- package/eslint.config.mjs +111 -0
- package/package.json +51 -0
- package/packages/nastech-ink/ambient.d.ts +83 -0
- package/packages/nastech-ink/index.d.ts +40 -0
- package/packages/nastech-ink/index.js +1 -0
- package/packages/nastech-ink/nastech-ink/ambient.d.ts +83 -0
- package/packages/nastech-ink/nastech-ink/index.d.ts +40 -0
- package/packages/nastech-ink/nastech-ink/index.js +1 -0
- package/packages/nastech-ink/nastech-ink/package.json +54 -0
- package/packages/nastech-ink/nastech-ink/src/bootstrap/state.ts +9 -0
- package/packages/nastech-ink/nastech-ink/src/entry-exports.ts +32 -0
- package/packages/nastech-ink/nastech-ink/src/hooks/use-stderr.ts +15 -0
- package/packages/nastech-ink/nastech-ink/src/hooks/use-stdout.ts +15 -0
- package/packages/nastech-ink/nastech-ink/src/ink/Ansi.tsx +435 -0
- package/packages/nastech-ink/nastech-ink/src/ink/app-mouse.test.ts +123 -0
- package/packages/nastech-ink/nastech-ink/src/ink/bidi.ts +145 -0
- package/packages/nastech-ink/nastech-ink/src/ink/cache-eviction.ts +45 -0
- package/packages/nastech-ink/nastech-ink/src/ink/clearTerminal.ts +68 -0
- package/packages/nastech-ink/nastech-ink/src/ink/colorize.test.ts +60 -0
- package/packages/nastech-ink/nastech-ink/src/ink/colorize.ts +277 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/AlternateScreen.tsx +133 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/App.tsx +830 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/AppContext.ts +20 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/Box.tsx +294 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/Button.tsx +236 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/ClockContext.tsx +133 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/CursorAdvanceContext.ts +35 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/CursorDeclarationContext.ts +28 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/ErrorOverview.tsx +130 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/Link.tsx +38 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/Newline.tsx +43 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/NoSelect.tsx +73 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/RawAnsi.tsx +61 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/ScrollBox.tsx +290 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/Spacer.tsx +23 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/StdinContext.ts +25 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/TerminalFocusContext.tsx +63 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/TerminalSizeContext.tsx +7 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/Text.test.ts +38 -0
- package/packages/nastech-ink/nastech-ink/src/ink/components/Text.tsx +336 -0
- package/packages/nastech-ink/nastech-ink/src/ink/constants.ts +6 -0
- package/packages/nastech-ink/nastech-ink/src/ink/cursor.ts +5 -0
- package/packages/nastech-ink/nastech-ink/src/ink/devtools.ts +2 -0
- package/packages/nastech-ink/nastech-ink/src/ink/dom.ts +495 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/click-event.ts +38 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/cmd-shortcuts.test.ts +65 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/dispatcher.ts +242 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/emitter.ts +40 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/event-handlers.ts +84 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/event.ts +11 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/focus-event.ts +18 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/input-event.ts +176 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/keyboard-event.ts +57 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/mouse-event.ts +18 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/paste-event.ts +10 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/resize-event.ts +12 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/terminal-event.ts +107 -0
- package/packages/nastech-ink/nastech-ink/src/ink/events/terminal-focus-event.ts +19 -0
- package/packages/nastech-ink/nastech-ink/src/ink/focus.ts +219 -0
- package/packages/nastech-ink/nastech-ink/src/ink/frame.ts +124 -0
- package/packages/nastech-ink/nastech-ink/src/ink/get-max-width.ts +27 -0
- package/packages/nastech-ink/nastech-ink/src/ink/global.d.ts +1 -0
- package/packages/nastech-ink/nastech-ink/src/ink/hit-test.test.ts +38 -0
- package/packages/nastech-ink/nastech-ink/src/ink/hit-test.ts +224 -0
- package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-animation-frame.ts +62 -0
- package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-app.ts +9 -0
- package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-cursor-advance.ts +33 -0
- package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-declared-cursor.ts +75 -0
- package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-external-process.ts +27 -0
- package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-input.ts +95 -0
- package/packages/nastech-ink/nastech-ink/src/ink/hooks/use-interval.ts +71 -0
- package/packages/nastech-ink/package.json +57 -0
- package/packages/nastech-ink/src/bootstrap/state.ts +9 -0
- package/packages/nastech-ink/src/entry-exports.ts +32 -0
- package/packages/nastech-ink/src/hooks/use-stderr.ts +15 -0
- package/packages/nastech-ink/src/hooks/use-stdout.ts +15 -0
- package/packages/nastech-ink/src/ink/Ansi.tsx +435 -0
- package/packages/nastech-ink/src/ink/app-mouse.test.ts +123 -0
- package/packages/nastech-ink/src/ink/app-rawmode-mouse.test.ts +91 -0
- package/packages/nastech-ink/src/ink/bidi.ts +145 -0
- package/packages/nastech-ink/src/ink/cache-eviction.ts +45 -0
- package/packages/nastech-ink/src/ink/clearTerminal.ts +68 -0
- package/packages/nastech-ink/src/ink/colorize.test.ts +60 -0
- package/packages/nastech-ink/src/ink/colorize.ts +277 -0
- package/packages/nastech-ink/src/ink/components/AlternateScreen.tsx +133 -0
- package/packages/nastech-ink/src/ink/components/App.tsx +855 -0
- package/packages/nastech-ink/src/ink/components/AppContext.ts +20 -0
- package/packages/nastech-ink/src/ink/components/Box.tsx +294 -0
- package/packages/nastech-ink/src/ink/components/Button.tsx +236 -0
- package/packages/nastech-ink/src/ink/components/ClockContext.tsx +133 -0
- package/packages/nastech-ink/src/ink/components/CursorAdvanceContext.ts +35 -0
- package/packages/nastech-ink/src/ink/components/CursorDeclarationContext.ts +28 -0
- package/packages/nastech-ink/src/ink/components/ErrorOverview.tsx +130 -0
- package/packages/nastech-ink/src/ink/components/Link.tsx +38 -0
- package/packages/nastech-ink/src/ink/components/Newline.tsx +43 -0
- package/packages/nastech-ink/src/ink/components/NoSelect.tsx +73 -0
- package/packages/nastech-ink/src/ink/components/RawAnsi.tsx +61 -0
- package/packages/nastech-ink/src/ink/components/ScrollBox.tsx +290 -0
- package/packages/nastech-ink/src/ink/components/Spacer.tsx +23 -0
- package/packages/nastech-ink/src/ink/components/StdinContext.ts +25 -0
- package/packages/nastech-ink/src/ink/components/TerminalFocusContext.tsx +63 -0
- package/packages/nastech-ink/src/ink/components/TerminalSizeContext.tsx +7 -0
- package/packages/nastech-ink/src/ink/components/Text.test.ts +38 -0
- package/packages/nastech-ink/src/ink/components/Text.tsx +336 -0
- package/packages/nastech-ink/src/ink/constants.ts +6 -0
- package/packages/nastech-ink/src/ink/cursor.ts +5 -0
- package/packages/nastech-ink/src/ink/devtools.ts +2 -0
- package/packages/nastech-ink/src/ink/dom.ts +495 -0
- package/packages/nastech-ink/src/ink/events/click-event.ts +38 -0
- package/packages/nastech-ink/src/ink/events/cmd-shortcuts.test.ts +65 -0
- package/packages/nastech-ink/src/ink/events/dispatcher.ts +242 -0
- package/packages/nastech-ink/src/ink/events/emitter.ts +40 -0
- package/packages/nastech-ink/src/ink/events/event-handlers.ts +84 -0
- package/packages/nastech-ink/src/ink/events/event.ts +11 -0
- package/packages/nastech-ink/src/ink/events/focus-event.ts +18 -0
- package/packages/nastech-ink/src/ink/events/input-event.ts +176 -0
- package/packages/nastech-ink/src/ink/events/keyboard-event.ts +57 -0
- package/packages/nastech-ink/src/ink/events/mouse-event.ts +18 -0
- package/packages/nastech-ink/src/ink/events/paste-event.ts +10 -0
- package/packages/nastech-ink/src/ink/events/resize-event.ts +12 -0
- package/packages/nastech-ink/src/ink/events/terminal-event.ts +107 -0
- package/packages/nastech-ink/src/ink/events/terminal-focus-event.ts +19 -0
- package/packages/nastech-ink/src/ink/focus.ts +219 -0
- package/packages/nastech-ink/src/ink/frame.ts +124 -0
- package/packages/nastech-ink/src/ink/get-max-width.ts +27 -0
- package/packages/nastech-ink/src/ink/global.d.ts +1 -0
- package/packages/nastech-ink/src/ink/hit-test.test.ts +38 -0
- package/packages/nastech-ink/src/ink/hit-test.ts +224 -0
- package/packages/nastech-ink/src/ink/hooks/use-animation-frame.ts +62 -0
- package/packages/nastech-ink/src/ink/hooks/use-app.ts +9 -0
- package/packages/nastech-ink/src/ink/hooks/use-cursor-advance.ts +33 -0
- package/packages/nastech-ink/src/ink/hooks/use-declared-cursor.ts +75 -0
- package/packages/nastech-ink/src/ink/hooks/use-external-process.ts +27 -0
- package/packages/nastech-ink/src/ink/hooks/use-input.ts +95 -0
- package/packages/nastech-ink/src/ink/hooks/use-interval.ts +71 -0
- package/packages/nastech-ink/src/ink/hooks/use-search-highlight.ts +56 -0
- package/packages/nastech-ink/src/ink/hooks/use-selection.ts +101 -0
- package/packages/nastech-ink/src/ink/hooks/use-stdin.ts +9 -0
- package/packages/nastech-ink/src/ink/hooks/use-tab-status.ts +71 -0
- package/packages/nastech-ink/src/ink/hooks/use-terminal-focus.ts +18 -0
- package/packages/nastech-ink/src/ink/hooks/use-terminal-title.ts +34 -0
- package/packages/nastech-ink/src/ink/hooks/use-terminal-viewport.ts +100 -0
- package/packages/nastech-ink/src/ink/hyperlinkHover.ts +52 -0
- package/packages/nastech-ink/src/ink/ink-cursor-advance.test.ts +234 -0
- package/packages/nastech-ink/src/ink/ink-resize.test.ts +50 -0
- package/packages/nastech-ink/src/ink/ink.tsx +2705 -0
- package/packages/nastech-ink/src/ink/instances.ts +10 -0
- package/packages/nastech-ink/src/ink/layout/engine.ts +6 -0
- package/packages/nastech-ink/src/ink/layout/geometry.ts +98 -0
- package/packages/nastech-ink/src/ink/layout/node.ts +145 -0
- package/packages/nastech-ink/src/ink/layout/yoga.ts +313 -0
- package/packages/nastech-ink/src/ink/line-width-cache.ts +38 -0
- package/packages/nastech-ink/src/ink/log-update.test.ts +223 -0
- package/packages/nastech-ink/src/ink/log-update.ts +752 -0
- package/packages/nastech-ink/src/ink/lru.ts +14 -0
- package/packages/nastech-ink/src/ink/measure-element.ts +23 -0
- package/packages/nastech-ink/src/ink/measure-text.ts +50 -0
- package/packages/nastech-ink/src/ink/node-cache.ts +53 -0
- package/packages/nastech-ink/src/ink/optimizer.ts +99 -0
- package/packages/nastech-ink/src/ink/output.ts +845 -0
- package/packages/nastech-ink/src/ink/parse-keypress.test.ts +133 -0
- package/packages/nastech-ink/src/ink/parse-keypress.ts +848 -0
- package/packages/nastech-ink/src/ink/reconciler.ts +382 -0
- package/packages/nastech-ink/src/ink/render-border.ts +206 -0
- package/packages/nastech-ink/src/ink/render-node-to-output.ts +1582 -0
- package/packages/nastech-ink/src/ink/render-to-screen.ts +236 -0
- package/packages/nastech-ink/src/ink/renderer.ts +169 -0
- package/packages/nastech-ink/src/ink/root.ts +204 -0
- package/packages/nastech-ink/src/ink/screen.ts +1590 -0
- package/packages/nastech-ink/src/ink/searchHighlight.ts +91 -0
- package/packages/nastech-ink/src/ink/selection.test.ts +82 -0
- package/packages/nastech-ink/src/ink/selection.ts +1143 -0
- package/packages/nastech-ink/src/ink/squash-text-nodes.ts +74 -0
- package/packages/nastech-ink/src/ink/stringWidth.ts +341 -0
- package/packages/nastech-ink/src/ink/styles.ts +750 -0
- package/packages/nastech-ink/src/ink/supports-hyperlinks.ts +51 -0
- package/packages/nastech-ink/src/ink/tabstops.ts +44 -0
- package/packages/nastech-ink/src/ink/terminal-focus-state.ts +52 -0
- package/packages/nastech-ink/src/ink/terminal-querier.ts +222 -0
- package/packages/nastech-ink/src/ink/terminal.test.ts +15 -0
- package/packages/nastech-ink/src/ink/terminal.ts +299 -0
- package/packages/nastech-ink/src/ink/termio/ansi.ts +75 -0
- package/packages/nastech-ink/src/ink/termio/csi.ts +334 -0
- package/packages/nastech-ink/src/ink/termio/dec.ts +99 -0
- package/packages/nastech-ink/src/ink/termio/esc.ts +69 -0
- package/packages/nastech-ink/src/ink/termio/osc.test.ts +191 -0
- package/packages/nastech-ink/src/ink/termio/osc.ts +724 -0
- package/packages/nastech-ink/src/ink/termio/parser.ts +467 -0
- package/packages/nastech-ink/src/ink/termio/sgr.ts +362 -0
- package/packages/nastech-ink/src/ink/termio/tokenize.test.ts +185 -0
- package/packages/nastech-ink/src/ink/termio/tokenize.ts +350 -0
- package/packages/nastech-ink/src/ink/termio/types.ts +230 -0
- package/packages/nastech-ink/src/ink/termio.ts +42 -0
- package/packages/nastech-ink/src/ink/useTerminalNotification.ts +110 -0
- package/packages/nastech-ink/src/ink/warn.ts +15 -0
- package/packages/nastech-ink/src/ink/widest-line.ts +22 -0
- package/packages/nastech-ink/src/ink/wrap-text.test.ts +17 -0
- package/packages/nastech-ink/src/ink/wrap-text.ts +144 -0
- package/packages/nastech-ink/src/ink/wrapAnsi.ts +13 -0
- package/packages/nastech-ink/src/native-ts/yoga-layout/enums.ts +112 -0
- package/packages/nastech-ink/src/native-ts/yoga-layout/index.ts +2326 -0
- package/packages/nastech-ink/src/utils/debug.ts +6 -0
- package/packages/nastech-ink/src/utils/earlyInput.ts +131 -0
- package/packages/nastech-ink/src/utils/env.ts +66 -0
- package/packages/nastech-ink/src/utils/envUtils.ts +13 -0
- package/packages/nastech-ink/src/utils/execFileNoThrow.test.ts +146 -0
- package/packages/nastech-ink/src/utils/execFileNoThrow.ts +115 -0
- package/packages/nastech-ink/src/utils/fullscreen.ts +3 -0
- package/packages/nastech-ink/src/utils/intl.ts +87 -0
- package/packages/nastech-ink/src/utils/log.ts +7 -0
- package/packages/nastech-ink/src/utils/semver.ts +57 -0
- package/packages/nastech-ink/src/utils/sliceAnsi.ts +106 -0
- package/packages/nastech-ink/text-input.d.ts +2 -0
- package/packages/nastech-ink/text-input.js +1 -0
- package/scripts/build.mjs +61 -0
- package/scripts/profile-tui.mjs +121 -0
- package/src/__tests__/activeSessionSwitcher.test.ts +157 -0
- package/src/__tests__/appChromeStatusRule.test.tsx +84 -0
- package/src/__tests__/appChromeStatusRuleDevCredits.test.tsx +73 -0
- package/src/__tests__/approvalAction.test.ts +50 -0
- package/src/__tests__/asCommandDispatch.test.ts +27 -0
- package/src/__tests__/blockLayout.test.ts +122 -0
- package/src/__tests__/clipboard.test.ts +369 -0
- package/src/__tests__/constants.test.ts +53 -0
- package/src/__tests__/createGatewayEventHandler.test.ts +1091 -0
- package/src/__tests__/createSlashHandler.test.ts +822 -0
- package/src/__tests__/creditsCommand.test.ts +144 -0
- package/src/__tests__/cursorDriftRegression.test.ts +114 -0
- package/src/__tests__/details.test.ts +115 -0
- package/src/__tests__/emoji.test.ts +64 -0
- package/src/__tests__/externalLink.test.ts +144 -0
- package/src/__tests__/forceTruecolor.test.ts +191 -0
- package/src/__tests__/gatewayClient.test.ts +394 -0
- package/src/__tests__/gatewayRecovery.test.ts +47 -0
- package/src/__tests__/markdown.test.ts +331 -0
- package/src/__tests__/mathUnicode.test.ts +293 -0
- package/src/__tests__/memoryMonitor.test.ts +102 -0
- package/src/__tests__/messageLine.test.ts +19 -0
- package/src/__tests__/messages.test.ts +92 -0
- package/src/__tests__/orchestratorPromptSession.test.ts +64 -0
- package/src/__tests__/osc52.test.ts +67 -0
- package/src/__tests__/parentLog.test.ts +75 -0
- package/src/__tests__/paths.test.ts +70 -0
- package/src/__tests__/platform.test.ts +556 -0
- package/src/__tests__/precisionWheel.test.ts +44 -0
- package/src/__tests__/prompt.test.ts +31 -0
- package/src/__tests__/providers.test.ts +65 -0
- package/src/__tests__/reasoning.test.ts +76 -0
- package/src/__tests__/rpc.test.ts +27 -0
- package/src/__tests__/scroll.test.ts +99 -0
- package/src/__tests__/slashParity.test.ts +123 -0
- package/src/__tests__/spawnHistoryStore.test.ts +46 -0
- package/src/__tests__/stateIsolation.test.ts +46 -0
- package/src/__tests__/statusBarTicker.test.ts +18 -0
- package/src/__tests__/statusRule.test.ts +32 -0
- package/src/__tests__/streamingMarkdown.test.ts +121 -0
- package/src/__tests__/subagentTree.test.ts +407 -0
- package/src/__tests__/syntax.test.ts +45 -0
- package/src/__tests__/terminalModes.test.ts +39 -0
- package/src/__tests__/terminalParity.test.ts +77 -0
- package/src/__tests__/terminalSetup.test.ts +386 -0
- package/src/__tests__/termux.test.ts +35 -0
- package/src/__tests__/termuxComposerLayout.test.ts +40 -0
- package/src/__tests__/text.test.ts +233 -0
- package/src/__tests__/textInputBurstInput.test.ts +40 -0
- package/src/__tests__/textInputCursorSourceOfTruth.test.ts +50 -0
- package/src/__tests__/textInputFastEcho.test.ts +200 -0
- package/src/__tests__/textInputLineNav.test.ts +55 -0
- package/src/__tests__/textInputPassThrough.test.ts +59 -0
- package/src/__tests__/textInputRightClick.test.ts +48 -0
- package/src/__tests__/textInputWrap.test.ts +151 -0
- package/src/__tests__/theme.test.ts +311 -0
- package/src/__tests__/turnControllerNotice.test.ts +43 -0
- package/src/__tests__/turnStore.test.ts +66 -0
- package/src/__tests__/useCompletion.test.ts +35 -0
- package/src/__tests__/useComposerState.test.ts +59 -0
- package/src/__tests__/useConfigSync.test.ts +460 -0
- package/src/__tests__/useInputHandlers.test.ts +77 -0
- package/src/__tests__/useQueue.test.ts +28 -0
- package/src/__tests__/useSessionLifecycle.test.ts +60 -0
- package/src/__tests__/useVirtualHistoryHeights.test.ts +39 -0
- package/src/__tests__/viewport.test.ts +58 -0
- package/src/__tests__/viewportStore.test.ts +85 -0
- package/src/__tests__/virtualHeights.test.ts +96 -0
- package/src/__tests__/virtualHistoryClamp.test.ts +19 -0
- package/src/__tests__/virtualHistoryOffsetCache.test.ts +282 -0
- package/src/__tests__/wheelAccel.test.ts +138 -0
- package/src/app/createGatewayEventHandler.ts +833 -0
- package/src/app/createSlashHandler.ts +130 -0
- package/src/app/delegationStore.ts +77 -0
- package/src/app/gatewayContext.tsx +19 -0
- package/src/app/gatewayRecovery.ts +35 -0
- package/src/app/inputSelectionStore.ts +15 -0
- package/src/app/interfaces.ts +394 -0
- package/src/app/overlayStore.ts +53 -0
- package/src/app/scroll.ts +71 -0
- package/src/app/setupHandoff.ts +54 -0
- package/src/app/slash/commands/core.ts +648 -0
- package/src/app/slash/commands/credits.ts +57 -0
- package/src/app/slash/commands/debug.ts +48 -0
- package/src/app/slash/commands/ops.ts +717 -0
- package/src/app/slash/commands/session.ts +554 -0
- package/src/app/slash/commands/setup.ts +20 -0
- package/src/app/slash/registry.ts +20 -0
- package/src/app/slash/types.ts +21 -0
- package/src/app/spawnHistoryStore.ts +159 -0
- package/src/app/turnController.ts +866 -0
- package/src/app/turnStore.ts +85 -0
- package/src/app/uiStore.ts +44 -0
- package/src/app/useComposerState.ts +367 -0
- package/src/app/useConfigSync.ts +288 -0
- package/src/app/useInputHandlers.ts +576 -0
- package/src/app/useLongRunToolCharms.ts +69 -0
- package/src/app/useMainApp.ts +1039 -0
- package/src/app/useSessionLifecycle.ts +366 -0
- package/src/app/useSubmission.ts +429 -0
- package/src/app.tsx +25 -0
- package/src/banner.ts +93 -0
- package/src/components/activeSessionSwitcher.tsx +635 -0
- package/src/components/agentsOverlay.tsx +1073 -0
- package/src/components/appChrome.tsx +554 -0
- package/src/components/appLayout.tsx +444 -0
- package/src/components/appOverlays.tsx +254 -0
- package/src/components/branding.tsx +466 -0
- package/src/components/fpsOverlay.tsx +30 -0
- package/src/components/helpHint.tsx +73 -0
- package/src/components/markdown.tsx +1119 -0
- package/src/components/maskedPrompt.tsx +34 -0
- package/src/components/messageLine.tsx +237 -0
- package/src/components/modelPicker.tsx +527 -0
- package/src/components/overlayControls.tsx +50 -0
- package/src/components/pluginsHub.tsx +238 -0
- package/src/components/prompts.tsx +276 -0
- package/src/components/queuedMessages.tsx +64 -0
- package/src/components/sessionPicker.tsx +227 -0
- package/src/components/skillsHub.tsx +308 -0
- package/src/components/streamingAssistant.tsx +110 -0
- package/src/components/streamingMarkdown.tsx +174 -0
- package/src/components/textInput.tsx +1340 -0
- package/src/components/themed.tsx +30 -0
- package/src/components/thinking.tsx +1224 -0
- package/src/components/todoPanel.tsx +93 -0
- package/src/config/env.ts +64 -0
- package/src/config/limits.ts +13 -0
- package/src/config/timing.ts +6 -0
- package/src/content/charms.ts +1 -0
- package/src/content/faces.ts +17 -0
- package/src/content/fortunes.ts +30 -0
- package/src/content/hotkeys.ts +37 -0
- package/src/content/placeholders.ts +13 -0
- package/src/content/setup.ts +17 -0
- package/src/content/verbs.ts +38 -0
- package/src/domain/blockLayout.ts +146 -0
- package/src/domain/details.ts +76 -0
- package/src/domain/messages.ts +91 -0
- package/src/domain/paths.ts +16 -0
- package/src/domain/providers.ts +11 -0
- package/src/domain/roles.ts +9 -0
- package/src/domain/slash.ts +10 -0
- package/src/domain/usage.ts +3 -0
- package/src/domain/viewport.ts +51 -0
- package/src/entry.tsx +104 -0
- package/src/gatewayClient.ts +730 -0
- package/src/gatewayTypes.ts +568 -0
- package/src/hooks/useCompletion.ts +112 -0
- package/src/hooks/useGitBranch.ts +72 -0
- package/src/hooks/useInputHistory.ts +11 -0
- package/src/hooks/useQueue.ts +76 -0
- package/src/hooks/useVirtualHistory.ts +554 -0
- package/src/lib/circularBuffer.ts +48 -0
- package/src/lib/clipboard.ts +182 -0
- package/src/lib/editor.test.ts +74 -0
- package/src/lib/editor.ts +47 -0
- package/src/lib/emoji.ts +55 -0
- package/src/lib/externalCli.ts +16 -0
- package/src/lib/externalLink.ts +435 -0
- package/src/lib/forceTruecolor.ts +60 -0
- package/src/lib/fpsStore.ts +51 -0
- package/src/lib/fuzzy.test.ts +109 -0
- package/src/lib/fuzzy.ts +177 -0
- package/src/lib/gracefulExit.ts +47 -0
- package/src/lib/history.ts +82 -0
- package/src/lib/inputMetrics.ts +203 -0
- package/src/lib/liveProgress.test.ts +116 -0
- package/src/lib/liveProgress.ts +79 -0
- package/src/lib/mathUnicode.ts +770 -0
- package/src/lib/memory.test.ts +155 -0
- package/src/lib/memory.ts +188 -0
- package/src/lib/memoryMonitor.ts +109 -0
- package/src/lib/messages.test.ts +29 -0
- package/src/lib/messages.ts +8 -0
- package/src/lib/openExternalUrl.test.ts +217 -0
- package/src/lib/openExternalUrl.ts +158 -0
- package/src/lib/osc52.ts +73 -0
- package/src/lib/parentLog.ts +57 -0
- package/src/lib/perfPane.tsx +107 -0
- package/src/lib/platform.ts +409 -0
- package/src/lib/precisionWheel.ts +48 -0
- package/src/lib/prompt.ts +35 -0
- package/src/lib/reasoning.ts +55 -0
- package/src/lib/rpc.ts +41 -0
- package/src/lib/subagentTree.ts +355 -0
- package/src/lib/syntax.ts +117 -0
- package/src/lib/terminalModes.ts +51 -0
- package/src/lib/terminalParity.ts +78 -0
- package/src/lib/terminalSetup.ts +444 -0
- package/src/lib/termux.ts +29 -0
- package/src/lib/text.test.ts +18 -0
- package/src/lib/text.ts +339 -0
- package/src/lib/todo.test.ts +21 -0
- package/src/lib/todo.ts +9 -0
- package/src/lib/viewportStore.ts +124 -0
- package/src/lib/virtualHeights.ts +145 -0
- package/src/lib/wheelAccel.ts +190 -0
- package/src/protocol/interpolation.ts +3 -0
- package/src/protocol/paste.ts +1 -0
- package/src/theme.ts +589 -0
- package/src/types/nastech-ink.d.ts +176 -0
- package/src/types.ts +212 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { applyPrintableInsert, shouldRouteMultiCharInputAsPaste } from '../components/textInput.js'
|
|
4
|
+
|
|
5
|
+
describe('applyPrintableInsert', () => {
|
|
6
|
+
it('applies non-bracketed multi-character bursts immediately', () => {
|
|
7
|
+
const burst = applyPrintableInsert('abc', 3, 'xxxxx')
|
|
8
|
+
|
|
9
|
+
const repeated = [...'xxxxx'].reduce(
|
|
10
|
+
(state, ch) => applyPrintableInsert(state.value, state.cursor, ch)!,
|
|
11
|
+
{ cursor: 3, value: 'abc' }
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
expect(burst).toEqual({ cursor: 8, value: 'abcxxxxx' })
|
|
15
|
+
expect(burst).toEqual(repeated)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('replaces the selected range for burst input', () => {
|
|
19
|
+
expect(applyPrintableInsert('abZZef', 4, 'cd', { end: 4, start: 2 })).toEqual({
|
|
20
|
+
cursor: 4,
|
|
21
|
+
value: 'abcdef'
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('rejects control or escape-bearing input', () => {
|
|
26
|
+
expect(applyPrintableInsert('abc', 3, '\x1b[200~pasted')).toBeNull()
|
|
27
|
+
expect(applyPrintableInsert('abc', 3, '\t')).toBeNull()
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('shouldRouteMultiCharInputAsPaste', () => {
|
|
32
|
+
it('keeps newline-bearing chunks on the paste path', () => {
|
|
33
|
+
expect(shouldRouteMultiCharInputAsPaste('hello\nworld')).toBe(true)
|
|
34
|
+
expect(shouldRouteMultiCharInputAsPaste('hello\r\nworld'.replace(/\r\n/g, '\n'))).toBe(true)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('treats repeated printable key bursts as immediate input', () => {
|
|
38
|
+
expect(shouldRouteMultiCharInputAsPaste('xxxxx')).toBe(false)
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from 'vitest'
|
|
6
|
+
|
|
7
|
+
// Locate textInput.tsx relative to this test file so the assertion
|
|
8
|
+
// survives moves of the test fixture itself.
|
|
9
|
+
const TEXT_INPUT_PATH = join(dirname(fileURLToPath(import.meta.url)), '..', 'components', 'textInput.tsx')
|
|
10
|
+
const source = readFileSync(TEXT_INPUT_PATH, 'utf8')
|
|
11
|
+
|
|
12
|
+
// Closes Copilot follow-up on PR #26717: the original cursor-drift
|
|
13
|
+
// fix bumped Ink's displayCursor / cursorDeclaration on fast-echo, but
|
|
14
|
+
// if TextInput itself re-renders before the deferred 16ms `setCur`
|
|
15
|
+
// flushes (parent state change, status-bar tick, spinner) the layout
|
|
16
|
+
// effect inside `useDeclaredCursor` re-publishes a declaration
|
|
17
|
+
// computed from the STALE React `cur` state and clobbers the Ink-level
|
|
18
|
+
// bump. The fix is structural: read `curRef.current` (always
|
|
19
|
+
// up-to-date) when computing the layout, not the `cur` state.
|
|
20
|
+
//
|
|
21
|
+
// This file pins that invariant. Switching back to `cur` state — or
|
|
22
|
+
// re-introducing a memo keyed on `cur` that uses `curRef.current`
|
|
23
|
+
// inside but stops re-computing on rerender — is a regression and
|
|
24
|
+
// should be caught here, not via a flaky integration test that mounts
|
|
25
|
+
// Ink + stdin.
|
|
26
|
+
describe('textInput cursor-layout source of truth', () => {
|
|
27
|
+
it('reads curRef.current (not the cur React state) for cursorLayout', () => {
|
|
28
|
+
// The line we care about. We allow whitespace / formatting drift,
|
|
29
|
+
// but the call itself must use `curRef.current`.
|
|
30
|
+
expect(source).toMatch(/cursorLayout\(\s*display\s*,\s*curRef\.current\s*,\s*columns\s*\)/)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('does not pass the bare `cur` React state into cursorLayout', () => {
|
|
34
|
+
// Any `cursorLayout(display, cur, columns)` invocation would
|
|
35
|
+
// reintroduce the stale-declaration window.
|
|
36
|
+
expect(source).not.toMatch(/cursorLayout\(\s*display\s*,\s*cur\s*,\s*columns\s*\)/)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('keeps the fast-echo notifier calls paired with the stdout writes', () => {
|
|
40
|
+
// Both fast-echo paths must call noteCursorAdvance, otherwise Ink
|
|
41
|
+
// never learns about the out-of-band write and drifts again. We
|
|
42
|
+
// tolerate explanatory comments in between (the rationale block is
|
|
43
|
+
// intentionally long), but the pairing itself must hold.
|
|
44
|
+
const backspacePattern = /stdout!\.write\(['"`]\\b \\b['"`]\)[\s\S]{0,1000}?noteCursorAdvance\(-1\)/
|
|
45
|
+
expect(source).toMatch(backspacePattern)
|
|
46
|
+
|
|
47
|
+
const appendPattern = /stdout!\.write\(text\)[\s\S]{0,1000}?noteCursorAdvance\(text\.length\)/
|
|
48
|
+
expect(source).toMatch(appendPattern)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { canFastAppendShape, canFastBackspaceShape, supportsFastEchoTerminal } from '../components/textInput.js'
|
|
4
|
+
|
|
5
|
+
// The fast-echo path bypasses Ink and writes characters directly to stdout
|
|
6
|
+
// for the common case of typing plain English at the end of the line. These
|
|
7
|
+
// tests pin the shape preconditions that make that bypass safe.
|
|
8
|
+
//
|
|
9
|
+
// Regression intent: any non-ASCII text — Vietnamese precomposed letters
|
|
10
|
+
// (one grapheme, `text.length === 1`, `stringWidth === 1`, but produced
|
|
11
|
+
// via IME composition across multiple keystrokes), combining marks
|
|
12
|
+
// (zero width), CJK (double width), emoji (variable width), or anything
|
|
13
|
+
// that could be produced by an in-flight IME composition — must NOT
|
|
14
|
+
// take the bypass. Closes:
|
|
15
|
+
// - "TUI is experiencing font errors when using Unicode to type Vietnamese"
|
|
16
|
+
// - #5221 TUI input box renders incorrectly for CJK / East-Asian wide
|
|
17
|
+
// - #7443 CLI TUI renders and deletes Chinese characters incorrectly
|
|
18
|
+
// - #17602 / #17603 Chinese text scattering / ghosting
|
|
19
|
+
|
|
20
|
+
describe('canFastAppendShape', () => {
|
|
21
|
+
const COLS = 40
|
|
22
|
+
|
|
23
|
+
it('accepts plain ASCII appended at end of single-line input', () => {
|
|
24
|
+
expect(canFastAppendShape('hello', 5, 'x', COLS, 5)).toBe(true)
|
|
25
|
+
expect(canFastAppendShape('hello', 5, ' world', COLS, 5)).toBe(true)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('rejects when cursor is not at end of line', () => {
|
|
29
|
+
expect(canFastAppendShape('hello', 3, 'x', COLS, 5)).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('rejects when current is empty (placeholder render path needed)', () => {
|
|
33
|
+
expect(canFastAppendShape('', 0, 'x', COLS, 0)).toBe(false)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('rejects when current contains a newline (multi-line layout)', () => {
|
|
37
|
+
expect(canFastAppendShape('hi\nthere', 8, 'x', COLS, 5)).toBe(false)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('rejects when appending would hit the wrap column', () => {
|
|
41
|
+
// Reaching cols on append must trigger a wrap, which the bypass
|
|
42
|
+
// cannot draw. Stay strictly below cols.
|
|
43
|
+
expect(canFastAppendShape('hello', 5, 'x', 6, 5)).toBe(false)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// -- Regression coverage: Vietnamese / combining marks / IME --
|
|
47
|
+
|
|
48
|
+
it('rejects Vietnamese precomposed letter ề (U+1EC1) — IME composition path', () => {
|
|
49
|
+
// 'ề' is one grapheme, length 1, width 1, but Vietnamese Telex/IME
|
|
50
|
+
// produces it via a multi-key composition. Fast-echo would commit the
|
|
51
|
+
// intermediate state to stdout and desync once the final commit
|
|
52
|
+
// arrives.
|
|
53
|
+
expect(canFastAppendShape('hello', 5, 'ề', COLS, 5)).toBe(false)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('rejects Vietnamese tone marks ă, ơ, ư (Latin-Extended-A/B)', () => {
|
|
57
|
+
for (const ch of ['ă', 'ắ', 'ơ', 'ờ', 'ư', 'ự']) {
|
|
58
|
+
expect(canFastAppendShape('hello', 5, ch, COLS, 5)).toBe(false)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('rejects NFD combining marks (U+0300 grave, U+0301 acute, U+0302 circumflex)', () => {
|
|
63
|
+
// Decomposed Vietnamese: 'e' + combining circumflex + combining grave
|
|
64
|
+
// = 'ề'. Each combining mark is zero-width but length 1; without the
|
|
65
|
+
// ASCII guard the second/third keypress would be fast-echoed and
|
|
66
|
+
// desync the cell column.
|
|
67
|
+
expect(canFastAppendShape('hello', 5, '\u0300', COLS, 5)).toBe(false)
|
|
68
|
+
expect(canFastAppendShape('hello', 5, '\u0301', COLS, 5)).toBe(false)
|
|
69
|
+
expect(canFastAppendShape('hello', 5, '\u0302', COLS, 5)).toBe(false)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('rejects CJK (East-Asian wide) characters', () => {
|
|
73
|
+
expect(canFastAppendShape('hello', 5, '你', COLS, 5)).toBe(false)
|
|
74
|
+
expect(canFastAppendShape('hello', 5, '日本', COLS, 5)).toBe(false)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('rejects emoji', () => {
|
|
78
|
+
expect(canFastAppendShape('hello', 5, '🙂', COLS, 5)).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('rejects ANSI-bearing or control text', () => {
|
|
82
|
+
expect(canFastAppendShape('hello', 5, '\x1b[31m', COLS, 5)).toBe(false)
|
|
83
|
+
expect(canFastAppendShape('hello', 5, '\t', COLS, 5)).toBe(false)
|
|
84
|
+
expect(canFastAppendShape('hello', 5, '\x7f', COLS, 5)).toBe(false)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('rejects NBSP and Latin-1 letters that would change the line shape', () => {
|
|
88
|
+
expect(canFastAppendShape('hello', 5, '\u00a0', COLS, 5)).toBe(false)
|
|
89
|
+
expect(canFastAppendShape('hello', 5, 'é', COLS, 5)).toBe(false)
|
|
90
|
+
expect(canFastAppendShape('hello', 5, 'ñ', COLS, 5)).toBe(false)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
describe('canFastBackspaceShape', () => {
|
|
95
|
+
it('accepts deleting the last ASCII char', () => {
|
|
96
|
+
expect(canFastBackspaceShape('hello', 5)).toBe(true)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('rejects when cursor is not at end', () => {
|
|
100
|
+
expect(canFastBackspaceShape('hello', 3)).toBe(false)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('rejects when there is nothing to delete', () => {
|
|
104
|
+
expect(canFastBackspaceShape('', 0)).toBe(false)
|
|
105
|
+
expect(canFastBackspaceShape('hello', 0)).toBe(false)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('rejects when value contains a newline', () => {
|
|
109
|
+
expect(canFastBackspaceShape('hi\nthere', 8)).toBe(false)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('rejects deleting Vietnamese precomposed letter ề', () => {
|
|
113
|
+
// The "\b \b" shortcut clears one terminal cell; that's fine for a
|
|
114
|
+
// 1-cell ASCII char but if the previous grapheme is a Vietnamese
|
|
115
|
+
// letter that the IME may still be holding open, we want Ink to
|
|
116
|
+
// re-render so composition state stays consistent.
|
|
117
|
+
expect(canFastBackspaceShape('helloề', 'helloề'.length)).toBe(false)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('rejects deleting a CJK character (2 cells)', () => {
|
|
121
|
+
expect(canFastBackspaceShape('hi你', 'hi你'.length)).toBe(false)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('rejects deleting a NFD-composed grapheme with combining marks', () => {
|
|
125
|
+
// 'e' + U+0302 (circumflex) + U+0300 (grave) — final grapheme is one
|
|
126
|
+
// cluster but the previous-grapheme slice is multi-codepoint. Width
|
|
127
|
+
// is 1 but the bypass would be unsafe because the rendered cell
|
|
128
|
+
// already contained the combined glyph.
|
|
129
|
+
const s = 'hello' + 'e\u0302\u0300'
|
|
130
|
+
expect(canFastBackspaceShape(s, s.length)).toBe(false)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('rejects deleting an emoji', () => {
|
|
134
|
+
expect(canFastBackspaceShape('hi🙂', 'hi🙂'.length)).toBe(false)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Closes Copilot PR #26717 round 3: the "\b \b" sequence cannot move
|
|
138
|
+
// the terminal cursor onto the previous visual row across a
|
|
139
|
+
// soft-wrap boundary. When the caret sits at visual column 0 of a
|
|
140
|
+
// wrapped row (column == 0 in the computed cursor layout), backspace
|
|
141
|
+
// would leave the physical cursor in place while the logical caret
|
|
142
|
+
// moves up to the end of the previous visual line — desyncing both
|
|
143
|
+
// Ink's displayCursor model and the user-visible position. The fast
|
|
144
|
+
// path must fall through in that case so the normal Ink render path
|
|
145
|
+
// can lay out the correct cursor position.
|
|
146
|
+
it('rejects fast-backspace at a soft-wrap boundary when columns is known', () => {
|
|
147
|
+
// value width 6 in a column of 6 → cursorLayout produces (line 1, col 0)
|
|
148
|
+
// i.e. the caret has overflowed onto the next visual line.
|
|
149
|
+
const value = 'hello '
|
|
150
|
+
expect(canFastBackspaceShape(value, value.length, 6)).toBe(false)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('rejects fast-backspace at an exact multiple of columns (wide wrap)', () => {
|
|
154
|
+
// 12 chars at width 6 → two full visual rows, caret at (line 2, col 0).
|
|
155
|
+
const value = 'abcdefghijkl'
|
|
156
|
+
expect(canFastBackspaceShape(value, value.length, 6)).toBe(false)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('still accepts fast-backspace inside a wrapped line', () => {
|
|
160
|
+
// Caret mid-visual-line — "\b \b" can move the cursor one cell left
|
|
161
|
+
// without crossing a wrap boundary.
|
|
162
|
+
expect(canFastBackspaceShape('hello world', 'hello world'.length, 20)).toBe(true)
|
|
163
|
+
expect(canFastBackspaceShape('abcdefghi', 9, 6)).toBe(true) // visual line 1, col 3 → ok
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('skips the wrap-boundary check when columns is omitted (legacy contract)', () => {
|
|
167
|
+
// Callers that don't pass `columns` fall back to the pre-wrap-aware
|
|
168
|
+
// behavior — the function does NOT magically reject anything that
|
|
169
|
+
// could be a wrap boundary without the width. Production callers
|
|
170
|
+
// must always pass `columns`; this case is for unit tests of the
|
|
171
|
+
// pre-wrap shape contract.
|
|
172
|
+
expect(canFastBackspaceShape('hello ', 'hello '.length)).toBe(true)
|
|
173
|
+
})
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
describe('supportsFastEchoTerminal', () => {
|
|
177
|
+
it('disables fast-echo in Apple Terminal', () => {
|
|
178
|
+
expect(supportsFastEchoTerminal({ TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBe(false)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('disables fast-echo by default in Termux mode', () => {
|
|
182
|
+
expect(
|
|
183
|
+
supportsFastEchoTerminal({ TERMUX_VERSION: '0.118.0', PREFIX: '/data/data/com.termux/files/usr' } as NodeJS.ProcessEnv)
|
|
184
|
+
).toBe(false)
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it('allows explicit Termux fast-echo opt-in via env override', () => {
|
|
188
|
+
expect(
|
|
189
|
+
supportsFastEchoTerminal({
|
|
190
|
+
NASTECH_TUI_TERMUX_FAST_ECHO: '1',
|
|
191
|
+
TERMUX_VERSION: '0.118.0'
|
|
192
|
+
} as NodeJS.ProcessEnv)
|
|
193
|
+
).toBe(true)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('keeps fast-echo enabled in VS Code and unknown non-Termux terminals', () => {
|
|
197
|
+
expect(supportsFastEchoTerminal({ TERM_PROGRAM: 'vscode' } as NodeJS.ProcessEnv)).toBe(true)
|
|
198
|
+
expect(supportsFastEchoTerminal({ TERM: 'xterm-256color' } as NodeJS.ProcessEnv)).toBe(true)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { lineNav } from '../components/textInput.js'
|
|
4
|
+
|
|
5
|
+
describe('lineNav', () => {
|
|
6
|
+
it('returns null for single-line input (up)', () => {
|
|
7
|
+
expect(lineNav('hello world', 6, -1)).toBeNull()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('returns null for single-line input (down)', () => {
|
|
11
|
+
expect(lineNav('hello world', 6, 1)).toBeNull()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('returns null when cursor already on first line of a multiline block', () => {
|
|
15
|
+
expect(lineNav('one\ntwo\nthree', 2, -1)).toBeNull()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('returns null when cursor on last line of a multiline block', () => {
|
|
19
|
+
expect(lineNav('one\ntwo\nthree', 10, 1)).toBeNull()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('moves cursor up one line preserving column', () => {
|
|
23
|
+
// "hello\nworld" — cursor at col 3 of line 1 ('l' in world) → col 3 of line 0 ('l' in hello)
|
|
24
|
+
expect(lineNav('hello\nworld', 9, -1)).toBe(3)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('moves cursor down one line preserving column', () => {
|
|
28
|
+
// cursor at col 2 of line 0 → col 2 of line 1
|
|
29
|
+
expect(lineNav('hello\nworld', 2, 1)).toBe(8)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('clamps to end of shorter destination line on up', () => {
|
|
33
|
+
// col 10 on long line → clamp to end of short line "abc"
|
|
34
|
+
const s = 'abc\nlong long text'
|
|
35
|
+
const from = 14
|
|
36
|
+
|
|
37
|
+
expect(lineNav(s, from, -1)).toBe(3)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('clamps to end of shorter destination line on down', () => {
|
|
41
|
+
// col 10 on line 0 → clamp to end of "abc" on line 1
|
|
42
|
+
const s = 'long long text\nabc'
|
|
43
|
+
|
|
44
|
+
expect(lineNav(s, 10, 1)).toBe(18)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('handles empty lines correctly', () => {
|
|
48
|
+
// "a\n\nb" — cursor at line 2 (b) → up to empty line 1
|
|
49
|
+
expect(lineNav('a\n\nb', 3, -1)).toBe(2)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('handles leading newline without crashing', () => {
|
|
53
|
+
expect(lineNav('\nfoo', 2, -1)).toBe(0)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { shouldPassThroughToGlobalHandler, shouldPreserveCtrlJNewline } from '../components/textInput.js'
|
|
4
|
+
import { DEFAULT_VOICE_RECORD_KEY, parseVoiceRecordKey } from '../lib/platform.js'
|
|
5
|
+
|
|
6
|
+
const key = (overrides: Record<string, unknown> = {}) =>
|
|
7
|
+
({ ctrl: false, meta: false, ...overrides }) as any
|
|
8
|
+
|
|
9
|
+
describe('shouldPreserveCtrlJNewline', () => {
|
|
10
|
+
it('preserves Ctrl+J as newline in Ghostty even when tmux masks TERM/TERM_PROGRAM', () => {
|
|
11
|
+
expect(
|
|
12
|
+
shouldPreserveCtrlJNewline({
|
|
13
|
+
GHOSTTY_RESOURCES_DIR: '/usr/share/ghostty',
|
|
14
|
+
TERM: 'tmux-256color',
|
|
15
|
+
TERM_PROGRAM: 'tmux'
|
|
16
|
+
})
|
|
17
|
+
).toBe(true)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('keeps bare local POSIX LF-compatible prompts submitting on Ctrl+J', () => {
|
|
21
|
+
expect(shouldPreserveCtrlJNewline({ TERM: 'xterm-256color' })).toBe(false)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('shouldPassThroughToGlobalHandler', () => {
|
|
26
|
+
it('passes through the configured voice shortcut while composer is focused', () => {
|
|
27
|
+
expect(
|
|
28
|
+
shouldPassThroughToGlobalHandler('o', key({ ctrl: true }), parseVoiceRecordKey('ctrl+o'))
|
|
29
|
+
).toBe(true)
|
|
30
|
+
expect(
|
|
31
|
+
shouldPassThroughToGlobalHandler('r', key({ meta: true }), parseVoiceRecordKey('alt+r'))
|
|
32
|
+
).toBe(true)
|
|
33
|
+
expect(
|
|
34
|
+
shouldPassThroughToGlobalHandler(' ', key({ ctrl: true }), parseVoiceRecordKey('ctrl+space'))
|
|
35
|
+
).toBe(true)
|
|
36
|
+
expect(
|
|
37
|
+
shouldPassThroughToGlobalHandler('', key({ ctrl: true, return: true }), parseVoiceRecordKey('ctrl+enter'))
|
|
38
|
+
).toBe(true)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('keeps the legacy default pass-through when no custom key is provided', () => {
|
|
42
|
+
expect(shouldPassThroughToGlobalHandler('b', key({ ctrl: true }), DEFAULT_VOICE_RECORD_KEY)).toBe(true)
|
|
43
|
+
expect(shouldPassThroughToGlobalHandler('b', key({ ctrl: true }))).toBe(true)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('does not swallow ordinary typing keys', () => {
|
|
47
|
+
expect(shouldPassThroughToGlobalHandler('h', key(), parseVoiceRecordKey('ctrl+o'))).toBe(false)
|
|
48
|
+
expect(shouldPassThroughToGlobalHandler('o', key(), parseVoiceRecordKey('ctrl+o'))).toBe(false)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('always passes through non-voice global control keys', () => {
|
|
52
|
+
expect(shouldPassThroughToGlobalHandler('c', key({ ctrl: true }))).toBe(true)
|
|
53
|
+
expect(shouldPassThroughToGlobalHandler('x', key({ ctrl: true }))).toBe(true)
|
|
54
|
+
expect(shouldPassThroughToGlobalHandler('', key({ escape: true }))).toBe(true)
|
|
55
|
+
expect(shouldPassThroughToGlobalHandler('', key({ tab: true }))).toBe(true)
|
|
56
|
+
expect(shouldPassThroughToGlobalHandler('', key({ pageUp: true }))).toBe(true)
|
|
57
|
+
expect(shouldPassThroughToGlobalHandler('', key({ pageDown: true }))).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { decideRightClickAction } from '../components/textInput.js'
|
|
4
|
+
|
|
5
|
+
describe('decideRightClickAction', () => {
|
|
6
|
+
it('returns paste when there is no selection', () => {
|
|
7
|
+
expect(decideRightClickAction('hello world', null)).toEqual({ action: 'paste' })
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('returns paste for a collapsed (empty) range', () => {
|
|
11
|
+
expect(decideRightClickAction('hello world', { end: 5, start: 5 })).toEqual({
|
|
12
|
+
action: 'paste'
|
|
13
|
+
})
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('copies the slice when range covers non-empty text', () => {
|
|
17
|
+
expect(decideRightClickAction('hello world', { end: 5, start: 0 })).toEqual({
|
|
18
|
+
action: 'copy',
|
|
19
|
+
text: 'hello'
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('copies a middle slice', () => {
|
|
24
|
+
expect(decideRightClickAction('hello world', { end: 11, start: 6 })).toEqual({
|
|
25
|
+
action: 'copy',
|
|
26
|
+
text: 'world'
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('falls back to paste when slice is empty (out-of-range indices)', () => {
|
|
31
|
+
expect(decideRightClickAction('', { end: 5, start: 0 })).toEqual({ action: 'paste' })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('handles unicode (emoji, CJK) in the slice', () => {
|
|
35
|
+
const value = 'hi 你好 🎉'
|
|
36
|
+
expect(decideRightClickAction(value, { end: 5, start: 3 })).toEqual({
|
|
37
|
+
action: 'copy',
|
|
38
|
+
text: '你好'
|
|
39
|
+
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('preserves leading/trailing whitespace in the copied slice', () => {
|
|
43
|
+
expect(decideRightClickAction(' spaced ', { end: 10, start: 0 })).toEqual({
|
|
44
|
+
action: 'copy',
|
|
45
|
+
text: ' spaced '
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { wrapAnsi } from '@nastechai/ink'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { offsetFromPosition } from '../components/textInput.js'
|
|
5
|
+
import { composerPromptWidth, cursorLayout, inputVisualHeight, stableComposerColumns } from '../lib/inputMetrics.js'
|
|
6
|
+
|
|
7
|
+
// Helper: compute the "end of text" position that wrap-ansi would render
|
|
8
|
+
// the input to. This is what Ink's <Text wrap="wrap"> uses, so cursorLayout
|
|
9
|
+
// MUST agree. Disagreement is the cursor-drift bug.
|
|
10
|
+
function wrapAnsiEndPosition(text: string, cols: number): { line: number; column: number } {
|
|
11
|
+
const wrapped = wrapAnsi(text, cols, { hard: true, trim: false })
|
|
12
|
+
const lines = wrapped.split('\n')
|
|
13
|
+
const last = lines[lines.length - 1] ?? ''
|
|
14
|
+
|
|
15
|
+
return { line: lines.length - 1, column: last.length }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('cursorLayout — word-wrap parity with wrap-ansi', () => {
|
|
19
|
+
it('places cursor mid-line at its column', () => {
|
|
20
|
+
expect(cursorLayout('hello world', 6, 40)).toEqual({ column: 6, line: 0 })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('places cursor at end of a non-full line', () => {
|
|
24
|
+
expect(cursorLayout('hi', 2, 10)).toEqual({ column: 2, line: 0 })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('does not push exact-fill text onto a phantom next line', () => {
|
|
28
|
+
// Regression: the previous hand-rolled wrap algorithm forced the cursor
|
|
29
|
+
// onto (line+1, 0) when the text exactly filled the row. wrap-ansi keeps
|
|
30
|
+
// it on the same row (no soft-wrap), so the cursor must too — otherwise
|
|
31
|
+
// useDeclaredCursor parks the hardware cursor below the last char and
|
|
32
|
+
// the user sees several blank cells between text and cursor block
|
|
33
|
+
// (#cursor-drift-multiline).
|
|
34
|
+
expect(cursorLayout('abcdefgh', 8, 8)).toEqual({ column: 8, line: 0 })
|
|
35
|
+
expect(cursorLayout('abcdefgh', 8, 8)).toEqual(wrapAnsiEndPosition('abcdefgh', 8))
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('keeps short words on the current line when they fit (no phantom wrap)', () => {
|
|
39
|
+
// wrap-ansi: "hello wo" at cols=8 stays as one line "hello wo".
|
|
40
|
+
// The old cursorLayout incorrectly pushed to (1,0) because column=8 hit
|
|
41
|
+
// the column>=width check, but that disagreed with what Ink actually
|
|
42
|
+
// rendered.
|
|
43
|
+
expect(cursorLayout('hello wo', 8, 8)).toEqual({ column: 8, line: 0 })
|
|
44
|
+
expect(cursorLayout('hello wo', 8, 8)).toEqual(wrapAnsiEndPosition('hello wo', 8))
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('moves words across wrap boundaries instead of splitting them', () => {
|
|
48
|
+
// "hello wor" at cols=8: wrap-ansi breaks at the space, "hello \nwor".
|
|
49
|
+
expect(cursorLayout('hello wor', 9, 8)).toEqual({ column: 3, line: 1 })
|
|
50
|
+
expect(cursorLayout('hello worl', 10, 8)).toEqual({ column: 4, line: 1 })
|
|
51
|
+
expect(cursorLayout('hello world', 11, 8)).toEqual({ column: 5, line: 1 })
|
|
52
|
+
|
|
53
|
+
// Each must match what wrap-ansi would actually render.
|
|
54
|
+
expect(cursorLayout('hello wor', 9, 8)).toEqual(wrapAnsiEndPosition('hello wor', 8))
|
|
55
|
+
expect(cursorLayout('hello worl', 10, 8)).toEqual(wrapAnsiEndPosition('hello worl', 8))
|
|
56
|
+
expect(cursorLayout('hello world', 11, 8)).toEqual(wrapAnsiEndPosition('hello world', 8))
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('wraps the next word instead of splitting it at the right edge', () => {
|
|
60
|
+
const text = 'hello world baby chickens are so cool its really rainy outside but wish'
|
|
61
|
+
|
|
62
|
+
expect(cursorLayout(text, text.length, 70)).toEqual({ column: 4, line: 1 })
|
|
63
|
+
expect(inputVisualHeight(text, 70)).toBe(2)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('honours explicit newlines', () => {
|
|
67
|
+
expect(cursorLayout('one\ntwo', 5, 40)).toEqual({ column: 1, line: 1 })
|
|
68
|
+
expect(cursorLayout('one\ntwo', 4, 40)).toEqual({ column: 0, line: 1 })
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('does not wrap when cursor is before the right edge', () => {
|
|
72
|
+
expect(cursorLayout('abcdefg', 7, 8)).toEqual({ column: 7, line: 0 })
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('matches wrap-ansi end-position for typing-style incremental input', () => {
|
|
76
|
+
// Pins the actual fix: type a long message char-by-char at a narrow
|
|
77
|
+
// width and assert the cursor follows wrap-ansi every step of the way.
|
|
78
|
+
// Before the fix, ~5 boundary positions per pass disagreed and Ink
|
|
79
|
+
// parked the cursor several cells past the last rendered character.
|
|
80
|
+
const MSG = 'on a new bb branch investigate and fix the cursor drift bug here'
|
|
81
|
+
|
|
82
|
+
for (const cols of [10, 14, 20, 30, 50, 80]) {
|
|
83
|
+
let acc = ''
|
|
84
|
+
|
|
85
|
+
for (const ch of MSG) {
|
|
86
|
+
acc += ch
|
|
87
|
+
expect(cursorLayout(acc, acc.length, cols)).toEqual(wrapAnsiEndPosition(acc, cols))
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe('input metrics helpers', () => {
|
|
94
|
+
it('computes visual height matching wrap-ansi line count', () => {
|
|
95
|
+
// Exact-fill text stays on one line in wrap-ansi (no phantom wrap), so
|
|
96
|
+
// visual height is 1. The previous implementation reported 2 here.
|
|
97
|
+
expect(inputVisualHeight('abcdefgh', 8)).toBe(1)
|
|
98
|
+
expect(inputVisualHeight('one\ntwo', 40)).toBe(2)
|
|
99
|
+
// Multi-line wrap case sanity
|
|
100
|
+
expect(inputVisualHeight('hello world', 8)).toBe(2)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('counts the prompt gap as its own cell', () => {
|
|
104
|
+
expect(composerPromptWidth('>')).toBe(2)
|
|
105
|
+
expect(composerPromptWidth('❯')).toBe(2)
|
|
106
|
+
expect(composerPromptWidth('Ψ >')).toBe(4)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('reserves gutters on wide panes without starving narrow composer width', () => {
|
|
110
|
+
expect(stableComposerColumns(100, 3)).toBe(93)
|
|
111
|
+
expect(stableComposerColumns(100, 5)).toBe(91)
|
|
112
|
+
expect(stableComposerColumns(10, 3)).toBe(5)
|
|
113
|
+
expect(stableComposerColumns(6, 3)).toBe(1)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('offsetFromPosition — word-wrap inverse of cursorLayout', () => {
|
|
118
|
+
it('returns 0 for empty input', () => {
|
|
119
|
+
expect(offsetFromPosition('', 0, 0, 10)).toBe(0)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('maps clicks within a single line', () => {
|
|
123
|
+
expect(offsetFromPosition('hello', 0, 3, 40)).toBe(3)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('maps clicks past end to value length', () => {
|
|
127
|
+
expect(offsetFromPosition('hi', 0, 10, 40)).toBe(2)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('maps clicks on a wrapped second row at cols boundary', () => {
|
|
131
|
+
// Long words still hard-wrap when there is no word boundary.
|
|
132
|
+
expect(offsetFromPosition('abcdefghij', 1, 0, 8)).toBe(8)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('maps clicks on a word-wrapped second row', () => {
|
|
136
|
+
// "hello world" at cols=8 wraps to "hello \nworld".
|
|
137
|
+
expect(offsetFromPosition('hello world', 1, 0, 8)).toBe(6)
|
|
138
|
+
expect(offsetFromPosition('hello world', 1, 3, 8)).toBe(9)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('maps clicks on the moved final word', () => {
|
|
142
|
+
const text = 'hello world baby chickens are so cool its really rainy outside but wish'
|
|
143
|
+
|
|
144
|
+
expect(offsetFromPosition(text, 1, 0, 70)).toBe(text.indexOf('wish'))
|
|
145
|
+
expect(offsetFromPosition(text, 1, 3, 70)).toBe(text.indexOf('wish') + 3)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('maps clicks past a \\n into the target line', () => {
|
|
149
|
+
expect(offsetFromPosition('one\ntwo', 1, 2, 40)).toBe(6)
|
|
150
|
+
})
|
|
151
|
+
})
|