rovecode 0.4.0-beta.3 → 0.4.0
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/README.md +57 -72
- package/THIRD_PARTY_NOTICES.md +0 -44
- package/bin/rovecode.ts +21 -0
- package/package.json +16 -38
- package/src/account/keys.ts +97 -0
- package/src/account/login.ts +158 -0
- package/src/account/provision.ts +47 -0
- package/src/account/store.ts +63 -0
- package/src/acp/server.ts +373 -0
- package/src/cli/account-cmd.ts +116 -0
- package/src/cli/connect.ts +244 -0
- package/src/cli/context-cmd.ts +199 -0
- package/src/cli/dispatch.ts +109 -0
- package/src/cli/doctor.ts +324 -0
- package/src/cli/export.ts +278 -0
- package/src/cli/help.ts +240 -0
- package/src/cli/is-tui-invocation.ts +8 -0
- package/src/cli/main.ts +599 -0
- package/src/cli/market-cmd.ts +658 -0
- package/src/cli/mcp-market-cmd.ts +299 -0
- package/src/cli/output.ts +382 -0
- package/src/cli/repl.ts +172 -0
- package/src/cli/resume.ts +32 -0
- package/src/cli/run-limits.ts +78 -0
- package/src/cli/runtime.ts +792 -0
- package/src/cli/setup.ts +187 -0
- package/src/cli/update-cmd.ts +78 -0
- package/src/cli/workflow-cmd.ts +100 -0
- package/src/coding/checkpoints.ts +270 -0
- package/src/coding/diff.ts +136 -0
- package/src/coding/files.ts +339 -0
- package/src/coding/hashline.ts +319 -0
- package/src/coding/lsp.ts +406 -0
- package/src/coding/repomap-cache.ts +99 -0
- package/src/coding/repomap-files.ts +110 -0
- package/src/coding/repomap.ts +392 -0
- package/src/core/compaction.ts +399 -0
- package/src/core/config.ts +289 -0
- package/src/core/context-report.ts +228 -0
- package/src/core/context.ts +60 -0
- package/src/core/count-remote.ts +107 -0
- package/src/core/execpolicy-rules.ts +196 -0
- package/src/core/execpolicy.ts +385 -0
- package/src/core/executor.ts +397 -0
- package/src/core/guardrails.ts +400 -0
- package/src/core/hooks.ts +398 -0
- package/src/core/images.ts +230 -0
- package/src/core/intro.ts +236 -0
- package/src/core/loop.ts +621 -0
- package/src/core/modes.ts +372 -0
- package/src/core/orchestrator.ts +207 -0
- package/src/core/reflection.ts +165 -0
- package/src/core/sandbox-config.ts +167 -0
- package/src/core/session-images.ts +73 -0
- package/src/core/session.ts +398 -0
- package/src/core/settings.ts +98 -0
- package/src/core/stuck-detector.ts +273 -0
- package/src/core/tasks.ts +374 -0
- package/src/core/token-scale.ts +108 -0
- package/src/core/tool-output-budget.ts +166 -0
- package/src/core/tools.ts +288 -0
- package/src/core/types.ts +330 -0
- package/src/core/update-check.ts +171 -0
- package/src/core/update.ts +158 -0
- package/src/core/usage.ts +204 -0
- package/src/core/validate.ts +121 -0
- package/src/core/verify-gate.ts +159 -0
- package/src/core/verify.ts +237 -0
- package/src/core/voice.ts +158 -0
- package/src/core/win-job.ts +183 -0
- package/src/design/audit.ts +797 -0
- package/src/design/direction.ts +190 -0
- package/src/design/rules.ts +157 -0
- package/src/eval/bench.ts +150 -0
- package/src/eval/gauntlet-runner.ts +218 -0
- package/src/eval/gauntlet.ts +226 -0
- package/src/eval/grader.ts +186 -0
- package/src/eval/record.ts +202 -0
- package/src/eval/redact.ts +141 -0
- package/src/eval/replay.ts +147 -0
- package/src/eval/trajectory.ts +373 -0
- package/src/index.ts +17 -0
- package/src/market/catalogs/mcp-docs.json +111 -0
- package/src/market/catalogs/plugins.json +111 -0
- package/src/market/catalogs/skills.json +478 -0
- package/src/market/clone.ts +72 -0
- package/src/market/context-cost.ts +121 -0
- package/src/market/digest.ts +106 -0
- package/src/market/index.ts +22 -0
- package/src/market/install.ts +578 -0
- package/src/market/manifest.ts +187 -0
- package/src/market/prereq.ts +145 -0
- package/src/market/registry.ts +363 -0
- package/src/market/resolve.ts +111 -0
- package/src/market/types.ts +236 -0
- package/src/market/validate.ts +227 -0
- package/src/mcp/client.ts +431 -0
- package/src/mcp/config.ts +239 -0
- package/src/mcp/local-package.ts +211 -0
- package/src/mcp/market-catalog.ts +84 -0
- package/src/mcp/market-install.ts +289 -0
- package/src/mcp/market.ts +0 -0
- package/src/mcp/tools.ts +131 -0
- package/src/mcp/trust.ts +49 -0
- package/src/memory/blocks.ts +175 -0
- package/src/memory/recall.ts +355 -0
- package/src/memory/store.ts +105 -0
- package/src/memory/tools.ts +99 -0
- package/src/plugins/cli.ts +123 -0
- package/src/plugins/discover.ts +108 -0
- package/src/plugins/index.ts +50 -0
- package/src/plugins/init.ts +140 -0
- package/src/plugins/install.ts +184 -0
- package/src/plugins/load.ts +149 -0
- package/src/plugins/manifest.ts +106 -0
- package/src/plugins/state.ts +83 -0
- package/src/providers/auth.ts +293 -0
- package/src/providers/cache.ts +223 -0
- package/src/providers/catalog-local.ts +160 -0
- package/src/providers/catalog.ts +408 -0
- package/src/providers/middleware-context.ts +86 -0
- package/src/providers/middleware.ts +373 -0
- package/src/providers/profile-glm53.ts +111 -0
- package/src/providers/profile-sonnet5-persona.ts +65 -0
- package/src/providers/profile-sonnet5-voice.ts +23 -0
- package/src/providers/profiles.ts +156 -0
- package/src/providers/provider-config.ts +311 -0
- package/src/providers/registry.ts +302 -0
- package/src/providers/response-validation.ts +80 -0
- package/src/providers/retry.ts +234 -0
- package/src/providers/router.ts +294 -0
- package/src/providers/sse.ts +26 -0
- package/src/providers/stream-errors.ts +117 -0
- package/src/providers/stream.ts +569 -0
- package/src/providers/thinking.ts +189 -0
- package/src/providers/wire-messages.ts +129 -0
- package/src/sdk/client.ts +225 -0
- package/src/sdk/index.ts +3 -0
- package/src/server/dashboard.ts +144 -0
- package/src/server/http.ts +343 -0
- package/src/server/openapi.ts +246 -0
- package/src/sextant/card-hits.ts +102 -0
- package/src/sextant/card-keys.ts +55 -0
- package/src/sextant/context-source.ts +157 -0
- package/src/sextant/draw-agents.ts +273 -0
- package/src/sextant/draw-code.ts +388 -0
- package/src/sextant/draw-context.ts +222 -0
- package/src/sextant/draw-frame.ts +164 -0
- package/src/sextant/draw-market.ts +573 -0
- package/src/sextant/draw-messages.ts +386 -0
- package/src/sextant/draw-pet.ts +230 -0
- package/src/sextant/draw-plan.ts +159 -0
- package/src/sextant/draw-tabs.ts +85 -0
- package/src/sextant/draw-util.ts +65 -0
- package/src/sextant/engine.ts +230 -0
- package/src/sextant/frame-hits.ts +25 -0
- package/src/sextant/frame.ts +101 -0
- package/src/sextant/git-status.ts +197 -0
- package/src/sextant/grid.ts +59 -0
- package/src/sextant/input.ts +119 -0
- package/src/sextant/keys.ts +488 -0
- package/src/sextant/layout.ts +86 -0
- package/src/sextant/local-commands.ts +156 -0
- package/src/sextant/market-source.ts +287 -0
- package/src/sextant/mentions.ts +141 -0
- package/src/sextant/message-hits.ts +26 -0
- package/src/sextant/model.ts +387 -0
- package/src/sextant/overlays.ts +451 -0
- package/src/sextant/panel-hits.ts +38 -0
- package/src/sextant/pet.ts +399 -0
- package/src/sextant/screen.ts +324 -0
- package/src/sextant/scroll-hits.ts +66 -0
- package/src/sextant/scrollbar.ts +82 -0
- package/src/sextant/selection.ts +123 -0
- package/src/sextant/sextant-bridge.ts +174 -0
- package/src/sextant/sextant-cards.ts +142 -0
- package/src/sextant/sextant-diff-base.ts +63 -0
- package/src/sextant/sextant-files.ts +154 -0
- package/src/sextant/sextant-frame-loop.ts +314 -0
- package/src/sextant/sextant-renderer.ts +478 -0
- package/src/sextant/sextant-repo.ts +131 -0
- package/src/sextant/theme.ts +66 -0
- package/src/sextant/tool-rows.ts +189 -0
- package/src/sextant/types.ts +473 -0
- package/src/skills/index.ts +306 -0
- package/src/skills/tools.ts +69 -0
- package/src/skills/versioned.ts +227 -0
- package/src/telemetry/otel.ts +353 -0
- package/src/telemetry/otlp.ts +68 -0
- package/src/tools/ask-user.ts +156 -0
- package/src/tools/design.ts +151 -0
- package/src/tools/evalcell.ts +338 -0
- package/src/tools/html-text.ts +139 -0
- package/src/tools/provider.ts +149 -0
- package/src/tools/task.ts +216 -0
- package/src/tools/todo.ts +320 -0
- package/src/tools/webfetch.ts +331 -0
- package/src/tui/app.ts +608 -0
- package/src/tui/attach.ts +127 -0
- package/src/tui/checkpoints-cmd.ts +70 -0
- package/src/tui/clipboard-image.ts +81 -0
- package/src/tui/commands.ts +277 -0
- package/src/tui/cost.ts +108 -0
- package/src/tui/info-cmd.ts +144 -0
- package/src/tui/mcp-cmd.ts +128 -0
- package/src/tui/modes-cmd.ts +45 -0
- package/src/tui/overlays.ts +97 -0
- package/src/tui/pi-renderer.ts +424 -0
- package/src/tui/providers-cmd.ts +366 -0
- package/src/tui/renderer.ts +101 -0
- package/src/tui/replay-marker.ts +29 -0
- package/src/tui/session-cmd.ts +146 -0
- package/src/tui/sextant-attach.ts +68 -0
- package/src/tui/sextant-io.ts +184 -0
- package/src/tui/sextant-smoke.ts +110 -0
- package/src/tui/smoke.ts +72 -0
- package/src/tui/theme.ts +59 -0
- package/src/tui/todo-label.ts +7 -0
- package/src/workflow/engine.ts +266 -0
- package/tsconfig.json +30 -0
- package/vendor/pi-tui/LICENSE +21 -0
- package/vendor/pi-tui/PATCHES.md +12 -0
- package/vendor/pi-tui/PROVENANCE.md +12 -0
- package/vendor/pi-tui/README.upstream.md +854 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-console-mode.node +0 -0
- package/vendor/pi-tui/src/alt-screen-search.ts +158 -0
- package/vendor/pi-tui/src/autocomplete.ts +827 -0
- package/vendor/pi-tui/src/components/alt-screen-flash.ts +52 -0
- package/vendor/pi-tui/src/components/box.ts +138 -0
- package/vendor/pi-tui/src/components/cancellable-loader.ts +41 -0
- package/vendor/pi-tui/src/components/editor.ts +2364 -0
- package/vendor/pi-tui/src/components/h-stack.ts +45 -0
- package/vendor/pi-tui/src/components/image.ts +128 -0
- package/vendor/pi-tui/src/components/input.ts +448 -0
- package/vendor/pi-tui/src/components/loader.ts +93 -0
- package/vendor/pi-tui/src/components/markdown.ts +1016 -0
- package/vendor/pi-tui/src/components/scroll-view.ts +217 -0
- package/vendor/pi-tui/src/components/select-list.ts +230 -0
- package/vendor/pi-tui/src/components/settings-list.ts +277 -0
- package/vendor/pi-tui/src/components/spacer.ts +29 -0
- package/vendor/pi-tui/src/components/stack.ts +155 -0
- package/vendor/pi-tui/src/components/text.ts +108 -0
- package/vendor/pi-tui/src/components/truncated-text.ts +66 -0
- package/vendor/pi-tui/src/components/v-stack.ts +34 -0
- package/vendor/pi-tui/src/editor-component.ts +75 -0
- package/vendor/pi-tui/src/fuzzy.ts +138 -0
- package/vendor/pi-tui/src/index.ts +149 -0
- package/vendor/pi-tui/src/keybindings.ts +321 -0
- package/vendor/pi-tui/src/keys.ts +1402 -0
- package/vendor/pi-tui/src/kill-ring.ts +47 -0
- package/vendor/pi-tui/src/latex.ts +1381 -0
- package/vendor/pi-tui/src/layout-node.ts +52 -0
- package/vendor/pi-tui/src/layout.ts +411 -0
- package/vendor/pi-tui/src/native-modifiers.ts +60 -0
- package/vendor/pi-tui/src/native-module-path.ts +32 -0
- package/vendor/pi-tui/src/stdin-buffer.ts +445 -0
- package/vendor/pi-tui/src/terminal-colors.ts +74 -0
- package/vendor/pi-tui/src/terminal-image.ts +701 -0
- package/vendor/pi-tui/src/terminal.ts +554 -0
- package/vendor/pi-tui/src/tui-alt-screen.ts +1379 -0
- package/vendor/pi-tui/src/tui-main-screen.ts +655 -0
- package/vendor/pi-tui/src/tui.ts +1264 -0
- package/vendor/pi-tui/src/undo-stack.ts +29 -0
- package/vendor/pi-tui/src/utils.ts +1327 -0
- package/vendor/pi-tui/src/word-navigation.ts +118 -0
- package/vendor/pi-tui/test/test-themes.ts +39 -0
- package/vendor/pi-tui/test/virtual-terminal.ts +219 -0
- package/CHANGELOG.md +0 -527
- package/bin/rovecode.js +0 -24
- package/dist/cli/app-j6gn14w3.js +0 -2
- package/dist/cli/ask-user-cwstt8fz.js +0 -2
- package/dist/cli/auth-login-9bbp9915.js +0 -2
- package/dist/cli/auth-m8p9grty.js +0 -2
- package/dist/cli/bench-16zqdms5.js +0 -9
- package/dist/cli/catalog-1xchffa4.js +0 -2
- package/dist/cli/cli-1n1zb64f.js +0 -2
- package/dist/cli/client-2t9gjkck.js +0 -2
- package/dist/cli/commands-exafvm2b.js +0 -2
- package/dist/cli/connect-6zde0kn3.js +0 -2
- package/dist/cli/context-cmd-5t43wgqt.js +0 -2
- package/dist/cli/context-report-kt01pw8y.js +0 -2
- package/dist/cli/count-remote-ap7x3vh6.js +0 -2
- package/dist/cli/design-ne5zszyh.js +0 -2
- package/dist/cli/dispatch-2r5myxye.js +0 -2
- package/dist/cli/doctor-ws4fh4tn.js +0 -3
- package/dist/cli/executor-bdrjn634.js +0 -2
- package/dist/cli/export-1mxb9g5p.js +0 -2
- package/dist/cli/files-g104xghh.js +0 -2
- package/dist/cli/gauntlet-07xrjpj7.js +0 -2
- package/dist/cli/gauntlet-runner-xvy64436.js +0 -10
- package/dist/cli/gauntlet-wave3-jm91yt5w.js +0 -5
- package/dist/cli/gauntlet-wave4-r13py7p1.js +0 -14
- package/dist/cli/hashline-znvrat11.js +0 -2
- package/dist/cli/http-xafw6fsh.js +0 -143
- package/dist/cli/index-1sgjm25y.js +0 -2
- package/dist/cli/init-g2m0tn4m.js +0 -51
- package/dist/cli/install-avaqjjqq.js +0 -2
- package/dist/cli/loop-mmpfft01.js +0 -2
- package/dist/cli/main-0904f6ps.js +0 -5
- package/dist/cli/main-0ab9fc26.js +0 -9
- package/dist/cli/main-0jys2ccn.js +0 -3
- package/dist/cli/main-0mtcdbs7.js +0 -3
- package/dist/cli/main-0z1w2zsg.js +0 -3
- package/dist/cli/main-1dchs7xv.js +0 -18
- package/dist/cli/main-1ereejm1.js +0 -3
- package/dist/cli/main-1k1kw6b5.js +0 -3
- package/dist/cli/main-27y4sm2k.js +0 -38
- package/dist/cli/main-2wwjex5j.js +0 -58
- package/dist/cli/main-2yeveeve.js +0 -6
- package/dist/cli/main-2yfck9b5.js +0 -3
- package/dist/cli/main-2zmzgkwh.js +0 -3
- package/dist/cli/main-351pz3z7.js +0 -7
- package/dist/cli/main-3gjqfh7a.js +0 -6
- package/dist/cli/main-3nf3kgve.js +0 -3
- package/dist/cli/main-3pjrb2hd.js +0 -3
- package/dist/cli/main-3rxcvgna.js +0 -19
- package/dist/cli/main-4b3jgy66.js +0 -19
- package/dist/cli/main-4wndhjdc.js +0 -7
- package/dist/cli/main-4xcmvxnk.js +0 -3
- package/dist/cli/main-5tbz0wbz.js +0 -4
- package/dist/cli/main-5ywnwthm.js +0 -3
- package/dist/cli/main-6b62vkz0.js +0 -14
- package/dist/cli/main-6dnk69vp.js +0 -3
- package/dist/cli/main-6genrmhs.js +0 -136
- package/dist/cli/main-73g7eff4.js +0 -15
- package/dist/cli/main-7c5thhjd.js +0 -5
- package/dist/cli/main-7rn6bqje.js +0 -3
- package/dist/cli/main-80haw7qk.js +0 -4
- package/dist/cli/main-875s60s2.js +0 -4
- package/dist/cli/main-8kjxbpw4.js +0 -8
- package/dist/cli/main-90ds1z4e.js +0 -10
- package/dist/cli/main-9etavkew.js +0 -3
- package/dist/cli/main-a9njrkk1.js +0 -3
- package/dist/cli/main-aecrjq2d.js +0 -12
- package/dist/cli/main-ck9asesq.js +0 -9
- package/dist/cli/main-cta9racd.js +0 -4
- package/dist/cli/main-ddv7j2ag.js +0 -3
- package/dist/cli/main-dfreez27.js +0 -10
- package/dist/cli/main-f7rw7des.js +0 -3
- package/dist/cli/main-ggcn7rd7.js +0 -5
- package/dist/cli/main-gzkmycnv.js +0 -3
- package/dist/cli/main-hq51jg8v.js +0 -18
- package/dist/cli/main-jft389w9.js +0 -8
- package/dist/cli/main-k1eqkg83.js +0 -3
- package/dist/cli/main-k2y8a2aw.js +0 -9
- package/dist/cli/main-kcpbykxz.js +0 -4
- package/dist/cli/main-kd488vje.js +0 -22
- package/dist/cli/main-kh32yvgk.js +0 -5
- package/dist/cli/main-kqxnqjnv.js +0 -25
- package/dist/cli/main-kyn0xnsg.js +0 -3
- package/dist/cli/main-m1kk6fp5.js +0 -21
- package/dist/cli/main-mv40pcr2.js +0 -4
- package/dist/cli/main-n0t3973w.js +0 -3
- package/dist/cli/main-nqveez48.js +0 -4
- package/dist/cli/main-pknhvrmj.js +0 -3
- package/dist/cli/main-pn1w7a7j.js +0 -3
- package/dist/cli/main-prxxs70n.js +0 -4
- package/dist/cli/main-q3vsesf9.js +0 -3
- package/dist/cli/main-qsevpgsv.js +0 -3
- package/dist/cli/main-rdgdw24b.js +0 -25
- package/dist/cli/main-rfth4tbm.js +0 -16
- package/dist/cli/main-rg0wn0xf.js +0 -5
- package/dist/cli/main-sdmxhtv8.js +0 -4
- package/dist/cli/main-skbp13js.js +0 -18
- package/dist/cli/main-t4xnd213.js +0 -7
- package/dist/cli/main-vqak588n.js +0 -4
- package/dist/cli/main-w2n1303f.js +0 -9
- package/dist/cli/main-wbrdspr2.js +0 -5
- package/dist/cli/main-wsrg79c1.js +0 -7
- package/dist/cli/main-x4r0fne4.js +0 -5
- package/dist/cli/main-xea2f3tn.js +0 -6
- package/dist/cli/main-xg704a3c.js +0 -3
- package/dist/cli/main-xvnrabfp.js +0 -16
- package/dist/cli/main-xy53xf0r.js +0 -4
- package/dist/cli/main-y1fqy60y.js +0 -3
- package/dist/cli/main-yn8cd281.js +0 -34
- package/dist/cli/main-yr0ksc0h.js +0 -4
- package/dist/cli/main-z2ex2vyf.js +0 -4
- package/dist/cli/main-z3aayzvq.js +0 -3
- package/dist/cli/main-zaqh35jg.js +0 -3
- package/dist/cli/main-zc2e8e46.js +0 -4
- package/dist/cli/main-zzrfw6cf.js +0 -13
- package/dist/cli/main.js +0 -280
- package/dist/cli/market-cmd-e14kmx9n.js +0 -5
- package/dist/cli/mcp-login-wq7ktdek.js +0 -2
- package/dist/cli/mcp-market-cmd-9mg3jecy.js +0 -2
- package/dist/cli/notify-b7qc0cjb.js +0 -2
- package/dist/cli/oauth-z8whcgfx.js +0 -2
- package/dist/cli/output-b3ewj3ps.js +0 -16
- package/dist/cli/profiles-6mr5he5e.js +0 -2
- package/dist/cli/provider-config-g7j42q8x.js +0 -2
- package/dist/cli/provider-jr1y8vvm.js +0 -2
- package/dist/cli/registry-s8yk86g0.js +0 -2
- package/dist/cli/registry-t6p8d4mn.js +0 -2
- package/dist/cli/repl-bajwe1mh.js +0 -11
- package/dist/cli/resume-rwn9nz7y.js +0 -2
- package/dist/cli/run-flags-nah7ndpt.js +0 -2
- package/dist/cli/runtime-n7gafzhb.js +0 -2
- package/dist/cli/sandbox-config-emdy18x4.js +0 -2
- package/dist/cli/server-b0nvs2bn.js +0 -5
- package/dist/cli/session-arg-y75wd4kj.js +0 -2
- package/dist/cli/session-j62evmjq.js +0 -2
- package/dist/cli/sessions-cmd-tsnwz0ns.js +0 -7
- package/dist/cli/settings-df10wfez.js +0 -2
- package/dist/cli/setup-jzvv72fg.js +0 -2
- package/dist/cli/sextant-smoke-37m81ke6.js +0 -5
- package/dist/cli/skills-cmd-gjxnxnhx.js +0 -2
- package/dist/cli/smoke-p7748apt.js +0 -8
- package/dist/cli/start-chat-s4st3mm0.js +0 -12
- package/dist/cli/stream-gmeyewds.js +0 -2
- package/dist/cli/task-gh0kkp3n.js +0 -2
- package/dist/cli/tasks-z1kfpe8e.js +0 -2
- package/dist/cli/thinking-0eqkrz6t.js +0 -2
- package/dist/cli/todo-5brcrt9m.js +0 -2
- package/dist/cli/tools-7pzm0vj9.js +0 -2
- package/dist/cli/tools-s635p6s8.js +0 -2
- package/dist/cli/trust-cmd-cjav8zgm.js +0 -2
- package/dist/cli/update-check-pt31bm2f.js +0 -2
- package/dist/cli/update-cmd-tk131s9t.js +0 -2
- package/dist/cli/voice-56nabd8d.js +0 -2
- package/dist/cli/webfetch-xd8q596m.js +0 -2
- package/dist/cli/websearch-5hkf98k1.js +0 -2
- package/dist/cli/workflow-cmd-cy3cvzjp.js +0 -4
- package/dist/cli/workspace-q10g5z3e.js +0 -2
- package/dist/lib/index.js +0 -62
- package/dist/lib/models-index.json +0 -1
- package/dist/lib/plugins.js +0 -55
- package/dist/lib/providers.js +0 -17
- package/dist/lib/public-api.js +0 -20
- package/dist/lib/sdk.js +0 -360
- /package/{dist/cli → src/providers}/models-index.json +0 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
/** Sextant input (port #43): keys, mouse, focus, prompt editing and the overlay key routes.
|
|
2
|
+
* Ported from the user's own sextant v0.4.0 prototype — src/app.js:1438-1584 (submit, insertText,
|
|
3
|
+
* FOCUS_ORDER, onKey incl. the ctrl map, esc / esc-esc, tab focus, per-focus enter, files nav,
|
|
4
|
+
* code scroll/mode, message paging, history/suggestion ↑↓, line editing; onMouse with wheel 64/65
|
|
5
|
+
* and the reverse hit order) and :1024-1029 (runSlash history push). Dropped: intent matching, the
|
|
6
|
+
* mock shell, /spawn /crew; /undo /permissions /mode become notes. Split for the line budget: the
|
|
7
|
+
* card key route is card-keys.ts; the renderer-local slash commands, those notes and the
|
|
8
|
+
* state-then-hook effect helpers are local-commands.ts.
|
|
9
|
+
* `handleInput` is a PURE state mutator: `now` is a parameter (esc-esc window), no Date.now(),
|
|
10
|
+
* timers or process access; everything that leaves the state goes through KeyCtx.hooks (rovecode's
|
|
11
|
+
* RendererHooks) or KeyCtx.local (renderer-owned effects: theme rebuild, file/diff loading,
|
|
12
|
+
* toasts); a state field is written BEFORE its local hook fires, so the renderer only loads
|
|
13
|
+
* content / rebuilds the palette.
|
|
14
|
+
* Deviations from the prototype, all deliberate: Enter runs the highlighted slash row only for a
|
|
15
|
+
* ≥ 2-letter prefix stem (`/hel` → /help) or a ↓-picked row — a fuzzy-only top row (`/x` → /exit,
|
|
16
|
+
* `/exot` → /export) or a one-letter stem (`/e` → /exit would quit) submits the line verbatim so
|
|
17
|
+
* handleSlash reports the unknown command (the #43 bar); ⇧Tab also completes while the box is
|
|
18
|
+
* open (README table); Esc closes a visible box first (sgSel = -1 until the text changes) before
|
|
19
|
+
* arming the interrupt or clearing the line; ↑↓ keep browsing history once started, and ↓ past the
|
|
20
|
+
* newest yields "" (faithful — no draft field; a draft is a types.ts question for #44); ⌃c with a
|
|
21
|
+
* pending card while a run is live denies/nulls the card AND interrupts (the prototype only
|
|
22
|
+
* answered the card); End re-sticks the messages tail but wheel-down never does — only the drawer
|
|
23
|
+
* knows maxScroll (#44 handoff: re-stick in the messages drawer at the tail); /help opens the
|
|
24
|
+
* keys card AND reaches handleSlash; a left-button drag never clicks; a click on a panel body with
|
|
25
|
+
* no hit zone focuses it; ←/→, Backspace and Delete step by code point (😀 is never split); the
|
|
26
|
+
* mouse goes through the #40 mouseKind, so a shift/alt/ctrl + wheel scrolls and never clicks.
|
|
27
|
+
* NOTE for #44: once sextant is the default, app.ts discoverCommands `reserved` should include the
|
|
28
|
+
* renderer-local names (theme open diff focus agents) so a custom command file cannot shadow them. */
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
THEME_ORDER, type CodeMode, type Focus, type HitZone, type InputEvent, type KeyEvent, type Layout, type MouseEvent, type Page,
|
|
32
|
+
type Rect, type SextantState, type ThemeName, type TreeRow,
|
|
33
|
+
} from "./types.ts";
|
|
34
|
+
import { type Fuzzy, type Suggestion, onPaletteKey, openPalette, parseInput, resolveFile, suggestions } from "./overlays.ts";
|
|
35
|
+
import { onMarketKey, type MarketViewRow } from "./draw-market.ts";
|
|
36
|
+
import { onContextKey } from "./draw-context.ts";
|
|
37
|
+
import { dispatch, openFile, runAction, setFocus, setMode, setTheme, showAgents } from "./local-commands.ts";
|
|
38
|
+
import { dismissCard, onCardKey } from "./card-keys.ts";
|
|
39
|
+
import { gridFor } from "./draw-agents.ts";
|
|
40
|
+
import { mouseKind } from "./input.ts";
|
|
41
|
+
import { mainPage, nextPage } from "./draw-tabs.ts";
|
|
42
|
+
import { openNotices, openOverlays } from "./overlays.ts";
|
|
43
|
+
|
|
44
|
+
// ------------------------------------------------------------------ contract
|
|
45
|
+
|
|
46
|
+
/** a click zone a drawer registered this frame; the LAST registered zone under the pointer wins (contract: types.ts) */
|
|
47
|
+
export type { HitZone } from "./types.ts";
|
|
48
|
+
|
|
49
|
+
export interface KeyCtx {
|
|
50
|
+
layout: Layout;
|
|
51
|
+
/** rovecode RendererHooks (tui/renderer.ts): the controller's submit / interrupt / exit */
|
|
52
|
+
hooks: { onSubmit(text: string): void; onInterrupt(): void; onExit(): void };
|
|
53
|
+
/** renderer-owned effects; the state field is already written when these fire */
|
|
54
|
+
local: {
|
|
55
|
+
setTheme(name: ThemeName): void; setMode(mode: CodeMode): void; openFile(path: string): void; toast(text: string): void;
|
|
56
|
+
/** open /market: the renderer loads the catalog (async) and fills s.market when it answers */
|
|
57
|
+
openMarket(): void;
|
|
58
|
+
/** open /context: the renderer asks the LIVE runtime for the system prompt and tool schemas — the two
|
|
59
|
+
* rows a transcript cannot know — and fills s.context when the count is done */
|
|
60
|
+
openContext(): void;
|
|
61
|
+
/** the human pressed Enter on a row (and answered the install-once chooser for an npx row: `local`):
|
|
62
|
+
* build the plan and write it into s.market.plan */
|
|
63
|
+
marketPlan(row: MarketViewRow, local?: boolean): void;
|
|
64
|
+
/** the human confirmed the plan card: run THAT plan (its `local` included) and write the outcome back onto the card */
|
|
65
|
+
marketInstall(row: MarketViewRow, local?: boolean): void;
|
|
66
|
+
/** the docs pane opened on a row whose body has not been read: fetch it and fill the row's lines */
|
|
67
|
+
marketDocs(row: MarketViewRow): void;
|
|
68
|
+
};
|
|
69
|
+
/** click zones the drawers registered while painting the current frame */
|
|
70
|
+
hits: readonly HitZone[];
|
|
71
|
+
/** the zone grabbed by the last click that had an onDrag (a scrollbar thumb); owned by the frame loop
|
|
72
|
+
* so it outlives the per-frame `hits` list — drag events go to it until the button is released */
|
|
73
|
+
drag: { zone: HitZone | null; y0: number };
|
|
74
|
+
/** the flattened files tree as drawn this frame (cursor / Enter / ←→ act on these rows) */
|
|
75
|
+
rows: readonly TreeRow[];
|
|
76
|
+
/** #40 engine.fuzzy when wired; defaults to the ported scorer in overlays.ts */
|
|
77
|
+
fuzzy?: Fuzzy;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** what the renderer must do after a key: repaint. A union of one today, kept extensible. */
|
|
81
|
+
export type KeyEffect = "render";
|
|
82
|
+
|
|
83
|
+
export const ESC_WINDOW_MS = 1500;
|
|
84
|
+
export const FOCUS_ORDER: readonly Focus[] = ["messages", "code", "files"];
|
|
85
|
+
/** ←/→ in the code panel (app.js:1547); `search` joins the cycle only while a result is shown */
|
|
86
|
+
export const CODE_MODE_CYCLE: readonly CodeMode[] = ["code", "diff", "run", "agents"];
|
|
87
|
+
/** "scroll to the tail" sentinel — the drawer clamps to the last row */
|
|
88
|
+
export const SCROLL_TAIL = 1e9;
|
|
89
|
+
|
|
90
|
+
const R = (): KeyEffect[] => ["render"], NONE = (): KeyEffect[] => [];
|
|
91
|
+
const clamp = (v: number, lo: number, hi: number) => Math.max(lo, Math.min(v, hi));
|
|
92
|
+
const inRect = (x: number, y: number, r: Rect) => x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h;
|
|
93
|
+
|
|
94
|
+
/** messages→code→files, files only while the layout shows the column (app.js:1520) */
|
|
95
|
+
/** files is in the Tab cycle when its panel is on screen — its own column, or paged into the main slot */
|
|
96
|
+
export function focusOrder(L: Layout, page: Page = "code"): Focus[] {
|
|
97
|
+
return FOCUS_ORDER.filter((f) => f !== "files" || L.files !== null || page === "files");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ------------------------------------------------------------------ entry
|
|
101
|
+
|
|
102
|
+
export function handleInput(s: SextantState, ev: InputEvent, ctx: KeyCtx, now: number): KeyEffect[] {
|
|
103
|
+
if (ev.type === "mouse") return onMouse(s, ev, ctx, now);
|
|
104
|
+
if (ev.type === "paste") return onPaste(s, ev.text, ctx);
|
|
105
|
+
if (s.palette) {
|
|
106
|
+
if (ev.ctrl && ev.name === "c") return ctrlC(s, ctx);
|
|
107
|
+
onPaletteKey(s, ev, (a) => runAction(s, a, ctx), ctx.fuzzy);
|
|
108
|
+
return R();
|
|
109
|
+
}
|
|
110
|
+
if (s.market) {
|
|
111
|
+
if (ev.ctrl && ev.name === "c") return ctrlC(s, ctx);
|
|
112
|
+
const req = onMarketKey(s, ev, ctx.fuzzy);
|
|
113
|
+
// the overlay decides WHAT should happen; the renderer owns the market module and does it
|
|
114
|
+
if (req.kind === "plan") ctx.local.marketPlan(req.row, req.local);
|
|
115
|
+
else if (req.kind === "docs") ctx.local.marketDocs(req.row);
|
|
116
|
+
// the plan card names an item; installing re-read the SELECTION instead, and the two can differ —
|
|
117
|
+
// the plan arrives asynchronously, so an arrow key (or a click on a list row that outlived the card)
|
|
118
|
+
// moved the selection while the card still showed the first item. Install what the human approved.
|
|
119
|
+
else if (req.kind === "install") ctx.local.marketInstall(req.plan.row, req.plan.local);
|
|
120
|
+
return R();
|
|
121
|
+
}
|
|
122
|
+
if (s.context) {
|
|
123
|
+
if (ev.ctrl && ev.name === "c") return ctrlC(s, ctx);
|
|
124
|
+
// a page is the drawn body height; the panel is centred and capped, so this matches drawContext
|
|
125
|
+
onContextKey(s, ev, Math.max(4, Math.min(ctx.layout.h - 4, 24)));
|
|
126
|
+
return R();
|
|
127
|
+
}
|
|
128
|
+
if (s.help) { // app.js:1488 — the card swallows every key; the usual closers dismiss it
|
|
129
|
+
const closer = ev.name === "escape" || ev.name === "enter" || ev.name === "space" || (ev.ctrl && ev.name === "c");
|
|
130
|
+
if (closer || (ev.ch && !ev.ctrl)) s.help = false;
|
|
131
|
+
return R();
|
|
132
|
+
}
|
|
133
|
+
if (ev.ctrl) return onCtrl(s, ev, ctx);
|
|
134
|
+
if (s.card && onCardKey(s, s.card, ev)) return R();
|
|
135
|
+
const { name } = ev;
|
|
136
|
+
if (name === "escape") return onEscape(s, ctx, now);
|
|
137
|
+
if (name === "tab" || name === "shift-tab") return onTab(s, name, ctx);
|
|
138
|
+
if (name === "enter") return onEnter(s, ctx);
|
|
139
|
+
if (s.focus === "files") { const fx = onFilesKey(s, name, ctx); if (fx) return fx; }
|
|
140
|
+
if (s.focus === "code") { const fx = onCodeKey(s, name, ctx); if (fx) return fx; }
|
|
141
|
+
return onPromptKey(s, ev, ctx);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ------------------------------------------------------------------ ctrl map (app.js:1489-1502)
|
|
145
|
+
|
|
146
|
+
/** The chords that put an overlay on the screen. A card is modal, and an overlay painted over it also
|
|
147
|
+
* registers its hit zones first — so the card underneath stops answering the mouse and the tool call
|
|
148
|
+
* waiting on it is stuck until the user finds their way back out. ⌃c was special-cased for this from
|
|
149
|
+
* the start; the rest fell through by omission. Typing still reaches the prompt while a card waits
|
|
150
|
+
* (onCardKey returning false is deliberate) — this only stops a second surface from covering it. */
|
|
151
|
+
const OPENS_OVERLAY = new Set(["k", "p", "b", "m", "g"]);
|
|
152
|
+
|
|
153
|
+
function onCtrl(s: SextantState, ev: KeyEvent, ctx: KeyCtx): KeyEffect[] {
|
|
154
|
+
if (s.card && ev.name !== undefined && OPENS_OVERLAY.has(ev.name)) return R();
|
|
155
|
+
switch (ev.name) {
|
|
156
|
+
case "c": return ctrlC(s, ctx);
|
|
157
|
+
case "k": case "p": openPalette(s); return R();
|
|
158
|
+
case "g": ctx.local.openContext(); return R();
|
|
159
|
+
case "t": {
|
|
160
|
+
const next: ThemeName = THEME_ORDER[(THEME_ORDER.indexOf(s.theme) + 1) % THEME_ORDER.length]!;
|
|
161
|
+
setTheme(s, ctx, next);
|
|
162
|
+
return R();
|
|
163
|
+
}
|
|
164
|
+
case "e": setFocus(s, ctx, "files"); return R();
|
|
165
|
+
// ⌃o: cycle the tab strip (draw-tabs.ts) — code → files → plan on a narrow terminal; inert when wide
|
|
166
|
+
case "o": {
|
|
167
|
+
const next = nextPage(ctx.layout, s);
|
|
168
|
+
if (next !== s.page) { s.page = next; s.focus = next === "files" ? "files" : "code"; }
|
|
169
|
+
return R();
|
|
170
|
+
}
|
|
171
|
+
case "b": openNotices(s); return R(); // the notification history (overlays.ts openNotices)
|
|
172
|
+
case "m": ctx.local.openMarket(); return R(); // the market overlay (draw-market.ts)
|
|
173
|
+
// ⌃v with an IMAGE on the clipboard: the terminal sends no paste for it, only this key — ask the
|
|
174
|
+
// OS for the image through /paste (tui/attach.ts cmdPasteImage). Text pastes never arrive here.
|
|
175
|
+
case "v": ctx.hooks.onSubmit("/paste"); return R();
|
|
176
|
+
case "s": setMode(s, ctx, "code"); s.focus = "code"; return R();
|
|
177
|
+
case "d": setMode(s, ctx, s.code.mode === "diff" ? "code" : "diff"); return R();
|
|
178
|
+
case "r": setMode(s, ctx, "run"); return R();
|
|
179
|
+
case "a": showAgents(s, ctx); return R();
|
|
180
|
+
case "n": ctx.hooks.onSubmit("/new"); return R();
|
|
181
|
+
case "u": clearInput(s); return R();
|
|
182
|
+
case "left": case "right":
|
|
183
|
+
s.input.cur = wordJump(s.input.text, s.input.cur, ev.name === "left" ? -1 : 1);
|
|
184
|
+
return R();
|
|
185
|
+
default: return NONE();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** ⌃c: a pending card is dismissed (deny / null) AND a running turn is interrupted (the prototype
|
|
190
|
+
* only answered the card); idle → exit, so the second ⌃c after a run was stopped leaves (app.js:1491) */
|
|
191
|
+
function ctrlC(s: SextantState, ctx: KeyCtx): KeyEffect[] {
|
|
192
|
+
if (s.card) dismissCard(s);
|
|
193
|
+
if (s.running) ctx.hooks.onInterrupt(); else ctx.hooks.onExit();
|
|
194
|
+
return R();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function clearInput(s: SextantState): void { s.input.text = ""; s.input.cur = 0; s.input.sgSel = 0; s.input.histIdx = -1; }
|
|
198
|
+
|
|
199
|
+
// ------------------------------------------------------------------ esc / tab / enter
|
|
200
|
+
|
|
201
|
+
function onEscape(s: SextantState, ctx: KeyCtx, now: number): KeyEffect[] {
|
|
202
|
+
if (s.focus !== "messages") { // a panel: close its open lane first, then hand focus back to the prompt
|
|
203
|
+
if (s.code.laneOpen) s.code.laneOpen = false; else s.focus = "messages";
|
|
204
|
+
return R();
|
|
205
|
+
}
|
|
206
|
+
if (openSuggestions(s, ctx).length) { s.input.sgSel = -1; return R(); } // close the box, keep the text
|
|
207
|
+
if (s.running) { // app.js:1513 — the first Esc arms the window, the second inside it interrupts
|
|
208
|
+
if (now < s.escUntil) { s.escUntil = 0; ctx.hooks.onInterrupt(); }
|
|
209
|
+
else s.escUntil = now + ESC_WINDOW_MS;
|
|
210
|
+
return R();
|
|
211
|
+
}
|
|
212
|
+
if (s.input.text) { clearInput(s); return R(); }
|
|
213
|
+
return NONE();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function onTab(s: SextantState, name: string, ctx: KeyCtx): KeyEffect[] {
|
|
217
|
+
const sugs = openSuggestions(s, ctx);
|
|
218
|
+
if (s.focus === "messages" && sugs.length) { applySuggestion(s, sugs[clamp(s.input.sgSel, 0, sugs.length - 1)]!); return R(); }
|
|
219
|
+
const order = focusOrder(ctx.layout, s.page), i = Math.max(0, order.indexOf(s.focus));
|
|
220
|
+
s.focus = order[(i + (name === "tab" ? 1 : order.length - 1)) % order.length]!;
|
|
221
|
+
return R();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function onEnter(s: SextantState, ctx: KeyCtx): KeyEffect[] {
|
|
225
|
+
if (s.focus === "files") {
|
|
226
|
+
const r = ctx.rows[s.files.cursor];
|
|
227
|
+
if (r?.dir) { // toggle the fold
|
|
228
|
+
if (s.files.expanded.has(r.path)) s.files.expanded.delete(r.path); else s.files.expanded.add(r.path);
|
|
229
|
+
} else if (r) openFile(s, ctx, r.path);
|
|
230
|
+
return R();
|
|
231
|
+
}
|
|
232
|
+
if (s.focus === "code") {
|
|
233
|
+
if (s.code.mode === "agents" && s.crew.length) { // open / close the selected lane; opened = at its tail
|
|
234
|
+
s.code.laneOpen = !s.code.laneOpen;
|
|
235
|
+
s.code.scroll = s.code.laneOpen ? SCROLL_TAIL : 0;
|
|
236
|
+
}
|
|
237
|
+
return R();
|
|
238
|
+
}
|
|
239
|
+
const sugs = openSuggestions(s, ctx);
|
|
240
|
+
if (sugs.length) {
|
|
241
|
+
const pick = sugs[clamp(s.input.sgSel, 0, sugs.length - 1)]!;
|
|
242
|
+
if (!autoRuns(s, pick)) return submitLine(s, s.input.text, ctx);
|
|
243
|
+
applySuggestion(s, pick);
|
|
244
|
+
if (pick.enter === "complete") return R();
|
|
245
|
+
}
|
|
246
|
+
return submitLine(s, s.input.text, ctx);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Enter takes the highlighted row when the user picked it with ↓, when it is an argument or mention
|
|
250
|
+
* row, or when the typed stem is a ≥ 2-letter prefix of the command (`/hel` → /help); a fuzzy-only
|
|
251
|
+
* top row (`/x` → /exit) or a one-letter stem (`/e`) would run a command the user never named */
|
|
252
|
+
function autoRuns(s: SextantState, pick: Suggestion): boolean {
|
|
253
|
+
if (pick.kind !== "slash" || s.input.sgSel !== 0) return true;
|
|
254
|
+
const stem = (parseInput(s.input.text).cmd ?? "").toLowerCase();
|
|
255
|
+
return stem.length >= 2 && pick.label.slice(1).toLowerCase().startsWith(stem);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** app.js:1438-1457 minus the mock paths: trimmed non-empty → history, clear, dispatch */
|
|
259
|
+
function submitLine(s: SextantState, raw: string, ctx: KeyCtx): KeyEffect[] {
|
|
260
|
+
const text = raw.trim();
|
|
261
|
+
if (!text) return NONE();
|
|
262
|
+
s.input.history.push(text);
|
|
263
|
+
clearInput(s);
|
|
264
|
+
dispatch(s, text, ctx);
|
|
265
|
+
return R();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ------------------------------------------------------------------ files / code panels
|
|
269
|
+
|
|
270
|
+
function onFilesKey(s: SextantState, name: string, ctx: KeyCtx): KeyEffect[] | null {
|
|
271
|
+
const rows = ctx.rows, F = s.files;
|
|
272
|
+
if (name === "up" || name === "down") {
|
|
273
|
+
F.cursor = clamp(F.cursor + (name === "up" ? -1 : 1), 0, Math.max(0, rows.length - 1));
|
|
274
|
+
const body = Math.max(1, (ctx.layout.files?.h ?? 3) - 2); // panel() body = rect minus borders
|
|
275
|
+
if (F.cursor < F.scroll) F.scroll = F.cursor; else if (F.cursor >= F.scroll + body) F.scroll = F.cursor - body + 1;
|
|
276
|
+
const r = rows[F.cursor];
|
|
277
|
+
if (r && !r.dir) openFile(s, ctx, r.path);
|
|
278
|
+
return R();
|
|
279
|
+
}
|
|
280
|
+
if (name === "left" || name === "right") {
|
|
281
|
+
const r = rows[F.cursor];
|
|
282
|
+
if (r?.dir) { if (name === "right") F.expanded.add(r.path); else F.expanded.delete(r.path); }
|
|
283
|
+
return R();
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** the agents board's body width: drawCode's inner rect (w − 4) minus the 3-wide rail and its 2-cell gap */
|
|
289
|
+
const agentsBodyWidth = (code: Rect): number => code.w - 9;
|
|
290
|
+
|
|
291
|
+
function onCodeKey(s: SextantState, name: string, ctx: KeyCtx): KeyEffect[] | null {
|
|
292
|
+
const c = s.code, n = s.crew.length;
|
|
293
|
+
if (c.mode === "agents" && n && !c.laneOpen) { // app.js:1535-1541 — lane grid navigation
|
|
294
|
+
const { cols } = gridFor(agentsBodyWidth(ctx.layout.code), n); // the board's own grid (#46), not a lane count
|
|
295
|
+
const step: Record<string, number> = { left: n - 1, right: 1, up: n - cols, down: cols };
|
|
296
|
+
// own keys only: a key named "constructor" or "__proto__" is not a move (`in` would make lane NaN)
|
|
297
|
+
if (Object.hasOwn(step, name)) { c.lane = (c.lane + step[name]!) % n; return R(); }
|
|
298
|
+
}
|
|
299
|
+
if (name === "up") { c.scroll = Math.max(0, c.scroll - 1); return R(); }
|
|
300
|
+
if (name === "down") { c.scroll += 1; return R(); }
|
|
301
|
+
if (name === "pageup") { c.scroll = Math.max(0, c.scroll - 10); return R(); }
|
|
302
|
+
if (name === "pagedown") { c.scroll += 10; return R(); }
|
|
303
|
+
if (name === "left" || name === "right") {
|
|
304
|
+
const modes = c.search ? [...CODE_MODE_CYCLE, "search" as CodeMode] : [...CODE_MODE_CYCLE];
|
|
305
|
+
const i = Math.max(0, modes.indexOf(c.mode));
|
|
306
|
+
setMode(s, ctx, modes[(i + (name === "right" ? 1 : modes.length - 1)) % modes.length]!);
|
|
307
|
+
return R();
|
|
308
|
+
}
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ------------------------------------------------------------------ prompt (app.js:1549-1562)
|
|
313
|
+
|
|
314
|
+
function onPromptKey(s: SextantState, ev: KeyEvent, ctx: KeyCtx): KeyEffect[] {
|
|
315
|
+
const { name } = ev, I = s.input;
|
|
316
|
+
if (name === "pageup") { s.stick = false; s.msgScroll = Math.max(0, s.msgScroll - 5); return R(); }
|
|
317
|
+
if (name === "pagedown") { s.msgScroll += 5; return R(); }
|
|
318
|
+
if (name === "up" || name === "down") return onHistoryKey(s, name === "up" ? -1 : 1, ctx);
|
|
319
|
+
if (name === "left" || name === "right") {
|
|
320
|
+
const d = name === "left" ? -1 : 1;
|
|
321
|
+
if (ev.alt) I.cur = wordJump(I.text, I.cur, d);
|
|
322
|
+
else I.cur = clamp(d < 0 ? I.cur - cpBefore(I.text, I.cur) : I.cur + cpAt(I.text, I.cur), 0, I.text.length);
|
|
323
|
+
return R();
|
|
324
|
+
}
|
|
325
|
+
if (name === "home") { I.cur = 0; return R(); }
|
|
326
|
+
if (name === "end") { I.cur = I.text.length; s.stick = true; return R(); }
|
|
327
|
+
if (name === "backspace") {
|
|
328
|
+
if (I.cur > 0) { // the whole code point before the cursor goes (😀 is two UTF-16 units)
|
|
329
|
+
const n = cpBefore(I.text, I.cur);
|
|
330
|
+
I.text = I.text.slice(0, I.cur - n) + I.text.slice(I.cur);
|
|
331
|
+
I.cur -= n; I.sgSel = 0; I.histIdx = -1;
|
|
332
|
+
}
|
|
333
|
+
s.focus = "messages";
|
|
334
|
+
return R();
|
|
335
|
+
}
|
|
336
|
+
if (name === "delete") {
|
|
337
|
+
if (I.cur < I.text.length) { // the whole code point under the cursor goes
|
|
338
|
+
I.text = I.text.slice(0, I.cur) + I.text.slice(I.cur + cpAt(I.text, I.cur));
|
|
339
|
+
I.sgSel = 0;
|
|
340
|
+
}
|
|
341
|
+
return R();
|
|
342
|
+
}
|
|
343
|
+
const ch = name === "space" ? " " : ev.ch;
|
|
344
|
+
if (ch && !ev.alt) { insertText(s, ch); return R(); }
|
|
345
|
+
return NONE();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** ↑↓: the open suggestion box first (wrap over its rows), else browse history (app.js:1556) */
|
|
349
|
+
function onHistoryKey(s: SextantState, d: -1 | 1, ctx: KeyCtx): KeyEffect[] {
|
|
350
|
+
const I = s.input, sugs = I.histIdx < 0 ? openSuggestions(s, ctx) : [];
|
|
351
|
+
if (sugs.length) { // no browse running and the box is open: move its highlight
|
|
352
|
+
I.sgSel = (clamp(I.sgSel, 0, sugs.length - 1) + d + sugs.length) % sugs.length;
|
|
353
|
+
return R();
|
|
354
|
+
}
|
|
355
|
+
const h = I.history;
|
|
356
|
+
if (!h.length) return NONE();
|
|
357
|
+
if (I.histIdx < 0) I.histIdx = h.length; // a browse starts just past the newest entry
|
|
358
|
+
I.histIdx = clamp(I.histIdx + d, 0, h.length);
|
|
359
|
+
I.text = h[I.histIdx] ?? ""; // past the newest again: an empty line (no draft is kept)
|
|
360
|
+
I.cur = I.text.length;
|
|
361
|
+
I.sgSel = 0;
|
|
362
|
+
return R();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** a pasted line that is ONE image path — what a terminal delivers when a file is dragged onto it
|
|
366
|
+
* (Windows Terminal quotes paths with spaces). Syntactic only: keys.ts is pure and never touches the
|
|
367
|
+
* disk; app.ts's /attach loads the file, sniffs the magic bytes and reports a non-image as a note. */
|
|
368
|
+
export function pastedImagePath(text: string): string | null {
|
|
369
|
+
const t = text.trim();
|
|
370
|
+
if (t.length === 0 || t.includes("\n")) return null;
|
|
371
|
+
const m = /^(?:"([^"]+)"|'([^']+)'|(\S+))$/.exec(t);
|
|
372
|
+
const p = m?.[1] ?? m?.[2] ?? m?.[3];
|
|
373
|
+
return p !== undefined && /\.(png|jpe?g|gif|webp)$/i.test(p) ? p : null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function onPaste(s: SextantState, text: string, ctx: KeyCtx): KeyEffect[] {
|
|
377
|
+
if (s.help) return NONE();
|
|
378
|
+
if (s.palette) { s.palette.query += text.replace(/\s+/g, " "); s.palette.sel = 0; return R(); }
|
|
379
|
+
// a dropped image file attaches instead of landing in the prompt as a path string
|
|
380
|
+
if (!s.card) {
|
|
381
|
+
const img = pastedImagePath(text);
|
|
382
|
+
if (img !== null) { ctx.hooks.onSubmit(`/attach "${img}"`); return R(); }
|
|
383
|
+
}
|
|
384
|
+
const c = s.card;
|
|
385
|
+
if (c?.kind === "question" && c.prompt.allowFreeText !== false) {
|
|
386
|
+
const onFreeText = c.selected === (c.prompt.options?.length ?? 0); // index options.length = the free-text row
|
|
387
|
+
if (onFreeText) { c.freeText += text; return R(); }
|
|
388
|
+
}
|
|
389
|
+
insertText(s, text);
|
|
390
|
+
return R();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** app.js:1476-1482 — typing pulls focus back to the prompt and drops a history browse */
|
|
394
|
+
function insertText(s: SextantState, t: string): void {
|
|
395
|
+
const I = s.input;
|
|
396
|
+
I.text = I.text.slice(0, I.cur) + t + I.text.slice(I.cur); I.cur += t.length;
|
|
397
|
+
I.sgSel = 0; I.histIdx = -1; s.focus = "messages";
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function applySuggestion(s: SextantState, sg: Suggestion): void {
|
|
401
|
+
s.input.text = sg.apply.text; s.input.cur = sg.apply.cur; s.input.sgSel = 0; s.input.histIdx = -1;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** the visible suggestion rows: none once Esc dismissed the box (sgSel = -1) */
|
|
405
|
+
function openSuggestions(s: SextantState, ctx: KeyCtx): Suggestion[] {
|
|
406
|
+
return s.input.sgSel < 0 ? [] : suggestions(s, s.files.paths, ctx.fuzzy);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** UTF-16 length of the code point that ends at `cur` — 2 for an astral pair such as 😀 */
|
|
410
|
+
function cpBefore(text: string, cur: number): number { return cur >= 2 && text.codePointAt(cur - 2)! > 0xffff ? 2 : 1; }
|
|
411
|
+
/** UTF-16 length of the code point that starts at `cur` (1 past the end — callers clamp) */
|
|
412
|
+
function cpAt(text: string, cur: number): number { return (text.codePointAt(cur) ?? 0) > 0xffff ? 2 : 1; }
|
|
413
|
+
|
|
414
|
+
function wordJump(text: string, cur: number, d: -1 | 1): number {
|
|
415
|
+
let i = cur;
|
|
416
|
+
if (d < 0) { while (i > 0 && /\s/.test(text[i - 1]!)) i--; while (i > 0 && !/\s/.test(text[i - 1]!)) i--; }
|
|
417
|
+
else { while (i < text.length && /\s/.test(text[i]!)) i++; while (i < text.length && !/\s/.test(text[i]!)) i++; }
|
|
418
|
+
return i;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// ------------------------------------------------------------------ mouse (app.js:1575-1584)
|
|
422
|
+
|
|
423
|
+
/** the button kinds come from the #40 parser's mouseKind, which masks the modifier bits (4 shift, 8 meta,
|
|
424
|
+
* 16 ctrl) first: shift/alt/ctrl + wheel (b 68/69, 72/73, 80/81) is a wheel — the prototype's raw
|
|
425
|
+
* `b === 64 || b === 65` let those fall through into the click walk, so shift+wheel over the `/exit`
|
|
426
|
+
* suggestion row ran it; a modified or non-left button, a drag and a release never click */
|
|
427
|
+
function onMouse(s: SextantState, ev: MouseEvent, ctx: KeyCtx, now: number): KeyEffect[] {
|
|
428
|
+
const kind = mouseKind(ev);
|
|
429
|
+
const L = ctx.layout;
|
|
430
|
+
if (kind === "wheel-up" || kind === "wheel-down") return onWheel(s, ev, L, kind === "wheel-up" ? -1 : 1, ctx);
|
|
431
|
+
// a grabbed zone (a scrollbar thumb) follows the pointer until the button is released
|
|
432
|
+
if (kind === "release") { const had = ctx.drag.zone !== null; ctx.drag.zone = null; return had ? R() : NONE(); }
|
|
433
|
+
if (kind === "drag") { const z = ctx.drag.zone; if (z?.onDrag) { z.onDrag(ev.y, ev.y - ctx.drag.y0); return R(); } return NONE(); }
|
|
434
|
+
if (kind !== "click") return NONE();
|
|
435
|
+
for (let i = ctx.hits.length - 1; i >= 0; i--) {
|
|
436
|
+
const z = ctx.hits[i]!;
|
|
437
|
+
if (!inRect(ev.x, ev.y, z.rect)) continue;
|
|
438
|
+
ctx.drag.zone = z.onDrag ? z : null;
|
|
439
|
+
ctx.drag.y0 = ev.y;
|
|
440
|
+
z.onClick();
|
|
441
|
+
if (z.key) handleInput(s, z.key, ctx, now);
|
|
442
|
+
return R();
|
|
443
|
+
}
|
|
444
|
+
// ANY open overlay swallows the fall-through, not just the two that existed when this was written:
|
|
445
|
+
// `market` and `context` were added later and never added here, so a click that missed every hit
|
|
446
|
+
// zone still moved focus in the cockpit behind the box. openOverlays is the invariant table — asking
|
|
447
|
+
// it means the next overlay is covered on the day it is added rather than the day someone notices.
|
|
448
|
+
if (openOverlays(s).length > 0) return NONE();
|
|
449
|
+
// the main slot is code unless the layout hid the paged panel and it is showing there instead
|
|
450
|
+
const main = mainPage(L, s);
|
|
451
|
+
const panels: [Focus, Rect | null][] = [
|
|
452
|
+
["messages", L.messages],
|
|
453
|
+
["code", main === "code" ? L.code : null],
|
|
454
|
+
["files", L.files ?? (main === "files" ? L.code : null)],
|
|
455
|
+
];
|
|
456
|
+
for (const [f, r] of panels) {
|
|
457
|
+
if (r && inRect(ev.x, ev.y, r)) { s.focus = f; return R(); }
|
|
458
|
+
}
|
|
459
|
+
return NONE();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** the panel under the pointer scrolls: messages by 2 (wheel-up unsticks the tail), code by 3,
|
|
463
|
+
* files by 2; wheel-down never re-sticks — only the drawer knows maxScroll (#44 handoff) */
|
|
464
|
+
/** the wheel scrolls the panel under the pointer. Messages: up unsticks (the frame loop re-sticks when the
|
|
465
|
+
* view reaches the tail again — scroll-hits.ts followTailIfAtEnd); code clamps in codeScrollTop; the files
|
|
466
|
+
* tree — in its panel, or paged into the main slot on a narrow terminal — clamps to its rows here and
|
|
467
|
+
* pulls the cursor along, because drawFiles snaps the window back to the cursor otherwise. */
|
|
468
|
+
function onWheel(s: SextantState, ev: MouseEvent, L: Layout, d: -1 | 1, ctx: KeyCtx): KeyEffect[] {
|
|
469
|
+
// the wheel never reaches the hit zones at all — it does its own rect maths against the panels — so
|
|
470
|
+
// with the market open, scrolling over the box scrolled the transcript UNDERNEATH it, invisibly.
|
|
471
|
+
// Same fix as onMouse: ask the overlay table rather than naming two of the four by hand.
|
|
472
|
+
if (openOverlays(s).length > 0) return NONE();
|
|
473
|
+
if (inRect(ev.x, ev.y, L.messages)) {
|
|
474
|
+
s.msgScroll = Math.max(0, s.msgScroll + d * 2);
|
|
475
|
+
if (d < 0) s.stick = false;
|
|
476
|
+
return R();
|
|
477
|
+
}
|
|
478
|
+
const main = mainPage(L, s);
|
|
479
|
+
const filesRect = L.files ?? (main === "files" ? L.code : null);
|
|
480
|
+
if (filesRect && inRect(ev.x, ev.y, filesRect)) {
|
|
481
|
+
const h = Math.max(1, filesRect.h - 2), f = s.files;
|
|
482
|
+
f.scroll = Math.max(0, Math.min(f.scroll + d * 2, Math.max(0, ctx.rows.length - h)));
|
|
483
|
+
f.cursor = Math.max(f.scroll, Math.min(f.cursor, f.scroll + h - 1));
|
|
484
|
+
return R();
|
|
485
|
+
}
|
|
486
|
+
if (main === "code" && inRect(ev.x, ev.y, L.code)) { s.code.scroll = Math.max(0, s.code.scroll + d * 3); return R(); }
|
|
487
|
+
return NONE();
|
|
488
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** ported from the user's sextant v0.4.0 prototype, src/app.js lines 184-220 (layout, panel) */
|
|
2
|
+
/* Panel geometry for the sextant surface and the rounded panel frame. Pure: no clock, no I/O.
|
|
3
|
+
layout() mirrors app.js layout() number for number (sextant-layout.test.ts pins nine sizes
|
|
4
|
+
computed from the prototype); panel() takes the screen and theme explicitly instead of closing
|
|
5
|
+
over them, and measures title/extra widths in cells (the prototype used string length). This is
|
|
6
|
+
the ONE panel() of the surface (#44 folded the #41 draw-frame and #42 draw-util copies into it):
|
|
7
|
+
the interior is cleared to the theme background and an extra that would not fit is clipped with
|
|
8
|
+
an ellipsis before it is dropped — text-identical to the prototype for every extra that fits. */
|
|
9
|
+
|
|
10
|
+
import { MIN_COLS, MIN_ROWS, strWidth } from "./screen.ts";
|
|
11
|
+
import { st } from "./theme.ts";
|
|
12
|
+
import { ATTR, type Layout, type LayoutOptions, type Rect, type ScreenLike, type Seg, type Theme } from "./types.ts";
|
|
13
|
+
|
|
14
|
+
/** files column at w ≥ 140 (30 wide from 150, else 26); right column at w ≥ 110 (34 / 28);
|
|
15
|
+
* messages = max(8, round(contentH · 0.34)) rows; usage 5 rows; pet 14 rows when contentH ≥ 36 —
|
|
16
|
+
* under files when present, else under plan; the size is clamped to the 40×12 floor like Screen. */
|
|
17
|
+
export function layout(w: number, h: number, opts: LayoutOptions): Layout {
|
|
18
|
+
const W = Math.max(MIN_COLS, w), H = Math.max(MIN_ROWS, h);
|
|
19
|
+
const frame: Rect = { x: 0, y: 0, w: W - 1, h: H };
|
|
20
|
+
const x0 = 2, x1 = W - 4, y0 = 1, y1 = H - 2;
|
|
21
|
+
const showFiles = W >= 140, showRight = W >= 110;
|
|
22
|
+
const filesW = showFiles ? (W >= 150 ? 30 : 26) : 0;
|
|
23
|
+
const rightW = showRight ? (W >= 150 ? 34 : 28) : 0;
|
|
24
|
+
const contentH = y1 - y0 + 1;
|
|
25
|
+
const files: Rect | null = showFiles ? { x: x0, y: y0, w: filesW, h: contentH } : null;
|
|
26
|
+
const right: Rect | null = showRight ? { x: x1 - rightW + 1, y: y0, w: rightW, h: contentH } : null;
|
|
27
|
+
const cx = x0 + (showFiles ? filesW + 1 : 0);
|
|
28
|
+
const cw = (right ? right.x - 1 : x1 + 1) - cx;
|
|
29
|
+
const msgH = Math.max(8, Math.round(contentH * 0.34));
|
|
30
|
+
const code: Rect = { x: cx, y: y0, w: cw, h: contentH - msgH };
|
|
31
|
+
const messages: Rect = { x: cx, y: y0 + code.h, w: cw, h: msgH };
|
|
32
|
+
const petH = opts.pet && contentH >= 36 ? 14 : 0;
|
|
33
|
+
let plan: Rect | null = null, usage: Rect | null = null, pet: Rect | null = null;
|
|
34
|
+
if (right) {
|
|
35
|
+
const usageH = 5;
|
|
36
|
+
plan = { x: right.x, y: y0, w: rightW, h: contentH - usageH };
|
|
37
|
+
usage = { x: right.x, y: y0 + plan.h, w: rightW, h: usageH };
|
|
38
|
+
}
|
|
39
|
+
if (petH && files) { files.h = contentH - petH; pet = { x: x0, y: y0 + files.h, w: filesW, h: petH }; }
|
|
40
|
+
else if (petH && right && plan) { plan.h -= petH; pet = { x: right.x, y: y0 + plan.h, w: rightW, h: petH }; }
|
|
41
|
+
return { w: W, h: H, frame, files, code, messages, plan, usage, pet };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** cells a segment list occupies (East-Asian-width aware, like the borders) */
|
|
45
|
+
export const segWidth = (segs: readonly Seg[]): number => segs.reduce((n, [t]) => n + strWidth(t), 0);
|
|
46
|
+
|
|
47
|
+
/** the longest prefix of `s` that fits `w` cells */
|
|
48
|
+
function headCells(s: string, w: number): string {
|
|
49
|
+
let out = "", used = 0;
|
|
50
|
+
for (const c of s) { const cw = strWidth(c); if (used + cw > w) break; out += c; used += cw; }
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** clip the widest segment (with an ellipsis) until the run fits `room` cells; [] when hopeless
|
|
55
|
+
* (room under 6 cells, or fewer than 3 cells of the clipped text would survive) */
|
|
56
|
+
export function fitSegs(segs: readonly Seg[], room: number): Seg[] {
|
|
57
|
+
let out: Seg[] = segs.filter(([t]) => t.length > 0);
|
|
58
|
+
let ew = segWidth(out);
|
|
59
|
+
if (ew <= room) return out;
|
|
60
|
+
if (room < 6) return [];
|
|
61
|
+
let widest = 0;
|
|
62
|
+
out.forEach(([t], i) => { if (strWidth(t) > strWidth(out[widest]?.[0] ?? "")) widest = i; });
|
|
63
|
+
const [text, style] = out[widest]!;
|
|
64
|
+
const keep = strWidth(text) - (ew - room) - 1;
|
|
65
|
+
if (keep < 3) return [];
|
|
66
|
+
out = out.map((sg, i) => (i === widest ? [headCells(text, keep) + "…", style] : sg));
|
|
67
|
+
ew = segWidth(out);
|
|
68
|
+
return ew <= room ? out : [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** rounded panel with the title in the top border and right-aligned extra segments; returns the inner rect.
|
|
72
|
+
* Interior cleared to the theme background; the title is clipped to the box; extras must leave a
|
|
73
|
+
* `─` between themselves and the title (the prototype's strict `ex > x + 4 + titleW`) — when they do
|
|
74
|
+
* not fit, the widest one is clipped with an ellipsis (fitSegs), else they are dropped.
|
|
75
|
+
* Border/title colors: `color` when given, else accent when focused, else frame (border) / fg2 (title). */
|
|
76
|
+
export function panel(scr: ScreenLike, P: Rect, title: string, focused: boolean, extra: readonly Seg[] | undefined, C: Theme, color?: number): Rect {
|
|
77
|
+
const col = color ?? (focused ? C.accent : C.frame);
|
|
78
|
+
scr.box(P.x, P.y, P.w, P.h, st(col), C.bg);
|
|
79
|
+
if (title) scr.text(P.x + 2, P.y, [[" ", st(-1)], [title, st(color ?? (focused ? C.accent : C.fg2), -1, ATTR.BOLD)], [" ", st(-1)]], Math.max(0, P.w - 4));
|
|
80
|
+
if (extra && extra.length) {
|
|
81
|
+
const segs = fitSegs(extra, P.w - 8 - strWidth(title));
|
|
82
|
+
const ew = segWidth(segs);
|
|
83
|
+
if (ew > 0) scr.text(P.x + P.w - 3 - ew, P.y, [[" ", st(-1)], ...segs, [" ", st(-1)]]);
|
|
84
|
+
}
|
|
85
|
+
return { x: P.x + 2, y: P.y + 1, w: P.w - 4, h: P.h - 2 };
|
|
86
|
+
}
|