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
package/src/lib/fuzzy.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Lightweight fuzzy subsequence scorer for picker filtering.
|
|
2
|
+
//
|
|
3
|
+
// Matches a query as an ordered subsequence of the target (so `g4o` matches
|
|
4
|
+
// `gpt-4o`) and scores by match quality so callers can rank results. Higher
|
|
5
|
+
// score is a better match. Returns the matched character indices so callers
|
|
6
|
+
// can highlight them.
|
|
7
|
+
//
|
|
8
|
+
// The scoring favours, in rough order: exact full match, prefix match, matches
|
|
9
|
+
// that start on a word boundary (after `-`, `_`, `/`, `.`, space, or a
|
|
10
|
+
// lower→upper case transition), contiguous runs, and earlier matches. This is
|
|
11
|
+
// intentionally simple — no external dependency — but good enough to make
|
|
12
|
+
// `son4` rank `claude-sonnet-4` above an incidental scattered hit.
|
|
13
|
+
//
|
|
14
|
+
// The WebUI ships a logically identical copy of this module at
|
|
15
|
+
// web/src/lib/fuzzy.ts (only prettier formatting differs); keep the two in
|
|
16
|
+
// sync. The TUI copy carries the vitest suite (the web package has no test
|
|
17
|
+
// runner), so changes should be validated here.
|
|
18
|
+
|
|
19
|
+
export interface FuzzyMatch {
|
|
20
|
+
/** Total score; higher is better. */
|
|
21
|
+
score: number
|
|
22
|
+
/** Indices into the original (non-lowercased) target that were matched. */
|
|
23
|
+
positions: number[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const WORD_BOUNDARY = /[-_/.\s]/
|
|
27
|
+
|
|
28
|
+
function isBoundary(target: string, index: number): boolean {
|
|
29
|
+
if (index === 0) {
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const prev = target[index - 1]
|
|
34
|
+
|
|
35
|
+
if (WORD_BOUNDARY.test(prev)) {
|
|
36
|
+
return true
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// camelCase / lower→upper transition (e.g. the `O` in `gptO`).
|
|
40
|
+
const cur = target[index]
|
|
41
|
+
|
|
42
|
+
return prev === prev.toLowerCase() && cur !== cur.toLowerCase() && cur === cur.toUpperCase()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Score a single query token against a target. Returns null when the token is
|
|
47
|
+
* not a subsequence of the target. An empty query scores 0 with no positions.
|
|
48
|
+
*/
|
|
49
|
+
export function fuzzyScore(target: string, query: string): FuzzyMatch | null {
|
|
50
|
+
if (!query) {
|
|
51
|
+
return { score: 0, positions: [] }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const lowerTarget = target.toLowerCase()
|
|
55
|
+
const lowerQuery = query.toLowerCase()
|
|
56
|
+
|
|
57
|
+
const positions: number[] = []
|
|
58
|
+
let score = 0
|
|
59
|
+
let prevIndex = -1
|
|
60
|
+
let searchFrom = 0
|
|
61
|
+
|
|
62
|
+
for (const ch of lowerQuery) {
|
|
63
|
+
const idx = lowerTarget.indexOf(ch, searchFrom)
|
|
64
|
+
|
|
65
|
+
if (idx < 0) {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
positions.push(idx)
|
|
70
|
+
|
|
71
|
+
// Base point for the matched character.
|
|
72
|
+
score += 1
|
|
73
|
+
|
|
74
|
+
// Contiguous with the previous match → strong bonus.
|
|
75
|
+
if (prevIndex >= 0 && idx === prevIndex + 1) {
|
|
76
|
+
score += 5
|
|
77
|
+
} else if (prevIndex >= 0) {
|
|
78
|
+
// Penalise the gap we had to skip (capped), so contiguous beats scattered.
|
|
79
|
+
score -= Math.min(idx - prevIndex - 1, 3)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Word-boundary / start-of-string matches are meaningful.
|
|
83
|
+
if (isBoundary(target, idx)) {
|
|
84
|
+
score += 3
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Matching the very first character of the target is the strongest signal.
|
|
88
|
+
if (idx === 0) {
|
|
89
|
+
score += 5
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
prevIndex = idx
|
|
93
|
+
searchFrom = idx + 1
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Prefix bonus: the query matched a contiguous prefix of the target.
|
|
97
|
+
if (positions.length && positions[0] === 0 && positions[positions.length - 1] === positions.length - 1) {
|
|
98
|
+
score += 8
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Exact full match dominates everything else.
|
|
102
|
+
if (lowerTarget === lowerQuery) {
|
|
103
|
+
score += 20
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Slightly prefer shorter targets when scores are otherwise close, so a
|
|
107
|
+
// query that fully prefixes a short id beats the same prefix on a long one.
|
|
108
|
+
score -= lowerTarget.length * 0.01
|
|
109
|
+
|
|
110
|
+
return { score, positions }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Score a target against a whitespace-separated, multi-token query. Every token
|
|
115
|
+
* must match (AND semantics); the result aggregates per-token scores and the
|
|
116
|
+
* union of matched positions. Returns null if any token fails to match.
|
|
117
|
+
*/
|
|
118
|
+
export function fuzzyScoreMulti(target: string, query: string): FuzzyMatch | null {
|
|
119
|
+
const tokens = query.trim().toLowerCase().split(/\s+/).filter(Boolean)
|
|
120
|
+
|
|
121
|
+
if (!tokens.length) {
|
|
122
|
+
return { score: 0, positions: [] }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let score = 0
|
|
126
|
+
const positionSet = new Set<number>()
|
|
127
|
+
|
|
128
|
+
for (const token of tokens) {
|
|
129
|
+
const match = fuzzyScore(target, token)
|
|
130
|
+
|
|
131
|
+
if (!match) {
|
|
132
|
+
return null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
score += match.score
|
|
136
|
+
|
|
137
|
+
for (const pos of match.positions) {
|
|
138
|
+
positionSet.add(pos)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { score, positions: [...positionSet].sort((a, b) => a - b) }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface RankedItem<T> {
|
|
146
|
+
item: T
|
|
147
|
+
score: number
|
|
148
|
+
positions: number[]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Filter + rank a list by a fuzzy query against a derived text key. Non-matching
|
|
153
|
+
* items are dropped; matches are sorted by score (descending), ties broken by
|
|
154
|
+
* the original index so ordering is stable for equal scores. An empty query
|
|
155
|
+
* returns every item in original order with no positions.
|
|
156
|
+
*/
|
|
157
|
+
export function fuzzyRank<T>(items: readonly T[], query: string, toText: (item: T) => string): RankedItem<T>[] {
|
|
158
|
+
const trimmed = query.trim()
|
|
159
|
+
|
|
160
|
+
if (!trimmed) {
|
|
161
|
+
return items.map(item => ({ item, score: 0, positions: [] }))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const ranked: Array<RankedItem<T> & { index: number }> = []
|
|
165
|
+
|
|
166
|
+
items.forEach((item, index) => {
|
|
167
|
+
const match = fuzzyScoreMulti(toText(item), trimmed)
|
|
168
|
+
|
|
169
|
+
if (match) {
|
|
170
|
+
ranked.push({ item, score: match.score, positions: match.positions, index })
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
ranked.sort((a, b) => b.score - a.score || a.index - b.index)
|
|
175
|
+
|
|
176
|
+
return ranked.map(({ item, score, positions }) => ({ item, score, positions }))
|
|
177
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
interface SetupOptions {
|
|
2
|
+
cleanups?: (() => Promise<void> | void)[]
|
|
3
|
+
failsafeMs?: number
|
|
4
|
+
onError?: (scope: 'uncaughtException' | 'unhandledRejection', err: unknown) => void
|
|
5
|
+
onSignal?: (signal: NodeJS.Signals) => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const SIGNAL_EXIT_CODE: Record<'SIGHUP' | 'SIGINT' | 'SIGTERM', number> = {
|
|
9
|
+
SIGHUP: 129,
|
|
10
|
+
SIGINT: 130,
|
|
11
|
+
SIGTERM: 143
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let wired = false
|
|
15
|
+
|
|
16
|
+
export function setupGracefulExit({ cleanups = [], failsafeMs = 4000, onError, onSignal }: SetupOptions = {}) {
|
|
17
|
+
if (wired) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
wired = true
|
|
22
|
+
|
|
23
|
+
let shuttingDown = false
|
|
24
|
+
|
|
25
|
+
const exit = (code: number, signal?: NodeJS.Signals) => {
|
|
26
|
+
if (shuttingDown) {
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
shuttingDown = true
|
|
31
|
+
|
|
32
|
+
if (signal) {
|
|
33
|
+
onSignal?.(signal)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
setTimeout(() => process.exit(code), failsafeMs).unref?.()
|
|
37
|
+
|
|
38
|
+
void Promise.allSettled(cleanups.map(fn => Promise.resolve().then(fn))).finally(() => process.exit(code))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP'] as const) {
|
|
42
|
+
process.on(sig, () => exit(SIGNAL_EXIT_CODE[sig], sig))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
process.on('uncaughtException', err => onError?.('uncaughtException', err))
|
|
46
|
+
process.on('unhandledRejection', reason => onError?.('unhandledRejection', reason))
|
|
47
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
const MAX = 1000
|
|
6
|
+
const dir = process.env.NASTECH_HOME ?? join(homedir(), '.nastech')
|
|
7
|
+
const file = join(dir, '.nastech_history')
|
|
8
|
+
|
|
9
|
+
let cache: string[] | null = null
|
|
10
|
+
|
|
11
|
+
export function load() {
|
|
12
|
+
if (cache) {
|
|
13
|
+
return cache
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
if (!existsSync(file)) {
|
|
18
|
+
cache = []
|
|
19
|
+
|
|
20
|
+
return cache
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const entries: string[] = []
|
|
24
|
+
let current: string[] = []
|
|
25
|
+
|
|
26
|
+
for (const line of readFileSync(file, 'utf8').split('\n')) {
|
|
27
|
+
if (line.startsWith('+')) {
|
|
28
|
+
current.push(line.slice(1))
|
|
29
|
+
} else if (current.length) {
|
|
30
|
+
entries.push(current.join('\n'))
|
|
31
|
+
current = []
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (current.length) {
|
|
36
|
+
entries.push(current.join('\n'))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
cache = entries.slice(-MAX)
|
|
40
|
+
} catch {
|
|
41
|
+
cache = []
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return cache
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function append(line: string) {
|
|
48
|
+
const trimmed = line.trim()
|
|
49
|
+
|
|
50
|
+
if (!trimmed) {
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const items = load()
|
|
55
|
+
|
|
56
|
+
if (items.at(-1) === trimmed) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
items.push(trimmed)
|
|
61
|
+
|
|
62
|
+
if (items.length > MAX) {
|
|
63
|
+
items.splice(0, items.length - MAX)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
if (!existsSync(dir)) {
|
|
68
|
+
mkdirSync(dir, { recursive: true })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const ts = new Date().toISOString().replace('T', ' ').replace('Z', '')
|
|
72
|
+
|
|
73
|
+
const encoded = trimmed
|
|
74
|
+
.split('\n')
|
|
75
|
+
.map(l => `+${l}`)
|
|
76
|
+
.join('\n')
|
|
77
|
+
|
|
78
|
+
appendFileSync(file, `\n# ${ts}\n${encoded}\n`)
|
|
79
|
+
} catch {
|
|
80
|
+
void 0
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { stringWidth, wrapAnsi } from '@nastechai/ink'
|
|
2
|
+
|
|
3
|
+
import type { Role } from '../types.js'
|
|
4
|
+
|
|
5
|
+
export const COMPOSER_PROMPT_GAP_WIDTH = 1
|
|
6
|
+
|
|
7
|
+
let _seg: Intl.Segmenter | null = null
|
|
8
|
+
const seg = () => (_seg ??= new Intl.Segmenter(undefined, { granularity: 'grapheme' }))
|
|
9
|
+
|
|
10
|
+
interface VisualLine {
|
|
11
|
+
end: number
|
|
12
|
+
start: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const graphemes = (value: string) =>
|
|
16
|
+
[...seg().segment(value)].map(({ segment, index }) => ({
|
|
17
|
+
end: index + segment.length,
|
|
18
|
+
index,
|
|
19
|
+
segment,
|
|
20
|
+
width: Math.max(1, stringWidth(segment))
|
|
21
|
+
}))
|
|
22
|
+
|
|
23
|
+
// Build VisualLines from wrap-ansi's output by mapping each emitted character
|
|
24
|
+
// back to its original offset in `value`. wrap-ansi only INSERTS '\n' at wrap
|
|
25
|
+
// boundaries — it never drops, reorders, or substitutes existing characters —
|
|
26
|
+
// so a parallel walk uniquely identifies each line's source range.
|
|
27
|
+
//
|
|
28
|
+
// This used to be a hand-rolled word-wrap whose break points disagreed with
|
|
29
|
+
// wrap-ansi in subtle but visible ways: exact-fill rows pushed the cursor to
|
|
30
|
+
// a phantom next line, mid-word breaks landed one grapheme off, etc. The
|
|
31
|
+
// composer's TextInput renders text via Ink's <Text wrap="wrap">, which
|
|
32
|
+
// delegates to wrap-ansi — so any drift between the two algorithms parks the
|
|
33
|
+
// hardware cursor several cells away from the last rendered character.
|
|
34
|
+
// Sourcing both from wrap-ansi guarantees agreement.
|
|
35
|
+
function visualLines(value: string, cols: number): VisualLine[] {
|
|
36
|
+
if (!value.length) {
|
|
37
|
+
return [{ start: 0, end: 0 }]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const width = Math.max(1, cols)
|
|
41
|
+
const wrapped = wrapAnsi(value, width, { hard: true, trim: false })
|
|
42
|
+
const lines: VisualLine[] = []
|
|
43
|
+
|
|
44
|
+
let originalIdx = 0
|
|
45
|
+
let lineStart = 0
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < wrapped.length; i += 1) {
|
|
48
|
+
const ch = wrapped[i]!
|
|
49
|
+
|
|
50
|
+
if (ch === '\n') {
|
|
51
|
+
// wrap-ansi inserts '\n' to mark a soft-wrap boundary OR copies a
|
|
52
|
+
// literal '\n' from the input. Either way the next char in `wrapped`
|
|
53
|
+
// begins a new visual line. If the source character is a hard '\n',
|
|
54
|
+
// consume it (it doesn't appear in either line). Otherwise the '\n'
|
|
55
|
+
// is purely a wrap marker and originalIdx stays put.
|
|
56
|
+
lines.push({ start: lineStart, end: originalIdx })
|
|
57
|
+
const isHardNewline = originalIdx < value.length && value[originalIdx] === '\n'
|
|
58
|
+
|
|
59
|
+
if (isHardNewline) {
|
|
60
|
+
originalIdx += 1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
lineStart = originalIdx
|
|
64
|
+
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Defensive sync check. wrap-ansi (with `hard: true, trim: false`, no
|
|
69
|
+
// styled input) is documented to only insert '\n' at break points and
|
|
70
|
+
// never substitute, drop, or reorder source characters — so under those
|
|
71
|
+
// options `wrapped[i]` should always equal `value[originalIdx]`. But
|
|
72
|
+
// future option changes, library upgrades, or callers that start passing
|
|
73
|
+
// styled input (ANSI escapes) could violate that invariant silently. If
|
|
74
|
+
// they do, we'd slide `originalIdx` past the end of `value` and emit
|
|
75
|
+
// garbage line ranges with no diagnostic. Realign by scanning forward
|
|
76
|
+
// for the matching character; bail out (return whatever we have) if the
|
|
77
|
+
// sync is unrecoverable rather than producing wrong-but-plausible output.
|
|
78
|
+
if (originalIdx >= value.length) {
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (value[originalIdx] !== ch) {
|
|
83
|
+
const reSync = value.indexOf(ch, originalIdx)
|
|
84
|
+
|
|
85
|
+
if (reSync === -1) {
|
|
86
|
+
break
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
originalIdx = reSync
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
originalIdx += 1
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
lines.push({ start: lineStart, end: originalIdx })
|
|
96
|
+
|
|
97
|
+
// wrap-ansi collapses an empty input into [""] which we already handled
|
|
98
|
+
// above; preserve the invariant that lines is never empty for any input.
|
|
99
|
+
return lines.length ? lines : [{ start: 0, end: 0 }]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function widthBetween(value: string, start: number, end: number) {
|
|
103
|
+
let width = 0
|
|
104
|
+
|
|
105
|
+
for (const part of graphemes(value.slice(start, end))) {
|
|
106
|
+
width += part.width
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return width
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Mirrors the word-wrap behavior used by the composer TextInput.
|
|
114
|
+
* Returns the zero-based visual line and column of the cursor cell.
|
|
115
|
+
*
|
|
116
|
+
* IMPORTANT: this MUST stay in lock-step with how Ink's `<Text wrap="wrap">`
|
|
117
|
+
* lays the value out (which uses `wrap-ansi`). Any divergence parks the
|
|
118
|
+
* hardware cursor several cells off the last rendered character — see the
|
|
119
|
+
* "cursor drift past blank cells" bug. `visualLines` is sourced directly
|
|
120
|
+
* from wrap-ansi to enforce that invariant.
|
|
121
|
+
*/
|
|
122
|
+
export function cursorLayout(value: string, cursor: number, cols: number) {
|
|
123
|
+
const pos = Math.max(0, Math.min(cursor, value.length))
|
|
124
|
+
const w = Math.max(1, cols)
|
|
125
|
+
const lines = visualLines(value, w)
|
|
126
|
+
let lineIndex = 0
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
129
|
+
if (lines[i]!.start <= pos) {
|
|
130
|
+
lineIndex = i
|
|
131
|
+
} else {
|
|
132
|
+
break
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const line = lines[lineIndex]!
|
|
137
|
+
const column = widthBetween(value, line.start, Math.min(pos, line.end))
|
|
138
|
+
|
|
139
|
+
// NOTE: the previous implementation forced an extra line break when
|
|
140
|
+
// `column >= w` (the "trailing cursor-cell overflows" rule). With
|
|
141
|
+
// `visualLines` sourcing breaks from wrap-ansi, the line wrapping
|
|
142
|
+
// above already matches what Ink will actually render. Pushing the
|
|
143
|
+
// cursor onto a phantom next line here would re-introduce the same
|
|
144
|
+
// drift we're fixing, so we don't.
|
|
145
|
+
return { column, line: lineIndex }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function offsetFromPosition(value: string, row: number, col: number, cols: number) {
|
|
149
|
+
if (!value.length) {
|
|
150
|
+
return 0
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const lines = visualLines(value, cols)
|
|
154
|
+
const target = lines[Math.max(0, Math.min(lines.length - 1, Math.floor(row)))]!
|
|
155
|
+
const targetCol = Math.max(0, Math.floor(col))
|
|
156
|
+
let column = 0
|
|
157
|
+
|
|
158
|
+
for (const part of graphemes(value.slice(target.start, target.end))) {
|
|
159
|
+
if (targetCol <= column + Math.max(0, part.width - 1)) {
|
|
160
|
+
return target.start + part.index
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
column += part.width
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return target.end
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function inputVisualHeight(value: string, columns: number) {
|
|
170
|
+
return cursorLayout(value, value.length, columns).line + 1
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function composerPromptWidth(promptText: string) {
|
|
174
|
+
return Math.max(1, stringWidth(promptText)) + COMPOSER_PROMPT_GAP_WIDTH
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function transcriptGutterWidth(role: Role, userPrompt: string) {
|
|
178
|
+
return role === 'user' ? composerPromptWidth(userPrompt) : 3
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function transcriptBodyWidth(totalCols: number, role: Role, userPrompt: string, termuxMode = false) {
|
|
182
|
+
const horizontalReserve = termuxMode ? 2 : 4
|
|
183
|
+
const available = Math.max(1, totalCols - transcriptGutterWidth(role, userPrompt) - horizontalReserve)
|
|
184
|
+
|
|
185
|
+
if (termuxMode) {
|
|
186
|
+
// On narrow / unusual aspect-ratio mobile panes, forcing a wide minimum
|
|
187
|
+
// width causes right-edge clipping and chopped words.
|
|
188
|
+
return available
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return Math.max(20, available)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function stableComposerColumns(totalCols: number, promptWidth: number, termuxMode = false) {
|
|
195
|
+
// Physical render/wrap width. Always reserve outer composer padding and
|
|
196
|
+
// prompt prefix. Only reserve the transcript scrollbar gutter when the
|
|
197
|
+
// terminal is wide enough; on narrow panes, preserving input columns beats
|
|
198
|
+
// keeping gutters visually aligned.
|
|
199
|
+
const afterPrompt = totalCols - promptWidth
|
|
200
|
+
const reserveScrollbar = afterPrompt >= (termuxMode ? 36 : 24) ? 2 : 0
|
|
201
|
+
|
|
202
|
+
return Math.max(1, totalCols - promptWidth - 2 - reserveScrollbar)
|
|
203
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type { Msg } from '../types.js'
|
|
4
|
+
|
|
5
|
+
import { appendToolShelfMessage, canHoldToolShelf, isTodoDone, mergeToolShelfInto } from './liveProgress.js'
|
|
6
|
+
|
|
7
|
+
describe('isTodoDone', () => {
|
|
8
|
+
it('only treats non-empty all-completed/cancelled lists as done', () => {
|
|
9
|
+
expect(isTodoDone([])).toBe(false)
|
|
10
|
+
expect(isTodoDone([{ content: 'x', id: 'x', status: 'completed' }])).toBe(true)
|
|
11
|
+
expect(isTodoDone([{ content: 'x', id: 'x', status: 'in_progress' }])).toBe(false)
|
|
12
|
+
expect(
|
|
13
|
+
isTodoDone([
|
|
14
|
+
{ content: 'x', id: 'x', status: 'completed' },
|
|
15
|
+
{ content: 'y', id: 'y', status: 'cancelled' }
|
|
16
|
+
])
|
|
17
|
+
).toBe(true)
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('tool shelf helpers', () => {
|
|
22
|
+
it('recognizes contextual thinking shelves as holders', () => {
|
|
23
|
+
expect(canHoldToolShelf({ kind: 'trail', role: 'system', text: '', thinking: 'plan' })).toBe(true)
|
|
24
|
+
expect(canHoldToolShelf({ kind: 'trail', role: 'system', text: '', tools: ['one ✓'] })).toBe(true)
|
|
25
|
+
expect(canHoldToolShelf({ role: 'assistant', text: 'done' })).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('merges source rows into an existing shelf', () => {
|
|
29
|
+
expect(
|
|
30
|
+
mergeToolShelfInto(
|
|
31
|
+
{ kind: 'trail', role: 'system', text: '', thinking: 'plan', tools: ['one ✓'] },
|
|
32
|
+
{ kind: 'trail', role: 'system', text: '', tools: ['two ✓'] }
|
|
33
|
+
)
|
|
34
|
+
).toEqual({ kind: 'trail', role: 'system', text: '', thinking: 'plan', tools: ['one ✓', 'two ✓'] })
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('appendToolShelfMessage', () => {
|
|
39
|
+
it('merges adjacent tool shelves into one contextual shelf', () => {
|
|
40
|
+
const merged = appendToolShelfMessage([{ kind: 'trail', role: 'system', text: '', tools: ['one ✓'] }], {
|
|
41
|
+
kind: 'trail',
|
|
42
|
+
role: 'system',
|
|
43
|
+
text: '',
|
|
44
|
+
tools: ['two ✓']
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
expect(merged).toEqual([{ kind: 'trail', role: 'system', text: '', tools: ['one ✓', 'two ✓'] }])
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('adds tools to the nearest contextual thinking shelf', () => {
|
|
51
|
+
const merged = appendToolShelfMessage(
|
|
52
|
+
[{ kind: 'trail', role: 'system', text: '', thinking: 'plan', tools: ['one ✓'] }],
|
|
53
|
+
{ kind: 'trail', role: 'system', text: '', tools: ['two ✓'] }
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
expect(merged).toEqual([{ kind: 'trail', role: 'system', text: '', thinking: 'plan', tools: ['one ✓', 'two ✓'] }])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('merges through intervening thinking-only rows back into the nearest holder', () => {
|
|
60
|
+
const prev: Msg[] = [
|
|
61
|
+
{ kind: 'trail', role: 'system', text: '', thinking: 'plan', tools: ['one ✓'] },
|
|
62
|
+
{ kind: 'trail', role: 'system', text: '', thinking: 'more plan' }
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
const merged = appendToolShelfMessage(prev, {
|
|
66
|
+
kind: 'trail',
|
|
67
|
+
role: 'system',
|
|
68
|
+
text: '',
|
|
69
|
+
tools: ['two ✓']
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
expect(merged).toHaveLength(2)
|
|
73
|
+
expect(merged[0]).toEqual({
|
|
74
|
+
kind: 'trail',
|
|
75
|
+
role: 'system',
|
|
76
|
+
text: '',
|
|
77
|
+
thinking: 'plan',
|
|
78
|
+
tools: ['one ✓', 'two ✓']
|
|
79
|
+
})
|
|
80
|
+
expect(merged[1]).toEqual({ kind: 'trail', role: 'system', text: '', thinking: 'more plan' })
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('collapses a chronological thinking/tool/thinking/tool stream into one shelf', () => {
|
|
84
|
+
const events: Msg[] = [
|
|
85
|
+
{ kind: 'trail', role: 'system', text: '', thinking: 'plan' },
|
|
86
|
+
{ kind: 'trail', role: 'system', text: '', tools: ['one ✓'] },
|
|
87
|
+
{ kind: 'trail', role: 'system', text: '', thinking: 'more plan' },
|
|
88
|
+
{ kind: 'trail', role: 'system', text: '', tools: ['two ✓'] },
|
|
89
|
+
{ kind: 'trail', role: 'system', text: '', tools: ['three ✓'] }
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
const reduced = events.reduce<Msg[]>((acc, msg) => appendToolShelfMessage(acc, msg), [])
|
|
93
|
+
|
|
94
|
+
expect(reduced).toHaveLength(2)
|
|
95
|
+
expect(reduced[0]).toEqual({
|
|
96
|
+
kind: 'trail',
|
|
97
|
+
role: 'system',
|
|
98
|
+
text: '',
|
|
99
|
+
thinking: 'plan',
|
|
100
|
+
tools: ['one ✓', 'two ✓', 'three ✓']
|
|
101
|
+
})
|
|
102
|
+
expect(reduced[1]).toEqual({ kind: 'trail', role: 'system', text: '', thinking: 'more plan' })
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('starts a new shelf across assistant text boundaries', () => {
|
|
106
|
+
const merged = appendToolShelfMessage(
|
|
107
|
+
[
|
|
108
|
+
{ kind: 'trail', role: 'system', text: '', tools: ['one ✓'] },
|
|
109
|
+
{ role: 'assistant', text: 'done' }
|
|
110
|
+
],
|
|
111
|
+
{ kind: 'trail', role: 'system', text: '', tools: ['two ✓'] }
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
expect(merged).toHaveLength(3)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Msg, TodoItem } from '../types.js'
|
|
2
|
+
|
|
3
|
+
export const countPendingTodos = (todos: readonly TodoItem[]) =>
|
|
4
|
+
todos.filter(todo => todo.status === 'in_progress' || todo.status === 'pending').length
|
|
5
|
+
|
|
6
|
+
export const isTodoDone = (todos: readonly TodoItem[]) =>
|
|
7
|
+
todos.length > 0 && todos.every(todo => todo.status === 'completed' || todo.status === 'cancelled')
|
|
8
|
+
|
|
9
|
+
export const isToolShelfMessage = (msg: Msg | undefined) =>
|
|
10
|
+
Boolean(msg?.kind === 'trail' && !msg.text && !msg.thinking?.trim() && msg.tools?.length)
|
|
11
|
+
|
|
12
|
+
export const canHoldToolShelf = (msg: Msg | undefined) =>
|
|
13
|
+
Boolean(msg?.kind === 'trail' && !msg.text && (msg.thinking?.trim() || msg.tools?.length))
|
|
14
|
+
|
|
15
|
+
export const mergeToolShelfInto = (target: Msg, source: Msg): Msg => ({
|
|
16
|
+
...target,
|
|
17
|
+
tools: [...(target.tools ?? []), ...(source.tools ?? [])]
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const isBarrierMessage = (msg: Msg | undefined) => {
|
|
21
|
+
if (!msg) {
|
|
22
|
+
return true
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Assistant text, user input, intro/panel rows all terminate the shelf.
|
|
26
|
+
if (msg.kind === 'intro' || msg.kind === 'panel' || msg.kind === 'diff') {
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (msg.role && msg.role !== 'system') {
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (msg.text) {
|
|
35
|
+
return true
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isToolCarryingTrail = (msg: Msg | undefined) => Boolean(msg?.kind === 'trail' && !msg.text && msg.tools?.length)
|
|
42
|
+
|
|
43
|
+
export const appendToolShelfMessage = (prev: readonly Msg[], msg: Msg): Msg[] => {
|
|
44
|
+
if (!isToolShelfMessage(msg)) {
|
|
45
|
+
return [...prev, msg]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let fallbackHolder: number | null = null
|
|
49
|
+
|
|
50
|
+
for (let index = prev.length - 1; index >= 0; index--) {
|
|
51
|
+
const candidate = prev[index]
|
|
52
|
+
|
|
53
|
+
if (isToolCarryingTrail(candidate)) {
|
|
54
|
+
const next = [...prev]
|
|
55
|
+
|
|
56
|
+
next[index] = mergeToolShelfInto(candidate!, msg)
|
|
57
|
+
|
|
58
|
+
return next
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (fallbackHolder === null && canHoldToolShelf(candidate)) {
|
|
62
|
+
fallbackHolder = index
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (isBarrierMessage(candidate)) {
|
|
66
|
+
break
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (fallbackHolder !== null) {
|
|
71
|
+
const next = [...prev]
|
|
72
|
+
|
|
73
|
+
next[fallbackHolder] = mergeToolShelfInto(prev[fallbackHolder]!, msg)
|
|
74
|
+
|
|
75
|
+
return next
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return [...prev, msg]
|
|
79
|
+
}
|