rovecode 0.4.0-beta.2 → 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 +67 -69
- package/THIRD_PARTY_NOTICES.md +0 -44
- package/bin/rovecode.ts +21 -0
- package/package.json +16 -37
- 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 -512
- package/bin/rovecode.js +0 -24
- package/dist/cli/app-dybnr56b.js +0 -2
- package/dist/cli/ask-user-p8hq4xgj.js +0 -2
- package/dist/cli/auth-login-ewpgw5sm.js +0 -2
- package/dist/cli/auth-m8p9grty.js +0 -2
- package/dist/cli/bench-xv3ypwev.js +0 -9
- package/dist/cli/catalog-737wb2s0.js +0 -2
- package/dist/cli/cli-arhg40m0.js +0 -2
- package/dist/cli/client-cf2pxx8q.js +0 -2
- package/dist/cli/commands-3p7e4xxs.js +0 -2
- package/dist/cli/connect-3q93d7cb.js +0 -2
- package/dist/cli/context-cmd-eqxmxhzq.js +0 -2
- package/dist/cli/context-report-hbw9zfes.js +0 -2
- package/dist/cli/count-remote-mby98cd0.js +0 -2
- package/dist/cli/design-122y0axd.js +0 -2
- package/dist/cli/dispatch-b4egzvvh.js +0 -2
- package/dist/cli/doctor-x4jkv72e.js +0 -3
- package/dist/cli/executor-ftvg6tsy.js +0 -2
- package/dist/cli/export-pdgdhkch.js +0 -2
- package/dist/cli/files-cez9a96p.js +0 -2
- package/dist/cli/gauntlet-r3xxaszc.js +0 -2
- package/dist/cli/gauntlet-runner-r515m7kk.js +0 -10
- package/dist/cli/gauntlet-wave3-bnkjk2v2.js +0 -5
- package/dist/cli/gauntlet-wave4-acs9s60q.js +0 -14
- package/dist/cli/hashline-ewg5hbe3.js +0 -2
- package/dist/cli/http-n0kehsk8.js +0 -5
- package/dist/cli/index-z5qt1s76.js +0 -2
- package/dist/cli/install-80mp63kx.js +0 -2
- package/dist/cli/loop-12twjcat.js +0 -2
- package/dist/cli/main-01pv9206.js +0 -4
- package/dist/cli/main-0jys2ccn.js +0 -3
- package/dist/cli/main-1ztz6fkj.js +0 -10
- package/dist/cli/main-23q7cmww.js +0 -9
- package/dist/cli/main-2rzbexn2.js +0 -3
- package/dist/cli/main-2wyax8k9.js +0 -9
- package/dist/cli/main-2yeveeve.js +0 -6
- package/dist/cli/main-2z3dek0b.js +0 -3
- package/dist/cli/main-2zgsknth.js +0 -3
- package/dist/cli/main-45ejth3a.js +0 -4
- package/dist/cli/main-45rn3trk.js +0 -22
- package/dist/cli/main-4p4e2w7x.js +0 -4
- package/dist/cli/main-4y0tnfpa.js +0 -16
- package/dist/cli/main-5py0rkmc.js +0 -4
- package/dist/cli/main-6dtqmbt6.js +0 -7
- package/dist/cli/main-6h9x282m.js +0 -4
- package/dist/cli/main-6vjeds42.js +0 -3
- package/dist/cli/main-78gq4bt9.js +0 -6
- package/dist/cli/main-7jd5vh3x.js +0 -4
- package/dist/cli/main-7kt6r53y.js +0 -4
- package/dist/cli/main-8c1tbazx.js +0 -58
- package/dist/cli/main-9a9rnh47.js +0 -19
- package/dist/cli/main-9ht36z12.js +0 -3
- package/dist/cli/main-a2yfvcy9.js +0 -7
- package/dist/cli/main-a3f51n0x.js +0 -5
- package/dist/cli/main-b8zq261k.js +0 -3
- package/dist/cli/main-bxtvnf6d.js +0 -13
- package/dist/cli/main-edxc3yzt.js +0 -4
- package/dist/cli/main-evgz4mp5.js +0 -21
- package/dist/cli/main-f33fc5je.js +0 -9
- package/dist/cli/main-fvnpq46y.js +0 -12
- package/dist/cli/main-gbbty4d4.js +0 -3
- package/dist/cli/main-gth53dnt.js +0 -25
- package/dist/cli/main-hqbz10aw.js +0 -9
- package/dist/cli/main-hrrvcfan.js +0 -38
- package/dist/cli/main-hzwtsb2m.js +0 -5
- package/dist/cli/main-j7ttv0sd.js +0 -34
- package/dist/cli/main-jak598k9.js +0 -5
- package/dist/cli/main-kba6zeyd.js +0 -6
- package/dist/cli/main-kwwsz6rq.js +0 -3
- package/dist/cli/main-m8vm17zq.js +0 -3
- package/dist/cli/main-mg4f96e1.js +0 -3
- package/dist/cli/main-mg9b20ac.js +0 -18
- package/dist/cli/main-mgb9ccnx.js +0 -3
- package/dist/cli/main-mjt2p7aj.js +0 -3
- package/dist/cli/main-n6qrdbmy.js +0 -3
- package/dist/cli/main-na7wse0x.js +0 -5
- package/dist/cli/main-nqveez48.js +0 -4
- package/dist/cli/main-ntqef02r.js +0 -10
- package/dist/cli/main-nvc3yjay.js +0 -136
- package/dist/cli/main-p0cfn6nr.js +0 -16
- package/dist/cli/main-qj2djy17.js +0 -19
- package/dist/cli/main-qsevpgsv.js +0 -3
- package/dist/cli/main-qvarybsp.js +0 -3
- package/dist/cli/main-rebtt91r.js +0 -5
- package/dist/cli/main-rpg7h8mb.js +0 -3
- package/dist/cli/main-rsy72qmw.js +0 -15
- package/dist/cli/main-rvetps99.js +0 -18
- package/dist/cli/main-s4bb0jav.js +0 -3
- package/dist/cli/main-s9v8k74e.js +0 -3
- package/dist/cli/main-tjvwmscs.js +0 -3
- package/dist/cli/main-tkgarpjj.js +0 -4
- package/dist/cli/main-v8y60bb2.js +0 -3
- package/dist/cli/main-vhrrq337.js +0 -3
- package/dist/cli/main-vp2dfb7s.js +0 -4
- package/dist/cli/main-vqbr22sz.js +0 -8
- package/dist/cli/main-vxnwe5xx.js +0 -18
- package/dist/cli/main-wgph00xf.js +0 -5
- package/dist/cli/main-wk2csfnj.js +0 -5
- package/dist/cli/main-wm997zjx.js +0 -3
- package/dist/cli/main-wpkyraxh.js +0 -3
- package/dist/cli/main-wqt32p5x.js +0 -4
- package/dist/cli/main-x9ct6y1a.js +0 -3
- package/dist/cli/main-xfekqh9m.js +0 -7
- package/dist/cli/main-xt9zc3n6.js +0 -7
- package/dist/cli/main-xx2z3zh5.js +0 -4
- package/dist/cli/main-y5c82rxr.js +0 -3
- package/dist/cli/main-yrjt2sqt.js +0 -14
- package/dist/cli/main-ys6zj3yr.js +0 -3
- package/dist/cli/main-ywbxshqc.js +0 -8
- package/dist/cli/main-z13755t8.js +0 -25
- package/dist/cli/main-zc7pyrbj.js +0 -4
- package/dist/cli/main.js +0 -279
- package/dist/cli/market-cmd-bm5xvn9f.js +0 -5
- package/dist/cli/mcp-login-bthtfpt7.js +0 -2
- package/dist/cli/mcp-market-cmd-mbeshfyd.js +0 -2
- package/dist/cli/notify-54v5z9dz.js +0 -2
- package/dist/cli/oauth-g5gme95c.js +0 -2
- package/dist/cli/output-satndjap.js +0 -16
- package/dist/cli/profiles-sfhpbq3m.js +0 -2
- package/dist/cli/provider-config-hv3xtdt4.js +0 -2
- package/dist/cli/provider-kwzq6g84.js +0 -2
- package/dist/cli/registry-fh0hdnyn.js +0 -2
- package/dist/cli/registry-y1y8e94r.js +0 -2
- package/dist/cli/repl-t4z03mqq.js +0 -11
- package/dist/cli/resume-fqt4chg8.js +0 -2
- package/dist/cli/run-flags-rysbag9t.js +0 -2
- package/dist/cli/runtime-j19fjbsa.js +0 -2
- package/dist/cli/sandbox-config-g4qxd7y5.js +0 -2
- package/dist/cli/server-r0b6bksk.js +0 -5
- package/dist/cli/session-arg-txmn5g4x.js +0 -2
- package/dist/cli/session-ed250d9j.js +0 -2
- package/dist/cli/sessions-cmd-adw7svfn.js +0 -7
- package/dist/cli/settings-y9rzcqx8.js +0 -2
- package/dist/cli/setup-jmbr11j0.js +0 -2
- package/dist/cli/sextant-smoke-tcth0vea.js +0 -5
- package/dist/cli/skills-cmd-zbdy99v6.js +0 -2
- package/dist/cli/smoke-1bg937kx.js +0 -8
- package/dist/cli/start-chat-p01cdks3.js +0 -12
- package/dist/cli/stream-4wmyaypz.js +0 -2
- package/dist/cli/task-eg4s093s.js +0 -2
- package/dist/cli/tasks-12v9rr9k.js +0 -2
- package/dist/cli/thinking-a5ngvqyh.js +0 -2
- package/dist/cli/todo-1wxpcecx.js +0 -2
- package/dist/cli/tools-2ftsya7w.js +0 -2
- package/dist/cli/tools-x1tj4fxm.js +0 -2
- package/dist/cli/trust-cmd-hccxehzb.js +0 -2
- package/dist/cli/update-check-ygt3vd7m.js +0 -2
- package/dist/cli/update-cmd-v23qhr8c.js +0 -2
- package/dist/cli/voice-g1gtck92.js +0 -2
- package/dist/cli/webfetch-0nnrjgb5.js +0 -2
- package/dist/cli/websearch-f0vr2p7d.js +0 -2
- package/dist/cli/workspace-9rq1w4ta.js +0 -2
- package/dist/lib/index.js +0 -62
- package/dist/lib/models-index.json +0 -1
- package/dist/lib/plugins.js +0 -6
- package/dist/lib/providers.js +0 -17
- package/dist/lib/public-api.js +0 -20
- package/dist/rovecode.exe +0 -4
- /package/{dist/cli → src/providers}/models-index.json +0 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/** Sextant surface (port #42) — the MESSAGES panel: user / assistant / tool / steer / compaction /
|
|
2
|
+
* system rows, the ONE modal card (approval or ask_user question) pinned above the ╌ rule, and the
|
|
3
|
+
* prompt line with its cursor cell. Ported from the user's own sextant v0.4.0 prototype,
|
|
4
|
+
* src/app.js:561-672 (toolRow, buildRows, drawMessages); the mock welcome/"try" rows, permission
|
|
5
|
+
* items and click hit-boxes are gone — rows come from SextantState.messages, the card from
|
|
6
|
+
* SextantState.card, the agent label reads "rovecode". Pure painter: no clock, no timers, no state
|
|
7
|
+
* mutation; the prototype's msgScroll write-back became the `messagesScroll` seam. */
|
|
8
|
+
|
|
9
|
+
import type { CardState, Rect, ScreenLike, Seg, SextantState, Theme, ToolRow } from "./types.ts";
|
|
10
|
+
import { ATTR } from "./types.ts";
|
|
11
|
+
import { moreMarker } from "../coding/diff.ts";
|
|
12
|
+
import { inner, panel, spinner, st, wrap } from "./draw-util.ts";
|
|
13
|
+
import { clipText } from "./tool-rows.ts";
|
|
14
|
+
import { segWidth } from "./layout.ts";
|
|
15
|
+
import { fmtElapsed, fmtK } from "./model.ts";
|
|
16
|
+
import { thinkingWord } from "./pet.ts";
|
|
17
|
+
import { scrollbar } from "./scrollbar.ts";
|
|
18
|
+
|
|
19
|
+
/** one painted line of the transcript; `path` marks a tool row that names a file (message-hits.ts opens it on click) */
|
|
20
|
+
export interface Row { segs: Seg[]; indent?: number; path?: string }
|
|
21
|
+
|
|
22
|
+
/** 1-slot cache: buildRows is called twice per frame (once in drawMessages, once in messagesScroll).
|
|
23
|
+
* Within a single frame now/w/theme are constant and messages is the same array, so the second call
|
|
24
|
+
* is always a cache hit. Keyed by array identity + length (cheap mutation guard) + w + theme.name + now. */
|
|
25
|
+
let _rowsCache: { msgs: readonly unknown[]; len: number; w: number; name: string; now: number; rows: Row[] } | null = null;
|
|
26
|
+
export function cachedBuildRows(s: SextantState, w: number, theme: Theme, now: number): Row[] {
|
|
27
|
+
if (_rowsCache && _rowsCache.msgs === s.messages && _rowsCache.len === s.messages.length
|
|
28
|
+
&& _rowsCache.w === w && _rowsCache.name === theme.name && _rowsCache.now === now) {
|
|
29
|
+
return _rowsCache.rows;
|
|
30
|
+
}
|
|
31
|
+
const rows = buildRows(s, w, theme, now);
|
|
32
|
+
_rowsCache = { msgs: s.messages, len: s.messages.length, w, name: theme.name, now, rows };
|
|
33
|
+
return rows;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** the attachment cap the chip row quotes (core/images.ts MAX_IMAGES_PER_MESSAGE; a literal here keeps this painter free of node:fs) */
|
|
37
|
+
const MAX_STAGED_HINT = 8;
|
|
38
|
+
/** prompt placeholder when the input is empty */
|
|
39
|
+
export const PLACEHOLDER = "ask rovecode — e.g. fix the failing test";
|
|
40
|
+
/** button label per verdict — `all edits` reads as the mode it turns on, not as a third yes */
|
|
41
|
+
export const VERDICT_LABEL: Record<"once" | "always" | "all-edits" | "deny", string> =
|
|
42
|
+
{ once: "allow", always: "always", "all-edits": "all edits", deny: "deny" };
|
|
43
|
+
/** the default row, for a card that offers no extra door */
|
|
44
|
+
export const VERDICTS: readonly string[] = ["allow", "always", "deny"];
|
|
45
|
+
export const FREE_TEXT_HINT = "type an answer…";
|
|
46
|
+
export const SKIP_LABEL = "skip this question";
|
|
47
|
+
/** the longest question shown in the card (wrapped rows) before it is elided */
|
|
48
|
+
const MAX_QUESTION_ROWS = 6;
|
|
49
|
+
|
|
50
|
+
const VERB_GLYPH: Record<string, string> = { read: "·", edit: "~", write: "+", remove: "−", run: "$", search: "⌕", fetch: "↗", task: "»" };
|
|
51
|
+
/** the label shrinks to this many cells before a detail that would still fit whole is touched (app.js) */
|
|
52
|
+
const LABEL_MIN = 8;
|
|
53
|
+
/** when the detail must be clipped instead, the label keeps up to this many cells (all of a shorter one) */
|
|
54
|
+
const LABEL_KEEP = 24;
|
|
55
|
+
/** a text detail that would keep fewer cells than this says nothing — dropped instead of clipped */
|
|
56
|
+
const DETAIL_MIN = 6;
|
|
57
|
+
|
|
58
|
+
/** cells a string takes: one per code point (types.ts) — `.length` would count an emoji twice */
|
|
59
|
+
const cells = (s: string): number => [...s].length;
|
|
60
|
+
const segCells = (segs: readonly Seg[]): number => segs.reduce((n, [s]) => n + cells(s), 0);
|
|
61
|
+
|
|
62
|
+
/** `<glyph> <verb> <label>` with the detail right-aligned: `· read x … N lines`, `~ edit x +a −b`,
|
|
63
|
+
* `$ run cmd … last line`; the spinner replaces the glyph while the call runs (app.js:561-571).
|
|
64
|
+
* A failed edit shows its rejection detail, never the +a −b it did not apply. When label and detail
|
|
65
|
+
* do not both fit, the label yields first (down to LABEL_MIN cells, the prototype's rule); when even
|
|
66
|
+
* that leaves no room, a TEXT detail is clipped with `…` beside a label kept at up to LABEL_KEEP cells
|
|
67
|
+
* — a rejection's reason stays on the row at the frame's 160×44 message width instead of vanishing;
|
|
68
|
+
* the `+a −b` pair is dropped whole when it cannot fit. */
|
|
69
|
+
export function toolRow(t: ToolRow, w: number, theme: Theme, now: number): Seg[] {
|
|
70
|
+
const failed = !t.running && t.ok === false;
|
|
71
|
+
const g = t.running ? spinner(now) : VERB_GLYPH[t.verb] ?? "·";
|
|
72
|
+
const gc = t.running ? theme.accent : failed ? theme.err : "~+−".includes(g) ? theme.fg2 : theme.dim;
|
|
73
|
+
const verb = ((t.verb === "other" ? t.tool : t.verb) + " ").padEnd(7); // always ≥1 space before the label (ask_user, todo_write, MCP names)
|
|
74
|
+
const detail: Seg[] = [];
|
|
75
|
+
if (t.verb === "edit" && !failed && (t.add || t.del)) {
|
|
76
|
+
if (t.add) detail.push([`+${t.add}`, st(theme.ok)]);
|
|
77
|
+
if (t.del) detail.push([`${t.add ? " " : ""}−${t.del}`, st(theme.err)]);
|
|
78
|
+
} else if (t.detail) detail.push([(t.verb === "edit" ? "" : "… ") + t.detail, st(failed ? theme.err : theme.muted)]);
|
|
79
|
+
let dw = segCells(detail);
|
|
80
|
+
let label = t.label;
|
|
81
|
+
const room = w - 2 - verb.length; // cells for label + gap + detail
|
|
82
|
+
let labelMax = room;
|
|
83
|
+
if (dw) {
|
|
84
|
+
const keep = Math.min(cells(label), LABEL_KEEP);
|
|
85
|
+
if (room - dw - 2 >= Math.min(cells(label), LABEL_MIN)) labelMax = room - dw - 2; // both fit once the label is clipped
|
|
86
|
+
else if (detail.length === 1 && room - keep - 2 >= DETAIL_MIN) { // clip the text detail beside the kept label
|
|
87
|
+
const [text, style] = detail[0]!;
|
|
88
|
+
detail[0] = [clipText(text, room - keep - 2), style];
|
|
89
|
+
labelMax = keep; dw = segCells(detail);
|
|
90
|
+
} else { detail.length = 0; dw = 0; }
|
|
91
|
+
}
|
|
92
|
+
if (cells(label) > labelMax) label = labelMax > 1 ? [...label].slice(0, labelMax - 1).join("") + "…" : "";
|
|
93
|
+
const segs: Seg[] = [[g + " ", st(gc)], [verb, st(t.running ? theme.fg2 : theme.muted)], [label, st(t.running ? theme.fg : failed ? theme.err : theme.fg2)]];
|
|
94
|
+
if (detail.length) segs.push([" ".repeat(Math.max(1, w - 2 - verb.length - cells(label) - dw)), st(-1)], ...detail);
|
|
95
|
+
return segs;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** the status word after `◆ rovecode ·` for the current run: `needs you` while a card waits (the frame
|
|
99
|
+
* header's word, draw-frame.ts), else the live activity while running, else the outcome */
|
|
100
|
+
export function activityLabel(s: SextantState, now?: number): string {
|
|
101
|
+
if (s.card) return "needs you";
|
|
102
|
+
const a = s.activity;
|
|
103
|
+
// a provider turn with no text yet: the word rotates with the clock so the eye can tell live from frozen
|
|
104
|
+
if (s.running) return now !== undefined && a.turnAt !== undefined && a.state === "THINKING" ? thinkingWord(now - a.turnAt) : a.label || a.state.toLowerCase();
|
|
105
|
+
switch (a.state) {
|
|
106
|
+
case "SUCCESS": return "done";
|
|
107
|
+
case "ERROR": return "error";
|
|
108
|
+
case "WAITING": return "needs you";
|
|
109
|
+
case "IDLE": return "";
|
|
110
|
+
default: return a.label || a.state.toLowerCase();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** ` 14s · 1.2k tokens` for the provider turn in flight — proof the run is alive while the model
|
|
115
|
+
* is silent (a reasoning phase runs 15 s and more at high effort). The clock starts at turn_start,
|
|
116
|
+
* the count is reasoning plus answer tokens so far. Degrades in steps to fit `room`: the token count
|
|
117
|
+
* goes first, then the clock, so a narrow panel never overflows. Empty when no turn is in flight. */
|
|
118
|
+
export function liveTail(s: SextantState, now: number, room: number): string {
|
|
119
|
+
const a = s.activity;
|
|
120
|
+
if (a.turnAt === undefined) return "";
|
|
121
|
+
const clock = fmtElapsed(now - a.turnAt);
|
|
122
|
+
const full = a.tokens ? ` ${clock} · ${fmtK(a.tokens)} tokens` : ` ${clock}`;
|
|
123
|
+
if ([...full].length <= room) return full;
|
|
124
|
+
const short = ` ${clock}`;
|
|
125
|
+
return [...short].length <= room ? short : "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** `@path` mentions in a user line get the accent (app.js:596) */
|
|
129
|
+
function mentionSegs(line: string, theme: Theme): Seg[] {
|
|
130
|
+
return line.split(/(@[\w./\\-]+)/).filter(Boolean).map((part): Seg => [part, part[0] === "@" ? st(theme.accent, -1, ATTR.BOLD) : st(theme.fg, -1, ATTR.BOLD)]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Flatten s.messages into drawable rows for a panel `w` cells wide (app.js:572-637). Each run
|
|
134
|
+
* (the rows after a user row) opens with a `◆ rovecode · <status>` header before its first row of any
|
|
135
|
+
* kind — a permission refusal can be the first thing a run says; the current run carries the live
|
|
136
|
+
* status, earlier ones just the diamond. System/compaction rows outside a run (idle notes before
|
|
137
|
+
* any user row) get no header. */
|
|
138
|
+
export function buildRows(s: SextantState, w: number, theme: Theme, now: number): Row[] {
|
|
139
|
+
const rows: Row[] = [];
|
|
140
|
+
const blank = (): void => { rows.push({ segs: [] }); };
|
|
141
|
+
const iw = Math.max(1, w - 2);
|
|
142
|
+
let lastUser = -1;
|
|
143
|
+
s.messages.forEach((m, i) => { if (m.kind === "user") lastUser = i; });
|
|
144
|
+
let prev: SextantState["messages"][number] | null = null;
|
|
145
|
+
let headerDue = true;
|
|
146
|
+
let inRun = s.running; // a live run, or the rows after a user row
|
|
147
|
+
const outcome = s.activity.state === "SUCCESS" ? theme.ok : s.activity.state === "ERROR" ? theme.err : theme.accentDim;
|
|
148
|
+
const header = (i: number): void => {
|
|
149
|
+
const current = i > lastUser;
|
|
150
|
+
const label = current ? activityLabel(s, now) : "";
|
|
151
|
+
const dc = !current ? theme.accentDim : s.running ? theme.accent : outcome;
|
|
152
|
+
const segs: Seg[] = [["◆ ", st(dc)], ["rovecode", st(theme.fg, -1, ATTR.BOLD)], [label ? " · " + label : "", st(s.running ? theme.accent : theme.muted)]];
|
|
153
|
+
if (current && s.running && !s.card) { const tail = liveTail(s, now, w - segWidth(segs)); if (tail) segs.push([tail, st(theme.muted)]); }
|
|
154
|
+
rows.push({ segs });
|
|
155
|
+
headerDue = false;
|
|
156
|
+
};
|
|
157
|
+
s.messages.forEach((m, i) => {
|
|
158
|
+
switch (m.kind) {
|
|
159
|
+
case "user": {
|
|
160
|
+
if (prev) blank();
|
|
161
|
+
const last = i === lastUser;
|
|
162
|
+
rows.push({ segs: [["you", st(last ? theme.accent : theme.muted)], [last ? " · sent" : "", st(theme.accentDim)]] });
|
|
163
|
+
for (const l of wrap(m.text, iw)) rows.push({ segs: mentionSegs(l, theme), indent: 2 });
|
|
164
|
+
if (m.images?.length) rows.push({ segs: m.images.flatMap((name): Seg[] => [[` ▣ ${name} `, st(theme.fg2, theme.selBg)], [" ", st(-1)]]), indent: 2 });
|
|
165
|
+
// one chip per @file attached (mentions.ts): the read block itself is in the session, not on screen
|
|
166
|
+
if (m.files?.length) for (const f of m.files) rows.push({ segs: [[` ▤ ${f} `, st(theme.fg2, theme.selBg)]], indent: 2 });
|
|
167
|
+
headerDue = true; inRun = true;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case "assistant": {
|
|
171
|
+
if (headerDue) { blank(); header(i); } else if (prev && prev.kind !== "assistant") blank();
|
|
172
|
+
const lines = m.text ? wrap(m.text, iw) : [];
|
|
173
|
+
lines.forEach((l, k) => {
|
|
174
|
+
const segs: Seg[] = [[l, st(theme.fg2)]];
|
|
175
|
+
if (m.streaming && k === lines.length - 1) segs.push(["▌", st(theme.accent)]);
|
|
176
|
+
rows.push({ segs, indent: 2 });
|
|
177
|
+
});
|
|
178
|
+
if (m.streaming && !lines.length) rows.push({ segs: [["▌", st(theme.accent)]], indent: 2 });
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
case "tool":
|
|
182
|
+
if (headerDue) { blank(); header(i); } else if (prev && prev.kind === "assistant") blank();
|
|
183
|
+
rows.push({ segs: toolRow(m, iw, theme, now), indent: 2, ...(m.path ? { path: m.path } : {}) });
|
|
184
|
+
break;
|
|
185
|
+
case "steer":
|
|
186
|
+
if (headerDue) { blank(); header(i); } else blank();
|
|
187
|
+
wrap(m.text, iw).forEach((l, k) => rows.push({ segs: [[k === 0 ? "» " : " ", st(theme.accent)], [l, st(theme.fg)]] }));
|
|
188
|
+
break;
|
|
189
|
+
case "compaction":
|
|
190
|
+
if (headerDue && inRun) { blank(); header(i); } else blank();
|
|
191
|
+
for (const l of wrap(m.text, iw)) rows.push({ segs: [["▸ ", st(theme.accentDim)], [l, st(theme.muted, -1, ATTR.ITALIC)]] });
|
|
192
|
+
break;
|
|
193
|
+
case "system": {
|
|
194
|
+
if (headerDue && inRun) { blank(); header(i); } else if (prev && prev.kind !== "system") blank();
|
|
195
|
+
const err = m.tone === "error";
|
|
196
|
+
const gc = err ? theme.err : m.tone === "warn" ? theme.warn : theme.accentDim;
|
|
197
|
+
for (const l of wrap(m.text, iw)) rows.push({ segs: [[err ? "× " : "▸ ", st(gc)], [l, st(err ? theme.err : theme.muted)]] });
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
prev = m;
|
|
202
|
+
});
|
|
203
|
+
// a run that has not said anything yet still shows it is alive: the header with the live line
|
|
204
|
+
// is the ONLY thing between the sent prompt and a first token that may be 15 s away. Not under a
|
|
205
|
+
// card — the card is the status then, and the message rows above it are budgeted to the row.
|
|
206
|
+
if (headerDue && s.running && inRun && !s.card) { blank(); header(s.messages.length); }
|
|
207
|
+
return rows;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** row skeleton of the card: total rows and the free-text row index (question card) — the shared
|
|
211
|
+
* geometry behind `cardRows` and `promptCursor` */
|
|
212
|
+
export function cardShape(card: CardState, w: number, maxDetail: number): { total: number; freeText: number | null } {
|
|
213
|
+
if (card.kind === "approval") {
|
|
214
|
+
const n = card.detail ? detailLines(card.detail).length : 0;
|
|
215
|
+
return { total: 3 + Math.min(n, Math.max(1, maxDetail)), freeText: null };
|
|
216
|
+
}
|
|
217
|
+
const { q, opts, freeRows } = questionRows(card, w);
|
|
218
|
+
return { total: 1 + q + opts + freeRows + 2, freeText: freeRows > 0 ? 1 + q + opts : null };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** the longest free-text answer shown (wrapped rows) before the head scrolls off — the tail keeps
|
|
222
|
+
* the caret and the last words in view, the way a chat box behaves */
|
|
223
|
+
export const MAX_FREE_TEXT_ROWS = 6;
|
|
224
|
+
|
|
225
|
+
/** Row arithmetic the question card shares between its shape, its rows, the caret and the click
|
|
226
|
+
* zones (card-hits.ts): `q` question rows (capped), `opts` option rows, and `freeRows` — the typed
|
|
227
|
+
* answer WRAPPED to the card width (Berkay: a long answer used to be clipped to one row with `…`,
|
|
228
|
+
* so the text you were typing vanished; it belongs on the next line). 0 when free text is off. */
|
|
229
|
+
export function questionRows(card: Extract<CardState, { kind: "question" }>, w: number): { q: number; opts: number; freeRows: number; freeLines: string[] } {
|
|
230
|
+
const q = Math.min(MAX_QUESTION_ROWS, wrap(card.prompt.question, Math.max(1, w - 2)).length);
|
|
231
|
+
const opts = card.prompt.options?.length ?? 0;
|
|
232
|
+
if (card.prompt.allowFreeText === false) return { q, opts, freeRows: 0, freeLines: [] };
|
|
233
|
+
// the hint occupies the single empty row; typed text wraps at the row width minus the `▌ ` marker
|
|
234
|
+
const all = card.freeText.length === 0 ? [""] : wrap(card.freeText, Math.max(1, w - 4));
|
|
235
|
+
const freeLines = all.length > MAX_FREE_TEXT_ROWS ? all.slice(all.length - MAX_FREE_TEXT_ROWS) : all;
|
|
236
|
+
return { q, opts, freeRows: freeLines.length, freeLines };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** the previewDiff lines worth a card row: the leading `--- a/x` / `+++ b/x` pair is dropped (the
|
|
240
|
+
* title's `<tool> <args>` already names the file); hunk headers, signed lines and markers stay */
|
|
241
|
+
function detailLines(detail: string): string[] {
|
|
242
|
+
const all = detail.split("\n");
|
|
243
|
+
return all[0]?.startsWith("--- ") && all[1]?.startsWith("+++ ") ? all.slice(2) : all;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** color of one previewDiff line inside the approval card */
|
|
247
|
+
function diffLineStyle(l: string, theme: Theme): Seg {
|
|
248
|
+
if (l.startsWith("+++") || l.startsWith("---") || l.startsWith("@@") || l.startsWith("…") || l.startsWith("\\")) return [l, st(theme.dim)];
|
|
249
|
+
if (l[0] === "+") return [l, st(theme.ok)];
|
|
250
|
+
if (l[0] === "-") return [l, st(theme.err)];
|
|
251
|
+
return [l, st(theme.fg2)];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The modal card rows (app.js:607-616): approval = `◆ needs your permission <tool> <args>`, the
|
|
255
|
+
* previewDiff detail bounded to `maxDetail` rows (clip marker), the verdict buttons
|
|
256
|
+
* `allow · always · deny` with the selected one inverted; question = the question, its options,
|
|
257
|
+
* the free-text row (when allowed) and `skip this question`, selection inverted the same way. */
|
|
258
|
+
export function cardRows(card: CardState, w: number, maxDetail: number, theme: Theme): Row[] {
|
|
259
|
+
const rows: Row[] = [{ segs: [] }];
|
|
260
|
+
const button = (label: string, sel: boolean): Seg => [` ${label} `, sel ? st(theme.bg, theme.accent, ATTR.BOLD) : st(theme.muted)];
|
|
261
|
+
if (card.kind === "approval") {
|
|
262
|
+
rows.push({ segs: [["◆ ", st(theme.warn)], ["needs your permission", st(theme.fg, -1, ATTR.BOLD)], [` ${card.tool} ${card.argsPreview}`.trimEnd(), st(theme.fg2)]] });
|
|
263
|
+
if (card.detail) {
|
|
264
|
+
const all = detailLines(card.detail);
|
|
265
|
+
const max = Math.max(1, maxDetail);
|
|
266
|
+
const lines = all.length > max ? [...all.slice(0, max - 1), moreMarker(all.length - max + 1)] : all;
|
|
267
|
+
for (const l of lines) rows.push({ segs: [diffLineStyle(l, theme)], indent: 2 });
|
|
268
|
+
}
|
|
269
|
+
const segs: Seg[] = [];
|
|
270
|
+
card.verdicts.forEach((v, i) => { segs.push(button(VERDICT_LABEL[v], i === card.selected), [" ", st(-1)]); });
|
|
271
|
+
segs.push([" ⏎ confirm ←→ choose esc deny", st(theme.dim)]);
|
|
272
|
+
rows.push({ segs, indent: 2 });
|
|
273
|
+
return rows;
|
|
274
|
+
}
|
|
275
|
+
const q = wrap(card.prompt.question, Math.max(1, w - 2));
|
|
276
|
+
const shown = q.length > MAX_QUESTION_ROWS ? [...q.slice(0, MAX_QUESTION_ROWS - 1), (q[MAX_QUESTION_ROWS - 1] ?? "").slice(0, Math.max(0, w - 4)) + "…"] : q;
|
|
277
|
+
shown.forEach((l, k) => rows.push({ segs: [[k === 0 ? "◆ " : " ", st(theme.warn)], [l, st(theme.fg, -1, ATTR.BOLD)]] }));
|
|
278
|
+
const opts = card.prompt.options ?? [];
|
|
279
|
+
opts.forEach((o, i) => rows.push({ segs: [button(o, i === card.selected)], indent: 2 }));
|
|
280
|
+
const { freeRows, freeLines } = questionRows(card, w);
|
|
281
|
+
const free = freeRows > 0;
|
|
282
|
+
if (free) {
|
|
283
|
+
const sel = card.selected === opts.length;
|
|
284
|
+
// the answer wraps: `▌ ` marks the first row, continuation rows align under the text
|
|
285
|
+
freeLines.forEach((line, k) => {
|
|
286
|
+
const marker: Seg = [k === 0 ? "▌ " : " ", st(sel ? theme.accent : theme.accentDim)];
|
|
287
|
+
const empty = card.freeText.length === 0;
|
|
288
|
+
rows.push({ segs: [marker, [empty ? FREE_TEXT_HINT : line, empty ? st(theme.dim) : st(theme.fg, -1, ATTR.BOLD)]], indent: 2 });
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
rows.push({ segs: [button(SKIP_LABEL, card.selected === opts.length + (free ? 1 : 0))], indent: 2 });
|
|
292
|
+
rows.push({ segs: [["⏎ confirm ↑↓ choose", st(theme.dim)]], indent: 2 });
|
|
293
|
+
return rows;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** detail rows an approval card may take inside a message area `h` rows tall: all but the card's
|
|
297
|
+
* blank + title + buttons and two message rows (the 160×44 frame's h = 10 → 5: a small hunk whole) */
|
|
298
|
+
const detailBudget = (h: number): number => Math.max(2, h - 5);
|
|
299
|
+
|
|
300
|
+
/** message rows above the rule, card rows (bounded so ≥1 message row survives), rows per message area */
|
|
301
|
+
export function areas(B: Rect, s: SextantState): { h: number; cardH: number; msgH: number; maxDetail: number } {
|
|
302
|
+
const h = Math.max(0, B.h - 2);
|
|
303
|
+
const maxDetail = detailBudget(h);
|
|
304
|
+
const cardH = s.card ? Math.min(cardShape(s.card, B.w, maxDetail).total, Math.max(0, h - 1)) : 0;
|
|
305
|
+
return { h, cardH, msgH: h - cardH, maxDetail };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Scroll geometry for the message rows: `max` = rows that do not fit, `offset` = the row drawn
|
|
309
|
+
* first — the tail when `stick`, else s.msgScroll clamped. The renderer writes `offset` back into
|
|
310
|
+
* s.msgScroll after a frame (the prototype mutated S.msgScroll in place; a pure painter cannot). */
|
|
311
|
+
export function messagesScroll(rect: Rect, s: SextantState, theme: Theme, now: number): { offset: number; max: number } {
|
|
312
|
+
const B = inner(rect);
|
|
313
|
+
const { msgH } = areas(B, s);
|
|
314
|
+
const rows = cachedBuildRows(s, B.w, theme, now);
|
|
315
|
+
const max = Math.max(0, rows.length - msgH);
|
|
316
|
+
return { offset: s.stick ? max : Math.max(0, Math.min(s.msgScroll, max)), max };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Where the terminal cursor belongs for the prompt (or the question card's free-text row when it
|
|
320
|
+
* is selected); null when the messages panel is not taking text (other focus, palette/help, approval). */
|
|
321
|
+
export function promptCursor(rect: Rect, s: SextantState): { x: number; y: number } | null {
|
|
322
|
+
if (s.palette || s.help) return null;
|
|
323
|
+
const B = inner(rect);
|
|
324
|
+
if (B.w < 3 || B.h < 3) return null;
|
|
325
|
+
const { cardH, msgH, maxDetail } = areas(B, s);
|
|
326
|
+
if (s.card) {
|
|
327
|
+
if (s.card.kind !== "question") return null;
|
|
328
|
+
const shape = cardShape(s.card, B.w, maxDetail);
|
|
329
|
+
if (shape.freeText === null || s.card.selected !== (s.card.prompt.options?.length ?? 0)) return null; // selection index = options.length is the free-text row
|
|
330
|
+
// the caret sits at the end of the LAST wrapped row of the answer (questionRows keeps the tail)
|
|
331
|
+
const { freeRows, freeLines } = questionRows(s.card, B.w);
|
|
332
|
+
const row = shape.freeText + freeRows - 1 - (shape.total - cardH); // rows hidden when the card is clipped scroll off the top
|
|
333
|
+
if (row < 0) return null;
|
|
334
|
+
const last = freeLines[freeLines.length - 1] ?? "";
|
|
335
|
+
return { x: Math.min(B.x + 4 + [...last].length, B.x + B.w - 1), y: B.y + msgH + row };
|
|
336
|
+
}
|
|
337
|
+
if (s.focus !== "messages") return null;
|
|
338
|
+
const inW = B.w - 2;
|
|
339
|
+
const off = Math.max(0, s.input.cur - inW + 1);
|
|
340
|
+
return { x: B.x + 2 + s.input.cur - off, y: B.y + B.h - 1 };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Paint the messages panel into `rect`: frame + count, rows (scrolled), the pinned card, the ╌
|
|
344
|
+
* rule, the prompt line `▌ <text>` (placeholder when empty). Pure over its inputs. */
|
|
345
|
+
export function drawMessages(scr: ScreenLike, rect: Rect, s: SextantState, theme: Theme, now: number): void {
|
|
346
|
+
if (rect.w < 4 || rect.h < 3) return;
|
|
347
|
+
const count = s.messages.filter((m) => m.kind === "user").length;
|
|
348
|
+
const focused = s.focus === "messages";
|
|
349
|
+
const B = panel(scr, rect, "messages", focused, count ? [[String(count), st(theme.muted)]] : [], theme);
|
|
350
|
+
const { h, cardH, msgH, maxDetail } = areas(B, s);
|
|
351
|
+
if (h > 0) {
|
|
352
|
+
const rows = cachedBuildRows(s, B.w, theme, now);
|
|
353
|
+
const max = Math.max(0, rows.length - msgH);
|
|
354
|
+
const offset = s.stick ? max : Math.max(0, Math.min(s.msgScroll, max));
|
|
355
|
+
scrollbar(scr, theme, B.x + B.w - 1, B.y, msgH, rows.length, msgH, offset); // scroll-hits.ts rebuilds this geometry for the drag zone
|
|
356
|
+
for (let i = 0; i < msgH; i++) {
|
|
357
|
+
const r = rows[offset + i];
|
|
358
|
+
if (!r) break;
|
|
359
|
+
const cx = B.x + (r.indent ?? 0);
|
|
360
|
+
scr.text(cx, B.y + i, r.segs, B.x + B.w - cx);
|
|
361
|
+
}
|
|
362
|
+
if (s.card && cardH > 0) {
|
|
363
|
+
const all = cardRows(s.card, B.w, maxDetail, theme);
|
|
364
|
+
const shown = all.slice(all.length - cardH); // when clipped keep the tail: the buttons must stay reachable
|
|
365
|
+
shown.forEach((r, i) => { const cx = B.x + (r.indent ?? 0); scr.text(cx, B.y + msgH + i, r.segs, B.x + B.w - cx); });
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const py = B.y + B.h - 1;
|
|
369
|
+
if (B.h >= 2) scr.hline(B.x, py - 1, B.w, st(theme.frameDim), "╌");
|
|
370
|
+
// staged images ride ON the rule row as chips — visible before the message is sent, and no row is
|
|
371
|
+
// taken from the transcript for them (Berkay: keep the surface simple). Clipped, oldest dropped first.
|
|
372
|
+
if (B.h >= 2 && s.staged.length > 0) {
|
|
373
|
+
const segs: Seg[] = [[" ", st(-1)]];
|
|
374
|
+
for (const name of s.staged) segs.push([` ▣ ${name} `, st(theme.fg2, theme.selBg)], [" ", st(-1)]);
|
|
375
|
+
segs.push([`${s.staged.length}/${MAX_STAGED_HINT} · enter sends · /attach clear`, st(theme.dim)]);
|
|
376
|
+
scr.text(B.x, py - 1, segs, B.w);
|
|
377
|
+
}
|
|
378
|
+
const hasText = s.input.text.length > 0;
|
|
379
|
+
scr.put(B.x, py, "▌", st(hasText || focused ? theme.accent : theme.accentDim));
|
|
380
|
+
const inW = B.w - 2;
|
|
381
|
+
if (inW <= 0) return;
|
|
382
|
+
if (hasText) {
|
|
383
|
+
const off = Math.max(0, s.input.cur - inW + 1);
|
|
384
|
+
scr.put(B.x + 2, py, s.input.text.slice(off), st(theme.fg, -1, ATTR.BOLD), inW);
|
|
385
|
+
} else scr.put(B.x + 2, py, PLACEHOLDER, st(theme.dim), inW);
|
|
386
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/** Port #45 — paints rovecode into its panel: a rounded frame titled `<name> lv N <mood>`, the cloud
|
|
2
|
+
* sprite on the prototype's 1.8 s step sway (pet.js:196), weather (code drizzle while editing, lightning while running/testing,
|
|
3
|
+
* sun on success, zzz when sleepy), the storm (dark filled body, furrowed brows, red eyes, rain + bolts,
|
|
4
|
+
* panel flash, reddened border), sparkles/hearts, and a ≤2-line speech bubble — everything clipped to
|
|
5
|
+
* the panel rect. Ported from the user's own sextant v0.4.0 prototype `src/pet.js` draw() + `app.js`
|
|
6
|
+
* drawPet()/panel() (user-owned). PURE: `now` is a parameter; no timers, no unseeded randomness. */
|
|
7
|
+
|
|
8
|
+
import { ATTR, type Rect, type ScreenLike, type Seg, type SextantState, type Style, type Theme } from "./types.ts";
|
|
9
|
+
import { CODE_RAIN, FACE, INNER, SPRITE, moodCtxFrom, type Mood, type MoodCtx, type Pet } from "./pet.ts";
|
|
10
|
+
import { mix, st } from "./theme.ts";
|
|
11
|
+
|
|
12
|
+
/** term.js mix() lives in theme.ts (port #40); re-exported for the pet tests/fixtures that import it here */
|
|
13
|
+
export { mix };
|
|
14
|
+
|
|
15
|
+
/** the sprite shifts one row every SWAY_MS — pet.js:196 `Math.floor(t / 1800) % 2`, the README's "1.8 s salınım"
|
|
16
|
+
* (full period 3.6 s); the prototype's ±1-column drift (pet.js:197) is deliberately not ported: "titreme yok" */
|
|
17
|
+
export const SWAY_MS = 1800;
|
|
18
|
+
/** a double-flicker strike every 2.6 s; the panel flashes for the first 260 ms */
|
|
19
|
+
export const STORM_PERIOD_MS = 2600;
|
|
20
|
+
export const STORM_FLASH_MS = 260;
|
|
21
|
+
export const SPRITE_W = 18;
|
|
22
|
+
|
|
23
|
+
/** the interior shading, per sprite row: light at the top, heaviest along the base (see the fill in
|
|
24
|
+
* drawPet). Rows past the end fall back to ░, so a re-drawn sprite cannot crash the painter. */
|
|
25
|
+
export const FILL_GLYPH: readonly string[] = ["░", "░", "░", "▒", "▓", "▓"];
|
|
26
|
+
export const FILL_MIX: readonly number[] = [0.10, 0.12, 0.14, 0.18, 0.24, 0.26];
|
|
27
|
+
|
|
28
|
+
/** the click zone the input layer registers (a click anywhere in the panel pokes); null when hidden */
|
|
29
|
+
export function petHit(rect: Rect | null): Rect | null { return rect; }
|
|
30
|
+
|
|
31
|
+
export function moodColor(m: Mood, theme: Theme): number {
|
|
32
|
+
if (m === "sunny") return theme.ok;
|
|
33
|
+
if (m === "furious") return theme.err;
|
|
34
|
+
if (m === "patient") return theme.warn;
|
|
35
|
+
if (m === "focused" || m === "zapping" || m === "conducting") return theme.accent;
|
|
36
|
+
return theme.muted;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** vertical sway phase: a step that flips every SWAY_MS (frames within one step are identical — no per-frame jitter) */
|
|
40
|
+
export function swayBob(now: number): 0 | 1 { return (Math.floor(now / SWAY_MS) % 2) as 0 | 1; }
|
|
41
|
+
|
|
42
|
+
/** word wrap to w cells (code points); a word longer than a row — a host, a path — is hard-split, so a
|
|
43
|
+
* bubble line is never wider than the row and the closing ” survives put()'s clip (it used to be left
|
|
44
|
+
* whole and the quote fell off the edge) */
|
|
45
|
+
export function wrapText(text: string, w: number): string[] {
|
|
46
|
+
const out: string[] = [];
|
|
47
|
+
const width = Math.max(1, w);
|
|
48
|
+
let line = "", len = 0;
|
|
49
|
+
for (const raw of text.split(" ")) {
|
|
50
|
+
const cps = [...raw];
|
|
51
|
+
while (cps.length > width) { if (line) { out.push(line); line = ""; len = 0; } out.push(cps.splice(0, width).join("")); }
|
|
52
|
+
if (!cps.length) continue;
|
|
53
|
+
const word = cps.join("");
|
|
54
|
+
if (!line) { line = word; len = cps.length; continue; }
|
|
55
|
+
if (len + 1 + cps.length <= width) { line += " " + word; len += 1 + cps.length; }
|
|
56
|
+
else { out.push(line); line = word; len = cps.length; }
|
|
57
|
+
}
|
|
58
|
+
if (line) out.push(line);
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** no-op when the layout hides the pet (rect null below 34 rows, or ROVECODE_PET=0) */
|
|
63
|
+
export function drawPet(scr: ScreenLike, rect: Rect | null, pet: Pet, s: SextantState, theme: Theme, now: number): void {
|
|
64
|
+
if (!rect || rect.w < 8 || rect.h < 4) return;
|
|
65
|
+
const ctx = moodCtxFrom(s, now);
|
|
66
|
+
pet.tick(now, ctx);
|
|
67
|
+
const m = pet.mood(ctx, now);
|
|
68
|
+
const B = panel(scr, rect, pet.state.name, pet.level(ctx.tokens), m, theme);
|
|
69
|
+
drawCloud(scr, B, pet, ctx, m, theme, now);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** app.js panel(): rounded box, bold title in the top border, right-aligned extras; returns the inner rect.
|
|
73
|
+
* When the row is too narrow for `lv N mood` the level is dropped first, then the mood. */
|
|
74
|
+
function panel(scr: ScreenLike, R: Rect, name: string, lvl: number, m: Mood, theme: Theme): Rect {
|
|
75
|
+
const furious = m === "furious";
|
|
76
|
+
const col = furious ? mix(theme.bg, theme.err, 0.6) : theme.frame;
|
|
77
|
+
scr.box(R.x, R.y, R.w, R.h, st(col));
|
|
78
|
+
scr.text(R.x + 2, R.y, [[" ", st(-1)], [name, st(furious ? col : theme.fg2, -1, ATTR.BOLD)], [" ", st(-1)]], R.w - 3);
|
|
79
|
+
const moodSeg: Seg = [m, st(moodColor(m, theme), -1, furious ? ATTR.BOLD : 0)];
|
|
80
|
+
const variants: Seg[][] = [[[`lv ${lvl}`, st(theme.muted)], [" ", st(-1)], moodSeg], [moodSeg]];
|
|
81
|
+
for (const extra of variants) {
|
|
82
|
+
const ew = extra.reduce((n, [t]) => n + t.length, 0);
|
|
83
|
+
const ex = R.x + R.w - 3 - ew;
|
|
84
|
+
if (ex > R.x + 4 + name.length) { scr.text(ex, R.y, [[" ", st(-1)], ...extra, [" ", st(-1)]]); break; }
|
|
85
|
+
}
|
|
86
|
+
return { x: R.x + 2, y: R.y + 1, w: R.w - 4, h: R.h - 2 };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function drawCloud(scr: ScreenLike, B: Rect, pet: Pet, ctx: MoodCtx, m: Mood, theme: Theme, t: number): void {
|
|
90
|
+
const P = pet.state;
|
|
91
|
+
const has = (k: string) => P.fx.find((f) => f.kind === k);
|
|
92
|
+
const inB = (x: number, y: number) => x >= B.x && x < B.x + B.w && y >= B.y && y < B.y + B.h;
|
|
93
|
+
const putSafe = (x: number, y: number, ch: string, s: Style) => { if (inB(x, y)) scr.put(x, y, ch, s); };
|
|
94
|
+
const sleeping = m === "sleepy", stormy = m === "furious", sunny = m === "sunny";
|
|
95
|
+
const zapping = ctx.state === "RUNNING" || ctx.state === "TESTING";
|
|
96
|
+
// storm timing: a double-flicker strike every 2.6 s; the panel lights up for the first 260 ms
|
|
97
|
+
let flash = false, boltCols: number[] = [];
|
|
98
|
+
if (stormy) {
|
|
99
|
+
const cyc = Math.floor(t / STORM_PERIOD_MS);
|
|
100
|
+
flash = t % STORM_PERIOD_MS < STORM_FLASH_MS;
|
|
101
|
+
boltCols = [3 + ((cyc * 7919) % 11), 3 + ((cyc * 104729 + 5) % 11)];
|
|
102
|
+
if (flash) scr.tint(B.x, B.y, B.w, B.h, mix(theme.bg, theme.warn, 0.07));
|
|
103
|
+
}
|
|
104
|
+
// body colour: dark red outline over a ░-filled body in a storm, dim while asleep, bright while busy
|
|
105
|
+
let body = theme.fg2;
|
|
106
|
+
if (sunny) body = theme.fg;
|
|
107
|
+
else if (stormy) body = flash ? theme.warn : mix(theme.bg, theme.err, 0.7);
|
|
108
|
+
else if (sleeping) body = theme.mixDim;
|
|
109
|
+
else if (ctx.state === "EDITING" || ctx.running) body = theme.fg;
|
|
110
|
+
// the interior is always painted now: ░ across the top rows, ▒ through the middle, ▓ along the base —
|
|
111
|
+
// two shading steps that read as volume without touching the outline. The tone is mixed from the body
|
|
112
|
+
// colour, so the cloud dims asleep and brightens while busy with the rest of the sprite. A storm keeps
|
|
113
|
+
// its own flat, brighter fill (and its flash), which is what makes it read as a different thing.
|
|
114
|
+
const stormFill = stormy ? mix(theme.bg, theme.fg, flash ? 0.35 : 0.22) : null;
|
|
115
|
+
const faceFg = stormy ? theme.err : sleeping ? theme.muted : theme.fg;
|
|
116
|
+
const mouthFg = stormy ? theme.fg2 : sleeping ? theme.muted : theme.fg;
|
|
117
|
+
// motion: bounce fx hops every 200 ms, shiver fx jitters sideways, a strike shakes the cloud,
|
|
118
|
+
// otherwise the calm 1.8 s sway (asleep: settled low)
|
|
119
|
+
const bob = has("bounce") ? Math.floor(t / 200) % 2 : sleeping ? 1 : swayBob(t);
|
|
120
|
+
const drift = has("shiver") ? Math.floor(t / 160) % 2 : flash ? 1 : 0;
|
|
121
|
+
const sx = B.x + Math.max(0, Math.floor((B.w - SPRITE_W) / 2)) + drift, sy = B.y + 2 - bob;
|
|
122
|
+
// face
|
|
123
|
+
let eyes = "••", mouth = "◡", eyeShift = 0;
|
|
124
|
+
const blink = !sleeping && t % 5200 < 170;
|
|
125
|
+
if (ctx.booting) eyes = "──";
|
|
126
|
+
else if (stormy) { eyes = flash ? "▪▪" : "••"; mouth = "∩"; }
|
|
127
|
+
else if (sleeping) eyes = "──";
|
|
128
|
+
else if (sunny) eyes = "◠◠";
|
|
129
|
+
else if (m === "patient") mouth = "○";
|
|
130
|
+
else if (zapping) mouth = "─";
|
|
131
|
+
else if (ctx.state === "READING") eyeShift = [-1, 0, 1, 0][Math.floor(t / 900) % 4] ?? 0;
|
|
132
|
+
else if (ctx.running) eyeShift = 1;
|
|
133
|
+
if (P.glance && P.glance.until > t && !sleeping && !stormy) eyeShift = P.glance.dir;
|
|
134
|
+
if (ctx.typing && !ctx.running) eyeShift = 1;
|
|
135
|
+
if (blink && !stormy) eyes = "──";
|
|
136
|
+
for (let r = 0; r < SPRITE.length; r++) {
|
|
137
|
+
const y = sy + r, row = SPRITE[r] ?? "", [a, b] = INNER[r] ?? [0, 0];
|
|
138
|
+
for (let i = 0; i < row.length; i++) {
|
|
139
|
+
const ch = row[i] ?? " ";
|
|
140
|
+
if (ch !== " ") putSafe(sx + i, y, ch, st(body));
|
|
141
|
+
else if (i > a && i < b) {
|
|
142
|
+
putSafe(sx + i, y, stormFill != null ? "░" : (FILL_GLYPH[r] ?? "░"),
|
|
143
|
+
st(stormFill ?? mix(theme.bg, body, FILL_MIX[r] ?? 0.14)));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (r === FACE.EYE_ROW) {
|
|
147
|
+
putSafe(sx + FACE.EYE_L + eyeShift, y, eyes[0] ?? "•", st(faceFg, -1, ATTR.BOLD));
|
|
148
|
+
putSafe(sx + FACE.EYE_R + eyeShift, y, eyes[1] ?? "•", st(faceFg, -1, ATTR.BOLD));
|
|
149
|
+
}
|
|
150
|
+
if (r === FACE.EYE_ROW - 1 && stormy) { // furrowed brows
|
|
151
|
+
putSafe(sx + FACE.EYE_L + eyeShift, y, "╲", st(theme.err, -1, ATTR.BOLD));
|
|
152
|
+
putSafe(sx + FACE.EYE_R + eyeShift, y, "╱", st(theme.err, -1, ATTR.BOLD));
|
|
153
|
+
}
|
|
154
|
+
if (r === FACE.MOUTH_ROW) putSafe(sx + FACE.MOUTH + (eyeShift > 0 ? 1 : 0), y, mouth, st(mouthFg));
|
|
155
|
+
}
|
|
156
|
+
// weather + extras
|
|
157
|
+
const wy = sy + SPRITE.length; // first row under the cloud
|
|
158
|
+
if (sunny) {
|
|
159
|
+
putSafe(sx + 15, sy - 1, "☼", st(theme.warn, -1, ATTR.BOLD));
|
|
160
|
+
putSafe(sx + 17, sy - 2, "·", st(theme.warn));
|
|
161
|
+
putSafe(sx + 13, sy - 2, "·", st(theme.warn));
|
|
162
|
+
putSafe(sx + 18, sy, "·", st(theme.warn));
|
|
163
|
+
}
|
|
164
|
+
if (sleeping) {
|
|
165
|
+
const ph = Math.floor(t / 1200) % 3;
|
|
166
|
+
putSafe(sx + 13, sy - (ph >= 1 ? 1 : 0), "z", st(theme.muted));
|
|
167
|
+
if (ph >= 1) putSafe(sx + 15, sy - 1 - (ph === 2 ? 1 : 0), "Z", st(theme.muted));
|
|
168
|
+
}
|
|
169
|
+
if (ctx.running && !stormy && m !== "patient" && ctx.state !== "EDITING" && !zapping) { // thinking
|
|
170
|
+
const ph = Math.floor(t / 800) % 4;
|
|
171
|
+
putSafe(sx + 17, sy + 1, "◌", st(theme.muted));
|
|
172
|
+
if (ph >= 1) putSafe(sx + 18, sy, "○", st(theme.muted));
|
|
173
|
+
if (ph >= 2) putSafe(sx + 20, sy - 1, ["…", "·", "?", "…"][Math.floor(t / 3000) % 4] ?? "…", st(theme.fg2));
|
|
174
|
+
}
|
|
175
|
+
if (m === "patient") putSafe(sx + 17, sy + (Math.floor(t / 900) % 2 ? 0 : 1), "?", st(theme.warn, -1, ATTR.BOLD)); // slow blink
|
|
176
|
+
if (ctx.state === "EDITING") { // code drizzle
|
|
177
|
+
for (let row = 0; row < 2; row++) {
|
|
178
|
+
const phase = Math.floor(t / 420) + row * 2;
|
|
179
|
+
for (let x = sx + 3; x < sx + 16; x++) {
|
|
180
|
+
if ((x + phase) % 4 !== 0) continue;
|
|
181
|
+
const ch = CODE_RAIN[(x * 7 + Math.floor(t / 1400) + row) % CODE_RAIN.length] ?? ".";
|
|
182
|
+
putSafe(x, wy + row, ch, st(row ? theme.dim : theme.accentDim));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (zapping && Math.floor(t / 700) % 2 === 0) { // lightning
|
|
187
|
+
const k = sx + 4 + (Math.floor(t / 2100) % 3) * 5;
|
|
188
|
+
putSafe(k, wy, "╲", st(theme.warn, -1, ATTR.BOLD));
|
|
189
|
+
putSafe(k, wy + 1, "╱", st(theme.warn, -1, ATTR.BOLD));
|
|
190
|
+
}
|
|
191
|
+
if (stormy) {
|
|
192
|
+
const rows = Math.max(1, Math.min(3, B.y + B.h - 2 - wy));
|
|
193
|
+
for (let row = 0; row < rows; row++) {
|
|
194
|
+
for (let x = sx + 1; x < sx + 17; x++) {
|
|
195
|
+
if ((x * 3 + Math.floor(t / 330) + row * 2) % 4 === 0) putSafe(x, wy + row, row % 2 ? "╷" : "│", st(flash ? theme.fg2 : theme.info));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (flash) {
|
|
199
|
+
for (const k of boltCols) for (let r = 0; r < rows; r++) putSafe(sx + k, wy + r, r % 2 ? "╱" : "╲", st(theme.warn, -1, ATTR.BOLD));
|
|
200
|
+
putSafe(sx - 1, sy + 2, "╲", st(theme.warn, -1, ATTR.BOLD));
|
|
201
|
+
putSafe(sx + 18, sy + 1, "╱", st(theme.warn, -1, ATTR.BOLD));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const sp = has("sparkle");
|
|
205
|
+
if (sp) {
|
|
206
|
+
for (let i = 0; i < 7; i++) {
|
|
207
|
+
const h = (sp.seed * (i + 3) * 2654435761) >>> 0;
|
|
208
|
+
const x = B.x + (h % B.w), y = B.y + ((h >>> 8) % Math.max(1, wy + 1 - B.y));
|
|
209
|
+
const on = Math.floor((t + (h % 500)) / 520) % 2 === 0;
|
|
210
|
+
if (on && !(y >= sy && y < wy && x >= sx + 1 && x < sx + 18)) putSafe(x, y, ["*", "·", "+"][i % 3] ?? "*", st(i % 2 ? theme.warn : theme.ok));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const hearts = has("hearts");
|
|
214
|
+
if (hearts) {
|
|
215
|
+
const age = t - (hearts.until - 1800);
|
|
216
|
+
const rise = Math.min(2, Math.floor(Math.max(0, age) / 700));
|
|
217
|
+
putSafe(sx + 16, sy + 1 - rise, "♥", st(theme.err));
|
|
218
|
+
if (rise >= 1) putSafe(sx + 19, sy + 2 - rise, "♥", st(theme.err));
|
|
219
|
+
}
|
|
220
|
+
// speech bubble: at most two lines at the bottom of the panel, clipped to its width
|
|
221
|
+
const q = P.quip && P.quip.until > t ? P.quip.text : null;
|
|
222
|
+
if (q) {
|
|
223
|
+
const qy = B.y + B.h - 2;
|
|
224
|
+
// B.w - 3: the two quote marks share the line and put() clips at B.w - 1, so a full line keeps its closing ”
|
|
225
|
+
const lines = wrapText(q, B.w - 3).slice(0, 2);
|
|
226
|
+
lines.forEach((l, i) => {
|
|
227
|
+
if (qy + i < B.y + B.h) scr.put(B.x + 1, qy + i, (i === 0 ? "“" : " ") + l + (i === lines.length - 1 ? "”" : ""), st(theme.fg2, -1, ATTR.ITALIC), B.w - 1);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|