rovecode 0.3.3 → 0.4.0-beta.3
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/CHANGELOG.md +527 -0
- package/README.md +46 -48
- package/bin/rovecode.js +24 -0
- package/dist/cli/app-j6gn14w3.js +2 -0
- package/dist/cli/ask-user-cwstt8fz.js +2 -0
- package/dist/cli/auth-login-9bbp9915.js +2 -0
- package/dist/cli/auth-m8p9grty.js +2 -0
- package/dist/cli/bench-16zqdms5.js +9 -0
- package/dist/cli/catalog-1xchffa4.js +2 -0
- package/dist/cli/cli-1n1zb64f.js +2 -0
- package/dist/cli/client-2t9gjkck.js +2 -0
- package/dist/cli/commands-exafvm2b.js +2 -0
- package/dist/cli/connect-6zde0kn3.js +2 -0
- package/dist/cli/context-cmd-5t43wgqt.js +2 -0
- package/dist/cli/context-report-kt01pw8y.js +2 -0
- package/dist/cli/count-remote-ap7x3vh6.js +2 -0
- package/dist/cli/design-ne5zszyh.js +2 -0
- package/dist/cli/dispatch-2r5myxye.js +2 -0
- package/dist/cli/doctor-ws4fh4tn.js +3 -0
- package/dist/cli/executor-bdrjn634.js +2 -0
- package/dist/cli/export-1mxb9g5p.js +2 -0
- package/dist/cli/files-g104xghh.js +2 -0
- package/dist/cli/gauntlet-07xrjpj7.js +2 -0
- package/dist/cli/gauntlet-runner-xvy64436.js +10 -0
- package/dist/cli/gauntlet-wave3-jm91yt5w.js +5 -0
- package/dist/cli/gauntlet-wave4-r13py7p1.js +14 -0
- package/dist/cli/hashline-znvrat11.js +2 -0
- package/dist/cli/http-xafw6fsh.js +143 -0
- package/dist/cli/index-1sgjm25y.js +2 -0
- package/dist/cli/init-g2m0tn4m.js +51 -0
- package/dist/cli/install-avaqjjqq.js +2 -0
- package/dist/cli/loop-mmpfft01.js +2 -0
- package/dist/cli/main-0904f6ps.js +5 -0
- package/dist/cli/main-0ab9fc26.js +9 -0
- package/dist/cli/main-0jys2ccn.js +3 -0
- package/dist/cli/main-0mtcdbs7.js +3 -0
- package/dist/cli/main-0z1w2zsg.js +3 -0
- package/dist/cli/main-1dchs7xv.js +18 -0
- package/dist/cli/main-1ereejm1.js +3 -0
- package/dist/cli/main-1k1kw6b5.js +3 -0
- package/dist/cli/main-27y4sm2k.js +38 -0
- package/dist/cli/main-2wwjex5j.js +58 -0
- package/dist/cli/main-2yeveeve.js +6 -0
- package/dist/cli/main-2yfck9b5.js +3 -0
- package/dist/cli/main-2zmzgkwh.js +3 -0
- package/dist/cli/main-351pz3z7.js +7 -0
- package/dist/cli/main-3gjqfh7a.js +6 -0
- package/dist/cli/main-3nf3kgve.js +3 -0
- package/dist/cli/main-3pjrb2hd.js +3 -0
- package/dist/cli/main-3rxcvgna.js +19 -0
- package/dist/cli/main-4b3jgy66.js +19 -0
- package/dist/cli/main-4wndhjdc.js +7 -0
- package/dist/cli/main-4xcmvxnk.js +3 -0
- package/dist/cli/main-5tbz0wbz.js +4 -0
- package/dist/cli/main-5ywnwthm.js +3 -0
- package/dist/cli/main-6b62vkz0.js +14 -0
- package/dist/cli/main-6dnk69vp.js +3 -0
- package/dist/cli/main-6genrmhs.js +136 -0
- package/dist/cli/main-73g7eff4.js +15 -0
- package/dist/cli/main-7c5thhjd.js +5 -0
- package/dist/cli/main-7rn6bqje.js +3 -0
- package/dist/cli/main-80haw7qk.js +4 -0
- package/dist/cli/main-875s60s2.js +4 -0
- package/dist/cli/main-8kjxbpw4.js +8 -0
- package/dist/cli/main-90ds1z4e.js +10 -0
- package/dist/cli/main-9etavkew.js +3 -0
- package/dist/cli/main-a9njrkk1.js +3 -0
- package/dist/cli/main-aecrjq2d.js +12 -0
- package/dist/cli/main-ck9asesq.js +9 -0
- package/dist/cli/main-cta9racd.js +4 -0
- package/dist/cli/main-ddv7j2ag.js +3 -0
- package/dist/cli/main-dfreez27.js +10 -0
- package/dist/cli/main-f7rw7des.js +3 -0
- package/dist/cli/main-ggcn7rd7.js +5 -0
- package/dist/cli/main-gzkmycnv.js +3 -0
- package/dist/cli/main-hq51jg8v.js +18 -0
- package/dist/cli/main-jft389w9.js +8 -0
- package/dist/cli/main-k1eqkg83.js +3 -0
- package/dist/cli/main-k2y8a2aw.js +9 -0
- package/dist/cli/main-kcpbykxz.js +4 -0
- package/dist/cli/main-kd488vje.js +22 -0
- package/dist/cli/main-kh32yvgk.js +5 -0
- package/dist/cli/main-kqxnqjnv.js +25 -0
- package/dist/cli/main-kyn0xnsg.js +3 -0
- package/dist/cli/main-m1kk6fp5.js +21 -0
- package/dist/cli/main-mv40pcr2.js +4 -0
- package/dist/cli/main-n0t3973w.js +3 -0
- package/dist/cli/main-nqveez48.js +4 -0
- package/dist/cli/main-pknhvrmj.js +3 -0
- package/dist/cli/main-pn1w7a7j.js +3 -0
- package/dist/cli/main-prxxs70n.js +4 -0
- package/dist/cli/main-q3vsesf9.js +3 -0
- package/dist/cli/main-qsevpgsv.js +3 -0
- package/dist/cli/main-rdgdw24b.js +25 -0
- package/dist/cli/main-rfth4tbm.js +16 -0
- package/dist/cli/main-rg0wn0xf.js +5 -0
- package/dist/cli/main-sdmxhtv8.js +4 -0
- package/dist/cli/main-skbp13js.js +18 -0
- package/dist/cli/main-t4xnd213.js +7 -0
- package/dist/cli/main-vqak588n.js +4 -0
- package/dist/cli/main-w2n1303f.js +9 -0
- package/dist/cli/main-wbrdspr2.js +5 -0
- package/dist/cli/main-wsrg79c1.js +7 -0
- package/dist/cli/main-x4r0fne4.js +5 -0
- package/dist/cli/main-xea2f3tn.js +6 -0
- package/dist/cli/main-xg704a3c.js +3 -0
- package/dist/cli/main-xvnrabfp.js +16 -0
- package/dist/cli/main-xy53xf0r.js +4 -0
- package/dist/cli/main-y1fqy60y.js +3 -0
- package/dist/cli/main-yn8cd281.js +34 -0
- package/dist/cli/main-yr0ksc0h.js +4 -0
- package/dist/cli/main-z2ex2vyf.js +4 -0
- package/dist/cli/main-z3aayzvq.js +3 -0
- package/dist/cli/main-zaqh35jg.js +3 -0
- package/dist/cli/main-zc2e8e46.js +4 -0
- package/dist/cli/main-zzrfw6cf.js +13 -0
- package/dist/cli/main.js +280 -0
- package/dist/cli/market-cmd-e14kmx9n.js +5 -0
- package/dist/cli/mcp-login-wq7ktdek.js +2 -0
- package/dist/cli/mcp-market-cmd-9mg3jecy.js +2 -0
- package/dist/cli/notify-b7qc0cjb.js +2 -0
- package/dist/cli/oauth-z8whcgfx.js +2 -0
- package/dist/cli/output-b3ewj3ps.js +16 -0
- package/dist/cli/profiles-6mr5he5e.js +2 -0
- package/dist/cli/provider-config-g7j42q8x.js +2 -0
- package/dist/cli/provider-jr1y8vvm.js +2 -0
- package/dist/cli/registry-s8yk86g0.js +2 -0
- package/dist/cli/registry-t6p8d4mn.js +2 -0
- package/dist/cli/repl-bajwe1mh.js +11 -0
- package/dist/cli/resume-rwn9nz7y.js +2 -0
- package/dist/cli/run-flags-nah7ndpt.js +2 -0
- package/dist/cli/runtime-n7gafzhb.js +2 -0
- package/dist/cli/sandbox-config-emdy18x4.js +2 -0
- package/dist/cli/server-b0nvs2bn.js +5 -0
- package/dist/cli/session-arg-y75wd4kj.js +2 -0
- package/dist/cli/session-j62evmjq.js +2 -0
- package/dist/cli/sessions-cmd-tsnwz0ns.js +7 -0
- package/dist/cli/settings-df10wfez.js +2 -0
- package/dist/cli/setup-jzvv72fg.js +2 -0
- package/dist/cli/sextant-smoke-37m81ke6.js +5 -0
- package/dist/cli/skills-cmd-gjxnxnhx.js +2 -0
- package/dist/cli/smoke-p7748apt.js +8 -0
- package/dist/cli/start-chat-s4st3mm0.js +12 -0
- package/dist/cli/stream-gmeyewds.js +2 -0
- package/dist/cli/task-gh0kkp3n.js +2 -0
- package/dist/cli/tasks-z1kfpe8e.js +2 -0
- package/dist/cli/thinking-0eqkrz6t.js +2 -0
- package/dist/cli/todo-5brcrt9m.js +2 -0
- package/dist/cli/tools-7pzm0vj9.js +2 -0
- package/dist/cli/tools-s635p6s8.js +2 -0
- package/dist/cli/trust-cmd-cjav8zgm.js +2 -0
- package/dist/cli/update-check-pt31bm2f.js +2 -0
- package/dist/cli/update-cmd-tk131s9t.js +2 -0
- package/dist/cli/voice-56nabd8d.js +2 -0
- package/dist/cli/webfetch-xd8q596m.js +2 -0
- package/dist/cli/websearch-5hkf98k1.js +2 -0
- package/dist/cli/workflow-cmd-cy3cvzjp.js +4 -0
- package/dist/cli/workspace-q10g5z3e.js +2 -0
- package/dist/lib/index.js +62 -0
- package/dist/lib/models-index.json +1 -0
- package/dist/lib/plugins.js +55 -0
- package/dist/lib/providers.js +17 -0
- package/dist/lib/public-api.js +20 -0
- package/dist/lib/sdk.js +360 -0
- package/package.json +84 -72
- package/bin/rovecode.ts +0 -21
- package/src/acp/server.ts +0 -374
- package/src/cli/auth-login.ts +0 -122
- package/src/cli/connect.ts +0 -244
- package/src/cli/context-cmd.ts +0 -199
- package/src/cli/dispatch.ts +0 -82
- package/src/cli/doctor.ts +0 -362
- package/src/cli/export.ts +0 -276
- package/src/cli/help.ts +0 -293
- package/src/cli/is-tui-invocation.ts +0 -8
- package/src/cli/main.ts +0 -583
- package/src/cli/market-cmd.ts +0 -658
- package/src/cli/mcp-login.ts +0 -141
- package/src/cli/mcp-market-cmd.ts +0 -302
- package/src/cli/output.ts +0 -382
- package/src/cli/repl.ts +0 -250
- package/src/cli/repomap-root.ts +0 -14
- package/src/cli/resume.ts +0 -57
- package/src/cli/run-flags.ts +0 -43
- package/src/cli/run-limits.ts +0 -78
- package/src/cli/runtime.ts +0 -931
- package/src/cli/session-arg.ts +0 -30
- package/src/cli/sessions-cmd.ts +0 -145
- package/src/cli/setup.ts +0 -153
- package/src/cli/skills-cmd.ts +0 -194
- package/src/cli/start-chat.ts +0 -65
- package/src/cli/trust-cmd.ts +0 -52
- package/src/coding/bash.ts +0 -148
- package/src/coding/checkpoints.ts +0 -327
- package/src/coding/diff.ts +0 -138
- package/src/coding/files.ts +0 -341
- package/src/coding/hashline.ts +0 -274
- package/src/coding/lsp-gate.ts +0 -254
- package/src/coding/lsp-servers.ts +0 -147
- package/src/coding/lsp.ts +0 -283
- package/src/coding/repomap-cache.ts +0 -99
- package/src/coding/repomap-files.ts +0 -192
- package/src/coding/repomap.ts +0 -481
- package/src/core/agents.ts +0 -255
- package/src/core/compaction.ts +0 -259
- package/src/core/config.ts +0 -289
- package/src/core/context-report.ts +0 -228
- package/src/core/context.ts +0 -60
- package/src/core/count-remote.ts +0 -107
- package/src/core/execpolicy-rules.ts +0 -196
- package/src/core/execpolicy.ts +0 -385
- package/src/core/executor.ts +0 -454
- package/src/core/guardrails.ts +0 -400
- package/src/core/hooks.ts +0 -411
- package/src/core/images.ts +0 -230
- package/src/core/intro.ts +0 -266
- package/src/core/loop.ts +0 -567
- package/src/core/modes.ts +0 -372
- package/src/core/orchestrator.ts +0 -245
- package/src/core/proc-group.ts +0 -48
- package/src/core/project-trust.ts +0 -98
- package/src/core/reflection.ts +0 -165
- package/src/core/sandbox-config.ts +0 -186
- package/src/core/session-id.ts +0 -24
- package/src/core/session-images.ts +0 -73
- package/src/core/session-ops.ts +0 -183
- package/src/core/session-text.ts +0 -29
- package/src/core/session.ts +0 -469
- package/src/core/settings.ts +0 -170
- package/src/core/tasks.ts +0 -646
- package/src/core/token-scale.ts +0 -108
- package/src/core/tools.ts +0 -309
- package/src/core/trust.ts +0 -104
- package/src/core/types.ts +0 -330
- package/src/core/update-check.ts +0 -171
- package/src/core/usage.ts +0 -204
- package/src/core/validate.ts +0 -121
- package/src/core/verify-gate.ts +0 -159
- package/src/core/verify.ts +0 -236
- package/src/core/voice.ts +0 -158
- package/src/core/win-job.ts +0 -183
- package/src/core/workspace.ts +0 -184
- package/src/design/audit.ts +0 -797
- package/src/design/direction.ts +0 -190
- package/src/design/rules.ts +0 -157
- package/src/eval/bench.ts +0 -150
- package/src/eval/gauntlet-runner.ts +0 -215
- package/src/eval/gauntlet-support.ts +0 -84
- package/src/eval/gauntlet-wave3.ts +0 -269
- package/src/eval/gauntlet-wave4.ts +0 -217
- package/src/eval/gauntlet.ts +0 -253
- package/src/index.ts +0 -17
- package/src/lanes/agy.ts +0 -95
- package/src/lanes/approval.ts +0 -24
- package/src/lanes/claude.ts +0 -129
- package/src/lanes/codex.ts +0 -127
- package/src/lanes/events.ts +0 -130
- package/src/lanes/job.ts +0 -142
- package/src/lanes/opencode.ts +0 -122
- package/src/lanes/process.ts +0 -184
- package/src/lanes/progress.ts +0 -183
- package/src/lanes/registry.ts +0 -178
- package/src/lanes/runner.ts +0 -124
- package/src/lanes/types.ts +0 -112
- package/src/market/catalogs/mcp-docs.json +0 -111
- package/src/market/catalogs/plugins.json +0 -111
- package/src/market/catalogs/skills.json +0 -478
- package/src/market/clone.ts +0 -72
- package/src/market/context-cost.ts +0 -121
- package/src/market/digest.ts +0 -106
- package/src/market/index.ts +0 -22
- package/src/market/install.ts +0 -578
- package/src/market/manifest.ts +0 -187
- package/src/market/prereq.ts +0 -145
- package/src/market/registry.ts +0 -363
- package/src/market/resolve.ts +0 -111
- package/src/market/types.ts +0 -236
- package/src/market/validate.ts +0 -227
- package/src/mcp/client.ts +0 -449
- package/src/mcp/config.ts +0 -252
- package/src/mcp/local-package.ts +0 -211
- package/src/mcp/market-catalog.ts +0 -84
- package/src/mcp/market-install.ts +0 -289
- package/src/mcp/market.ts +0 -362
- package/src/mcp/oauth.ts +0 -251
- package/src/mcp/prompts-resources.ts +0 -249
- package/src/mcp/shared.ts +0 -149
- package/src/mcp/status.ts +0 -67
- package/src/mcp/tools.ts +0 -275
- package/src/mcp/transport.ts +0 -122
- package/src/mcp/trust.ts +0 -25
- package/src/memory/blocks.ts +0 -278
- package/src/memory/recall.ts +0 -355
- package/src/memory/scope.ts +0 -182
- package/src/memory/store.ts +0 -105
- package/src/memory/tools.ts +0 -99
- package/src/plugins/cli.ts +0 -119
- package/src/plugins/discover.ts +0 -108
- package/src/plugins/index.ts +0 -50
- package/src/plugins/install.ts +0 -184
- package/src/plugins/load.ts +0 -124
- package/src/plugins/manifest.ts +0 -92
- package/src/plugins/state.ts +0 -83
- package/src/providers/auth.ts +0 -408
- package/src/providers/cache.ts +0 -223
- package/src/providers/catalog-local.ts +0 -160
- package/src/providers/catalog.ts +0 -421
- package/src/providers/middleware-context.ts +0 -86
- package/src/providers/middleware.ts +0 -373
- package/src/providers/model-list.ts +0 -23
- package/src/providers/oauth/common.ts +0 -105
- package/src/providers/oauth/device-code.ts +0 -107
- package/src/providers/oauth/github-copilot.ts +0 -146
- package/src/providers/oauth/loopback.ts +0 -158
- package/src/providers/oauth/openai.ts +0 -163
- package/src/providers/oauth/openrouter.ts +0 -89
- package/src/providers/oauth/pkce.ts +0 -45
- package/src/providers/oauth/registry.ts +0 -39
- package/src/providers/oauth/seam.ts +0 -89
- package/src/providers/profile-glm53.ts +0 -111
- package/src/providers/profile-sonnet5-persona.ts +0 -65
- package/src/providers/profile-sonnet5-voice.ts +0 -23
- package/src/providers/profiles.ts +0 -156
- package/src/providers/provider-config.ts +0 -311
- package/src/providers/registry.ts +0 -333
- package/src/providers/responses.ts +0 -209
- package/src/providers/retry.ts +0 -234
- package/src/providers/router.ts +0 -294
- package/src/providers/sse.ts +0 -26
- package/src/providers/stream-errors.ts +0 -117
- package/src/providers/stream.ts +0 -566
- package/src/providers/thinking.ts +0 -189
- package/src/providers/wire-messages.ts +0 -129
- package/src/providers/wire-responses.ts +0 -79
- package/src/providers/wire-select.ts +0 -53
- package/src/server/http.ts +0 -291
- package/src/server/openapi.ts +0 -246
- package/src/sextant/card-hits.ts +0 -102
- package/src/sextant/card-keys.ts +0 -55
- package/src/sextant/context-source.ts +0 -157
- package/src/sextant/crew-cards.ts +0 -350
- package/src/sextant/draw-agents.ts +0 -273
- package/src/sextant/draw-code.ts +0 -388
- package/src/sextant/draw-context.ts +0 -222
- package/src/sextant/draw-frame.ts +0 -164
- package/src/sextant/draw-market.ts +0 -573
- package/src/sextant/draw-messages.ts +0 -386
- package/src/sextant/draw-pet.ts +0 -230
- package/src/sextant/draw-plan.ts +0 -187
- package/src/sextant/draw-tabs.ts +0 -85
- package/src/sextant/draw-util.ts +0 -65
- package/src/sextant/draw-wizard.ts +0 -378
- package/src/sextant/engine.ts +0 -230
- package/src/sextant/frame-hits.ts +0 -25
- package/src/sextant/frame.ts +0 -101
- package/src/sextant/git-status.ts +0 -197
- package/src/sextant/grid.ts +0 -59
- package/src/sextant/input.ts +0 -119
- package/src/sextant/keys.ts +0 -521
- package/src/sextant/layout.ts +0 -86
- package/src/sextant/local-commands.ts +0 -169
- package/src/sextant/market-source.ts +0 -287
- package/src/sextant/mentions.ts +0 -200
- package/src/sextant/message-hits.ts +0 -26
- package/src/sextant/model.ts +0 -387
- package/src/sextant/overlays.ts +0 -456
- package/src/sextant/panel-hits.ts +0 -38
- package/src/sextant/pet.ts +0 -399
- package/src/sextant/screen.ts +0 -324
- package/src/sextant/scroll-hits.ts +0 -66
- package/src/sextant/scrollbar.ts +0 -82
- package/src/sextant/sextant-bridge.ts +0 -174
- package/src/sextant/sextant-cards.ts +0 -142
- package/src/sextant/sextant-diff-base.ts +0 -63
- package/src/sextant/sextant-files.ts +0 -154
- package/src/sextant/sextant-frame-loop.ts +0 -335
- package/src/sextant/sextant-renderer.ts +0 -574
- package/src/sextant/sextant-repo.ts +0 -140
- package/src/sextant/theme.ts +0 -66
- package/src/sextant/tool-rows.ts +0 -189
- package/src/sextant/types.ts +0 -493
- package/src/skills/index.ts +0 -387
- package/src/skills/pack.ts +0 -220
- package/src/skills/spec.ts +0 -162
- package/src/skills/tools.ts +0 -69
- package/src/skills/versioned.ts +0 -227
- package/src/telemetry/otel-export.ts +0 -122
- package/src/telemetry/otel-lanes.ts +0 -89
- package/src/telemetry/otel-logs.ts +0 -131
- package/src/telemetry/otel-metrics.ts +0 -136
- package/src/telemetry/otel.ts +0 -397
- package/src/telemetry/otlp.ts +0 -76
- package/src/tools/ask-user.ts +0 -156
- package/src/tools/bash-bg.ts +0 -94
- package/src/tools/bash-jobs.ts +0 -237
- package/src/tools/design.ts +0 -151
- package/src/tools/evalcell.ts +0 -338
- package/src/tools/html-text.ts +0 -139
- package/src/tools/provider.ts +0 -149
- package/src/tools/task.ts +0 -250
- package/src/tools/todo.ts +0 -320
- package/src/tools/webfetch.ts +0 -332
- package/src/tools/websearch.ts +0 -359
- package/src/tui/agents-cmd.ts +0 -41
- package/src/tui/app.ts +0 -749
- package/src/tui/attach.ts +0 -127
- package/src/tui/boot-notes.ts +0 -41
- package/src/tui/builtin-prompts.ts +0 -59
- package/src/tui/checkpoints-cmd.ts +0 -70
- package/src/tui/clipboard-image.ts +0 -81
- package/src/tui/clipboard.ts +0 -78
- package/src/tui/commands.ts +0 -283
- package/src/tui/config-view.ts +0 -53
- package/src/tui/context-cmds.ts +0 -282
- package/src/tui/cost.ts +0 -108
- package/src/tui/crash-guard.ts +0 -173
- package/src/tui/focus-terminal.ts +0 -34
- package/src/tui/git-cmds.ts +0 -273
- package/src/tui/git-plain.ts +0 -58
- package/src/tui/info-cmd.ts +0 -150
- package/src/tui/input-plain.ts +0 -76
- package/src/tui/mcp-cmd.ts +0 -128
- package/src/tui/memory-note.ts +0 -77
- package/src/tui/modes-cmd.ts +0 -45
- package/src/tui/notify-seq.ts +0 -100
- package/src/tui/notify.ts +0 -318
- package/src/tui/overlays.ts +0 -97
- package/src/tui/pi-renderer.ts +0 -428
- package/src/tui/providers-cmd.ts +0 -377
- package/src/tui/reasoning-view.ts +0 -56
- package/src/tui/renderer.ts +0 -128
- package/src/tui/replay-marker.ts +0 -29
- package/src/tui/session-cmd.ts +0 -148
- package/src/tui/session-manage.ts +0 -95
- package/src/tui/sextant-attach.ts +0 -102
- package/src/tui/sextant-io.ts +0 -202
- package/src/tui/sextant-smoke.ts +0 -110
- package/src/tui/shell-cmd.ts +0 -158
- package/src/tui/smoke.ts +0 -72
- package/src/tui/staged-terminal.ts +0 -50
- package/src/tui/startup.ts +0 -12
- package/src/tui/theme.ts +0 -59
- package/src/tui/todo-label.ts +0 -7
- package/src/tui/trust-card.ts +0 -107
- package/src/tui/tui-commands.ts +0 -87
- package/tsconfig.json +0 -30
- package/vendor/pi-tui/LICENSE +0 -21
- package/vendor/pi-tui/PATCHES.md +0 -12
- package/vendor/pi-tui/PROVENANCE.md +0 -12
- package/vendor/pi-tui/README.upstream.md +0 -854
- 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 +0 -158
- package/vendor/pi-tui/src/autocomplete.ts +0 -827
- package/vendor/pi-tui/src/components/alt-screen-flash.ts +0 -52
- package/vendor/pi-tui/src/components/box.ts +0 -138
- package/vendor/pi-tui/src/components/cancellable-loader.ts +0 -41
- package/vendor/pi-tui/src/components/editor.ts +0 -2364
- package/vendor/pi-tui/src/components/h-stack.ts +0 -45
- package/vendor/pi-tui/src/components/image.ts +0 -128
- package/vendor/pi-tui/src/components/input.ts +0 -448
- package/vendor/pi-tui/src/components/loader.ts +0 -93
- package/vendor/pi-tui/src/components/markdown.ts +0 -1016
- package/vendor/pi-tui/src/components/scroll-view.ts +0 -217
- package/vendor/pi-tui/src/components/select-list.ts +0 -230
- package/vendor/pi-tui/src/components/settings-list.ts +0 -277
- package/vendor/pi-tui/src/components/spacer.ts +0 -29
- package/vendor/pi-tui/src/components/stack.ts +0 -155
- package/vendor/pi-tui/src/components/text.ts +0 -108
- package/vendor/pi-tui/src/components/truncated-text.ts +0 -66
- package/vendor/pi-tui/src/components/v-stack.ts +0 -34
- package/vendor/pi-tui/src/editor-component.ts +0 -75
- package/vendor/pi-tui/src/fuzzy.ts +0 -138
- package/vendor/pi-tui/src/index.ts +0 -149
- package/vendor/pi-tui/src/keybindings.ts +0 -321
- package/vendor/pi-tui/src/keys.ts +0 -1402
- package/vendor/pi-tui/src/kill-ring.ts +0 -47
- package/vendor/pi-tui/src/latex.ts +0 -1381
- package/vendor/pi-tui/src/layout-node.ts +0 -52
- package/vendor/pi-tui/src/layout.ts +0 -411
- package/vendor/pi-tui/src/native-modifiers.ts +0 -60
- package/vendor/pi-tui/src/native-module-path.ts +0 -32
- package/vendor/pi-tui/src/stdin-buffer.ts +0 -445
- package/vendor/pi-tui/src/terminal-colors.ts +0 -74
- package/vendor/pi-tui/src/terminal-image.ts +0 -701
- package/vendor/pi-tui/src/terminal.ts +0 -554
- package/vendor/pi-tui/src/tui-alt-screen.ts +0 -1379
- package/vendor/pi-tui/src/tui-main-screen.ts +0 -655
- package/vendor/pi-tui/src/tui.ts +0 -1264
- package/vendor/pi-tui/src/undo-stack.ts +0 -29
- package/vendor/pi-tui/src/utils.ts +0 -1327
- package/vendor/pi-tui/src/word-navigation.ts +0 -118
- package/vendor/pi-tui/test/test-themes.ts +0 -39
- package/vendor/pi-tui/test/virtual-terminal.ts +0 -219
- /package/{src/providers → dist/cli}/models-index.json +0 -0
package/src/tui/cost.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/** /cost note body (ports #5+#6), extracted from app.ts for the ADR-002 line cap.
|
|
2
|
-
* Usage is priced PER MESSAGE at the model recorded in Message.origin —
|
|
3
|
-
* a session that switched models mid-way is not silently re-priced at the current model.
|
|
4
|
-
* Messages without an origin fall back to the current model WITH an explicit caveat; messages
|
|
5
|
-
* whose model has no catalog pricing are excluded and flagged (cost becomes a lower bound).
|
|
6
|
-
* Context health counts ALL parts (partsTokenText): tool calls/results dominate agentic
|
|
7
|
-
* sessions, and a text-only count reads ~0% forever.
|
|
8
|
-
* Port #44: the same math feeds the sextant usage panel through sessionUsage(). */
|
|
9
|
-
|
|
10
|
-
import { partsTokenText } from "../core/loop.ts";
|
|
11
|
-
import { ModelCatalog, ratesFor } from "../providers/catalog.ts";
|
|
12
|
-
import { contextHealth, costUsdTiered, countTokens, countTokensIfLoaded } from "../core/usage.ts";
|
|
13
|
-
import { estimateTokens } from "../core/context.ts";
|
|
14
|
-
import { tokenScaleFor } from "../core/token-scale.ts";
|
|
15
|
-
import type { Message } from "../core/types.ts";
|
|
16
|
-
|
|
17
|
-
export interface UsageSummary {
|
|
18
|
-
inTok: number; outTok: number; cacheRead: number; cacheWrite: number;
|
|
19
|
-
/** USD over the priced messages; `priced`/`unpriced`/`noOrigin` say how much of the transcript it covers */
|
|
20
|
-
cost: number; priced: number; unpriced: number; noOrigin: number;
|
|
21
|
-
/** estimated prompt tokens, corrected towards the current model's own tokenizer, and the catalog's
|
|
22
|
-
* window when it knows one. `estRaw` is the uncorrected count the correction was applied to, and
|
|
23
|
-
* `counter` says which estimator produced it — both are estimates, with different measured errors
|
|
24
|
-
* (core/token-scale.ts): "o200k" is countTokens (exact for OpenAI models), "chars" is estimateTokens
|
|
25
|
-
* (chars/4, what compaction budgets with). The scale applied is the one measured for that counter. */
|
|
26
|
-
est: number; estRaw: number; counter: "o200k" | "chars"; scale: { scale: number; measured: boolean; note: string }; window?: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface SummarizeOptions {
|
|
30
|
-
/** "exact": count with o200k, loading its table if it is not resident (385–520 ms and ~136 MB the
|
|
31
|
-
* first time — fine for /cost, which a person asked for). "cheap": o200k only if the table is already
|
|
32
|
-
* resident, else chars/4 — for the sextant usage panel, which is computed at boot and after every
|
|
33
|
-
* turn and must never be the reason the table loads. Default "exact". */
|
|
34
|
-
counter?: "exact" | "cheap";
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function summarizeUsage(messages: Message[], catalog: ModelCatalog, current: { provider: string; model: string }, opts: SummarizeOptions = {}): UsageSummary {
|
|
38
|
-
const u: UsageSummary = { inTok: 0, outTok: 0, cacheRead: 0, cacheWrite: 0, cost: 0, priced: 0, unpriced: 0, noOrigin: 0, est: 0, estRaw: 0, counter: "o200k", scale: { scale: 1, measured: false, note: "" } };
|
|
39
|
-
for (const m of messages) {
|
|
40
|
-
const usage = m.usage;
|
|
41
|
-
if (!usage) continue;
|
|
42
|
-
u.inTok += usage.input; u.outTok += usage.output;
|
|
43
|
-
u.cacheRead += usage.cacheRead ?? 0; u.cacheWrite += usage.cacheWrite ?? 0;
|
|
44
|
-
if (usage.input === 0 && usage.output === 0 && !usage.cacheRead && !usage.cacheWrite) continue; // nothing to price
|
|
45
|
-
if (!m.origin) u.noOrigin += 1;
|
|
46
|
-
const origin = m.origin ?? current;
|
|
47
|
-
const info = catalog.lookup(origin.provider, origin.model);
|
|
48
|
-
const n = { input: usage.input, output: usage.output, cacheRead: usage.cacheRead ?? 0, cacheWrite: usage.cacheWrite ?? 0 };
|
|
49
|
-
// the prompt this turn actually carried decides the rate on a tiered model: xAI and Google bill a
|
|
50
|
-
// prompt over 200k at the upper rate — xAI for the whole request. A flat model's breakdown is trivial.
|
|
51
|
-
const c = info?.pricing
|
|
52
|
-
? costUsdTiered(n, ratesFor(info, n.input + n.cacheRead + n.cacheWrite))
|
|
53
|
-
: undefined;
|
|
54
|
-
if (c === undefined) u.unpriced += 1;
|
|
55
|
-
else { u.cost += c; u.priced += 1; }
|
|
56
|
-
}
|
|
57
|
-
const window = catalog.lookup(current.provider, current.model)?.contextWindow;
|
|
58
|
-
if (window) u.window = window;
|
|
59
|
-
const text = messages.map((m) => partsTokenText(m.parts)).join("\n");
|
|
60
|
-
const exact = opts.counter === "cheap" ? countTokensIfLoaded(text) : countTokens(text);
|
|
61
|
-
// Neither counter is this model's tokenizer (o200k is OpenAI's; chars/4 is nobody's). `rovecode context`
|
|
62
|
-
// has corrected for that since the factors were measured; this panel had not, so the live meter a user
|
|
63
|
-
// actually watches read up to 1.8x low on Claude 5 — the worst place for it, because this is the number
|
|
64
|
-
// you look at to decide whether there is room for another turn. Each counter gets ITS OWN measured
|
|
65
|
-
// factor: the chars/4 error is not the o200k error (token-scale.ts charScale).
|
|
66
|
-
const ts = tokenScaleFor(current);
|
|
67
|
-
if (exact !== null) {
|
|
68
|
-
u.counter = "o200k"; u.estRaw = exact;
|
|
69
|
-
u.scale = { scale: ts.scale, measured: ts.measured, note: ts.note };
|
|
70
|
-
} else {
|
|
71
|
-
u.counter = "chars"; u.estRaw = estimateTokens(text);
|
|
72
|
-
u.scale = { scale: ts.charScale, measured: ts.measured, note: ts.note };
|
|
73
|
-
}
|
|
74
|
-
u.est = Math.ceil(u.estRaw * u.scale.scale);
|
|
75
|
-
return u;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** the sextant usage panel's numbers: cost = the priced lower bound (null until something is priced), context = the
|
|
79
|
-
* estimate. Runs at boot and after every turn, so it is the "cheap" counter: the panel's "estimated context fill"
|
|
80
|
-
* is chars/4 × the model's measured char factor until someone loads the o200k table (/cost, /context), and the
|
|
81
|
-
* o200k figure from then on. Both are estimates; neither is ever presented as the provider's count — that is
|
|
82
|
-
* drift() in core/context-report.ts, which counts exactly and compares against what the provider reported. */
|
|
83
|
-
export function sessionUsage(messages: Message[], catalog: ModelCatalog, current: { provider: string; model: string }): { costUsd: number | null; contextTokens: number; counter: "o200k" | "chars" } {
|
|
84
|
-
const u = summarizeUsage(messages, catalog, current, { counter: "cheap" });
|
|
85
|
-
return { costUsd: u.priced > 0 ? u.cost : null, contextTokens: u.est, counter: u.counter };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export function buildCostNote(messages: Message[], catalog: ModelCatalog, current: { provider: string; model: string }): string {
|
|
89
|
-
const u = summarizeUsage(messages, catalog, current);
|
|
90
|
-
const health = u.window ? contextHealth(u.est, u.window) : undefined;
|
|
91
|
-
let costLine: string;
|
|
92
|
-
if (u.priced === 0 && u.unpriced > 0) {
|
|
93
|
-
costLine = `pricing unknown for ${current.provider}/${current.model}`;
|
|
94
|
-
} else {
|
|
95
|
-
costLine = `estimated cost: $${u.cost.toFixed(4)}`;
|
|
96
|
-
if (u.unpriced > 0) costLine += ` — ${u.unpriced} message${u.unpriced > 1 ? "s" : ""} unpriced (lower bound)`;
|
|
97
|
-
if (u.noOrigin > 0) costLine += ` — ${u.noOrigin} without origin priced at the current model`;
|
|
98
|
-
}
|
|
99
|
-
return [
|
|
100
|
-
`tokens: ${u.inTok} in / ${u.outTok} out · cache: ${u.cacheRead} read / ${u.cacheWrite} written`,
|
|
101
|
-
health
|
|
102
|
-
? `context: ~${u.est} of ${u.window} (${Math.round(health.fraction * 100)}%${health.nearLimit ? " — near limit" : ""})`
|
|
103
|
-
: `context: ~${u.est} tokens (window unknown)`,
|
|
104
|
-
// a corrected number that does not say it was corrected is indistinguishable from a wrong one
|
|
105
|
-
...(u.scale.scale !== 1 ? [` ${u.counter === "o200k" ? "o200k counted" : "chars/4 estimated"} ${u.estRaw}, scaled ${u.scale.scale}× for ${current.model}`] : []),
|
|
106
|
-
costLine,
|
|
107
|
-
].join("\n");
|
|
108
|
-
}
|
package/src/tui/crash-guard.ts
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
/** What happens to your terminal when rovecode dies badly, and what you get to read afterwards.
|
|
2
|
-
*
|
|
3
|
-
* THE BUG THIS EXISTS FOR (reported 2026-09-07, with a screenshot). The TUI puts the terminal into a
|
|
4
|
-
* mode the shell cannot live in: alt screen (`?1049h`), raw stdin, mouse reporting (`?1000/1002/1006h`)
|
|
5
|
-
* and focus reporting (`?1004h`). Quitting cleanly undoes every one of those. Dying does not. The quit
|
|
6
|
-
* path was careful and the crash path did not exist, so an uncaught exception left the terminal in that
|
|
7
|
-
* mode with nobody to speak for it: the shell prompt came back INSIDE the alt screen, still covered by
|
|
8
|
-
* the last painted frame, and every mouse movement afterwards typed escape sequences at the prompt —
|
|
9
|
-
* `[<222;39;51M[<222;38;51M…` walking down the screen as the pointer moved. To the person at the
|
|
10
|
-
* keyboard that is not "rovecode crashed", it is "my terminal is broken", and the actual error had
|
|
11
|
-
* already scrolled away underneath a frame that was never erased.
|
|
12
|
-
*
|
|
13
|
-
* So this does two things, in this order, and the order is the point:
|
|
14
|
-
* 1. RESTORE THE TERMINAL, before anything that could itself throw. Whatever else goes wrong, the
|
|
15
|
-
* person gets their shell back.
|
|
16
|
-
* 2. WRITE THE REASON DOWN, to a file, because the one place a crash message cannot survive is the
|
|
17
|
-
* screen it was printed on — the alt screen is discarded on restore, taking the stack with it.
|
|
18
|
-
* Then one line on stderr saying where that file is.
|
|
19
|
-
*
|
|
20
|
-
* WHY A FILE AND NOT JUST STDERR. Restoring the alt screen erases everything printed while the TUI was
|
|
21
|
-
* up. A stack trace written a microsecond before `?1049l` is gone by the time the shell redraws. The
|
|
22
|
-
* log is the only copy that survives the fix.
|
|
23
|
-
*
|
|
24
|
-
* Handlers, and why exactly these. `uncaughtException` and `unhandledRejection` are the two ways a
|
|
25
|
-
* surface actually dies — the second is the more common one here, because almost everything in the boot
|
|
26
|
-
* path is async. `exit` is the belt-and-braces restore for any path that reaches `process.exit()`
|
|
27
|
-
* without going through the quit code, and it logs nothing, because a normal quit is not a crash.
|
|
28
|
-
* SIGINT is deliberately NOT handled: in raw mode the terminal delivers Ctrl+C as a keystroke to the
|
|
29
|
-
* app rather than as a signal, so a handler here would be dead code on the path it looks like it covers
|
|
30
|
-
* and would silently change Ctrl+C's meaning if raw mode ever failed to engage. */
|
|
31
|
-
|
|
32
|
-
import { appendFileSync, mkdirSync } from "node:fs";
|
|
33
|
-
import { join } from "node:path";
|
|
34
|
-
|
|
35
|
-
/** the process surface this guard binds to — an object in tests, `process` in production */
|
|
36
|
-
export interface CrashProcess {
|
|
37
|
-
on(event: string, listener: (...args: unknown[]) => void): unknown;
|
|
38
|
-
off(event: string, listener: (...args: unknown[]) => void): unknown;
|
|
39
|
-
exit(code: number): void;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface CrashGuardDeps {
|
|
43
|
-
/** put the terminal back the way it was found. MUST be idempotent and MUST NOT throw — it runs first,
|
|
44
|
-
* on every path, including inside the `exit` handler where nothing async can be awaited. */
|
|
45
|
-
restore: () => void;
|
|
46
|
-
/** where crash logs go; `<rovecodeHome>/logs` in production */
|
|
47
|
-
logDir: string;
|
|
48
|
-
/** version stamped into the log, so a report names the build it came from */
|
|
49
|
-
version?: string;
|
|
50
|
-
process?: CrashProcess;
|
|
51
|
-
/** seams (tests) */
|
|
52
|
-
now?: () => Date;
|
|
53
|
-
writeFile?: (path: string, text: string) => void;
|
|
54
|
-
stderr?: (line: string) => void;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** what a crash log is called: one file per crash, sortable, no collisions within a second */
|
|
58
|
-
export function crashLogName(at: Date, seq = 0): string {
|
|
59
|
-
const p = (n: number, w = 2): string => String(n).padStart(w, "0");
|
|
60
|
-
const stamp = `${at.getFullYear()}${p(at.getMonth() + 1)}${p(at.getDate())}-${p(at.getHours())}${p(at.getMinutes())}${p(at.getSeconds())}`;
|
|
61
|
-
return seq === 0 ? `crash-${stamp}.log` : `crash-${stamp}-${seq}.log`;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** An error rendered for someone reading the file a day later: name, message, stack, and the plain
|
|
65
|
-
* value when what was thrown is not an Error at all (a rejected string, a thrown object — common
|
|
66
|
-
* enough in async code that omitting it would blank out the interesting cases). */
|
|
67
|
-
export function describeError(err: unknown): string {
|
|
68
|
-
if (err instanceof Error) {
|
|
69
|
-
const cause = err.cause !== undefined ? `\ncaused by: ${describeError(err.cause)}` : "";
|
|
70
|
-
return `${err.name}: ${err.message}\n${err.stack ?? "(no stack)"}${cause}`;
|
|
71
|
-
}
|
|
72
|
-
if (typeof err === "object" && err !== null) {
|
|
73
|
-
try { return `non-Error value thrown: ${JSON.stringify(err)}`; } catch { return `non-Error value thrown: ${String(err)}`; }
|
|
74
|
-
}
|
|
75
|
-
return `non-Error value thrown: ${String(err)}`;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Is this error a TRANSIENT NETWORK failure that cannot be fixed from inside the process — Bun's
|
|
79
|
-
* fetch emits DNS/resolve failures BOTH as the promise rejection every caller catches AND as a
|
|
80
|
-
* stream `error` event on an internal socket (node:streams/destroy → emitErrorNT), and when the
|
|
81
|
-
* network died mid-boot (adapter switch, VPN drop, DNS unresponsive) that second emission reaches
|
|
82
|
-
* `uncaughtException` with an empty ResolveMessage. Killing the TUI for it loses the session and
|
|
83
|
-
* the terminal for something that heals on its own; the honest move is to log it and keep going.
|
|
84
|
-
* Deliberately NARROW: only the resolve/connect family, only as an uncaught stream error — a
|
|
85
|
-
* rejected promise with the same text still travels its normal path. */
|
|
86
|
-
export function recoverableNetworkError(err: unknown): boolean {
|
|
87
|
-
// "Unhandled error. (ResolveMessage {})" — the stream wrapper stringifies its payload into the message
|
|
88
|
-
const msg = err instanceof Error ? err.message : typeof err === "string" ? err : "";
|
|
89
|
-
if (/\(ResolveMessage \{\}\)|getAddrInfo|EAI_AGAIN|ENOTFOUND|ECONNRESET|ECONNREFUSED|ECONNABORTED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH/.test(msg)) return true;
|
|
90
|
-
// the payload's own facts: a `code` property rides on Errors (Node convention) and bare objects alike
|
|
91
|
-
if (typeof err === "object" && err !== null) {
|
|
92
|
-
const name = (err as { name?: unknown }).name;
|
|
93
|
-
const code = (err as { code?: unknown }).code;
|
|
94
|
-
if (String(name).includes("Resolve") || /^(EAI_AGAIN|ENOTFOUND|ECONNRESET|ECONNREFUSED|ECONNABORTED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH)$/.test(String(code))) return true;
|
|
95
|
-
}
|
|
96
|
-
return false;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** the body of a crash log. Kept plain text and self-describing: it is meant to be pasted into a
|
|
100
|
-
* message, so it must make sense with no surrounding context. */
|
|
101
|
-
export function crashReport(kind: string, err: unknown, at: Date, version?: string): string {
|
|
102
|
-
return [
|
|
103
|
-
`rovecode crash — ${kind}`,
|
|
104
|
-
`when: ${at.toISOString()}`,
|
|
105
|
-
`version: ${version ?? "unknown"}`,
|
|
106
|
-
`platform: ${process.platform} ${process.arch}`,
|
|
107
|
-
`cwd: ${process.cwd()}`,
|
|
108
|
-
"",
|
|
109
|
-
describeError(err),
|
|
110
|
-
"",
|
|
111
|
-
].join("\n");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Install the guard. Returns an uninstall function — the clean quit path calls it, so a normal exit
|
|
115
|
-
* does not run a restore twice and a test does not leave listeners on the real process. */
|
|
116
|
-
export function installCrashGuard(deps: CrashGuardDeps): () => void {
|
|
117
|
-
const proc = deps.process ?? (process as unknown as CrashProcess);
|
|
118
|
-
const now = deps.now ?? (() => new Date());
|
|
119
|
-
const stderr = deps.stderr ?? ((line: string) => { try { process.stderr.write(line + "\n"); } catch { /* the pipe may be gone too */ } });
|
|
120
|
-
const writeFile = deps.writeFile ?? ((path: string, text: string) => { appendFileSync(path, text, "utf8"); });
|
|
121
|
-
|
|
122
|
-
let restored = false;
|
|
123
|
-
const restoreOnce = (): void => {
|
|
124
|
-
if (restored) return;
|
|
125
|
-
restored = true;
|
|
126
|
-
try { deps.restore(); } catch { /* a restore that throws must not stop the log or the exit */ }
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
/** returns the path written, or null when the log could not be written — which must not be fatal:
|
|
130
|
-
* a crash we cannot record is still a crash we have to get the terminal back from */
|
|
131
|
-
const log = (kind: string, err: unknown): string | null => {
|
|
132
|
-
const at = now();
|
|
133
|
-
try {
|
|
134
|
-
mkdirSync(deps.logDir, { recursive: true });
|
|
135
|
-
const path = join(deps.logDir, crashLogName(at));
|
|
136
|
-
writeFile(path, crashReport(kind, err, at, deps.version));
|
|
137
|
-
return path;
|
|
138
|
-
} catch {
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const fatal = (kind: string) => (err: unknown): void => {
|
|
144
|
-
// a transient network failure arriving as a stream error (Bun's fetch emits DNS/resolve failures
|
|
145
|
-
// both ways) is NOT a crash: log it to the same file the crashes go to, keep the session alive.
|
|
146
|
-
// Everything else keeps the old contract — restore first, then the reason, then exit 1.
|
|
147
|
-
if (recoverableNetworkError(err)) {
|
|
148
|
-
const path = log(`transient network error (recovered, ${kind})`, err);
|
|
149
|
-
if (path !== null) stderr(`rovecode: a network call failed and was left behind (${describeError(err).split("\n")[0]}) — details: ${path}`);
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
restoreOnce(); // FIRST: the terminal is usable even if everything below fails
|
|
153
|
-
const path = log(kind, err);
|
|
154
|
-
// Both lines go to stderr because the screen they would have been printed on has just been erased.
|
|
155
|
-
stderr(`rovecode crashed (${kind}). ${path !== null ? `Details: ${path}` : "The crash log could not be written."}`);
|
|
156
|
-
stderr(describeError(err));
|
|
157
|
-
proc.exit(1);
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
const onUncaught = fatal("uncaught exception");
|
|
161
|
-
const onRejection = fatal("unhandled rejection");
|
|
162
|
-
const onExit = (): void => { restoreOnce(); }; // no log: a normal exit is not a crash
|
|
163
|
-
|
|
164
|
-
proc.on("uncaughtException", onUncaught as (...a: unknown[]) => void);
|
|
165
|
-
proc.on("unhandledRejection", onRejection as (...a: unknown[]) => void);
|
|
166
|
-
proc.on("exit", onExit);
|
|
167
|
-
|
|
168
|
-
return () => {
|
|
169
|
-
proc.off("uncaughtException", onUncaught as (...a: unknown[]) => void);
|
|
170
|
-
proc.off("unhandledRejection", onRejection as (...a: unknown[]) => void);
|
|
171
|
-
proc.off("exit", onExit);
|
|
172
|
-
};
|
|
173
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/** FocusTerminal: a pi-tui `Terminal` decorator for the classic surface. Every post-StdinBuffer input sequence goes through
|
|
2
|
-
* a FocusTracker BEFORE the TUI's input handlers — CSI I / CSI O flip the tracker and never reach the editor; a chunk
|
|
3
|
-
* emptied by the filter is not forwarded. Nothing else changes: the remaining members forward to the inner terminal
|
|
4
|
-
* (vendor/pi-tui/src/terminal.ts Terminal). The DECSET 1004 writes are NOT here: withNotifications (tui/notify.ts) writes
|
|
5
|
-
* them around the surface's own start / stop writes, on both surfaces, only when something can fire — so this decorator
|
|
6
|
-
* alone adds zero bytes to the stdout stream. Compose any further Terminal decorator OUTSIDE this one (it must stay
|
|
7
|
-
* innermost after ProcessTerminal to see complete sequences). */
|
|
8
|
-
|
|
9
|
-
import type { Terminal } from "../../vendor/pi-tui/src/index.ts";
|
|
10
|
-
import type { FocusTracker } from "./notify-seq.ts";
|
|
11
|
-
|
|
12
|
-
export class FocusTerminal implements Terminal {
|
|
13
|
-
constructor(private readonly inner: Terminal, private readonly tracker: FocusTracker) {}
|
|
14
|
-
start(onInput: (data: string) => void, onResize: () => void): void {
|
|
15
|
-
this.inner.start((data) => {
|
|
16
|
-
const rest = this.tracker.feed(data); // CSI I / CSI O consumed here; the editor never sees them
|
|
17
|
-
if (rest.length > 0) onInput(rest);
|
|
18
|
-
}, onResize);
|
|
19
|
-
}
|
|
20
|
-
stop(): void { this.inner.stop(); }
|
|
21
|
-
drainInput(maxMs?: number, idleMs?: number): Promise<void> { return this.inner.drainInput(maxMs, idleMs); }
|
|
22
|
-
write(data: string): void { this.inner.write(data); }
|
|
23
|
-
get columns(): number { return this.inner.columns; }
|
|
24
|
-
get rows(): number { return this.inner.rows; }
|
|
25
|
-
get kittyProtocolActive(): boolean { return this.inner.kittyProtocolActive; }
|
|
26
|
-
moveBy(lines: number): void { this.inner.moveBy(lines); }
|
|
27
|
-
hideCursor(): void { this.inner.hideCursor(); }
|
|
28
|
-
showCursor(): void { this.inner.showCursor(); }
|
|
29
|
-
clearLine(): void { this.inner.clearLine(); }
|
|
30
|
-
clearFromCursor(): void { this.inner.clearFromCursor(); }
|
|
31
|
-
clearScreen(): void { this.inner.clearScreen(); }
|
|
32
|
-
setTitle(title: string): void { this.inner.setTitle(title); }
|
|
33
|
-
setProgress(active: boolean): void { this.inner.setProgress(active); }
|
|
34
|
-
}
|
package/src/tui/git-cmds.ts
DELETED
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
/** `/commit` and `/undo` (port #65). Pattern sources, no code copied: aider `cmd_commit` / `cmd_undo`
|
|
2
|
-
* (Apache-2.0 — research/source_snapshots/Aider-AI-aider/aider/commands.py:337-350 dirty-check → model-drafted
|
|
3
|
-
* message unless one is given; :553-640 undo = restore the files the last change touched and say what moved) and
|
|
4
|
-
* opencode `messages_undo` / session revert (MIT — packages/opencode/src/session/revert.ts: files go back to a
|
|
5
|
-
* snapshot; deviation here: the conversation is left untouched).
|
|
6
|
-
*
|
|
7
|
-
* /commit — the staged diff (the working-tree diff of tracked files when nothing is staged, with a note; `commit -a`
|
|
8
|
-
* then) is collected, the COMMIT router role drafts a conventional message (`rt.router.resolve("commit")` —
|
|
9
|
-
* ROVECODE_MODEL_COMMIT, else the default chain; ONE stream invocation, no agent loop), the message rides in the
|
|
10
|
-
* approval card's detail, and `git commit -m <message>` runs through the ONE bash tool dispatch — ToolRegistry.dispatch
|
|
11
|
-
* under the session's permission rules and buildCfg's approval chain (lane → execpolicy → approval hook → the human
|
|
12
|
-
* card), exactly like a model-issued or `!cmd` bash call (ADR-005; shell-cmd.ts idiom). `/commit <message>` skips the
|
|
13
|
-
* draft (no model call). The argv never carries --amend or --no-verify. EVERY git invocation here — the diffs too — goes
|
|
14
|
-
* through that dispatch: this module owns no spawn path (pinned by a source grep in its test); read-only git argv is
|
|
15
|
-
* allow-listed by execpolicy, so the diffs never prompt. A denied card commits nothing and puts `/commit <draft>` in
|
|
16
|
-
* the prompt so the message can be edited and resubmitted.
|
|
17
|
-
*
|
|
18
|
-
* /undo — steps the workspace back ONE snapshot through the existing Checkpoints.restore(hash, "files") after a
|
|
19
|
-
* confirmation card listing what will change; the transcript is untouched (a conversation rewind stays /restore's job).
|
|
20
|
-
* Snapshots land AFTER each mutating tool call, so the last one IS the agent's latest change: when the workspace still
|
|
21
|
-
* matches a snapshot (Checkpoints.position) the target is that snapshot's nearest differing ancestor — the pre-edit
|
|
22
|
-
* state — and a second /undo walks one more back (or notes that nothing older differs); a workspace that drifted from
|
|
23
|
-
* the last snapshot (your own edits, an interrupted run's partial writes) goes back to the last snapshot first,
|
|
24
|
-
* discarding the drift. No checkpoint → a note. */
|
|
25
|
-
|
|
26
|
-
import { randomUUID } from "node:crypto";
|
|
27
|
-
import type { Runtime } from "../cli/runtime.ts";
|
|
28
|
-
import type { Checkpoint } from "../coding/checkpoints.ts";
|
|
29
|
-
import { OUTPUT_CAP_CHARS } from "../core/executor.ts"; // the bash tool's output cap — a constant, not a spawn path
|
|
30
|
-
import { partsText } from "../core/loop.ts";
|
|
31
|
-
import type { SessionStore } from "../core/session.ts";
|
|
32
|
-
import type { AssistantTurn, Message, ModelRef, RunEvent, ToolCallPart, ToolContext } from "../core/types.ts";
|
|
33
|
-
import type { Renderer } from "./renderer.ts";
|
|
34
|
-
import type { ApprovalFn } from "../core/types.ts";
|
|
35
|
-
import { REFUSED_DETAIL } from "./shell-cmd.ts";
|
|
36
|
-
|
|
37
|
-
export interface GitCmdCtx {
|
|
38
|
-
renderer: Renderer;
|
|
39
|
-
rt: Pick<Runtime, "registry" | "hooks" | "buildCfg" | "cwd" | "stream" | "router" | "checkpointsFor">;
|
|
40
|
-
/** the surface's own human approver — undefined under `auto`/yolo, where nothing is asked. /commit's
|
|
41
|
-
* card, like a model-issued bash call's, only exists when the session has an approver at all. */
|
|
42
|
-
approve(): ApprovalFn | undefined;
|
|
43
|
-
/** the ACTIVE store, read live */
|
|
44
|
-
store(): SessionStore;
|
|
45
|
-
yolo(): boolean;
|
|
46
|
-
/** the app's busy flag (a live run, a `!cmd`, or another git command) */
|
|
47
|
-
busy(): boolean;
|
|
48
|
-
setBusy(b: boolean): void;
|
|
49
|
-
/** the app's interrupt target — Esc / ⌃c abort the command; bound for its lifetime, then cleared */
|
|
50
|
-
bindAbort(ac: AbortController | null): void;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const BUSY_NOTE = "finish or interrupt the run first (Esc) — /commit and /undo run only while the agent is idle";
|
|
54
|
-
/** the bash tool caps its output (OUTPUT_CAP_CHARS): a capped diff is flagged to the model and on the card */
|
|
55
|
-
const CAP_NOTE = "(diff truncated at the 10k output cap for the model)";
|
|
56
|
-
/** a dash-led message word would reach git as a flag (the quoting keeps bare dash-led words) — refused before any git call */
|
|
57
|
-
const DASH_NOTE = "a commit message cannot start with -; quote it or begin with a word — git would read a dash-led message as a flag, so nothing was committed";
|
|
58
|
-
|
|
59
|
-
export const COMMIT_SYSTEM =
|
|
60
|
-
"You write git commit messages. Reply with the commit message only: a first line `type(scope): summary` in the imperative mood, " +
|
|
61
|
-
"at most 72 characters, type one of feat, fix, refactor, docs, test, chore, perf, build, ci, style; then, only when the change needs it, " +
|
|
62
|
-
"one blank line and a short body. No quotes, no code fences, no commentary.";
|
|
63
|
-
|
|
64
|
-
// ------------------------------------------------------------------ shell quoting + git through the seam
|
|
65
|
-
|
|
66
|
-
/** POSIX single-quoting for one argv word (the executor runs the line through bash); safe words pass bare */
|
|
67
|
-
export function shellQuote(word: string): string {
|
|
68
|
-
return /^[A-Za-z0-9_./:=@%+,-]+$/.test(word) ? word : `'${word.replace(/'/g, "'\\''")}'`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** the exact bash line a git argv becomes — exported so tests can pin the emitted command */
|
|
72
|
-
export function gitCommand(args: readonly string[]): string {
|
|
73
|
-
return ["git", ...args.map(shellQuote)].join(" ");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export interface GitResult {
|
|
77
|
-
/** the bash tool ran (a refusal — policy, hook, a denied card — leaves it false) */
|
|
78
|
-
executed: boolean;
|
|
79
|
-
/** the human saw the approval card */
|
|
80
|
-
asked: boolean;
|
|
81
|
-
/** the tool's `exit=N` header; -1 when absent */
|
|
82
|
-
exit: number;
|
|
83
|
-
/** the output minus that header */
|
|
84
|
-
body: string;
|
|
85
|
-
/** the body filled the bash tool's output cap — a diff this long is a prefix, not the whole change */
|
|
86
|
-
capped: boolean;
|
|
87
|
-
command: string;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** One git invocation through the ONE bash tool dispatch — the session's rules + buildCfg's approval chain; `detail`
|
|
91
|
-
* is what the approval card shows above the verdicts (the drafted commit message). Never spawns anything itself. */
|
|
92
|
-
export async function runGit(ctx: GitCmdCtx, args: readonly string[], signal: AbortSignal, detail?: string): Promise<GitResult> {
|
|
93
|
-
const command = gitCommand(args);
|
|
94
|
-
const call: ToolCallPart = { kind: "tool_call", id: `git-${randomUUID().slice(0, 8)}`, tool: "bash", args: { command } };
|
|
95
|
-
let asked = false, executed = false;
|
|
96
|
-
// the SAME chain a model-issued bash call gets: buildCfg wraps the surface's approver in the rules,
|
|
97
|
-
// execpolicy and the hooks. The wrapper IS the card: /commit's detail (the drafted message) is what
|
|
98
|
-
// the card shows above the verdicts, and `asked` records that the human was reached so a policy
|
|
99
|
-
// refusal is never misreported as their denial.
|
|
100
|
-
const human = ctx.approve();
|
|
101
|
-
const cfg = ctx.rt.buildCfg(ctx.yolo(), human === undefined ? undefined : async (req) => {
|
|
102
|
-
asked = true;
|
|
103
|
-
return (await ctx.renderer.askApproval(req.tool, JSON.stringify(req.revisedArgs).slice(0, 140), detail)) === "deny" ? "deny" : "once";
|
|
104
|
-
});
|
|
105
|
-
const toolCtx: ToolContext = { sessionId: ctx.store().id, cwd: ctx.rt.cwd, signal, permissions: { effect: "allow" } };
|
|
106
|
-
const { renderer } = ctx;
|
|
107
|
-
// the app's per-event Renderer calls (shell-cmd.ts idiom): onEvent FIRST for a renderer that owns its rows (sextant);
|
|
108
|
-
// a policy refusal (no card) is corrected to the honest detail so neither surface says "user denied" for it
|
|
109
|
-
const emit = (raw: RunEvent): void => {
|
|
110
|
-
const ev = raw.type === "tool_call_failed" && !asked && raw.detail === "user denied" ? { ...raw, detail: REFUSED_DETAIL } : raw;
|
|
111
|
-
renderer.onEvent?.(ev);
|
|
112
|
-
if (ev.type === "tool_execution_start") renderer.toolStart(ev.callId, ev.tool, JSON.stringify(ev.args).slice(0, 120));
|
|
113
|
-
else if (ev.type === "tool_execution_update") renderer.toolUpdate(ev.callId, ev.note);
|
|
114
|
-
else if (ev.type === "tool_execution_end") { executed = true; renderer.toolEnd(ev.callId, ev.ok, ev.output.slice(0, 160).replace(/\n/g, " ⏎ "), ev.durationMs); }
|
|
115
|
-
else if (ev.type === "tool_call_failed") renderer.toolEnd(ev.callId, false, `${ev.reason}: ${ev.detail}`.slice(0, 160), 0);
|
|
116
|
-
};
|
|
117
|
-
// no loop guard: the user's own commands are not a model loop
|
|
118
|
-
const out = await ctx.rt.registry.dispatch(call, toolCtx, ctx.rt.hooks, cfg.permissionRules, cfg.approval, emit, undefined);
|
|
119
|
-
const m = /^exit=(-?\d+)\r?\n?/.exec(out.output);
|
|
120
|
-
const raw = m ? out.output.slice(m[0].length) : out.output;
|
|
121
|
-
return { executed, asked, exit: m ? Number(m[1]) : -1, body: raw.replace(/\r\n/g, "\n"), capped: raw.length >= OUTPUT_CAP_CHARS, command };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const firstLine = (s: string): string => s.trim().split("\n")[0] ?? "";
|
|
125
|
-
const refusalNote = (r: GitResult, signal: AbortSignal): string =>
|
|
126
|
-
signal.aborted ? "was interrupted before it ran" : r.asked ? "was denied at the approval card" : "was refused before any approval prompt (exec policy, a permission rule or a hook)";
|
|
127
|
-
/** a failed git call as one honest line: the first real output line (the executor's `stderr:` label skipped); outside a
|
|
128
|
-
* repository `git diff` degrades to `--no-index` usage (exit 129) or says "not a git repository" (128) — name the cwd then */
|
|
129
|
-
export function gitFailure(r: GitResult, cwd: string): string {
|
|
130
|
-
const line = r.body.split("\n").map((l) => l.trim()).find((l) => l !== "" && l !== "stderr:") ?? "no output";
|
|
131
|
-
const notRepo = /not a git repository|--no-index/.test(r.body);
|
|
132
|
-
return `${r.command.split(" ").slice(0, 2).join(" ")} failed (exit ${r.exit}): ${line}${notRepo ? ` — ${cwd} is not inside a git repository` : ""}`;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ------------------------------------------------------------------ the commit message
|
|
136
|
-
|
|
137
|
-
/** strip the wrappers models add around a message: code fences, surrounding quotes, a `commit message:` label */
|
|
138
|
-
export function cleanMessage(raw: string): string {
|
|
139
|
-
let t = raw.replace(/\r\n/g, "\n").trim();
|
|
140
|
-
const fence = /^```[^\n]*\n([\s\S]*?)\n?```$/.exec(t);
|
|
141
|
-
if (fence) t = fence[1]!.trim();
|
|
142
|
-
t = t.replace(/^commit message:\s*/i, "");
|
|
143
|
-
if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) t = t.slice(1, -1).trim();
|
|
144
|
-
return t.split("\n").map((l) => l.trimEnd()).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/** ONE stream invocation on the COMMIT role's model (router: ROVECODE_MODEL_COMMIT, else the default chain) — a system
|
|
148
|
-
* message + the diff; the terminal turn's text is the draft. Errors come back structured, never thrown. */
|
|
149
|
-
export async function draftMessage(ctx: GitCmdCtx, diff: string, model: ModelRef, signal: AbortSignal): Promise<{ text: string } | { error: string }> {
|
|
150
|
-
const stream = ctx.rt.stream;
|
|
151
|
-
if (!stream) return { error: "no provider configured — nothing can draft a message" };
|
|
152
|
-
const now = Date.now();
|
|
153
|
-
const sys: Message = { id: randomUUID(), role: "system", parts: [{ kind: "text", text: COMMIT_SYSTEM }], parentId: null, createdAt: now };
|
|
154
|
-
const user: Message = { id: randomUUID(), role: "user", parts: [{ kind: "text", text: `Write the commit message for this diff:\n\n${diff}` }], parentId: sys.id, createdAt: now };
|
|
155
|
-
let turn: AssistantTurn | null = null;
|
|
156
|
-
try { for await (const ev of stream(model, [sys, user], { signal })) if (ev.type === "turn") turn = ev.turn; }
|
|
157
|
-
catch (e) { return { error: `commit model failed: ${e instanceof Error ? e.message : String(e)}` }; }
|
|
158
|
-
if (signal.aborted) return { error: "interrupted while drafting the message" };
|
|
159
|
-
if (!turn) return { error: "commit model returned no turn" };
|
|
160
|
-
if (turn.stopReason === "error") return { error: `commit model failed: ${turn.error ?? "error"}` };
|
|
161
|
-
const text = cleanMessage(partsText(turn.parts));
|
|
162
|
-
return text ? { text } : { error: "the commit model returned an empty message" };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** file paths named by a unified diff's `diff --git a/x b/x` headers — the card's file list without a second git call */
|
|
166
|
-
export function diffFiles(diff: string): string[] {
|
|
167
|
-
const out: string[] = [];
|
|
168
|
-
for (const m of diff.matchAll(/^diff --git a\/(.+?) b\/(.+)$/gm)) out.push(m[2]!);
|
|
169
|
-
return out;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// ------------------------------------------------------------------ /commit
|
|
173
|
-
|
|
174
|
-
/** /commit [message] — see the header. `rawArg` is the raw remainder of the typed line (newlines intact: a body). */
|
|
175
|
-
export async function cmdCommit(ctx: GitCmdCtx, rawArg = ""): Promise<void> {
|
|
176
|
-
const { renderer } = ctx;
|
|
177
|
-
if (ctx.busy()) { renderer.addSystemNote(BUSY_NOTE, "warn"); return; }
|
|
178
|
-
const given = rawArg.replace(/\r\n/g, "\n").trim();
|
|
179
|
-
if (given.startsWith("-")) { renderer.addSystemNote(DASH_NOTE, "warn"); return; }
|
|
180
|
-
const ac = new AbortController();
|
|
181
|
-
ctx.setBusy(true);
|
|
182
|
-
ctx.bindAbort(ac);
|
|
183
|
-
renderer.setBusy(true, "collecting the diff…");
|
|
184
|
-
let outcome: "done" | "error" = "error";
|
|
185
|
-
try {
|
|
186
|
-
// 1. the staged diff (allow-listed argv → no card under gated rules); nothing staged → the working-tree diff of tracked files
|
|
187
|
-
const staged = await runGit(ctx, ["diff", "--cached"], ac.signal);
|
|
188
|
-
if (!staged.executed) { renderer.addSystemNote(`\`${staged.command}\` ${refusalNote(staged, ac.signal)} — nothing committed`, "warn"); return; }
|
|
189
|
-
if (staged.exit !== 0) { renderer.addSystemNote(gitFailure(staged, ctx.rt.cwd), "error"); return; }
|
|
190
|
-
let scope: "staged" | "working" = "staged", diff = staged.body, capped = staged.capped;
|
|
191
|
-
if (!diff.trim()) {
|
|
192
|
-
const work = await runGit(ctx, ["diff"], ac.signal);
|
|
193
|
-
if (!work.executed) { renderer.addSystemNote(`\`${work.command}\` ${refusalNote(work, ac.signal)} — nothing committed`, "warn"); return; }
|
|
194
|
-
if (work.exit !== 0) { renderer.addSystemNote(gitFailure(work, ctx.rt.cwd), "error"); return; }
|
|
195
|
-
if (!work.body.trim()) { renderer.addSystemNote("nothing to commit — the index and the tracked working tree are clean (untracked files need `git add` first)"); outcome = "done"; return; }
|
|
196
|
-
scope = "working"; diff = work.body; capped = work.capped;
|
|
197
|
-
renderer.addSystemNote("nothing staged — committing the working-tree changes to tracked files instead (`git commit -a`)");
|
|
198
|
-
}
|
|
199
|
-
// a complete diff names every file in its headers; a capped one is a prefix, so the card's list comes from git itself
|
|
200
|
-
// (`--name-only`, the same allow-listed reader through the seam) and both the model and the human are told it was cut
|
|
201
|
-
let files = diffFiles(diff);
|
|
202
|
-
if (capped) {
|
|
203
|
-
const named = await runGit(ctx, scope === "working" ? ["diff", "--name-only"] : ["diff", "--cached", "--name-only"], ac.signal);
|
|
204
|
-
if (named.executed && named.exit === 0) files = named.body.split("\n").map((l) => l.trim()).filter((l) => l !== "");
|
|
205
|
-
}
|
|
206
|
-
// 2. the message: given → no model call; else ONE stream invocation on the COMMIT role's model
|
|
207
|
-
let message = given, drafted = "";
|
|
208
|
-
if (!message) {
|
|
209
|
-
const model = ctx.rt.router.resolve("commit");
|
|
210
|
-
renderer.setBusy(true, `drafting the commit message (${model.model})…`);
|
|
211
|
-
const d = await draftMessage(ctx, capped ? `${diff}\n… ${CAP_NOTE}` : diff, model, ac.signal);
|
|
212
|
-
if ("error" in d) { renderer.addSystemNote(`${d.error} — pass the message yourself: /commit <message>`, "error"); return; }
|
|
213
|
-
message = d.text; drafted = `${model.provider}/${model.model}`;
|
|
214
|
-
if (message.startsWith("-")) { renderer.addSystemNote(`the drafted message starts with - (${firstLine(message)}); ${DASH_NOTE}`, "error"); return; }
|
|
215
|
-
}
|
|
216
|
-
// 3. the commit through the seam — the card shows the message; never --amend, never --no-verify
|
|
217
|
-
renderer.setBusy(true, "committing…");
|
|
218
|
-
const detail = [
|
|
219
|
-
`commit message${drafted ? ` (drafted by ${drafted}${given ? "" : " — deny to edit it in the prompt"})` : ""}:`,
|
|
220
|
-
...message.split("\n").map((l) => ` ${l}`),
|
|
221
|
-
"",
|
|
222
|
-
scope === "working" ? "scope: working-tree changes to tracked files (git commit -a)" : "scope: the staged changes",
|
|
223
|
-
`files (${files.length}): ${files.slice(0, 12).join(", ")}${files.length > 12 ? ", …" : ""}`,
|
|
224
|
-
...(capped ? [CAP_NOTE] : []),
|
|
225
|
-
].join("\n");
|
|
226
|
-
const r = await runGit(ctx, scope === "working" ? ["commit", "-a", "-m", message] : ["commit", "-m", message], ac.signal, detail);
|
|
227
|
-
if (!r.executed) {
|
|
228
|
-
renderer.addSystemNote(`commit ${refusalNote(r, ac.signal)} — nothing committed`, "warn");
|
|
229
|
-
if (r.asked && !ac.signal.aborted) { renderer.prefillEditor(`/commit ${message}`); renderer.addSystemNote("the message is in the prompt — edit it and press Enter to commit"); }
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
if (r.exit !== 0) { renderer.addSystemNote(`${gitFailure(r, ctx.rt.cwd)}\n${r.body.trim().split("\n").slice(-8).join("\n")}`, "error"); return; }
|
|
233
|
-
outcome = "done";
|
|
234
|
-
renderer.addSystemNote(`committed ${firstLine(r.body) || firstLine(message)}${drafted ? ` · message by ${drafted}` : ""}`);
|
|
235
|
-
} finally {
|
|
236
|
-
ctx.bindAbort(null);
|
|
237
|
-
renderer.setBusy(false, outcome);
|
|
238
|
-
ctx.setBusy(false);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// ------------------------------------------------------------------ /undo
|
|
243
|
-
|
|
244
|
-
const when = (c: Checkpoint): string => new Date(c.createdAt).toLocaleTimeString();
|
|
245
|
-
const ident = (c: Checkpoint): string => `${c.hash.slice(0, 8)} (${c.label}, ${when(c)})`;
|
|
246
|
-
const STATUS_WORD: Record<string, string> = { M: "rewritten", D: "recreated", A: "removed (added since that checkpoint)", "?": "removed (created after the checkpoint)" };
|
|
247
|
-
|
|
248
|
-
/** /undo — step the workspace back one snapshot (see the header): confirmation card first, conversation untouched. */
|
|
249
|
-
export async function cmdUndo(ctx: GitCmdCtx): Promise<void> {
|
|
250
|
-
const { renderer } = ctx;
|
|
251
|
-
if (ctx.busy()) { renderer.addSystemNote(BUSY_NOTE, "warn"); return; }
|
|
252
|
-
const cp = await ctx.rt.checkpointsFor(ctx.store().id);
|
|
253
|
-
if (!cp) { renderer.addSystemNote("checkpoints unavailable (git missing or ROVECODE_NO_CHECKPOINTS=1) — nothing to undo", "warn"); return; }
|
|
254
|
-
const last = cp.list().at(-1);
|
|
255
|
-
if (!last) { renderer.addSystemNote("no checkpoint to undo to — snapshots land after each mutating tool call (edit, write, bash)"); return; }
|
|
256
|
-
// where the workspace sits: AT a snapshot → its pre-edit ancestor is the target (the agent's last change); drifted → the last snapshot
|
|
257
|
-
const pos = await cp.position();
|
|
258
|
-
const target = pos.at ? pos.previous : last;
|
|
259
|
-
if (!target) { renderer.addSystemNote(`nothing to undo — the workspace matches checkpoint ${ident(pos.at!)} and no older snapshot differs from it (/checkpoints lists them; /restore <ref> reaches any)`); return; }
|
|
260
|
-
const id = ident(target);
|
|
261
|
-
const changes = await cp.changedSince(target.hash);
|
|
262
|
-
if (changes !== null && changes.length === 0) { renderer.addSystemNote(`nothing to undo — the workspace already matches the last checkpoint ${id}`); return; }
|
|
263
|
-
const head = pos.at ? `undo the agent's last change (checkpoint ${pos.at.hash.slice(0, 8)}, ${pos.at.label}): restore ${id}` : `restore the workspace to checkpoint ${id}`;
|
|
264
|
-
const detail = changes === null
|
|
265
|
-
? `${head}: every file goes back to that snapshot (the change list could not be computed)`
|
|
266
|
-
: [`${head} — ${changes.length} path${changes.length === 1 ? "" : "s"}:`, ...changes.slice(0, 40).map((c) => ` ${c.status} ${c.path} — ${STATUS_WORD[c.status] ?? c.status}`), ...(changes.length > 40 ? [` … ${changes.length - 40} more`] : []), "", "the conversation is untouched; /restore <ref> conversation branches it too"].join("\n");
|
|
267
|
-
const answer = await renderer.askApproval("undo", `restore checkpoint ${target.hash.slice(0, 8)} · files only`, detail);
|
|
268
|
-
if (answer === "deny") { renderer.addSystemNote("undo cancelled — nothing changed", "warn"); return; }
|
|
269
|
-
const res = await cp.restore(target.hash, "files");
|
|
270
|
-
if (!res.ok) { renderer.addSystemNote(`undo failed: ${res.error}`, "error"); return; }
|
|
271
|
-
const what = changes === null ? "the workspace" : `${changes.length} path${changes.length === 1 ? "" : "s"}: ${changes.slice(0, 8).map((c) => c.path).join(", ")}${changes.length > 8 ? ", …" : ""}`;
|
|
272
|
-
renderer.addSystemNote(`undo: restored ${what} to checkpoint ${id} — files only; the conversation is untouched (snapshots keep history: /checkpoints)`);
|
|
273
|
-
}
|