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,101 @@
|
|
|
1
|
+
import { useContext, useMemo, useSyncExternalStore } from 'react'
|
|
2
|
+
|
|
3
|
+
import StdinContext from '../components/StdinContext.js'
|
|
4
|
+
import instances from '../instances.js'
|
|
5
|
+
import { type FocusMove, type SelectionState, shiftAnchor } from '../selection.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Access to text selection operations on the Ink instance (fullscreen only).
|
|
9
|
+
* Returns no-op functions when fullscreen mode is disabled.
|
|
10
|
+
*/
|
|
11
|
+
export function useSelection(): {
|
|
12
|
+
copySelection: () => Promise<string>
|
|
13
|
+
/** Copy without clearing the highlight (for copy-on-select). */
|
|
14
|
+
copySelectionNoClear: () => Promise<string>
|
|
15
|
+
clearSelection: () => void
|
|
16
|
+
hasSelection: () => boolean
|
|
17
|
+
/** Read the raw mutable selection state (for drag-to-scroll). */
|
|
18
|
+
getState: () => SelectionState | null
|
|
19
|
+
/** Subscribe to selection mutations (start/update/finish/clear). */
|
|
20
|
+
subscribe: (cb: () => void) => () => void
|
|
21
|
+
/** Shift the anchor row by dRow, clamped to [minRow, maxRow]. */
|
|
22
|
+
shiftAnchor: (dRow: number, minRow: number, maxRow: number) => void
|
|
23
|
+
/** Shift anchor AND focus by dRow (keyboard scroll: whole selection
|
|
24
|
+
* tracks content). Clamped points get col reset to the full-width edge
|
|
25
|
+
* since their content was captured by captureScrolledRows. Reads
|
|
26
|
+
* screen.width from the ink instance for the col-reset boundary. */
|
|
27
|
+
shiftSelection: (dRow: number, minRow: number, maxRow: number) => void
|
|
28
|
+
/** Keyboard selection extension (shift+arrow): move focus, anchor fixed.
|
|
29
|
+
* Left/right wrap across rows; up/down clamp at viewport edges. */
|
|
30
|
+
moveFocus: (move: FocusMove) => void
|
|
31
|
+
/** Capture text from rows about to scroll out of the viewport (call
|
|
32
|
+
* BEFORE scrollBy so the screen buffer still has the outgoing rows). */
|
|
33
|
+
captureScrolledRows: (firstRow: number, lastRow: number, side: 'above' | 'below') => void
|
|
34
|
+
/** Set the selection highlight bg color (theme-piping; solid bg
|
|
35
|
+
* replaces the old SGR-7 inverse so syntax highlighting stays readable
|
|
36
|
+
* under selection). Call once on mount + whenever theme changes. */
|
|
37
|
+
setSelectionBgColor: (color: string) => void
|
|
38
|
+
/** Monotonic counter incremented on every selection mutation. */
|
|
39
|
+
version: () => number
|
|
40
|
+
} {
|
|
41
|
+
// Look up the Ink instance via stdout — same pattern as instances map.
|
|
42
|
+
// StdinContext is available (it's always provided), and the Ink instance
|
|
43
|
+
// is keyed by stdout which we can get from process.stdout since there's
|
|
44
|
+
// only one Ink instance per process in practice.
|
|
45
|
+
useContext(StdinContext) // anchor to App subtree for hook rules
|
|
46
|
+
const ink = instances.get(process.stdout)
|
|
47
|
+
|
|
48
|
+
// Memoize so callers can safely use the return value in dependency arrays.
|
|
49
|
+
// ink is a singleton per stdout — stable across renders.
|
|
50
|
+
return useMemo(() => {
|
|
51
|
+
if (!ink) {
|
|
52
|
+
return {
|
|
53
|
+
copySelection: async () => '',
|
|
54
|
+
copySelectionNoClear: async () => '',
|
|
55
|
+
clearSelection: () => {},
|
|
56
|
+
hasSelection: () => false,
|
|
57
|
+
getState: () => null,
|
|
58
|
+
subscribe: () => () => {},
|
|
59
|
+
shiftAnchor: () => {},
|
|
60
|
+
shiftSelection: () => {},
|
|
61
|
+
moveFocus: () => {},
|
|
62
|
+
captureScrolledRows: () => {},
|
|
63
|
+
setSelectionBgColor: () => {},
|
|
64
|
+
version: () => 0
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
copySelection: () => ink.copySelection(),
|
|
70
|
+
copySelectionNoClear: () => ink.copySelectionNoClear(),
|
|
71
|
+
clearSelection: () => ink.clearTextSelection(),
|
|
72
|
+
hasSelection: () => ink.hasTextSelection(),
|
|
73
|
+
getState: () => ink.selection,
|
|
74
|
+
subscribe: (cb: () => void) => ink.subscribeToSelectionChange(cb),
|
|
75
|
+
shiftAnchor: (dRow: number, minRow: number, maxRow: number) => shiftAnchor(ink.selection, dRow, minRow, maxRow),
|
|
76
|
+
shiftSelection: (dRow, minRow, maxRow) => ink.shiftSelectionForScroll(dRow, minRow, maxRow),
|
|
77
|
+
moveFocus: (move: FocusMove) => ink.moveSelectionFocus(move),
|
|
78
|
+
captureScrolledRows: (firstRow, lastRow, side) => ink.captureScrolledRows(firstRow, lastRow, side),
|
|
79
|
+
setSelectionBgColor: (color: string) => ink.setSelectionBgColor(color),
|
|
80
|
+
version: () => ink.getSelectionVersion()
|
|
81
|
+
}
|
|
82
|
+
}, [ink])
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const NO_SUBSCRIBE = () => () => {}
|
|
86
|
+
const ALWAYS_FALSE = () => false
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Reactive selection-exists state. Re-renders the caller when a text
|
|
90
|
+
* selection is created or cleared. Always returns false outside
|
|
91
|
+
* fullscreen mode (selection is only available in alt-screen).
|
|
92
|
+
*/
|
|
93
|
+
export function useHasSelection(): boolean {
|
|
94
|
+
useContext(StdinContext)
|
|
95
|
+
const ink = instances.get(process.stdout)
|
|
96
|
+
|
|
97
|
+
return useSyncExternalStore(
|
|
98
|
+
ink ? ink.subscribeToSelectionChange : NO_SUBSCRIBE,
|
|
99
|
+
ink ? ink.hasTextSelection : ALWAYS_FALSE
|
|
100
|
+
)
|
|
101
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useContext, useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import { CLEAR_TAB_STATUS, supportsTabStatus, tabStatus, wrapForMultiplexer } from '../termio/osc.js'
|
|
4
|
+
import type { Color } from '../termio/types.js'
|
|
5
|
+
import { TerminalWriteContext } from '../useTerminalNotification.js'
|
|
6
|
+
|
|
7
|
+
export type TabStatusKind = 'idle' | 'busy' | 'waiting'
|
|
8
|
+
|
|
9
|
+
const rgb = (r: number, g: number, b: number): Color => ({
|
|
10
|
+
type: 'rgb',
|
|
11
|
+
r,
|
|
12
|
+
g,
|
|
13
|
+
b
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
// Per the OSC 21337 usage guide's suggested mapping.
|
|
17
|
+
const TAB_STATUS_PRESETS: Record<TabStatusKind, { indicator: Color; status: string; statusColor: Color }> = {
|
|
18
|
+
idle: {
|
|
19
|
+
indicator: rgb(0, 215, 95),
|
|
20
|
+
status: 'Idle',
|
|
21
|
+
statusColor: rgb(136, 136, 136)
|
|
22
|
+
},
|
|
23
|
+
busy: {
|
|
24
|
+
indicator: rgb(255, 149, 0),
|
|
25
|
+
status: 'Working…',
|
|
26
|
+
statusColor: rgb(255, 149, 0)
|
|
27
|
+
},
|
|
28
|
+
waiting: {
|
|
29
|
+
indicator: rgb(95, 135, 255),
|
|
30
|
+
status: 'Waiting',
|
|
31
|
+
statusColor: rgb(95, 135, 255)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Declaratively set the tab-status indicator (OSC 21337).
|
|
37
|
+
*
|
|
38
|
+
* Emits a colored dot + short status text to the tab sidebar. Terminals
|
|
39
|
+
* that don't support OSC 21337 discard the sequence silently, so this is
|
|
40
|
+
* safe to call unconditionally. Wrapped for tmux/screen passthrough.
|
|
41
|
+
*
|
|
42
|
+
* Pass `null` to opt out. If a status was previously set, transitioning to
|
|
43
|
+
* `null` emits CLEAR_TAB_STATUS so toggling off mid-session doesn't leave
|
|
44
|
+
* a stale dot. Process-exit cleanup is handled by ink.tsx's unmount path.
|
|
45
|
+
*/
|
|
46
|
+
export function useTabStatus(kind: TabStatusKind | null): void {
|
|
47
|
+
const writeRaw = useContext(TerminalWriteContext)
|
|
48
|
+
const prevKindRef = useRef<TabStatusKind | null>(null)
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
// When kind transitions from non-null to null (e.g. user toggles off
|
|
52
|
+
// showStatusInTerminalTab mid-session), clear the stale dot.
|
|
53
|
+
if (kind === null) {
|
|
54
|
+
if (prevKindRef.current !== null && writeRaw && supportsTabStatus()) {
|
|
55
|
+
writeRaw(wrapForMultiplexer(CLEAR_TAB_STATUS))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
prevKindRef.current = null
|
|
59
|
+
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
prevKindRef.current = kind
|
|
64
|
+
|
|
65
|
+
if (!writeRaw || !supportsTabStatus()) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
writeRaw(wrapForMultiplexer(tabStatus(TAB_STATUS_PRESETS[kind])))
|
|
70
|
+
}, [kind, writeRaw])
|
|
71
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
import TerminalFocusContext from '../components/TerminalFocusContext.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook to check if the terminal has focus.
|
|
7
|
+
*
|
|
8
|
+
* Uses DECSET 1004 focus reporting - the terminal sends escape sequences
|
|
9
|
+
* when it gains or loses focus. These are handled automatically
|
|
10
|
+
* by Ink and filtered from useInput.
|
|
11
|
+
*
|
|
12
|
+
* @returns true if the terminal is focused (or focus state is unknown)
|
|
13
|
+
*/
|
|
14
|
+
export function useTerminalFocus(): boolean {
|
|
15
|
+
const { isTerminalFocused } = useContext(TerminalFocusContext)
|
|
16
|
+
|
|
17
|
+
return isTerminalFocused
|
|
18
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useContext, useEffect } from 'react'
|
|
2
|
+
import stripAnsi from 'strip-ansi'
|
|
3
|
+
|
|
4
|
+
import { OSC, osc } from '../termio/osc.js'
|
|
5
|
+
import { TerminalWriteContext } from '../useTerminalNotification.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Declaratively set the terminal tab/window title.
|
|
9
|
+
*
|
|
10
|
+
* Pass a string to set the title. ANSI escape sequences are stripped
|
|
11
|
+
* automatically so callers don't need to know about terminal encoding.
|
|
12
|
+
* Pass `null` to opt out — the hook becomes a no-op and leaves the
|
|
13
|
+
* terminal title untouched.
|
|
14
|
+
*
|
|
15
|
+
* On Windows, uses `process.title` (classic conhost doesn't support OSC).
|
|
16
|
+
* Elsewhere, writes OSC 0 (set title+icon) via Ink's stdout.
|
|
17
|
+
*/
|
|
18
|
+
export function useTerminalTitle(title: string | null): void {
|
|
19
|
+
const writeRaw = useContext(TerminalWriteContext)
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (title === null || !writeRaw) {
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const clean = stripAnsi(title)
|
|
27
|
+
|
|
28
|
+
if (process.platform === 'win32') {
|
|
29
|
+
process.title = clean
|
|
30
|
+
} else {
|
|
31
|
+
writeRaw(osc(OSC.SET_TITLE_AND_ICON, clean))
|
|
32
|
+
}
|
|
33
|
+
}, [title, writeRaw])
|
|
34
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useCallback, useContext, useLayoutEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
import { TerminalSizeContext } from '../components/TerminalSizeContext.js'
|
|
4
|
+
import type { DOMElement } from '../dom.js'
|
|
5
|
+
|
|
6
|
+
type ViewportEntry = {
|
|
7
|
+
/**
|
|
8
|
+
* Whether the element is currently within the terminal viewport
|
|
9
|
+
*/
|
|
10
|
+
isVisible: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Hook to detect if a component is within the terminal viewport.
|
|
15
|
+
*
|
|
16
|
+
* Returns a callback ref and a viewport entry object.
|
|
17
|
+
* Attach the ref to the component you want to track.
|
|
18
|
+
*
|
|
19
|
+
* The entry is updated during the layout phase (useLayoutEffect) so callers
|
|
20
|
+
* always read fresh values during render. Visibility changes do NOT trigger
|
|
21
|
+
* re-renders on their own — callers that re-render for other reasons (e.g.
|
|
22
|
+
* animation ticks, state changes) will pick up the latest value naturally.
|
|
23
|
+
* This avoids infinite update loops when combined with other layout effects
|
|
24
|
+
* that also call setState.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const [ref, entry] = useTerminalViewport()
|
|
28
|
+
* return <Box ref={ref}><Animation enabled={entry.isVisible}>...</Animation></Box>
|
|
29
|
+
*/
|
|
30
|
+
export function useTerminalViewport(): [ref: (element: DOMElement | null) => void, entry: ViewportEntry] {
|
|
31
|
+
const terminalSize = useContext(TerminalSizeContext)
|
|
32
|
+
const elementRef = useRef<DOMElement | null>(null)
|
|
33
|
+
const entryRef = useRef<ViewportEntry>({ isVisible: true })
|
|
34
|
+
|
|
35
|
+
const setElement = useCallback((el: DOMElement | null) => {
|
|
36
|
+
elementRef.current = el
|
|
37
|
+
}, [])
|
|
38
|
+
|
|
39
|
+
// Runs on every render because yoga layout values can change
|
|
40
|
+
// without React being aware. Only updates the ref — no setState
|
|
41
|
+
// to avoid cascading re-renders during the commit phase.
|
|
42
|
+
// Walks the DOM ancestor chain fresh each time to avoid holding stale
|
|
43
|
+
// references after yoga tree rebuilds.
|
|
44
|
+
useLayoutEffect(() => {
|
|
45
|
+
const element = elementRef.current
|
|
46
|
+
|
|
47
|
+
if (!element?.yogaNode || !terminalSize) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const height = element.yogaNode.getComputedHeight()
|
|
52
|
+
const rows = terminalSize.rows
|
|
53
|
+
|
|
54
|
+
// Walk the DOM parent chain (not yoga.getParent()) so we can detect
|
|
55
|
+
// scroll containers and subtract their scrollTop. Yoga computes layout
|
|
56
|
+
// positions without scroll offset — scrollTop is applied at render time.
|
|
57
|
+
// Without this, an element inside a ScrollBox whose yoga position exceeds
|
|
58
|
+
// terminalRows would be considered offscreen even when scrolled into view
|
|
59
|
+
// (e.g., the spinner in fullscreen mode after enough messages accumulate).
|
|
60
|
+
let absoluteTop = element.yogaNode.getComputedTop()
|
|
61
|
+
let parent: DOMElement | undefined = element.parentNode
|
|
62
|
+
let root = element.yogaNode
|
|
63
|
+
|
|
64
|
+
while (parent) {
|
|
65
|
+
if (parent.yogaNode) {
|
|
66
|
+
absoluteTop += parent.yogaNode.getComputedTop()
|
|
67
|
+
root = parent.yogaNode
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// scrollTop is only ever set on scroll containers (by ScrollBox + renderer).
|
|
71
|
+
// Non-scroll nodes have undefined scrollTop → falsy fast-path.
|
|
72
|
+
if (parent.scrollTop) {
|
|
73
|
+
absoluteTop -= parent.scrollTop
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
parent = parent.parentNode
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Only the root's height matters
|
|
80
|
+
const screenHeight = root.getComputedHeight()
|
|
81
|
+
|
|
82
|
+
const bottom = absoluteTop + height
|
|
83
|
+
// When content overflows the viewport (screenHeight > rows), the
|
|
84
|
+
// cursor-restore at frame end scrolls one extra row into scrollback.
|
|
85
|
+
// log-update.ts accounts for this with scrollbackRows = viewportY + 1.
|
|
86
|
+
// We must match, otherwise an element at the boundary is considered
|
|
87
|
+
// "visible" here (animation keeps ticking) but its row is treated as
|
|
88
|
+
// scrollback by log-update (content change → full reset → flicker).
|
|
89
|
+
const cursorRestoreScroll = screenHeight > rows ? 1 : 0
|
|
90
|
+
const viewportY = Math.max(0, screenHeight - rows) + cursorRestoreScroll
|
|
91
|
+
const viewportBottom = viewportY + rows
|
|
92
|
+
const visible = bottom > viewportY && absoluteTop < viewportBottom
|
|
93
|
+
|
|
94
|
+
if (visible !== entryRef.current.isVisible) {
|
|
95
|
+
entryRef.current = { isVisible: visible }
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return [setElement, entryRef.current]
|
|
100
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { cellAtIndex, CellWidth, type Screen, setCellStyleId, type StylePool } from './screen.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Highlight every cell whose OSC 8 hyperlink matches `hoveredUrl` by inverting
|
|
5
|
+
* its style. This is the cursor-hover affordance for clickable links: terminal
|
|
6
|
+
* applications can't change the system mouse cursor, so we light up the link
|
|
7
|
+
* itself when the pointer is over it. Same overlay machinery as
|
|
8
|
+
* applySearchHighlight — post-layout, pure SGR, picked up by the diff.
|
|
9
|
+
*
|
|
10
|
+
* Returns true if any cell was highlighted. The caller decides whether to
|
|
11
|
+
* promote that into a full-frame damage request — for hover specifically,
|
|
12
|
+
* full damage is only useful on enter/leave/change transitions (so the
|
|
13
|
+
* previous frame's inverted cells get re-emitted), not on every steady-state
|
|
14
|
+
* frame the pointer sits on the link.
|
|
15
|
+
*/
|
|
16
|
+
export function applyHyperlinkHoverHighlight(
|
|
17
|
+
screen: Screen,
|
|
18
|
+
hoveredUrl: string | undefined,
|
|
19
|
+
stylePool: StylePool
|
|
20
|
+
): boolean {
|
|
21
|
+
if (!hoveredUrl) {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const w = screen.width
|
|
26
|
+
const height = screen.height
|
|
27
|
+
let applied = false
|
|
28
|
+
|
|
29
|
+
for (let row = 0; row < height; row++) {
|
|
30
|
+
const rowOff = row * w
|
|
31
|
+
|
|
32
|
+
for (let col = 0; col < w; col++) {
|
|
33
|
+
const cell = cellAtIndex(screen, rowOff + col)
|
|
34
|
+
|
|
35
|
+
// Skip SpacerTail — the head cell at col-1 owns the hyperlink, and
|
|
36
|
+
// setCellStyleId on the tail would split the styling of a wide-char
|
|
37
|
+
// glyph mid-cell. The head's restyle covers both halves.
|
|
38
|
+
if (cell.width === CellWidth.SpacerTail) {
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (cell.hyperlink !== hoveredUrl) {
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
applied = true
|
|
47
|
+
setCellStyleId(screen, col, row, stylePool.withInverse(cell.styleId))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return applied
|
|
52
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { EventEmitter } from 'events'
|
|
2
|
+
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { describe, expect, it } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import Text from './components/Text.js'
|
|
7
|
+
import Ink from './ink.js'
|
|
8
|
+
|
|
9
|
+
class FakeTty extends EventEmitter {
|
|
10
|
+
chunks: string[] = []
|
|
11
|
+
columns = 40
|
|
12
|
+
rows = 8
|
|
13
|
+
isTTY = true
|
|
14
|
+
|
|
15
|
+
write(chunk: string | Uint8Array, cb?: (err?: Error | null) => void): boolean {
|
|
16
|
+
this.chunks.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'))
|
|
17
|
+
cb?.()
|
|
18
|
+
|
|
19
|
+
return true
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makeInk() {
|
|
24
|
+
const stdout = new FakeTty()
|
|
25
|
+
const stdin = new FakeTty()
|
|
26
|
+
const stderr = new FakeTty()
|
|
27
|
+
|
|
28
|
+
const ink = new Ink({
|
|
29
|
+
exitOnCtrlC: false,
|
|
30
|
+
patchConsole: false,
|
|
31
|
+
stderr: stderr as unknown as NodeJS.WriteStream,
|
|
32
|
+
stdin: stdin as unknown as NodeJS.ReadStream,
|
|
33
|
+
stdout: stdout as unknown as NodeJS.WriteStream
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return { ink, stdout, stdin, stderr }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Cast helper instead of exposing __get*ForTest methods on production Ink —
|
|
40
|
+
// these are internal frame/cursor caches we only inspect from tests.
|
|
41
|
+
type InkPrivate = {
|
|
42
|
+
displayCursor: { x: number; y: number } | null
|
|
43
|
+
cursorDeclaration: { node: unknown; relativeX: number; relativeY: number } | null
|
|
44
|
+
frontFrame: { cursor: { x: number; y: number } }
|
|
45
|
+
}
|
|
46
|
+
const peek = (ink: Ink): InkPrivate => ink as unknown as InkPrivate
|
|
47
|
+
|
|
48
|
+
// Closes the cursor-drift bug: when TextInput's fast-echo path writes a
|
|
49
|
+
// printable character directly to stdout, the hardware cursor advances by
|
|
50
|
+
// one cell BUT Ink's `displayCursor` cache (used as the basis for the
|
|
51
|
+
// next frame's relative cursor preamble) wasn't being updated. On long
|
|
52
|
+
// sessions an unrelated re-render (status bar timer, streaming
|
|
53
|
+
// reasoning, etc.) would then park the hardware cursor N cells offset
|
|
54
|
+
// from the actual caret — visible as "extra whitespace between my last
|
|
55
|
+
// typed character and the cursor block".
|
|
56
|
+
describe('Ink.noteExternalCursorAdvance', () => {
|
|
57
|
+
it('bumps an already-tracked displayCursor by the given delta', () => {
|
|
58
|
+
const { ink } = makeInk()
|
|
59
|
+
|
|
60
|
+
ink.render(React.createElement(Text, null, 'hi'))
|
|
61
|
+
ink.onRender()
|
|
62
|
+
|
|
63
|
+
// Seed a known parked position directly. In production this is set by
|
|
64
|
+
// the cursor-park branch in onRender when a useDeclaredCursor caller
|
|
65
|
+
// commits a declaration; this test bypasses React for hermeticity.
|
|
66
|
+
peek(ink).displayCursor = { x: 5, y: 0 }
|
|
67
|
+
|
|
68
|
+
ink.noteExternalCursorAdvance(3)
|
|
69
|
+
expect(peek(ink).displayCursor).toEqual({ x: 8, y: 0 })
|
|
70
|
+
|
|
71
|
+
ink.noteExternalCursorAdvance(-1)
|
|
72
|
+
expect(peek(ink).displayCursor).toEqual({ x: 7, y: 0 })
|
|
73
|
+
|
|
74
|
+
ink.noteExternalCursorAdvance(0, 2)
|
|
75
|
+
expect(peek(ink).displayCursor).toEqual({ x: 7, y: 2 })
|
|
76
|
+
|
|
77
|
+
ink.unmount()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('seeds displayCursor from frontFrame.cursor when nothing was parked', () => {
|
|
81
|
+
const { ink } = makeInk()
|
|
82
|
+
|
|
83
|
+
ink.render(React.createElement(Text, null, 'hello'))
|
|
84
|
+
ink.onRender()
|
|
85
|
+
|
|
86
|
+
expect(peek(ink).displayCursor).toBeNull()
|
|
87
|
+
const base = { x: peek(ink).frontFrame.cursor.x, y: peek(ink).frontFrame.cursor.y }
|
|
88
|
+
|
|
89
|
+
ink.noteExternalCursorAdvance(4)
|
|
90
|
+
expect(peek(ink).displayCursor).toEqual({ x: base.x + 4, y: base.y })
|
|
91
|
+
|
|
92
|
+
ink.unmount()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('is a no-op when the delta is zero', () => {
|
|
96
|
+
const { ink } = makeInk()
|
|
97
|
+
|
|
98
|
+
ink.render(React.createElement(Text, null, 'hi'))
|
|
99
|
+
ink.onRender()
|
|
100
|
+
|
|
101
|
+
ink.noteExternalCursorAdvance(0)
|
|
102
|
+
expect(peek(ink).displayCursor).toBeNull()
|
|
103
|
+
|
|
104
|
+
ink.noteExternalCursorAdvance(0, 0)
|
|
105
|
+
expect(peek(ink).displayCursor).toBeNull()
|
|
106
|
+
|
|
107
|
+
ink.unmount()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('skips displayCursor on alt-screen — CSI H resets every frame', () => {
|
|
111
|
+
const { ink } = makeInk()
|
|
112
|
+
|
|
113
|
+
ink.setAltScreenActive(true)
|
|
114
|
+
ink.render(React.createElement(Text, null, 'hi'))
|
|
115
|
+
ink.onRender()
|
|
116
|
+
peek(ink).displayCursor = { x: 5, y: 0 }
|
|
117
|
+
|
|
118
|
+
ink.noteExternalCursorAdvance(3)
|
|
119
|
+
|
|
120
|
+
expect(peek(ink).displayCursor).toEqual({ x: 5, y: 0 })
|
|
121
|
+
|
|
122
|
+
ink.unmount()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// Closes Copilot follow-up on PR #26717: the default TUI wraps the
|
|
126
|
+
// composer in <AlternateScreen>, so alt-screen is the production
|
|
127
|
+
// path. CSI H only resets the log-update relative-move basis — the
|
|
128
|
+
// declared cursor target is still consulted by onRender's alt-screen
|
|
129
|
+
// park branch (`cursorPosition(row, col)` using rect + decl). So
|
|
130
|
+
// cursorDeclaration MUST advance on alt-screen too, even though
|
|
131
|
+
// displayCursor doesn't need to.
|
|
132
|
+
it('still advances cursorDeclaration on alt-screen', () => {
|
|
133
|
+
const { ink } = makeInk()
|
|
134
|
+
|
|
135
|
+
ink.setAltScreenActive(true)
|
|
136
|
+
ink.render(React.createElement(Text, null, 'hi'))
|
|
137
|
+
ink.onRender()
|
|
138
|
+
|
|
139
|
+
const fakeNode = {} as unknown as Record<string, unknown>
|
|
140
|
+
|
|
141
|
+
peek(ink).cursorDeclaration = { node: fakeNode, relativeX: 7, relativeY: 0 }
|
|
142
|
+
peek(ink).displayCursor = { x: 12, y: 0 }
|
|
143
|
+
|
|
144
|
+
ink.noteExternalCursorAdvance(3)
|
|
145
|
+
|
|
146
|
+
// displayCursor untouched on alt-screen
|
|
147
|
+
expect(peek(ink).displayCursor).toEqual({ x: 12, y: 0 })
|
|
148
|
+
// declaration still advanced — onRender's alt-screen park reads this
|
|
149
|
+
expect(peek(ink).cursorDeclaration).toEqual({ node: fakeNode, relativeX: 10, relativeY: 0 })
|
|
150
|
+
|
|
151
|
+
ink.unmount()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// Closes Copilot review feedback on PR #26717: even after the
|
|
155
|
+
// TextInput-level fix where layout reads `curRef.current` directly,
|
|
156
|
+
// there's still a window where a fast-echo wrote to stdout but the
|
|
157
|
+
// current cursor declaration on Ink (set by an earlier render's
|
|
158
|
+
// useDeclaredCursor commit) points at the PRE-keystroke caret
|
|
159
|
+
// column. If we advanced only `displayCursor`, an unrelated re-render
|
|
160
|
+
// in that window would re-run onRender's cursor-park branch with the
|
|
161
|
+
// stale declaration and visually undo the fast-echo's advance. We
|
|
162
|
+
// must bump BOTH so the cursor stays anchored to the physical caret
|
|
163
|
+
// until the next React commit publishes a fresh declaration
|
|
164
|
+
// (computed from `curRef.current` via the cursorLayout call in
|
|
165
|
+
// textInput.tsx) that supersedes the bump.
|
|
166
|
+
it('advances the active cursorDeclaration in lock-step with displayCursor', () => {
|
|
167
|
+
const { ink } = makeInk()
|
|
168
|
+
|
|
169
|
+
ink.render(React.createElement(Text, null, 'hi'))
|
|
170
|
+
ink.onRender()
|
|
171
|
+
|
|
172
|
+
const fakeNode = {} as unknown as Record<string, unknown>
|
|
173
|
+
|
|
174
|
+
peek(ink).cursorDeclaration = { node: fakeNode, relativeX: 7, relativeY: 0 }
|
|
175
|
+
peek(ink).displayCursor = { x: 12, y: 0 }
|
|
176
|
+
|
|
177
|
+
ink.noteExternalCursorAdvance(3)
|
|
178
|
+
|
|
179
|
+
expect(peek(ink).displayCursor).toEqual({ x: 15, y: 0 })
|
|
180
|
+
expect(peek(ink).cursorDeclaration).toEqual({ node: fakeNode, relativeX: 10, relativeY: 0 })
|
|
181
|
+
|
|
182
|
+
ink.noteExternalCursorAdvance(-1)
|
|
183
|
+
expect(peek(ink).displayCursor).toEqual({ x: 14, y: 0 })
|
|
184
|
+
expect(peek(ink).cursorDeclaration).toEqual({ node: fakeNode, relativeX: 9, relativeY: 0 })
|
|
185
|
+
|
|
186
|
+
ink.unmount()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// Closes Copilot follow-up on PR #26717: the dy half of the notifier
|
|
190
|
+
// contract was tested for `displayCursor` but not for
|
|
191
|
+
// `cursorDeclaration.relativeY`. Newlines in fast-echoed text never
|
|
192
|
+
// hit the bypass today (canFastAppendShape rejects '\n'), but `dy`
|
|
193
|
+
// is part of the public API and must propagate symmetrically with
|
|
194
|
+
// dx so future callers (e.g. multi-line paste shortcuts) don't get
|
|
195
|
+
// a half-implemented contract.
|
|
196
|
+
it('advances cursorDeclaration.relativeY when dy is non-zero', () => {
|
|
197
|
+
const { ink } = makeInk()
|
|
198
|
+
|
|
199
|
+
ink.render(React.createElement(Text, null, 'hi'))
|
|
200
|
+
ink.onRender()
|
|
201
|
+
|
|
202
|
+
const fakeNode = {} as unknown as Record<string, unknown>
|
|
203
|
+
|
|
204
|
+
peek(ink).cursorDeclaration = { node: fakeNode, relativeX: 2, relativeY: 1 }
|
|
205
|
+
peek(ink).displayCursor = { x: 4, y: 2 }
|
|
206
|
+
|
|
207
|
+
ink.noteExternalCursorAdvance(1, 3)
|
|
208
|
+
|
|
209
|
+
expect(peek(ink).displayCursor).toEqual({ x: 5, y: 5 })
|
|
210
|
+
expect(peek(ink).cursorDeclaration).toEqual({ node: fakeNode, relativeX: 3, relativeY: 4 })
|
|
211
|
+
|
|
212
|
+
// Negative dy too — cursor moving up across visual rows.
|
|
213
|
+
ink.noteExternalCursorAdvance(0, -2)
|
|
214
|
+
expect(peek(ink).displayCursor).toEqual({ x: 5, y: 3 })
|
|
215
|
+
expect(peek(ink).cursorDeclaration).toEqual({ node: fakeNode, relativeX: 3, relativeY: 2 })
|
|
216
|
+
|
|
217
|
+
ink.unmount()
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('leaves cursorDeclaration unchanged when no declaration is active', () => {
|
|
221
|
+
const { ink } = makeInk()
|
|
222
|
+
|
|
223
|
+
ink.render(React.createElement(Text, null, 'hi'))
|
|
224
|
+
ink.onRender()
|
|
225
|
+
|
|
226
|
+
expect(peek(ink).cursorDeclaration).toBeNull()
|
|
227
|
+
|
|
228
|
+
ink.noteExternalCursorAdvance(3)
|
|
229
|
+
|
|
230
|
+
expect(peek(ink).cursorDeclaration).toBeNull()
|
|
231
|
+
|
|
232
|
+
ink.unmount()
|
|
233
|
+
})
|
|
234
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { EventEmitter } from 'events'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import Text from './components/Text.js'
|
|
6
|
+
import Ink from './ink.js'
|
|
7
|
+
import { CURSOR_HOME, ERASE_SCREEN } from './termio/csi.js'
|
|
8
|
+
|
|
9
|
+
class FakeTty extends EventEmitter {
|
|
10
|
+
chunks: string[] = []
|
|
11
|
+
columns = 20
|
|
12
|
+
rows = 5
|
|
13
|
+
isTTY = true
|
|
14
|
+
|
|
15
|
+
write(chunk: string | Uint8Array, cb?: (err?: Error | null) => void): boolean {
|
|
16
|
+
this.chunks.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'))
|
|
17
|
+
cb?.()
|
|
18
|
+
return true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const tick = () => new Promise<void>(resolve => queueMicrotask(resolve))
|
|
23
|
+
|
|
24
|
+
describe('Ink resize healing', () => {
|
|
25
|
+
it('heals same-dimension alt-screen resize events with an erase before repaint', async () => {
|
|
26
|
+
const stdout = new FakeTty()
|
|
27
|
+
const stdin = new FakeTty()
|
|
28
|
+
const stderr = new FakeTty()
|
|
29
|
+
const ink = new Ink({
|
|
30
|
+
exitOnCtrlC: false,
|
|
31
|
+
patchConsole: false,
|
|
32
|
+
stderr: stderr as unknown as NodeJS.WriteStream,
|
|
33
|
+
stdin: stdin as unknown as NodeJS.ReadStream,
|
|
34
|
+
stdout: stdout as unknown as NodeJS.WriteStream
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
ink.setAltScreenActive(true)
|
|
38
|
+
ink.render(React.createElement(Text, null, 'hello'))
|
|
39
|
+
ink.onRender()
|
|
40
|
+
stdout.chunks = []
|
|
41
|
+
|
|
42
|
+
stdout.emit('resize')
|
|
43
|
+
ink.onRender()
|
|
44
|
+
await tick()
|
|
45
|
+
|
|
46
|
+
expect(stdout.chunks.join('')).toContain(ERASE_SCREEN + CURSOR_HOME)
|
|
47
|
+
|
|
48
|
+
ink.unmount()
|
|
49
|
+
})
|
|
50
|
+
})
|