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,730 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from 'node:child_process'
|
|
2
|
+
import { EventEmitter } from 'node:events'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
4
|
+
import { delimiter, resolve } from 'node:path'
|
|
5
|
+
import { createInterface } from 'node:readline'
|
|
6
|
+
|
|
7
|
+
import type { GatewayEvent } from './gatewayTypes.js'
|
|
8
|
+
import { CircularBuffer } from './lib/circularBuffer.js'
|
|
9
|
+
|
|
10
|
+
const MAX_GATEWAY_LOG_LINES = 200
|
|
11
|
+
const MAX_LOG_LINE_BYTES = 4096
|
|
12
|
+
const MAX_BUFFERED_EVENTS = 2000
|
|
13
|
+
const MAX_LOG_PREVIEW = 240
|
|
14
|
+
const STARTUP_TIMEOUT_MS = Math.max(5000, parseInt(process.env.NASTECH_TUI_STARTUP_TIMEOUT_MS ?? '15000', 10) || 15000)
|
|
15
|
+
const REQUEST_TIMEOUT_MS = Math.max(30000, parseInt(process.env.NASTECH_TUI_RPC_TIMEOUT_MS ?? '120000', 10) || 120000)
|
|
16
|
+
const WS_CONNECTING = 0
|
|
17
|
+
const WS_OPEN = 1
|
|
18
|
+
const WS_CLOSING = 2
|
|
19
|
+
const WS_CLOSED = 3
|
|
20
|
+
|
|
21
|
+
const truncateLine = (line: string) =>
|
|
22
|
+
line.length > MAX_LOG_LINE_BYTES ? `${line.slice(0, MAX_LOG_LINE_BYTES)}… [truncated ${line.length} bytes]` : line
|
|
23
|
+
|
|
24
|
+
const describeChild = (proc: ChildProcess | null) => {
|
|
25
|
+
if (!proc) {
|
|
26
|
+
return 'pid=none'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return `pid=${proc.pid ?? 'unknown'} killed=${proc.killed} exitCode=${proc.exitCode ?? 'null'} signal=${proc.signalCode ?? 'null'}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const resolveGatewayAttachUrl = () => {
|
|
33
|
+
const raw = process.env.NASTECH_TUI_GATEWAY_URL?.trim()
|
|
34
|
+
|
|
35
|
+
return raw ? raw : null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const resolveSidecarUrl = () => {
|
|
39
|
+
const raw = process.env.NASTECH_TUI_SIDECAR_URL?.trim()
|
|
40
|
+
|
|
41
|
+
return raw ? raw : null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const resolvePython = (root: string) => {
|
|
45
|
+
const configured = process.env.NASTECH_PYTHON?.trim() || process.env.PYTHON?.trim()
|
|
46
|
+
|
|
47
|
+
if (configured) {
|
|
48
|
+
return configured
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const venv = process.env.VIRTUAL_ENV?.trim()
|
|
52
|
+
|
|
53
|
+
const hit = [
|
|
54
|
+
venv && resolve(venv, 'bin/python'),
|
|
55
|
+
venv && resolve(venv, 'Scripts/python.exe'),
|
|
56
|
+
resolve(root, '.venv/bin/python'),
|
|
57
|
+
resolve(root, '.venv/bin/python3'),
|
|
58
|
+
resolve(root, 'venv/bin/python'),
|
|
59
|
+
resolve(root, 'venv/bin/python3')
|
|
60
|
+
].find(p => p && existsSync(p))
|
|
61
|
+
|
|
62
|
+
return hit || (process.platform === 'win32' ? 'python' : 'python3')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const asGatewayEvent = (value: unknown): GatewayEvent | null =>
|
|
66
|
+
value && typeof value === 'object' && !Array.isArray(value) && typeof (value as { type?: unknown }).type === 'string'
|
|
67
|
+
? (value as GatewayEvent)
|
|
68
|
+
: null
|
|
69
|
+
|
|
70
|
+
// Hoisted decoder: attach mode can drive high-frequency binary frames
|
|
71
|
+
// (tool deltas, reasoning streams) and constructing a fresh TextDecoder
|
|
72
|
+
// per message creates avoidable GC pressure. One module-level instance
|
|
73
|
+
// is fine because UTF-8 is stateless and we always pass entire frames.
|
|
74
|
+
const _wireDecoder = new TextDecoder()
|
|
75
|
+
|
|
76
|
+
const asWireText = (raw: unknown): string | null => {
|
|
77
|
+
if (typeof raw === 'string') {
|
|
78
|
+
return raw
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (raw instanceof ArrayBuffer) {
|
|
82
|
+
return _wireDecoder.decode(raw)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (ArrayBuffer.isView(raw)) {
|
|
86
|
+
return _wireDecoder.decode(raw)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Matches `<scheme>://user:pass@host…` style user-info segments in
|
|
93
|
+
// otherwise-malformed URLs that the WHATWG `URL` parser can't accept.
|
|
94
|
+
// Used by the `redactUrl` fallback so embedded credentials are
|
|
95
|
+
// scrubbed from log lines even when the URL is unparseable.
|
|
96
|
+
const _USERINFO_FALLBACK_RE = /^([a-z][a-z0-9+.-]*:\/\/)[^/?#@]*@/i
|
|
97
|
+
|
|
98
|
+
// Connection URLs (gateway, sidecar) often carry bearer tokens in the query
|
|
99
|
+
// string. We surface them in user-facing log lines and the
|
|
100
|
+
// `gateway.start_timeout` payload, so always strip the query string and any
|
|
101
|
+
// embedded user-info before logging.
|
|
102
|
+
const redactUrl = (raw: string): string => {
|
|
103
|
+
if (!raw) {
|
|
104
|
+
return raw
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const url = new URL(raw)
|
|
109
|
+
const userInfo = url.username || url.password ? '***@' : ''
|
|
110
|
+
const query = url.search ? '?***' : ''
|
|
111
|
+
|
|
112
|
+
return `${url.protocol}//${userInfo}${url.host}${url.pathname}${query}`
|
|
113
|
+
} catch {
|
|
114
|
+
// WHATWG URL rejected the input. Best-effort: strip an embedded
|
|
115
|
+
// `user:pass@` segment AND the query string so a malformed token
|
|
116
|
+
// bearer can never escape into the log tail.
|
|
117
|
+
const noUserInfo = raw.replace(_USERINFO_FALLBACK_RE, '$1***@')
|
|
118
|
+
const queryIdx = noUserInfo.indexOf('?')
|
|
119
|
+
|
|
120
|
+
return queryIdx >= 0 ? `${noUserInfo.slice(0, queryIdx)}?***` : noUserInfo
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
interface Pending {
|
|
125
|
+
id: string
|
|
126
|
+
method: string
|
|
127
|
+
reject: (e: Error) => void
|
|
128
|
+
resolve: (v: unknown) => void
|
|
129
|
+
timeout: ReturnType<typeof setTimeout>
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export class GatewayClient extends EventEmitter {
|
|
133
|
+
private proc: ChildProcess | null = null
|
|
134
|
+
private ws: WebSocket | null = null
|
|
135
|
+
private wsConnectPromise: Promise<void> | null = null
|
|
136
|
+
private sidecarWs: WebSocket | null = null
|
|
137
|
+
private attachUrl: null | string = null
|
|
138
|
+
private sidecarUrl: null | string = null
|
|
139
|
+
private reqId = 0
|
|
140
|
+
private logs = new CircularBuffer<string>(MAX_GATEWAY_LOG_LINES)
|
|
141
|
+
private pending = new Map<string, Pending>()
|
|
142
|
+
private bufferedEvents = new CircularBuffer<GatewayEvent>(MAX_BUFFERED_EVENTS)
|
|
143
|
+
private pendingExit: number | null | undefined
|
|
144
|
+
private ready = false
|
|
145
|
+
private readyTimer: ReturnType<typeof setTimeout> | null = null
|
|
146
|
+
private subscribed = false
|
|
147
|
+
private stdoutRl: ReturnType<typeof createInterface> | null = null
|
|
148
|
+
private stderrRl: ReturnType<typeof createInterface> | null = null
|
|
149
|
+
|
|
150
|
+
constructor() {
|
|
151
|
+
super()
|
|
152
|
+
// useInput / createGatewayEventHandler can legitimately attach many
|
|
153
|
+
// listeners. Default 10-cap triggers spurious warnings.
|
|
154
|
+
this.setMaxListeners(0)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private publish(ev: GatewayEvent) {
|
|
158
|
+
if (ev.type === 'gateway.ready') {
|
|
159
|
+
this.ready = true
|
|
160
|
+
|
|
161
|
+
if (this.readyTimer) {
|
|
162
|
+
clearTimeout(this.readyTimer)
|
|
163
|
+
this.readyTimer = null
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (this.subscribed) {
|
|
168
|
+
return void this.emit('event', ev)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.bufferedEvents.push(ev)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private clearReadyTimer() {
|
|
175
|
+
if (this.readyTimer) {
|
|
176
|
+
clearTimeout(this.readyTimer)
|
|
177
|
+
this.readyTimer = null
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private closeSidecarSocket() {
|
|
182
|
+
try {
|
|
183
|
+
this.sidecarWs?.close()
|
|
184
|
+
} catch {
|
|
185
|
+
// best effort
|
|
186
|
+
} finally {
|
|
187
|
+
this.sidecarWs = null
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private closeGatewaySocket() {
|
|
192
|
+
// Null the active reference BEFORE invoking close(): real WebSocket
|
|
193
|
+
// implementations dispatch the 'close' event after a microtask hop,
|
|
194
|
+
// so by the time the handler runs `this.ws` should already be null
|
|
195
|
+
// and the identity guard will correctly classify the close as
|
|
196
|
+
// belonging to a discarded socket. (Test fakes emit synchronously,
|
|
197
|
+
// so doing the swap up front is also what makes the identity guard
|
|
198
|
+
// match real timing in tests.)
|
|
199
|
+
const ws = this.ws
|
|
200
|
+
this.ws = null
|
|
201
|
+
this.wsConnectPromise = null
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
ws?.close()
|
|
205
|
+
} catch {
|
|
206
|
+
// best effort
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private resetStartupState() {
|
|
211
|
+
// Reject any in-flight RPCs left over from the previous transport
|
|
212
|
+
// before we swap. Otherwise the old transport's stale exit/close
|
|
213
|
+
// handlers (now identity-gated to ignore unrelated transports)
|
|
214
|
+
// never fire `rejectPending`, leaving callers hanging on promises
|
|
215
|
+
// attached to a discarded child / socket.
|
|
216
|
+
this.rejectPending(new Error('gateway restarting'))
|
|
217
|
+
this.ready = false
|
|
218
|
+
this.bufferedEvents.clear()
|
|
219
|
+
this.pendingExit = undefined
|
|
220
|
+
this.stdoutRl?.close()
|
|
221
|
+
this.stderrRl?.close()
|
|
222
|
+
this.stdoutRl = null
|
|
223
|
+
this.stderrRl = null
|
|
224
|
+
this.clearReadyTimer()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private startReadyTimer(python: string, cwd: string) {
|
|
228
|
+
this.readyTimer = setTimeout(() => {
|
|
229
|
+
if (this.ready) {
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Append the most recent gateway stderr/log lines to the timeout
|
|
234
|
+
// event so users can tell apart "wrong python", "missing dep",
|
|
235
|
+
// and "config parse failure" from one glance instead of having
|
|
236
|
+
// to dig through `/logs`. Capped to keep the activity feed
|
|
237
|
+
// readable on slow boots.
|
|
238
|
+
const stderrTail = this.getLogTail(20)
|
|
239
|
+
|
|
240
|
+
this.pushLog(`[startup] timed out waiting for gateway.ready (python=${python}, cwd=${cwd})`)
|
|
241
|
+
this.publish({
|
|
242
|
+
type: 'gateway.start_timeout',
|
|
243
|
+
payload: { cwd, python, stderr_tail: stderrTail }
|
|
244
|
+
})
|
|
245
|
+
}, STARTUP_TIMEOUT_MS)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private handleTransportExit(code: null | number, reason?: string) {
|
|
249
|
+
this.clearReadyTimer()
|
|
250
|
+
this.closeSidecarSocket()
|
|
251
|
+
this.pushLog(`[lifecycle] transport exit code=${code ?? 'null'} reason=${reason ?? 'none'}`)
|
|
252
|
+
this.rejectPending(new Error(reason || `gateway exited${code === null ? '' : ` (${code})`}`))
|
|
253
|
+
|
|
254
|
+
if (this.subscribed) {
|
|
255
|
+
this.emit('exit', code)
|
|
256
|
+
} else {
|
|
257
|
+
this.pendingExit = code
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private connectSidecarMirror() {
|
|
262
|
+
this.closeSidecarSocket()
|
|
263
|
+
|
|
264
|
+
if (!this.sidecarUrl) {
|
|
265
|
+
return
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (typeof WebSocket === 'undefined') {
|
|
269
|
+
this.pushLog(`[sidecar] WebSocket unavailable; skipping mirror to ${redactUrl(this.sidecarUrl)}`)
|
|
270
|
+
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const ws = new WebSocket(this.sidecarUrl)
|
|
276
|
+
|
|
277
|
+
this.sidecarWs = ws
|
|
278
|
+
ws.addEventListener('close', () => {
|
|
279
|
+
if (this.sidecarWs === ws) {
|
|
280
|
+
this.sidecarWs = null
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
ws.addEventListener('error', () => {
|
|
284
|
+
this.pushLog('[sidecar] mirror connection error')
|
|
285
|
+
})
|
|
286
|
+
} catch (err) {
|
|
287
|
+
this.pushLog(`[sidecar] failed to connect ${redactUrl(this.sidecarUrl)} (constructor error)`)
|
|
288
|
+
this.sidecarWs = null
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private mirrorEventToSidecar(rawFrame: string) {
|
|
293
|
+
const ws = this.sidecarWs
|
|
294
|
+
|
|
295
|
+
if (!ws || ws.readyState !== WS_OPEN) {
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
ws.send(rawFrame)
|
|
301
|
+
} catch {
|
|
302
|
+
// best effort
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private handleWebSocketFrame(raw: unknown) {
|
|
307
|
+
const text = asWireText(raw)
|
|
308
|
+
|
|
309
|
+
if (!text) {
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
const frame = JSON.parse(text) as Record<string, unknown>
|
|
315
|
+
|
|
316
|
+
if (frame.method === 'event') {
|
|
317
|
+
this.mirrorEventToSidecar(text)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
this.dispatch(frame)
|
|
321
|
+
} catch {
|
|
322
|
+
const preview = text.trim().slice(0, MAX_LOG_PREVIEW) || '(empty frame)'
|
|
323
|
+
|
|
324
|
+
this.pushLog(`[protocol] malformed websocket frame: ${preview}`)
|
|
325
|
+
this.publish({ type: 'gateway.protocol_error', payload: { preview } })
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private startSpawnedGateway(root: string) {
|
|
330
|
+
const python = resolvePython(root)
|
|
331
|
+
const cwd = process.env.NASTECH_CWD || root
|
|
332
|
+
const env = { ...process.env }
|
|
333
|
+
const pyPath = env.PYTHONPATH?.trim()
|
|
334
|
+
|
|
335
|
+
env.PYTHONPATH = pyPath ? `${root}${delimiter}${pyPath}` : root
|
|
336
|
+
this.startReadyTimer(python, cwd)
|
|
337
|
+
this.proc = spawn(python, ['-m', 'tui_gateway.entry'], { cwd, env, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
338
|
+
this.pushLog(`[lifecycle] spawned gateway child ${describeChild(this.proc)} python=${python} cwd=${cwd}`)
|
|
339
|
+
|
|
340
|
+
this.stdoutRl = createInterface({ input: this.proc.stdout! })
|
|
341
|
+
this.stdoutRl.on('line', raw => {
|
|
342
|
+
try {
|
|
343
|
+
this.dispatch(JSON.parse(raw))
|
|
344
|
+
} catch {
|
|
345
|
+
const preview = raw.trim().slice(0, MAX_LOG_PREVIEW) || '(empty line)'
|
|
346
|
+
|
|
347
|
+
this.pushLog(`[protocol] malformed stdout: ${preview}`)
|
|
348
|
+
this.publish({ type: 'gateway.protocol_error', payload: { preview } })
|
|
349
|
+
}
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
this.stderrRl = createInterface({ input: this.proc.stderr! })
|
|
353
|
+
this.stderrRl.on('line', raw => {
|
|
354
|
+
const line = truncateLine(raw.trim())
|
|
355
|
+
|
|
356
|
+
if (!line) {
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
this.pushLog(line)
|
|
361
|
+
this.publish({ type: 'gateway.stderr', payload: { line } })
|
|
362
|
+
})
|
|
363
|
+
|
|
364
|
+
const ownedProc = this.proc
|
|
365
|
+
this.proc.on('error', err => {
|
|
366
|
+
// Skip stale errors on an already-replaced child.
|
|
367
|
+
if (this.proc !== ownedProc) {
|
|
368
|
+
this.pushLog(`[lifecycle] stale child error ignored ${describeChild(ownedProc)} message=${err.message}`)
|
|
369
|
+
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const line = `[spawn] ${err.message}`
|
|
374
|
+
|
|
375
|
+
this.pushLog(`[lifecycle] child error ${describeChild(ownedProc)} message=${err.message}`)
|
|
376
|
+
this.pushLog(line)
|
|
377
|
+
this.publish({ type: 'gateway.stderr', payload: { line } })
|
|
378
|
+
// Detach the reference up front so the late `exit` event for
|
|
379
|
+
// this same child is identity-skipped (we don't want to emit
|
|
380
|
+
// 'exit' twice). Then run the full teardown — clears the
|
|
381
|
+
// startup timer so we don't fire a misleading
|
|
382
|
+
// `gateway.start_timeout`, rejects pending RPCs, and emits or
|
|
383
|
+
// queues a single `exit`.
|
|
384
|
+
this.proc = null
|
|
385
|
+
this.handleTransportExit(1, `gateway error: ${err.message}`)
|
|
386
|
+
})
|
|
387
|
+
this.proc.on('exit', (code, signal) => {
|
|
388
|
+
// start() can replace `this.proc` while an old child is still
|
|
389
|
+
// tearing down. Skip stale exits so we don't clear the new
|
|
390
|
+
// startup timer or reject newly-issued pending requests.
|
|
391
|
+
if (this.proc !== ownedProc) {
|
|
392
|
+
this.pushLog(
|
|
393
|
+
`[lifecycle] stale child exit ignored ${describeChild(ownedProc)} code=${code ?? 'null'} signal=${signal ?? 'null'}`
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
this.pushLog(`[lifecycle] child exit ${describeChild(ownedProc)} code=${code ?? 'null'} signal=${signal ?? 'null'}`)
|
|
400
|
+
this.handleTransportExit(code)
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private startAttachedGateway(attachUrl: string) {
|
|
405
|
+
const safeAttachUrl = redactUrl(attachUrl)
|
|
406
|
+
this.startReadyTimer('websocket', safeAttachUrl)
|
|
407
|
+
|
|
408
|
+
if (typeof WebSocket === 'undefined') {
|
|
409
|
+
const line = `[startup] WebSocket API unavailable; cannot attach to ${safeAttachUrl}`
|
|
410
|
+
|
|
411
|
+
this.pushLog(line)
|
|
412
|
+
this.publish({ type: 'gateway.stderr', payload: { line } })
|
|
413
|
+
this.handleTransportExit(1, 'gateway websocket unavailable')
|
|
414
|
+
|
|
415
|
+
return
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
const ws = new WebSocket(attachUrl)
|
|
420
|
+
let settled = false
|
|
421
|
+
|
|
422
|
+
this.ws = ws
|
|
423
|
+
|
|
424
|
+
const connectPromise = new Promise<void>((resolve, reject) => {
|
|
425
|
+
ws.addEventListener(
|
|
426
|
+
'open',
|
|
427
|
+
() => {
|
|
428
|
+
if (!settled) {
|
|
429
|
+
settled = true
|
|
430
|
+
resolve()
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
this.connectSidecarMirror()
|
|
434
|
+
},
|
|
435
|
+
{ once: true }
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
ws.addEventListener(
|
|
439
|
+
'error',
|
|
440
|
+
() => {
|
|
441
|
+
if (!settled) {
|
|
442
|
+
this.pushLog('[startup] gateway websocket connect error')
|
|
443
|
+
settled = true
|
|
444
|
+
reject(new Error('gateway websocket connection failed'))
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
{ once: true }
|
|
448
|
+
)
|
|
449
|
+
ws.addEventListener(
|
|
450
|
+
'close',
|
|
451
|
+
ev => {
|
|
452
|
+
if (!settled) {
|
|
453
|
+
settled = true
|
|
454
|
+
reject(new Error(`gateway websocket closed (${ev.code}) during connect`))
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
{ once: true }
|
|
458
|
+
)
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
// The connect promise is only awaited by RPCs that arrive while
|
|
462
|
+
// the socket is still connecting. If no request races the open
|
|
463
|
+
// (or a teardown drops the reference before anyone observes it),
|
|
464
|
+
// a connect-error / early-close rejection would surface as an
|
|
465
|
+
// unhandled promise rejection in Node. Attach a no-op handler to
|
|
466
|
+
// ensure the rejection is always observed.
|
|
467
|
+
connectPromise.catch(() => {})
|
|
468
|
+
this.wsConnectPromise = connectPromise
|
|
469
|
+
|
|
470
|
+
ws.addEventListener('message', ev => this.handleWebSocketFrame(ev.data))
|
|
471
|
+
ws.addEventListener('close', ev => {
|
|
472
|
+
// Skip close events from sockets that have already been
|
|
473
|
+
// replaced — start() / closeGatewaySocket() can swap `this.ws`
|
|
474
|
+
// before an in-flight close lands, and we must not clear the
|
|
475
|
+
// new ready timer or reject the new pending requests on behalf
|
|
476
|
+
// of a stale socket.
|
|
477
|
+
if (this.ws !== ws) {
|
|
478
|
+
this.pushLog(`[lifecycle] stale websocket close ignored code=${ev.code}`)
|
|
479
|
+
|
|
480
|
+
return
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
this.pushLog(`[lifecycle] websocket close code=${ev.code}`)
|
|
484
|
+
this.ws = null
|
|
485
|
+
this.wsConnectPromise = null
|
|
486
|
+
this.handleTransportExit(ev.code, `gateway websocket closed${ev.code ? ` (${ev.code})` : ''}`)
|
|
487
|
+
})
|
|
488
|
+
ws.addEventListener('error', () => {
|
|
489
|
+
const line = '[gateway] websocket transport error'
|
|
490
|
+
|
|
491
|
+
this.pushLog(line)
|
|
492
|
+
this.publish({ type: 'gateway.stderr', payload: { line } })
|
|
493
|
+
})
|
|
494
|
+
} catch (err) {
|
|
495
|
+
this.pushLog(`[startup] failed to connect websocket gateway ${safeAttachUrl} (constructor error)`)
|
|
496
|
+
this.handleTransportExit(1, 'gateway websocket startup failed')
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
start() {
|
|
501
|
+
const root = process.env.NASTECH_PYTHON_SRC_ROOT ?? resolve(import.meta.dirname, '../../')
|
|
502
|
+
const attachUrl = resolveGatewayAttachUrl()
|
|
503
|
+
const sidecarUrl = resolveSidecarUrl()
|
|
504
|
+
|
|
505
|
+
this.attachUrl = attachUrl
|
|
506
|
+
this.sidecarUrl = sidecarUrl
|
|
507
|
+
this.resetStartupState()
|
|
508
|
+
|
|
509
|
+
if (this.proc && !this.proc.killed && this.proc.exitCode === null) {
|
|
510
|
+
this.pushLog(`[lifecycle] replacing live gateway child ${describeChild(this.proc)}`)
|
|
511
|
+
this.proc.kill()
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
this.proc = null
|
|
515
|
+
this.closeGatewaySocket()
|
|
516
|
+
this.closeSidecarSocket()
|
|
517
|
+
|
|
518
|
+
if (attachUrl) {
|
|
519
|
+
this.startAttachedGateway(attachUrl)
|
|
520
|
+
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
this.startSpawnedGateway(root)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private dispatch(msg: Record<string, unknown>) {
|
|
528
|
+
const id = msg.id as string | undefined
|
|
529
|
+
const p = id ? this.pending.get(id) : undefined
|
|
530
|
+
|
|
531
|
+
if (p) {
|
|
532
|
+
this.settle(p, msg.error ? this.toError(msg.error) : null, msg.result)
|
|
533
|
+
|
|
534
|
+
return
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (msg.method === 'event') {
|
|
538
|
+
const ev = asGatewayEvent(msg.params)
|
|
539
|
+
|
|
540
|
+
if (ev) {
|
|
541
|
+
this.publish(ev)
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private toError(raw: unknown): Error {
|
|
547
|
+
const err = raw as { message?: unknown } | null | undefined
|
|
548
|
+
|
|
549
|
+
return new Error(typeof err?.message === 'string' ? err.message : 'request failed')
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private settle(p: Pending, err: Error | null, result: unknown) {
|
|
553
|
+
clearTimeout(p.timeout)
|
|
554
|
+
this.pending.delete(p.id)
|
|
555
|
+
|
|
556
|
+
if (err) {
|
|
557
|
+
p.reject(err)
|
|
558
|
+
} else {
|
|
559
|
+
p.resolve(result)
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private pushLog(line: string) {
|
|
564
|
+
this.logs.push(truncateLine(line))
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
private rejectPending(err: Error) {
|
|
568
|
+
for (const p of this.pending.values()) {
|
|
569
|
+
clearTimeout(p.timeout)
|
|
570
|
+
p.reject(err)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
this.pending.clear()
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Arrow class-field — stable identity, so `setTimeout(this.onTimeout, …, id)`
|
|
577
|
+
// doesn't allocate a bound function per request.
|
|
578
|
+
private onTimeout = (id: string) => {
|
|
579
|
+
const p = this.pending.get(id)
|
|
580
|
+
|
|
581
|
+
if (p) {
|
|
582
|
+
this.pending.delete(id)
|
|
583
|
+
p.reject(new Error(`timeout: ${p.method}`))
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
drain() {
|
|
588
|
+
this.subscribed = true
|
|
589
|
+
|
|
590
|
+
for (const ev of this.bufferedEvents.drain()) {
|
|
591
|
+
this.emit('event', ev)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (this.pendingExit !== undefined) {
|
|
595
|
+
const code = this.pendingExit
|
|
596
|
+
|
|
597
|
+
this.pendingExit = undefined
|
|
598
|
+
this.emit('exit', code)
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
getLogTail(limit = 20): string {
|
|
603
|
+
return this.logs.tail(Math.max(1, limit)).join('\n')
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private async ensureAttachedWebSocket(method: string): Promise<WebSocket> {
|
|
607
|
+
if (!this.attachUrl) {
|
|
608
|
+
throw new Error('gateway not running')
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (!this.ws || this.ws.readyState === WS_CLOSED || this.ws.readyState === WS_CLOSING) {
|
|
612
|
+
this.start()
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (this.ws?.readyState === WS_CONNECTING) {
|
|
616
|
+
try {
|
|
617
|
+
await this.wsConnectPromise
|
|
618
|
+
} catch (err) {
|
|
619
|
+
throw err instanceof Error ? err : new Error(String(err))
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (!this.ws || this.ws.readyState !== WS_OPEN) {
|
|
624
|
+
throw new Error(`gateway not connected: ${method}`)
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
return this.ws
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
private requestOverWebSocket<T = unknown>(method: string, params: Record<string, unknown> = {}): Promise<T> {
|
|
631
|
+
return this.ensureAttachedWebSocket(method).then(
|
|
632
|
+
ws =>
|
|
633
|
+
new Promise<T>((resolve, reject) => {
|
|
634
|
+
const id = `r${++this.reqId}`
|
|
635
|
+
const timeout = setTimeout(this.onTimeout, REQUEST_TIMEOUT_MS, id)
|
|
636
|
+
|
|
637
|
+
timeout.unref?.()
|
|
638
|
+
this.pending.set(id, {
|
|
639
|
+
id,
|
|
640
|
+
method,
|
|
641
|
+
reject,
|
|
642
|
+
resolve: v => resolve(v as T),
|
|
643
|
+
timeout
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
ws.send(JSON.stringify({ id, jsonrpc: '2.0', method, params }))
|
|
648
|
+
} catch (e) {
|
|
649
|
+
const pending = this.pending.get(id)
|
|
650
|
+
|
|
651
|
+
if (pending) {
|
|
652
|
+
clearTimeout(pending.timeout)
|
|
653
|
+
this.pending.delete(id)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
reject(e instanceof Error ? e : new Error(String(e)))
|
|
657
|
+
}
|
|
658
|
+
})
|
|
659
|
+
)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
request<T = unknown>(method: string, params: Record<string, unknown> = {}): Promise<T> {
|
|
663
|
+
const attachUrl = resolveGatewayAttachUrl()
|
|
664
|
+
|
|
665
|
+
if (attachUrl) {
|
|
666
|
+
if (this.attachUrl !== attachUrl) {
|
|
667
|
+
// The env var rotated at runtime — restart the transport so
|
|
668
|
+
// switching from spawned-gateway mode to attach mode also
|
|
669
|
+
// tears down the old Python child. Merely closing `this.ws`
|
|
670
|
+
// would leave a previously spawned gateway process alive.
|
|
671
|
+
this.rejectPending(new Error('gateway attach url changed'))
|
|
672
|
+
this.start()
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return this.requestOverWebSocket<T>(method, params)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (!this.proc?.stdin || this.proc.killed || this.proc.exitCode !== null) {
|
|
679
|
+
this.start()
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (!this.proc?.stdin) {
|
|
683
|
+
return Promise.reject(new Error('gateway not running'))
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const id = `r${++this.reqId}`
|
|
687
|
+
|
|
688
|
+
return new Promise<T>((resolve, reject) => {
|
|
689
|
+
const timeout = setTimeout(this.onTimeout, REQUEST_TIMEOUT_MS, id)
|
|
690
|
+
|
|
691
|
+
timeout.unref?.()
|
|
692
|
+
|
|
693
|
+
this.pending.set(id, {
|
|
694
|
+
id,
|
|
695
|
+
method,
|
|
696
|
+
reject,
|
|
697
|
+
resolve: v => resolve(v as T),
|
|
698
|
+
timeout
|
|
699
|
+
})
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
this.proc!.stdin!.write(JSON.stringify({ id, jsonrpc: '2.0', method, params }) + '\n')
|
|
703
|
+
} catch (e) {
|
|
704
|
+
const pending = this.pending.get(id)
|
|
705
|
+
|
|
706
|
+
if (pending) {
|
|
707
|
+
clearTimeout(pending.timeout)
|
|
708
|
+
this.pending.delete(id)
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
reject(e instanceof Error ? e : new Error(String(e)))
|
|
712
|
+
}
|
|
713
|
+
})
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
kill(reason = 'requested') {
|
|
717
|
+
const proc = this.proc
|
|
718
|
+
const killed = proc?.kill()
|
|
719
|
+
|
|
720
|
+
this.pushLog(`[lifecycle] GatewayClient.kill reason=${reason} ${describeChild(proc)} killResult=${killed ?? 'none'}`)
|
|
721
|
+
this.closeGatewaySocket()
|
|
722
|
+
this.closeSidecarSocket()
|
|
723
|
+
this.clearReadyTimer()
|
|
724
|
+
// The ws 'close' handler is identity-gated on `this.ws === ws`
|
|
725
|
+
// and we just nulled `this.ws`, so it will short-circuit and
|
|
726
|
+
// skip handleTransportExit. Reject pending RPCs explicitly so
|
|
727
|
+
// attach-mode promises do not hang after an intentional kill.
|
|
728
|
+
this.rejectPending(new Error('gateway closed'))
|
|
729
|
+
}
|
|
730
|
+
}
|