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,144 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { creditsCommands } from '../app/slash/commands/credits.js'
|
|
4
|
+
import { getOverlayState, resetOverlayState } from '../app/overlayStore.js'
|
|
5
|
+
import type { CreditsViewResponse } from '../gatewayTypes.js'
|
|
6
|
+
|
|
7
|
+
// The command opens the top-up URL through this helper on confirm. Mock it so
|
|
8
|
+
// the test never shells out to a real browser/`xdg-open` and we can assert the
|
|
9
|
+
// success/failure messaging deterministically.
|
|
10
|
+
vi.mock('../lib/openExternalUrl.js', () => ({
|
|
11
|
+
openExternalUrl: vi.fn(() => true)
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
import { openExternalUrl } from '../lib/openExternalUrl.js'
|
|
15
|
+
|
|
16
|
+
const openExternalUrlMock = vi.mocked(openExternalUrl)
|
|
17
|
+
|
|
18
|
+
const creditsCommand = creditsCommands.find(cmd => cmd.name === 'credits')!
|
|
19
|
+
|
|
20
|
+
const buildView = (overrides: Partial<CreditsViewResponse> = {}): CreditsViewResponse => ({
|
|
21
|
+
balance_lines: ['Grant: $9.50 left', 'Top-up: $25.00'],
|
|
22
|
+
depleted: false,
|
|
23
|
+
identity_line: 'Signed in as ada@example.com',
|
|
24
|
+
logged_in: true,
|
|
25
|
+
topup_url: 'https://portal.nastechai.com/billing/topup',
|
|
26
|
+
...overrides
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Mirror createSlashHandler's real `guarded` wrapper: skip the handler when the
|
|
30
|
+
// command is stale OR the response is falsy. Tests stay non-stale, so this is a
|
|
31
|
+
// straightforward "run the handler when we got a response" shim.
|
|
32
|
+
const guarded =
|
|
33
|
+
<T,>(fn: (r: T) => void) =>
|
|
34
|
+
(r: null | T) => {
|
|
35
|
+
if (r) {
|
|
36
|
+
fn(r)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const buildCtx = (rpcResult: CreditsViewResponse) => {
|
|
41
|
+
const sys = vi.fn()
|
|
42
|
+
const rpc = vi.fn(() => Promise.resolve(rpcResult))
|
|
43
|
+
const guardedErr = vi.fn()
|
|
44
|
+
|
|
45
|
+
const ctx = {
|
|
46
|
+
gateway: { rpc },
|
|
47
|
+
guarded,
|
|
48
|
+
guardedErr,
|
|
49
|
+
sid: 'sid-abc',
|
|
50
|
+
stale: () => false,
|
|
51
|
+
transcript: { page: vi.fn(), panel: vi.fn(), sys }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Run the command, then await the rpc promise so the .then() handler has
|
|
55
|
+
// flushed before assertions — deterministic, no polling/timeouts.
|
|
56
|
+
const run = async () => {
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
creditsCommand.run('', ctx as any, 'credits')
|
|
59
|
+
await rpc.mock.results[0]?.value
|
|
60
|
+
// Allow the chained .then() microtask to settle.
|
|
61
|
+
await Promise.resolve()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { ctx, rpc, run, sys }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('/credits slash command', () => {
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
resetOverlayState()
|
|
70
|
+
openExternalUrlMock.mockClear()
|
|
71
|
+
openExternalUrlMock.mockReturnValue(true)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('renders the balance (including top-up URL) and arms the confirm overlay', async () => {
|
|
75
|
+
const view = buildView()
|
|
76
|
+
const { rpc, run, sys } = buildCtx(view)
|
|
77
|
+
|
|
78
|
+
await run()
|
|
79
|
+
|
|
80
|
+
expect(rpc).toHaveBeenCalledWith('credits.view', { session_id: 'sid-abc' })
|
|
81
|
+
|
|
82
|
+
// (a) sys received the balance text including the topup_url
|
|
83
|
+
const printed = sys.mock.calls.map(call => call[0]).join('\n')
|
|
84
|
+
expect(printed).toContain('💳 NasTech credits')
|
|
85
|
+
expect(printed).toContain('Grant: $9.50 left')
|
|
86
|
+
expect(printed).toContain('Signed in as ada@example.com')
|
|
87
|
+
expect(printed).toContain(view.topup_url)
|
|
88
|
+
|
|
89
|
+
// (b) confirm overlay set with the expected label + detail
|
|
90
|
+
const confirm = getOverlayState().confirm
|
|
91
|
+
expect(confirm).toBeTruthy()
|
|
92
|
+
expect(confirm?.confirmLabel).toBe('Open top-up in browser')
|
|
93
|
+
expect(confirm?.cancelLabel).toBe('Cancel')
|
|
94
|
+
expect(confirm?.title).toBe('Add credits?')
|
|
95
|
+
expect(confirm?.detail).toBe(view.topup_url)
|
|
96
|
+
|
|
97
|
+
// onConfirm opens the URL and reports success back to the transcript
|
|
98
|
+
confirm?.onConfirm()
|
|
99
|
+
expect(openExternalUrlMock).toHaveBeenCalledWith(view.topup_url)
|
|
100
|
+
expect(sys).toHaveBeenCalledWith(
|
|
101
|
+
'Complete your top-up in the browser — credits will appear in /credits shortly.'
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('falls back to printing the URL when the browser open is rejected', async () => {
|
|
106
|
+
openExternalUrlMock.mockReturnValue(false)
|
|
107
|
+
const view = buildView()
|
|
108
|
+
const { run, sys } = buildCtx(view)
|
|
109
|
+
|
|
110
|
+
await run()
|
|
111
|
+
|
|
112
|
+
const confirm = getOverlayState().confirm
|
|
113
|
+
expect(confirm).toBeTruthy()
|
|
114
|
+
confirm?.onConfirm()
|
|
115
|
+
expect(sys).toHaveBeenCalledWith(`Open this URL to top up: ${view.topup_url}`)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('does not arm the confirm overlay when there is no top-up URL', async () => {
|
|
119
|
+
const view = buildView({ topup_url: null })
|
|
120
|
+
const { run, sys } = buildCtx(view)
|
|
121
|
+
|
|
122
|
+
await run()
|
|
123
|
+
|
|
124
|
+
const printed = sys.mock.calls.map(call => call[0]).join('\n')
|
|
125
|
+
expect(printed).toContain('💳 NasTech credits')
|
|
126
|
+
expect(getOverlayState().confirm).toBeNull()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('shows the not-logged-in message and does NOT arm the confirm overlay', async () => {
|
|
130
|
+
const view = buildView({
|
|
131
|
+
balance_lines: [],
|
|
132
|
+
identity_line: null,
|
|
133
|
+
logged_in: false,
|
|
134
|
+
topup_url: null
|
|
135
|
+
})
|
|
136
|
+
const { run, sys } = buildCtx(view)
|
|
137
|
+
|
|
138
|
+
await run()
|
|
139
|
+
|
|
140
|
+
expect(sys).toHaveBeenCalledWith('💳 Not logged into NasTech Portal — run /portal to log in.')
|
|
141
|
+
expect(getOverlayState().confirm).toBeNull()
|
|
142
|
+
expect(openExternalUrlMock).not.toHaveBeenCalled()
|
|
143
|
+
})
|
|
144
|
+
})
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pinned regression for the multi-line composer cursor-drift bug.
|
|
3
|
+
*
|
|
4
|
+
* Symptom: in `nastech --tui`, typing into the composer until the input
|
|
5
|
+
* wraps across multiple visual rows would leave several blank cells
|
|
6
|
+
* between the last typed character and the (hardware) cursor block.
|
|
7
|
+
* Worse on narrow terminals (the Cursor IDE built-in terminal in
|
|
8
|
+
* particular).
|
|
9
|
+
*
|
|
10
|
+
* Root cause: the composer's `cursorLayout` (used by `useDeclaredCursor`
|
|
11
|
+
* to place the hardware cursor) ran a hand-rolled word-wrap algorithm,
|
|
12
|
+
* while Ink's `<Text wrap="wrap">` renders via `wrap-ansi`. The two
|
|
13
|
+
* disagreed on many real inputs — wrap-ansi would keep "branch
|
|
14
|
+
* investigate" on one row while cursorLayout claimed it had wrapped,
|
|
15
|
+
* etc. — so the declared cursor position drifted from where the text
|
|
16
|
+
* was actually rendered. The fix sources cursorLayout's line breaks
|
|
17
|
+
* directly from wrap-ansi, guaranteeing agreement.
|
|
18
|
+
*
|
|
19
|
+
* This test pins the contract: for every char that would be typed into
|
|
20
|
+
* the composer, the cursor position reported by cursorLayout MUST equal
|
|
21
|
+
* the end-of-text position that wrap-ansi would render. Any future
|
|
22
|
+
* regression that lets the two diverge re-introduces the drift.
|
|
23
|
+
*/
|
|
24
|
+
import { wrapAnsi } from '@nastechai/ink'
|
|
25
|
+
import { describe, expect, it } from 'vitest'
|
|
26
|
+
|
|
27
|
+
import { cursorLayout, inputVisualHeight } from '../lib/inputMetrics.js'
|
|
28
|
+
|
|
29
|
+
function wrapAnsiEnd(text: string, cols: number): { line: number; column: number } {
|
|
30
|
+
const wrapped = wrapAnsi(text, cols, { hard: true, trim: false })
|
|
31
|
+
const lines = wrapped.split('\n')
|
|
32
|
+
const last = lines[lines.length - 1] ?? ''
|
|
33
|
+
|
|
34
|
+
return { line: lines.length - 1, column: last.length }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const USER_REPORT_MESSAGE =
|
|
38
|
+
// Paraphrase of the user's actual bug report, included verbatim so the
|
|
39
|
+
// test is grounded in a realistic typing pattern (long single line,
|
|
40
|
+
// mixed-length words, punctuation, no hard newlines).
|
|
41
|
+
'im in cursor terminal using nastech --tui and as i type multiline my caret at the end will often ' +
|
|
42
|
+
'go.. randomly.. like multiple spaces away lol and idk why. theres no rhyme/reason really but ' +
|
|
43
|
+
'there should literally never be a non-user added space at the end of my composer input right? ' +
|
|
44
|
+
'i dont think it happens on new sessions but only existing ones. there have been a few prs to ' +
|
|
45
|
+
'try to fix this and all not working. ok it just happened, to me, nowso attaching screenshot ' +
|
|
46
|
+
'and you can see its multiline, new session. on a new bb/<xxx> branch investigate'
|
|
47
|
+
|
|
48
|
+
describe('cursor-drift regression — composer cursorLayout matches Ink rendering', () => {
|
|
49
|
+
it('agrees with wrap-ansi at every typing-prefix of the user-reported message', () => {
|
|
50
|
+
// Walks the message char-by-char (mirroring what the TUI sees when a
|
|
51
|
+
// user types). At every prefix, cursorLayout must place the cursor
|
|
52
|
+
// exactly where wrap-ansi would render the end of the text.
|
|
53
|
+
//
|
|
54
|
+
// Pre-fix: this failed on most narrow widths because the hand-rolled
|
|
55
|
+
// wrap algorithm broke at slightly different points than wrap-ansi.
|
|
56
|
+
for (const cols of [40, 50, 55, 60, 65, 70, 80]) {
|
|
57
|
+
let acc = ''
|
|
58
|
+
|
|
59
|
+
for (const ch of USER_REPORT_MESSAGE) {
|
|
60
|
+
acc += ch
|
|
61
|
+
const layout = cursorLayout(acc, acc.length, cols)
|
|
62
|
+
const expected = wrapAnsiEnd(acc, cols)
|
|
63
|
+
|
|
64
|
+
expect(
|
|
65
|
+
layout,
|
|
66
|
+
`mismatch at cols=${cols}, len=${acc.length}, last-char=${JSON.stringify(ch)}, ` +
|
|
67
|
+
`tail=${JSON.stringify(acc.slice(-30))}`
|
|
68
|
+
).toEqual(expected)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('keeps cursor on the same row when text exactly fills the terminal width', () => {
|
|
74
|
+
// wrap-ansi does NOT push exact-fill text onto a phantom next line.
|
|
75
|
+
// The previous algorithm did — that's what produced the visible
|
|
76
|
+
// "cursor parked one row below the last char" symptom on narrow
|
|
77
|
+
// terminals at certain message lengths.
|
|
78
|
+
for (const cols of [8, 12, 18, 24]) {
|
|
79
|
+
const text = 'a'.repeat(cols)
|
|
80
|
+
const layout = cursorLayout(text, text.length, cols)
|
|
81
|
+
const inkLines = wrapAnsi(text, cols, { hard: true, trim: false }).split('\n')
|
|
82
|
+
|
|
83
|
+
expect(layout.line).toBe(0)
|
|
84
|
+
expect(layout.column).toBe(cols)
|
|
85
|
+
expect(inkLines).toHaveLength(1)
|
|
86
|
+
expect(inputVisualHeight(text, cols)).toBe(1)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('does not stuff a trailing whitespace word onto a phantom line', () => {
|
|
91
|
+
// "branch investigate" at cols=20 fits on one row in wrap-ansi. The
|
|
92
|
+
// bug claimed otherwise, parking the cursor at (line=1, col=?) and
|
|
93
|
+
// leaving the user's "branch investigate" rendered alone on row 0
|
|
94
|
+
// with the cursor block several cells past it.
|
|
95
|
+
const text = 'branch investigate'
|
|
96
|
+
const cols = 20
|
|
97
|
+
|
|
98
|
+
expect(cursorLayout(text, text.length, cols)).toEqual({ column: text.length, line: 0 })
|
|
99
|
+
expect(cursorLayout(text, text.length, cols)).toEqual(wrapAnsiEnd(text, cols))
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('agrees with wrap-ansi for word-wrap that pushes a word onto the next line', () => {
|
|
103
|
+
// "hello world" at cols=8 wraps to ["hello ", "world"] in wrap-ansi.
|
|
104
|
+
// The cursor at end-of-text must land at line=1, col=5 — where Ink
|
|
105
|
+
// actually renders the last 'd'. The previous algorithm reported
|
|
106
|
+
// (line=2, col=0) here (phantom extra wrap), which parked the
|
|
107
|
+
// cursor on a row Ink never painted.
|
|
108
|
+
const text = 'hello world'
|
|
109
|
+
const cols = 8
|
|
110
|
+
|
|
111
|
+
expect(cursorLayout(text, text.length, cols)).toEqual({ column: 5, line: 1 })
|
|
112
|
+
expect(cursorLayout(text, text.length, cols)).toEqual(wrapAnsiEnd(text, cols))
|
|
113
|
+
})
|
|
114
|
+
})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { isSectionName, parseDetailsMode, resolveSections, SECTION_NAMES, sectionMode } from '../domain/details.js'
|
|
4
|
+
|
|
5
|
+
describe('parseDetailsMode', () => {
|
|
6
|
+
it('accepts the canonical modes case-insensitively', () => {
|
|
7
|
+
expect(parseDetailsMode('hidden')).toBe('hidden')
|
|
8
|
+
expect(parseDetailsMode(' COLLAPSED ')).toBe('collapsed')
|
|
9
|
+
expect(parseDetailsMode('Expanded')).toBe('expanded')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('rejects junk', () => {
|
|
13
|
+
expect(parseDetailsMode('truncated')).toBeNull()
|
|
14
|
+
expect(parseDetailsMode('')).toBeNull()
|
|
15
|
+
expect(parseDetailsMode(undefined)).toBeNull()
|
|
16
|
+
expect(parseDetailsMode(42)).toBeNull()
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('isSectionName', () => {
|
|
21
|
+
it('only lets the four canonical sections through', () => {
|
|
22
|
+
expect(isSectionName('thinking')).toBe(true)
|
|
23
|
+
expect(isSectionName('tools')).toBe(true)
|
|
24
|
+
expect(isSectionName('subagents')).toBe(true)
|
|
25
|
+
expect(isSectionName('activity')).toBe(true)
|
|
26
|
+
|
|
27
|
+
expect(isSectionName('Thinking')).toBe(false) // case-sensitive on purpose
|
|
28
|
+
expect(isSectionName('bogus')).toBe(false)
|
|
29
|
+
expect(isSectionName('')).toBe(false)
|
|
30
|
+
expect(isSectionName(7)).toBe(false)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('SECTION_NAMES exposes them all', () => {
|
|
34
|
+
expect([...SECTION_NAMES].sort()).toEqual(['activity', 'subagents', 'thinking', 'tools'])
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('resolveSections', () => {
|
|
39
|
+
it('parses a well-formed sections object', () => {
|
|
40
|
+
expect(
|
|
41
|
+
resolveSections({
|
|
42
|
+
thinking: 'expanded',
|
|
43
|
+
tools: 'expanded',
|
|
44
|
+
subagents: 'collapsed',
|
|
45
|
+
activity: 'hidden'
|
|
46
|
+
})
|
|
47
|
+
).toEqual({
|
|
48
|
+
thinking: 'expanded',
|
|
49
|
+
tools: 'expanded',
|
|
50
|
+
subagents: 'collapsed',
|
|
51
|
+
activity: 'hidden'
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('drops unknown section names and unknown modes', () => {
|
|
56
|
+
expect(
|
|
57
|
+
resolveSections({
|
|
58
|
+
thinking: 'expanded',
|
|
59
|
+
tools: 'maximised',
|
|
60
|
+
bogus: 'hidden',
|
|
61
|
+
activity: 'hidden'
|
|
62
|
+
})
|
|
63
|
+
).toEqual({ thinking: 'expanded', activity: 'hidden' })
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('treats nullish/non-objects as empty overrides', () => {
|
|
67
|
+
expect(resolveSections(undefined)).toEqual({})
|
|
68
|
+
expect(resolveSections(null)).toEqual({})
|
|
69
|
+
expect(resolveSections('hidden')).toEqual({})
|
|
70
|
+
expect(resolveSections([])).toEqual({})
|
|
71
|
+
})
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('sectionMode', () => {
|
|
75
|
+
it('falls back to the global mode for sections without a built-in default', () => {
|
|
76
|
+
expect(sectionMode('subagents', 'collapsed', {})).toBe('collapsed')
|
|
77
|
+
expect(sectionMode('subagents', 'expanded', undefined)).toBe('expanded')
|
|
78
|
+
expect(sectionMode('subagents', 'hidden', {})).toBe('hidden')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('streams thinking + tools expanded by default for persisted config values', () => {
|
|
82
|
+
expect(sectionMode('thinking', 'collapsed', {})).toBe('expanded')
|
|
83
|
+
expect(sectionMode('thinking', 'hidden', undefined)).toBe('expanded')
|
|
84
|
+
expect(sectionMode('tools', 'collapsed', {})).toBe('expanded')
|
|
85
|
+
expect(sectionMode('tools', 'hidden', undefined)).toBe('expanded')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('hides the activity panel by default for persisted config values', () => {
|
|
89
|
+
expect(sectionMode('activity', 'collapsed', {})).toBe('hidden')
|
|
90
|
+
expect(sectionMode('activity', 'expanded', undefined)).toBe('hidden')
|
|
91
|
+
expect(sectionMode('activity', 'hidden', {})).toBe('hidden')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('applies in-session /details mode globally over built-in defaults', () => {
|
|
95
|
+
expect(sectionMode('thinking', 'collapsed', {}, true)).toBe('collapsed')
|
|
96
|
+
expect(sectionMode('tools', 'hidden', {}, true)).toBe('hidden')
|
|
97
|
+
expect(sectionMode('activity', 'expanded', undefined, true)).toBe('expanded')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('honours per-section overrides over both the section default and global mode', () => {
|
|
101
|
+
expect(sectionMode('thinking', 'collapsed', { thinking: 'collapsed' })).toBe('collapsed')
|
|
102
|
+
expect(sectionMode('tools', 'collapsed', { tools: 'hidden' })).toBe('hidden')
|
|
103
|
+
expect(sectionMode('activity', 'collapsed', { activity: 'expanded' })).toBe('expanded')
|
|
104
|
+
expect(sectionMode('activity', 'expanded', { activity: 'collapsed' })).toBe('collapsed')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('lets per-section overrides escape the global hidden mode', () => {
|
|
108
|
+
// Regression for the case where global details_mode: hidden used to
|
|
109
|
+
// short-circuit the entire accordion and prevent overrides from
|
|
110
|
+
// surfacing — `sections.tools: expanded` must still resolve to expanded.
|
|
111
|
+
expect(sectionMode('subagents', 'hidden', { subagents: 'expanded' })).toBe('expanded')
|
|
112
|
+
expect(sectionMode('thinking', 'hidden', { thinking: 'collapsed' })).toBe('collapsed')
|
|
113
|
+
expect(sectionMode('activity', 'hidden', { activity: 'expanded' })).toBe('expanded')
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { ensureEmojiPresentation } from '../lib/emoji.js'
|
|
4
|
+
|
|
5
|
+
const VS16 = '\uFE0F'
|
|
6
|
+
|
|
7
|
+
describe('ensureEmojiPresentation', () => {
|
|
8
|
+
it('passes through ASCII unchanged', () => {
|
|
9
|
+
expect(ensureEmojiPresentation('hello world')).toBe('hello world')
|
|
10
|
+
expect(ensureEmojiPresentation('')).toBe('')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('passes through emoji that already defaults to emoji presentation', () => {
|
|
14
|
+
expect(ensureEmojiPresentation('🚀 rocket')).toBe('🚀 rocket')
|
|
15
|
+
expect(ensureEmojiPresentation('😀')).toBe('😀')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('injects VS16 after text-default emoji codepoints', () => {
|
|
19
|
+
expect(ensureEmojiPresentation('⚠ careful')).toBe(`⚠${VS16} careful`)
|
|
20
|
+
expect(ensureEmojiPresentation('ℹ info')).toBe(`ℹ${VS16} info`)
|
|
21
|
+
expect(ensureEmojiPresentation('love ❤ you')).toBe(`love ❤${VS16} you`)
|
|
22
|
+
expect(ensureEmojiPresentation('✔ done')).toBe(`✔${VS16} done`)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('is idempotent when VS16 is already present', () => {
|
|
26
|
+
const already = `⚠${VS16} ℹ${VS16} ❤${VS16}`
|
|
27
|
+
|
|
28
|
+
expect(ensureEmojiPresentation(already)).toBe(already)
|
|
29
|
+
expect(ensureEmojiPresentation(ensureEmojiPresentation('⚠'))).toBe(`⚠${VS16}`)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('leaves keycap sequences alone when the base is not a text-default emoji', () => {
|
|
33
|
+
expect(ensureEmojiPresentation('1\u20e3')).toBe('1\u20e3')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('injects VS16 before ZWJ so text-default bases participate in emoji sequences', () => {
|
|
37
|
+
// ❤ + ZWJ + 🔥 → ❤️🔥 (heart on fire). Without VS16 between the heart
|
|
38
|
+
// and the ZWJ, terminals render the heart in text/monochrome form and
|
|
39
|
+
// the ZWJ ligature can fail to form.
|
|
40
|
+
const heartFire = '\u2764\u200d\ud83d\udd25'
|
|
41
|
+
|
|
42
|
+
expect(ensureEmojiPresentation(heartFire)).toBe(`\u2764\uFE0F\u200d\ud83d\udd25`)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('leaves explicit text-presentation selector (VS15) alone', () => {
|
|
46
|
+
// `❤︎` (U+2764 + U+FE0E) asks for text presentation — injecting VS16
|
|
47
|
+
// would create an invalid double-variation sequence.
|
|
48
|
+
const explicitText = '\u2764\ufe0e'
|
|
49
|
+
|
|
50
|
+
expect(ensureEmojiPresentation(explicitText)).toBe(explicitText)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('returns the original reference when no change is needed', () => {
|
|
54
|
+
const already = `⚠${VS16} ℹ${VS16} ❤${VS16}`
|
|
55
|
+
|
|
56
|
+
// Reference equality — the lazy allocator should short-circuit to the
|
|
57
|
+
// input when nothing needed injection.
|
|
58
|
+
expect(ensureEmojiPresentation(already)).toBe(already)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('handles mixed content', () => {
|
|
62
|
+
expect(ensureEmojiPresentation('⚠ path: /tmp/x ❤ done')).toBe(`⚠${VS16} path: /tmp/x ❤${VS16} done`)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
__resetLinkTitleCache,
|
|
5
|
+
fetchLinkTitle,
|
|
6
|
+
hostPathLabel,
|
|
7
|
+
isTitleFetchable,
|
|
8
|
+
normalizeExternalUrl,
|
|
9
|
+
urlSlugTitleLabel
|
|
10
|
+
} from '../lib/externalLink.js'
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
__resetLinkTitleCache()
|
|
14
|
+
vi.restoreAllMocks()
|
|
15
|
+
vi.unstubAllGlobals()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('external link helpers', () => {
|
|
19
|
+
it('formats URL fallbacks as host + path', () => {
|
|
20
|
+
expect(
|
|
21
|
+
hostPathLabel(
|
|
22
|
+
'https://www.getyourguide.com/culebra-island-l145468/from-fajardo-full-day-cordillera-islands-catamaran-tour-t19894/'
|
|
23
|
+
)
|
|
24
|
+
).toBe('getyourguide.com/culebra-island-l145468/from-fajardo-full-day-cordillera-islands-catamaran-tour-t19894')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('derives readable title fallbacks from URL slugs', () => {
|
|
28
|
+
expect(
|
|
29
|
+
urlSlugTitleLabel('https://www.getyourguide.com/fajardo-l882/from-fajardo-icacos-island-full-day-catamaran-trip-t19891/')
|
|
30
|
+
).toBe('From Fajardo Icacos Island Full Day Catamaran Trip')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('keeps x.com status fallbacks link-like instead of generic Status labels', () => {
|
|
34
|
+
expect(urlSlugTitleLabel('https://x.com/grok/status/2056065022749479209')).toBe(
|
|
35
|
+
'x.com/grok/status/2056065022749479209'
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('normalizes scheme-less links', () => {
|
|
40
|
+
expect(normalizeExternalUrl(' expedia.com/things-to-do/puerto-rico-el-yunque ')).toBe(
|
|
41
|
+
'https://expedia.com/things-to-do/puerto-rico-el-yunque'
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('filters out local/non-http targets for title fetches', () => {
|
|
46
|
+
expect(isTitleFetchable('https://www.expedia.com/things-to-do/foo')).toBe(true)
|
|
47
|
+
expect(isTitleFetchable('http://localhost:5174')).toBe(false)
|
|
48
|
+
expect(isTitleFetchable('file:///tmp/demo.html')).toBe(false)
|
|
49
|
+
expect(isTitleFetchable('mailto:hello@example.com')).toBe(false)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('blocks private, link-local, and intranet hosts', () => {
|
|
53
|
+
expect(isTitleFetchable('http://10.0.0.12/path')).toBe(false)
|
|
54
|
+
expect(isTitleFetchable('http://172.22.5.4/path')).toBe(false)
|
|
55
|
+
expect(isTitleFetchable('http://192.168.1.22/path')).toBe(false)
|
|
56
|
+
expect(isTitleFetchable('http://169.254.169.254/latest/meta-data')).toBe(false)
|
|
57
|
+
expect(isTitleFetchable('http://[fd00::1]/')).toBe(false)
|
|
58
|
+
expect(isTitleFetchable('http://[fe80::1]/')).toBe(false)
|
|
59
|
+
expect(isTitleFetchable('http://printer.local/status')).toBe(false)
|
|
60
|
+
expect(isTitleFetchable('http://intranet/status')).toBe(false)
|
|
61
|
+
expect(isTitleFetchable('https://8.8.8.8/status')).toBe(true)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('deduplicates in-flight title fetches and caches results', async () => {
|
|
65
|
+
const fetchMock = vi.fn().mockResolvedValue(
|
|
66
|
+
new Response('<html><head><title>El Yunque Tour Water Slide, Rope Swing & Pickup</title></head></html>', {
|
|
67
|
+
headers: { 'content-type': 'text/html; charset=utf-8' },
|
|
68
|
+
status: 200
|
|
69
|
+
})
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
73
|
+
|
|
74
|
+
const url = 'https://www.expedia.com/things-to-do/puerto-rico-el-yunque-rainforest-adventure.a46272756.activity-details'
|
|
75
|
+
const [first, second] = await Promise.all([fetchLinkTitle(url), fetchLinkTitle(url)])
|
|
76
|
+
|
|
77
|
+
expect(first).toBe('El Yunque Tour Water Slide, Rope Swing & Pickup')
|
|
78
|
+
expect(second).toBe('El Yunque Tour Water Slide, Rope Swing & Pickup')
|
|
79
|
+
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
80
|
+
|
|
81
|
+
const third = await fetchLinkTitle(url)
|
|
82
|
+
|
|
83
|
+
expect(third).toBe('El Yunque Tour Water Slide, Rope Swing & Pickup')
|
|
84
|
+
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('shares cache across protocol/www URL variants', async () => {
|
|
88
|
+
const fetchMock = vi.fn().mockResolvedValue(
|
|
89
|
+
new Response('<html><head><title>Shared Canonical Title</title></head></html>', {
|
|
90
|
+
headers: { 'content-type': 'text/html' },
|
|
91
|
+
status: 200
|
|
92
|
+
})
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
96
|
+
|
|
97
|
+
const first = 'https://www.getyourguide.com/san-juan-puerto-rico-l355/sunset-tours-tc306/'
|
|
98
|
+
const second = 'http://getyourguide.com/san-juan-puerto-rico-l355/sunset-tours-tc306/'
|
|
99
|
+
|
|
100
|
+
const [a, b] = await Promise.all([fetchLinkTitle(first), fetchLinkTitle(second)])
|
|
101
|
+
|
|
102
|
+
expect(a).toBe('Shared Canonical Title')
|
|
103
|
+
expect(b).toBe('Shared Canonical Title')
|
|
104
|
+
expect(fetchMock).toHaveBeenCalledTimes(1)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('ignores error-like fetched titles', async () => {
|
|
108
|
+
const fetchMock = vi.fn().mockResolvedValue(
|
|
109
|
+
new Response('<html><head><title>Just a moment...</title></head></html>', {
|
|
110
|
+
headers: { 'content-type': 'text/html' },
|
|
111
|
+
status: 200
|
|
112
|
+
})
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
116
|
+
|
|
117
|
+
const url = 'https://www.getyourguide.com/culebra-island-l145468/from-fajardo-full-day-cordillera-islands-catamaran-tour-t19894/'
|
|
118
|
+
|
|
119
|
+
await expect(fetchLinkTitle(url)).resolves.toBe('')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('decodes HTML entities in fetched titles', async () => {
|
|
123
|
+
const fetchMock = vi.fn().mockResolvedValue(
|
|
124
|
+
new Response('<html><head><title>AT&T 'Deals'</title></head></html>', {
|
|
125
|
+
headers: { 'content-type': 'text/html' },
|
|
126
|
+
status: 200
|
|
127
|
+
})
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
131
|
+
|
|
132
|
+
await expect(fetchLinkTitle('https://example.com/offers')).resolves.toBe("AT&T 'Deals'")
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('skips network fetch for non-fetchable targets', async () => {
|
|
136
|
+
const fetchMock = vi.fn()
|
|
137
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
138
|
+
|
|
139
|
+
await expect(fetchLinkTitle('http://localhost:3000/path')).resolves.toBe('')
|
|
140
|
+
await expect(fetchLinkTitle('mailto:hello@example.com')).resolves.toBe('')
|
|
141
|
+
await expect(fetchLinkTitle('file:///tmp/demo.html')).resolves.toBe('')
|
|
142
|
+
expect(fetchMock).not.toHaveBeenCalled()
|
|
143
|
+
})
|
|
144
|
+
})
|