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,217 @@
|
|
|
1
|
+
import type { ChildProcess, spawn as SpawnFn } from 'node:child_process'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { openCommand, openExternalUrl, parseSafeUrl } from './openExternalUrl.js'
|
|
7
|
+
|
|
8
|
+
type SpawnLike = typeof SpawnFn
|
|
9
|
+
|
|
10
|
+
describe('parseSafeUrl', () => {
|
|
11
|
+
it('accepts http and https URLs', () => {
|
|
12
|
+
expect(parseSafeUrl('https://example.com')?.href).toBe('https://example.com/')
|
|
13
|
+
expect(parseSafeUrl('http://example.com/path?q=1')?.href).toBe('http://example.com/path?q=1')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('rejects file: URLs (would let a hostile model trigger arbitrary local handlers)', () => {
|
|
17
|
+
expect(parseSafeUrl('file:///etc/passwd')).toBeNull()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('rejects javascript:, data:, and vbscript: URLs', () => {
|
|
21
|
+
expect(parseSafeUrl('javascript:alert(1)')).toBeNull()
|
|
22
|
+
expect(parseSafeUrl('data:text/html,<script>alert(1)</script>')).toBeNull()
|
|
23
|
+
expect(parseSafeUrl('vbscript:msgbox')).toBeNull()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('rejects mailto:, ftp:, and other non-web protocols', () => {
|
|
27
|
+
expect(parseSafeUrl('mailto:test@example.com')).toBeNull()
|
|
28
|
+
expect(parseSafeUrl('ftp://example.com')).toBeNull()
|
|
29
|
+
expect(parseSafeUrl('ssh://example.com')).toBeNull()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('rejects unparseable strings', () => {
|
|
33
|
+
expect(parseSafeUrl('not a url')).toBeNull()
|
|
34
|
+
expect(parseSafeUrl('')).toBeNull()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('rejects non-string inputs defensively', () => {
|
|
38
|
+
expect(parseSafeUrl(undefined as unknown as string)).toBeNull()
|
|
39
|
+
expect(parseSafeUrl(null as unknown as string)).toBeNull()
|
|
40
|
+
expect(parseSafeUrl(123 as unknown as string)).toBeNull()
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('openCommand', () => {
|
|
45
|
+
it('returns macOS open(1) on darwin', () => {
|
|
46
|
+
expect(openCommand('darwin')).toEqual({ command: 'open', args: [] })
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('routes through explorer.exe on win32 — not cmd.exe — so URLs with & | ^ < > stay safe', () => {
|
|
50
|
+
// win32 must not route through cmd.exe — see comment in openCommand.
|
|
51
|
+
// Test pins the contract that we use explorer.exe (non-shell) so URLs
|
|
52
|
+
// with `&`/`|`/`^`/`<`/`>` aren't reparsed by cmd's tokenizer.
|
|
53
|
+
const cmd = openCommand('win32')
|
|
54
|
+
expect(cmd?.command).toBe('explorer.exe')
|
|
55
|
+
expect(cmd?.args).toEqual([])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('falls back to xdg-open on linux/bsd', () => {
|
|
59
|
+
expect(openCommand('linux')).toEqual({ command: 'xdg-open', args: [] })
|
|
60
|
+
expect(openCommand('freebsd')).toEqual({ command: 'xdg-open', args: [] })
|
|
61
|
+
expect(openCommand('openbsd')).toEqual({ command: 'xdg-open', args: [] })
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('returns null for unknown platforms (aix, sunos, cygwin, etc.)', () => {
|
|
65
|
+
// Avoid optimistically dispatching xdg-open on platforms where it
|
|
66
|
+
// probably isn't installed — the caller's `if (!command) return false`
|
|
67
|
+
// path surfaces "no opener" honestly instead.
|
|
68
|
+
expect(openCommand('aix')).toBeNull()
|
|
69
|
+
expect(openCommand('sunos')).toBeNull()
|
|
70
|
+
expect(openCommand('cygwin')).toBeNull()
|
|
71
|
+
expect(openCommand('haiku')).toBeNull()
|
|
72
|
+
expect(openCommand('')).toBeNull()
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('openExternalUrl on unsupported platforms', () => {
|
|
77
|
+
it('returns false without spawning when the platform has no known opener', () => {
|
|
78
|
+
const spawn = vi.fn() as unknown as SpawnLike
|
|
79
|
+
|
|
80
|
+
expect(openExternalUrl('https://example.com/', { spawn, platform: () => 'aix' })).toBe(false)
|
|
81
|
+
expect(spawn).not.toHaveBeenCalled()
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe('openExternalUrl', () => {
|
|
86
|
+
// Tracks the most recent fake child so tests can inspect its 'error'
|
|
87
|
+
// handlers and emit on it. Use a loose EventEmitter alias rather than
|
|
88
|
+
// ChildProcess — the latter's `unref` signature is strictly `() => void`
|
|
89
|
+
// and doesn't accept `vi.fn()` without a generic.
|
|
90
|
+
type FakeChild = EventEmitter & { unref: () => void }
|
|
91
|
+
|
|
92
|
+
function mockSpawn(): {
|
|
93
|
+
spawn: SpawnLike
|
|
94
|
+
calls: Array<{ command: string; args: readonly string[] }>
|
|
95
|
+
lastChild: () => FakeChild | undefined
|
|
96
|
+
} {
|
|
97
|
+
const calls: Array<{ command: string; args: readonly string[] }> = []
|
|
98
|
+
let lastChild: FakeChild | undefined
|
|
99
|
+
|
|
100
|
+
const spawn = vi.fn((command: string, args: readonly string[]) => {
|
|
101
|
+
calls.push({ command, args })
|
|
102
|
+
|
|
103
|
+
// Use a real EventEmitter so .once('error', cb) wires up correctly
|
|
104
|
+
// and we can synthesize async failures by emitting 'error' from the
|
|
105
|
+
// test. The cast is the same one Node uses internally — ChildProcess
|
|
106
|
+
// extends EventEmitter.
|
|
107
|
+
const child = new EventEmitter() as FakeChild
|
|
108
|
+
|
|
109
|
+
child.unref = () => {}
|
|
110
|
+
lastChild = child
|
|
111
|
+
|
|
112
|
+
return child as unknown as ChildProcess
|
|
113
|
+
}) as unknown as SpawnLike
|
|
114
|
+
|
|
115
|
+
return { spawn, calls, lastChild: () => lastChild }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
it('opens a normal https URL via the platform command', () => {
|
|
119
|
+
const { spawn, calls } = mockSpawn()
|
|
120
|
+
|
|
121
|
+
expect(openExternalUrl('https://example.com/foo', { spawn, platform: () => 'darwin' })).toBe(true)
|
|
122
|
+
expect(calls).toHaveLength(1)
|
|
123
|
+
expect(calls[0]!.command).toBe('open')
|
|
124
|
+
expect(calls[0]!.args).toEqual(['https://example.com/foo'])
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('uses xdg-open on linux', () => {
|
|
128
|
+
const { spawn, calls } = mockSpawn()
|
|
129
|
+
|
|
130
|
+
openExternalUrl('https://example.com/', { spawn, platform: () => 'linux' })
|
|
131
|
+
expect(calls[0]!.command).toBe('xdg-open')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('refuses to open file: URLs and does not spawn', () => {
|
|
135
|
+
const { spawn, calls } = mockSpawn()
|
|
136
|
+
|
|
137
|
+
expect(openExternalUrl('file:///etc/passwd', { spawn, platform: () => 'darwin' })).toBe(false)
|
|
138
|
+
expect(calls).toHaveLength(0)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('refuses to open javascript: URLs and does not spawn', () => {
|
|
142
|
+
const { spawn, calls } = mockSpawn()
|
|
143
|
+
|
|
144
|
+
expect(openExternalUrl('javascript:alert(1)', { spawn, platform: () => 'darwin' })).toBe(false)
|
|
145
|
+
expect(calls).toHaveLength(0)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('passes URLs containing shell metacharacters as plain args (no shell interpolation)', () => {
|
|
149
|
+
const { spawn, calls } = mockSpawn()
|
|
150
|
+
|
|
151
|
+
// A URL with `; & ` plus URL-encoded backticks. spawn(..., args) without
|
|
152
|
+
// shell:true means the OS receives these as a single argv element.
|
|
153
|
+
const hostile = 'https://example.com/path%3Bevil%20%26%20rm%20-rf'
|
|
154
|
+
|
|
155
|
+
openExternalUrl(hostile, { spawn, platform: () => 'darwin' })
|
|
156
|
+
expect(calls).toHaveLength(1)
|
|
157
|
+
expect(calls[0]!.args[calls[0]!.args.length - 1]).toBe(hostile)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('on win32, a URL with & | ^ < > is forwarded as a single argv element via explorer.exe', () => {
|
|
161
|
+
const { spawn, calls } = mockSpawn()
|
|
162
|
+
|
|
163
|
+
// Plain http URL with & in query (very common, e.g. analytics params)
|
|
164
|
+
// plus other cmd metacharacters that would split or reinterpret the
|
|
165
|
+
// command if win32 routed through cmd.exe /c start. Note that the URL
|
|
166
|
+
// parser percent-encodes `<` and `>` (which is fine — encoded forms
|
|
167
|
+
// can't be reinterpreted by any shell), but `&`, `|`, `^` survive
|
|
168
|
+
// and would tokenize cmd.exe if we ever regressed back to it.
|
|
169
|
+
const meta = 'https://example.com/q?a=1&b=2|c^d<e>f'
|
|
170
|
+
|
|
171
|
+
expect(openExternalUrl(meta, { spawn, platform: () => 'win32' })).toBe(true)
|
|
172
|
+
expect(calls).toHaveLength(1)
|
|
173
|
+
expect(calls[0]!.command).toBe('explorer.exe')
|
|
174
|
+
// The URL must arrive as exactly one argv element — not split on &/|/^/etc.
|
|
175
|
+
const forwarded = calls[0]!.args[0]!
|
|
176
|
+
expect(calls[0]!.args).toHaveLength(1)
|
|
177
|
+
expect(forwarded).toContain('a=1&b=2')
|
|
178
|
+
expect(forwarded).toContain('|c^d')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('on win32, common http URLs with & query params are forwarded intact', () => {
|
|
182
|
+
const { spawn, calls } = mockSpawn()
|
|
183
|
+
const url = 'https://example.com/search?q=foo&page=2&utm_source=nastech'
|
|
184
|
+
|
|
185
|
+
openExternalUrl(url, { spawn, platform: () => 'win32' })
|
|
186
|
+
expect(calls[0]!.args).toEqual([url])
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('returns false on synchronous spawn failure', () => {
|
|
190
|
+
const spawn = vi.fn(() => {
|
|
191
|
+
throw new Error('ENOENT')
|
|
192
|
+
}) as unknown as SpawnLike
|
|
193
|
+
|
|
194
|
+
expect(openExternalUrl('https://example.com/', { spawn, platform: () => 'linux' })).toBe(false)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('does not crash the host when the spawned process emits an async error', () => {
|
|
198
|
+
// Real-world case: `xdg-open` / `explorer.exe` missing on PATH. spawn()
|
|
199
|
+
// returns a ChildProcess synchronously, then emits 'error' once the
|
|
200
|
+
// exec actually fails. Without a registered 'error' listener, Node
|
|
201
|
+
// re-throws the event as an uncaught exception → TUI dies. We attach
|
|
202
|
+
// a no-op listener inside openExternalUrl; this test pins that contract.
|
|
203
|
+
const { spawn, lastChild } = mockSpawn()
|
|
204
|
+
|
|
205
|
+
expect(openExternalUrl('https://example.com/', { spawn, platform: () => 'linux' })).toBe(true)
|
|
206
|
+
|
|
207
|
+
const child = lastChild()
|
|
208
|
+
expect(child).toBeDefined()
|
|
209
|
+
// Must have a listener registered BEFORE we emit, or EventEmitter will
|
|
210
|
+
// throw synchronously here (which is exactly the crash we're preventing).
|
|
211
|
+
expect(child!.listenerCount('error')).toBeGreaterThan(0)
|
|
212
|
+
|
|
213
|
+
// Emit and assert it doesn't throw. If the listener weren't attached,
|
|
214
|
+
// this would throw 'Unhandled error' and fail the test.
|
|
215
|
+
expect(() => child!.emit('error', new Error('ENOENT: xdg-open not found'))).not.toThrow()
|
|
216
|
+
})
|
|
217
|
+
})
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { spawn, type SpawnOptions } from 'node:child_process'
|
|
2
|
+
import { platform } from 'node:os'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Opens an external URL in the user's default browser/handler.
|
|
6
|
+
*
|
|
7
|
+
* Wired into the Ink instance via `onHyperlinkClick` in entry.tsx, so any
|
|
8
|
+
* mouse click on a `<Link>` cell (or a row containing a plain-text URL the
|
|
9
|
+
* renderer detected) goes here. Mouse tracking inside the TUI prevents
|
|
10
|
+
* Terminal.app's native Cmd+click from firing — the click is captured
|
|
11
|
+
* before the terminal application sees it — so we have to handle the open
|
|
12
|
+
* ourselves.
|
|
13
|
+
*
|
|
14
|
+
* Safety:
|
|
15
|
+
* - http(s) only. Anything else (`file:`, `data:`, `javascript:`, etc.) is
|
|
16
|
+
* rejected — a hostile model could otherwise emit `<Link url="file:///">`
|
|
17
|
+
* and trick a click into running an arbitrary local handler.
|
|
18
|
+
* - Hostname is parsed via `URL`; only well-formed URLs are forwarded.
|
|
19
|
+
* - Spawned via `child_process.spawn` with arg array (no shell), so a URL
|
|
20
|
+
* containing shell metacharacters (`;`, `&`, backticks) cannot be
|
|
21
|
+
* interpreted as a command.
|
|
22
|
+
*
|
|
23
|
+
* Returns `true` if the spawn was attempted, `false` if the open could
|
|
24
|
+
* not proceed — covers (a) URL rejected by `parseSafeUrl` (non-http(s),
|
|
25
|
+
* malformed, etc.), (b) no known opener for the current platform
|
|
26
|
+
* (`openCommand` returned null), or (c) `spawn()` threw synchronously
|
|
27
|
+
* before the child was created. Async failures after spawn (`'error'`
|
|
28
|
+
* event because the binary couldn't exec) still return `true` because
|
|
29
|
+
* the spawn was attempted — the no-op error listener absorbs the event
|
|
30
|
+
* so the TUI doesn't crash, and the user just doesn't see their browser
|
|
31
|
+
* pop.
|
|
32
|
+
*/
|
|
33
|
+
export function openExternalUrl(rawUrl: string, dependencies: OpenDependencies = {}): boolean {
|
|
34
|
+
const url = parseSafeUrl(rawUrl)
|
|
35
|
+
|
|
36
|
+
if (!url) {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const spawnFn = dependencies.spawn ?? spawn
|
|
41
|
+
const platformId = dependencies.platform?.() ?? platform()
|
|
42
|
+
|
|
43
|
+
const command = openCommand(platformId)
|
|
44
|
+
|
|
45
|
+
if (!command) {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const child = spawnFn(command.command, [...command.args, url.toString()], {
|
|
51
|
+
// Detach so closing the TUI later doesn't kill the browser process,
|
|
52
|
+
// and ignore stdio so we don't leak FDs into our raw-mode terminal.
|
|
53
|
+
// Without `ignore` here, Chrome's stderr can land in the alt screen.
|
|
54
|
+
detached: true,
|
|
55
|
+
stdio: 'ignore'
|
|
56
|
+
} satisfies SpawnOptions)
|
|
57
|
+
|
|
58
|
+
// Async failure path: spawn returns a ChildProcess synchronously even
|
|
59
|
+
// when the binary is missing (ENOENT on `xdg-open` / `explorer.exe`),
|
|
60
|
+
// unreachable (EACCES), or otherwise unusable — the failure surfaces
|
|
61
|
+
// later as an 'error' event. Without a handler, an unhandled 'error'
|
|
62
|
+
// on an EventEmitter crashes Node, which would tear down the whole
|
|
63
|
+
// TUI. Attach a no-op listener BEFORE unref() so the event has a
|
|
64
|
+
// consumer; we already returned `true` synchronously, so the user
|
|
65
|
+
// just won't see their browser open — same as if the URL had been
|
|
66
|
+
// rejected upstream.
|
|
67
|
+
child.once('error', () => {
|
|
68
|
+
// Intentional no-op. The TUI keeps running; user gets no browser
|
|
69
|
+
// pop, which is the failure mode we promised in the doc comment.
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
child.unref()
|
|
73
|
+
|
|
74
|
+
return true
|
|
75
|
+
} catch {
|
|
76
|
+
// spawn can also throw synchronously on argv-validation failures
|
|
77
|
+
// (e.g. NUL in the path). Treat it as a no-op rather than crashing.
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type OpenDependencies = {
|
|
83
|
+
spawn?: typeof spawn
|
|
84
|
+
platform?: () => string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validate and normalize a URL for opening externally.
|
|
89
|
+
* Exported for testing.
|
|
90
|
+
*/
|
|
91
|
+
export function parseSafeUrl(value: string): null | URL {
|
|
92
|
+
if (!value || typeof value !== 'string') {
|
|
93
|
+
return null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let parsed: URL
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
parsed = new URL(value)
|
|
100
|
+
} catch {
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// http(s) only — opening file://, data:, javascript:, vbscript:, etc.
|
|
105
|
+
// would let a malicious model run a local handler with attacker-controlled
|
|
106
|
+
// input on a single click.
|
|
107
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Reject empty or all-whitespace hostnames defensively. URL parsing
|
|
112
|
+
// accepts URLs like 'http:///foo' on some Node versions; we don't want
|
|
113
|
+
// to forward those to `open`.
|
|
114
|
+
if (!parsed.hostname.trim()) {
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return parsed
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
type OpenCommand = { command: string; args: readonly string[] }
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Per-platform open command. We deliberately avoid `cmd.exe /c start` on
|
|
125
|
+
* Windows even though it's the canonical example, because `start` is a cmd
|
|
126
|
+
* builtin: the URL string is reparsed by cmd's command-line tokenizer and
|
|
127
|
+
* characters like `&`, `|`, `^`, `<`, `>` either break the command or get
|
|
128
|
+
* interpreted as additional commands. That undermines the protocol
|
|
129
|
+
* allowlist's safety story and also breaks plain http(s) URLs with `&` in
|
|
130
|
+
* query strings. `explorer.exe <url>` is the safe, non-shell alternative —
|
|
131
|
+
* it invokes the registered protocol handler for http(s) without going
|
|
132
|
+
* through cmd. Linux/BSD use `xdg-open` directly with no shell wrapping.
|
|
133
|
+
*
|
|
134
|
+
* Returns null for platforms where we don't know a safe opener (e.g. `aix`,
|
|
135
|
+
* `sunos`, `cygwin`). The caller's `if (!command) return false` path then
|
|
136
|
+
* surfaces "no opener" instead of optimistically trying `xdg-open` on a
|
|
137
|
+
* platform that probably doesn't have it.
|
|
138
|
+
*/
|
|
139
|
+
export function openCommand(platformId: string): OpenCommand | null {
|
|
140
|
+
if (platformId === 'darwin') {
|
|
141
|
+
return { command: 'open', args: [] }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (platformId === 'win32') {
|
|
145
|
+
return { command: 'explorer.exe', args: [] }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Linux + the BSD family ship xdg-open via xdg-utils. Everything else
|
|
149
|
+
// (aix, sunos, cygwin, haiku, etc.) returns null so openExternalUrl's
|
|
150
|
+
// command-not-found fallback fires honestly.
|
|
151
|
+
const XDG_OPEN_PLATFORMS = new Set(['linux', 'freebsd', 'openbsd', 'netbsd', 'dragonfly'])
|
|
152
|
+
|
|
153
|
+
if (XDG_OPEN_PLATFORMS.has(platformId)) {
|
|
154
|
+
return { command: 'xdg-open', args: [] }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return null
|
|
158
|
+
}
|
package/src/lib/osc52.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const ESC = '\x1b'
|
|
2
|
+
const BEL = '\x07'
|
|
3
|
+
const ST = `${ESC}\\`
|
|
4
|
+
|
|
5
|
+
export const OSC52_CLIPBOARD_QUERY = `${ESC}]52;c;?${BEL}`
|
|
6
|
+
|
|
7
|
+
type OscResponse = { code: number; data: string; type: 'osc' }
|
|
8
|
+
|
|
9
|
+
type OscQuerier = {
|
|
10
|
+
flush: () => Promise<void>
|
|
11
|
+
send: <T>(query: { match: (r: unknown) => r is T; request: string }) => Promise<T | undefined>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function wrapForMultiplexer(sequence: string): string {
|
|
15
|
+
if (process.env['TMUX']) {
|
|
16
|
+
return `${ESC}Ptmux;${sequence.split(ESC).join(ESC + ESC)}${ST}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (process.env['STY']) {
|
|
20
|
+
return `${ESC}P${sequence}${ST}`
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return sequence
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildOsc52ClipboardQuery(): string {
|
|
27
|
+
return wrapForMultiplexer(OSC52_CLIPBOARD_QUERY)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function parseOsc52ClipboardData(data: string): null | string {
|
|
31
|
+
const firstSep = data.indexOf(';')
|
|
32
|
+
|
|
33
|
+
if (firstSep === -1) {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const selection = data.slice(0, firstSep)
|
|
38
|
+
const payload = data.slice(firstSep + 1)
|
|
39
|
+
|
|
40
|
+
if ((selection !== 'c' && selection !== 'p') || !payload || payload === '?') {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
return Buffer.from(payload, 'base64').toString('utf8')
|
|
46
|
+
} catch {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function readOsc52Clipboard(querier: null | OscQuerier, timeoutMs = 500): Promise<null | string> {
|
|
52
|
+
if (!querier) {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const timeout = new Promise<undefined>(resolve => setTimeout(resolve, timeoutMs))
|
|
57
|
+
|
|
58
|
+
const query = querier.send<OscResponse>({
|
|
59
|
+
request: buildOsc52ClipboardQuery(),
|
|
60
|
+
match: (r: unknown): r is OscResponse => {
|
|
61
|
+
return !!r && typeof r === 'object' && (r as OscResponse).type === 'osc' && (r as OscResponse).code === 52
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const response = await Promise.race([query, timeout])
|
|
66
|
+
|
|
67
|
+
await querier.flush()
|
|
68
|
+
|
|
69
|
+
return response ? parseOsc52ClipboardData(response.data) : null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const writeOsc52Clipboard = (s: string) =>
|
|
73
|
+
process.stdout.write(`\x1b]52;c;${Buffer.from(s, 'utf8').toString('base64')}\x07`)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
// Mirror the Python gateway's panic log (tui_gateway/server.py::_CRASH_LOG) from
|
|
6
|
+
// the Node parent so lifecycle breadcrumbs interleave, by timestamp, with the
|
|
7
|
+
// child's `=== SIGTERM received ===` / `=== gateway exit ===` entries.
|
|
8
|
+
//
|
|
9
|
+
// A backend SIGTERM is *usually* a parent action — `gw.kill()` (graceful-exit on
|
|
10
|
+
// a signal to Node, or an explicit /quit) or `start()` replacing a live child —
|
|
11
|
+
// but it can also come straight from an external supervisor (s6, a cgroup OOM
|
|
12
|
+
// reaper, a stray `kill`) signalling the child directly. Telling those apart is
|
|
13
|
+
// exactly the point: #31051 left these breadcrumbs in an in-memory CircularBuffer
|
|
14
|
+
// that dies with the process, so SIGTERM crash reports arrived with no parent
|
|
15
|
+
// context. A `[tui-parent]` line immediately before the child's panic means a
|
|
16
|
+
// parent kill; its absence *suggests* an external signal — not definitive,
|
|
17
|
+
// since this logger is best-effort (disabled under VITEST, and a failed append
|
|
18
|
+
// is swallowed). Persisting the death-explaining events here is what makes that
|
|
19
|
+
// distinction (and a memory-critical `process.exit(137)`, which closes stdin →
|
|
20
|
+
// clean EOF, not SIGTERM) diagnosable after the fact.
|
|
21
|
+
const logDir = join(process.env.NASTECH_HOME?.trim() || join(homedir(), '.nastech'), 'logs')
|
|
22
|
+
const CRASH_LOG = join(logDir, 'tui_gateway_crash.log')
|
|
23
|
+
|
|
24
|
+
// Skipped under vitest so unit tests exercising start()/kill() can't write into
|
|
25
|
+
// a real ~/.nastech (tests must stay hermetic — see AGENTS.md).
|
|
26
|
+
const enabled = !process.env.VITEST
|
|
27
|
+
// Slice a single breadcrumb's value to MAX_BREADCRUMB chars (a short
|
|
28
|
+
// "[truncated …]" marker is appended, so the written line is slightly longer)
|
|
29
|
+
// so a pathological value (e.g. a giant error) can't bloat the shared crash log
|
|
30
|
+
// or add noticeable blocking on the synchronous append. Mirrors the spirit of
|
|
31
|
+
// GatewayClient's in-memory log-line cap.
|
|
32
|
+
const MAX_BREADCRUMB = 4096
|
|
33
|
+
let warned = false
|
|
34
|
+
|
|
35
|
+
export function recordParentLifecycle(line: string): void {
|
|
36
|
+
if (!enabled) {
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Collapse embedded newlines so a multi-line value (e.g. an error message)
|
|
42
|
+
// stays one breadcrumb and can't masquerade as a separate log entry or as
|
|
43
|
+
// the child's panic output sharing this file.
|
|
44
|
+
const oneLine = line.replace(/[\r\n]+/g, ' ↵ ')
|
|
45
|
+
|
|
46
|
+
const capped =
|
|
47
|
+
oneLine.length > MAX_BREADCRUMB ? `${oneLine.slice(0, MAX_BREADCRUMB)}… [truncated ${oneLine.length} chars]` : oneLine
|
|
48
|
+
|
|
49
|
+
mkdirSync(logDir, { recursive: true })
|
|
50
|
+
appendFileSync(CRASH_LOG, `[tui-parent] ${new Date().toISOString()} ${capped}\n`)
|
|
51
|
+
} catch {
|
|
52
|
+
if (!warned) {
|
|
53
|
+
warned = true
|
|
54
|
+
process.stderr.write('nastech-tui: parent lifecycle log unavailable\n')
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Perf instrumentation for the full render pipeline.
|
|
2
|
+
//
|
|
3
|
+
// PerfPane (React.Profiler) → per-pane commit times
|
|
4
|
+
// logFrameEvent (ink.onFrame) → yoga / renderer / diff / optimize / write
|
|
5
|
+
// phases + yoga counters + scroll fast-path
|
|
6
|
+
//
|
|
7
|
+
// Both gate on NASTECH_DEV_PERF=1 and dump JSON-lines (default ~/.nastech/perf.log,
|
|
8
|
+
// override NASTECH_DEV_PERF_LOG). Tagged { src: 'react' | 'frame' } for jq.
|
|
9
|
+
// NASTECH_DEV_PERF_MS (default 2) skips sub-ms idle frames; set 0 to capture all.
|
|
10
|
+
//
|
|
11
|
+
// Zero cost when unset: PerfPane returns children directly, logFrameEvent is
|
|
12
|
+
// undefined so ink doesn't pay the timing cost.
|
|
13
|
+
|
|
14
|
+
import { appendFileSync, mkdirSync } from 'node:fs'
|
|
15
|
+
import { homedir } from 'node:os'
|
|
16
|
+
import { dirname, join } from 'node:path'
|
|
17
|
+
|
|
18
|
+
import type { FrameEvent } from '@nastechai/ink'
|
|
19
|
+
import { scrollFastPathStats } from '@nastechai/ink'
|
|
20
|
+
import { Profiler, type ProfilerOnRenderCallback, type ReactNode } from 'react'
|
|
21
|
+
|
|
22
|
+
const ENABLED = /^(?:1|true|yes|on)$/i.test((process.env.NASTECH_DEV_PERF ?? '').trim())
|
|
23
|
+
const THRESHOLD_MS = Number(process.env.NASTECH_DEV_PERF_MS ?? '2') || 0
|
|
24
|
+
const LOG_PATH = process.env.NASTECH_DEV_PERF_LOG?.trim() || join(homedir(), '.nastech', 'perf.log')
|
|
25
|
+
|
|
26
|
+
let logReady = false
|
|
27
|
+
|
|
28
|
+
const writeRow = (row: Record<string, unknown>) => {
|
|
29
|
+
if (!logReady) {
|
|
30
|
+
logReady = true
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
mkdirSync(dirname(LOG_PATH), { recursive: true })
|
|
34
|
+
} catch {
|
|
35
|
+
// Best-effort — never crash the TUI to log a sample.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
appendFileSync(LOG_PATH, `${JSON.stringify(row)}\n`)
|
|
41
|
+
} catch {
|
|
42
|
+
/* best-effort */
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const round2 = (n: number) => Math.round(n * 100) / 100
|
|
47
|
+
|
|
48
|
+
const onRender: ProfilerOnRenderCallback = (id, phase, actualMs, baseMs, startTime, commitTime) => {
|
|
49
|
+
if (actualMs < THRESHOLD_MS) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
writeRow({
|
|
54
|
+
actualMs: round2(actualMs),
|
|
55
|
+
baseMs: round2(baseMs),
|
|
56
|
+
commitTimeMs: round2(commitTime),
|
|
57
|
+
id,
|
|
58
|
+
phase,
|
|
59
|
+
src: 'react',
|
|
60
|
+
startTimeMs: round2(startTime),
|
|
61
|
+
ts: Date.now()
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function PerfPane({ children, id }: { children: ReactNode; id: string }) {
|
|
66
|
+
if (!ENABLED) {
|
|
67
|
+
return children
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Profiler id={id} onRender={onRender}>
|
|
72
|
+
{children}
|
|
73
|
+
</Profiler>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const logFrameEvent = ENABLED
|
|
78
|
+
? (event: FrameEvent) => {
|
|
79
|
+
if (event.durationMs < THRESHOLD_MS) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
writeRow({
|
|
84
|
+
durationMs: round2(event.durationMs),
|
|
85
|
+
// Cumulative counters — consumers diff pairs to get per-frame deltas.
|
|
86
|
+
fastPath: { ...scrollFastPathStats, declined: { ...scrollFastPathStats.declined } },
|
|
87
|
+
flickers: event.flickers.length ? event.flickers : undefined,
|
|
88
|
+
phases: event.phases
|
|
89
|
+
? {
|
|
90
|
+
...event.phases,
|
|
91
|
+
commit: round2(event.phases.commit),
|
|
92
|
+
diff: round2(event.phases.diff),
|
|
93
|
+
optimize: round2(event.phases.optimize),
|
|
94
|
+
prevFrameDrainMs: round2(event.phases.prevFrameDrainMs),
|
|
95
|
+
renderer: round2(event.phases.renderer),
|
|
96
|
+
write: round2(event.phases.write),
|
|
97
|
+
yoga: round2(event.phases.yoga)
|
|
98
|
+
}
|
|
99
|
+
: undefined,
|
|
100
|
+
src: 'frame',
|
|
101
|
+
ts: Date.now()
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
: undefined
|
|
105
|
+
|
|
106
|
+
export const PERF_ENABLED = ENABLED
|
|
107
|
+
export const PERF_LOG_PATH = LOG_PATH
|