chainlesschain 0.162.157 → 0.162.159
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 +1 -1
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-CfMP2Oud.js → AIOps-ByIiF5bT.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Zu0N8BQL.js → ActionButton-NlgSFZq5.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DZTwj5T_.js → Analytics-D17cOUcb.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-CVFJh47v.js → AppLayout-B4mEZcbl.js} +5 -5
- package/src/assets/web-panel/assets/Artifacts-CASsmyQw.js +1 -0
- package/src/assets/web-panel/assets/Artifacts-CTVt96zW.css +1 -0
- package/src/assets/web-panel/assets/{Audit-BMfiLj7T.js → Audit-DYAia2e2.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BslPCdGF.js → BackgroundAgents-C1lvYUSu.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CQ_fMC1r.js → Backup-B6agK6bA.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CsFsARiC.js → BaseInput-DM43R2vy.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CwfBobco.js → Chat-Dtdp3eXp.js} +6 -6
- package/src/assets/web-panel/assets/Chat-KSGPtU0L.css +1 -0
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-K6Nf3xeN.js → ChatBubbleRenderer-BL5c_OQ1.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-DkSyjjPp.js → Checkbox-D2xf7uZt.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-B7Fx3zZ6.js → Codegen-D3j45jJL.js} +1 -1
- package/src/assets/web-panel/assets/{Col-Dzfqn8Ep.js → Col-voPJ9fUZ.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DhHPdzMM.js → Community-DEsyTESF.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Ct18_8y7.js → Compact-OHV_QXLD.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CY64IqBi.js → Compliance-eDTJWC-J.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BYSmWlUl.js → Cowork-BBvy-6d1.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-_4xtP3Ug.js → Cron-Dw2eoJxu.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-B4qY-Ycr.js → Crosschain-DQHms_Bh.js} +1 -1
- package/src/assets/web-panel/assets/{DID-CkCzWkx1.js → DID-Cyu7Id1c.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-C1scT7Ke.js → Dashboard-sIDzkG92.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DBnQ2EWd.js → Dropdown-DUW3T2ey.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-C9JhOTq0.js → EmailListRenderer-CECELXW8.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CWXv5RUv.js → FamilyGuardDashboard-BIZR37ov.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-fYdcbz9W.js → Federation-BEZIQX84.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CJqOT3Yj.js → FormItemContext-BcdUMznL.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DGCsHc5K.js → GenericCardRenderer-DsN_fr5H.js} +1 -1
- package/src/assets/web-panel/assets/{Git-EH2K-H0Q.js → Git-63LhjBx2.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DJn1X2pR.js → Governance-CYtQtIcH.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Dch1E8qd.js → Inference-BwR6UUox.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-QIur8j8-.js → KnowledgeGraph-D9lVNTUA.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DUgHldPg.js → Logs-C4q5h7Ut.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-DbzhFwvz.css +1 -0
- package/src/assets/web-panel/assets/MarkdownRenderer-DexLlXfN.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-DafrSfvd.js → Marketplace-Plksliuw.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-D2xRbfJx.js → McpTools-DZUFfeuG.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-C6RbVHAo.js → Memory-NoDYt29V.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BPtVBKqn.js → MobileBridge-CEXGTkdh.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-Cuw832cr.js → MobileProjects-DnbJkbZo.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-D6O9fWFa.js → Mtc-DTAa0cwy.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-CWZcXIH1.js → MtcAudit-CVV7LnNS.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-DhIBxTFd.js → Multisig-BqqGrjt_.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-wkBFS33e.js → NLProgramming-C4WP8DRp.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-B09FxncA.js → Notes-Zo4DJUVI.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-oQwtH6-9.js → NotificationSettings-DBcjRaz2.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-BCQel9bD.js +1 -0
- package/src/assets/web-panel/assets/{Organization-CsF-NIjz.js → Organization-BT9WPBOX.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-bLxv9Dqi.js → Overflow-D6b4bZ3q.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DVwQtm4q.js → P2P-Cw3OsYx-.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Bgic3QKm.js → PdhVaultBrowser-C0nyiK2b.js} +4 -4
- package/src/assets/web-panel/assets/{Permissions-as80-9ep.js → Permissions-zVBZEB_C.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-BGmUw36a.js → PersonalDataHub-BOspsFew.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-BM96zBxG.js → Pipeline-CQWfZ4XH.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BBOzBSWe.js → Privacy-C2oGJ_WL.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-GHX41V28.js → ProjectInit-BZMR4qY1.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DraqzxKD.js → ProjectSettings-DAujoP63.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BzzdUml4.js → Projects-DvH_P2Uq.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-CSeYNAtW.js → Providers-D_RGHM-v.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-DlzBCdXh.js → QrScannerModal-Mf6KfL2J.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-p8iB7od_.js → QuickAsk-5D8URoI_.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DjOVXacd.js → Recommend-Df4kC8Gy.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-DGi3f3vO.js +4 -0
- package/src/assets/web-panel/assets/{Reputation-CjT3M0Di.js → Reputation-jmkGFNWb.js} +1 -1
- package/src/assets/web-panel/assets/{Row-tw8m6T0c.js → Row-DP6Pcu3m.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-B47KBx_f.js → RssFeed-D60hSuPO.js} +3 -3
- package/src/assets/web-panel/assets/{Search-D2sx1PBe.js → Search-0mf1BQUK.js} +1 -1
- package/src/assets/web-panel/assets/{Security-JmtL8g7D.js → Security-CfZtLFYV.js} +3 -3
- package/src/assets/web-panel/assets/{Services-BxrNqjin.js → Services-D9zpZLWz.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-udk8L18t.js → Skeleton-D1_44eMD.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DooLT9of.js → Skills-cVFa8zkz.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-ITVZE1xI.js → Sla-Cinpzc3L.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DCkfqY1n.js → SpeechSettings-bqdiJE_P.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CkNlgFjM.js → SyncSettings-Dgl_SLgj.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BMAoyCM_.js → Tasks-DawWAEhy.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BnrFTlrz.js → Templates-Ca0nY651.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BjNsy3-i.js → Tenant-CgaGEhLm.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-C0gxLL7V.js → Terminal-D-9EnpRx.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CUtj_KHA.js → TimelineRenderer-CSOxDHZ6.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DOg61zlq.js → Tokens-BpwLZpL3.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-D4ldCewj.js → Trigger-DgowwBK5.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-3lk_Zd8j.js → Trust-BZutapjE.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-kWhFR6Li.js → UkeySign-QZPt8_Sp.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-D3GOFVWg.js → VideoEditing-DlQx5UDJ.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DzrZfBuh.js → Wallet-ZfdTgtWA.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-CyVJVp_g.js → WebAuthn-CAp0y_Cr.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-i_b0wi9b.js → WorkflowEditor-BVITZEmr.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DwPCK8I7.js → chat-CC2tPaNg.js} +1 -1
- package/src/assets/web-panel/assets/{colors-D0vaVqZU.js → colors-31brc9CA.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-Don7nGW2.js → compact-item-DLMkC9C-.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-D84R5qy2.js → createContext-BMtveI6F.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CB8pPOtd.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BeUqVVKU.js → hasIn-DbD6WHW2.js} +1 -1
- package/src/assets/web-panel/assets/{index-m9bn0F1p.js → index-3oBsXZmF.js} +1 -1
- package/src/assets/web-panel/assets/{index-CfYRv-zA.js → index-ALvoNTRF.js} +1 -1
- package/src/assets/web-panel/assets/{index-4MkfPLGv.js → index-AaIKG60c.js} +1 -1
- package/src/assets/web-panel/assets/{index-LMo9bBLJ.js → index-BFRLmCR3.js} +1 -1
- package/src/assets/web-panel/assets/{index-wesQQ2cH.js → index-BKAB6-6V.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzT56x-_.js → index-BOM-VFlv.js} +1 -1
- package/src/assets/web-panel/assets/{index-L73NrmlW.js → index-BPpS-lGb.js} +1 -1
- package/src/assets/web-panel/assets/{index-WRw1pM5E.js → index-Bbc2JeQG.js} +1 -1
- package/src/assets/web-panel/assets/index-BhXiaiev.js +1 -0
- package/src/assets/web-panel/assets/{index-Br3SOdER.js → index-BiRQUoyE.js} +1 -1
- package/src/assets/web-panel/assets/{index-D8r7WKk-.js → index-BnFLzejT.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAdCTTBC.js → index-BoO4WEJB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlfTf0ka.js → index-C-cGlCDW.js} +1 -1
- package/src/assets/web-panel/assets/{index-B57al0Z5.js → index-C1PfeI88.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4b5XqIy.js → index-C21pdBqs.js} +1 -1
- package/src/assets/web-panel/assets/{index-CnnxzIx0.js → index-C7LHNw_N.js} +1 -1
- package/src/assets/web-panel/assets/{index-BNpJf-sf.js → index-CEXa-4zd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DT40zd7K.js → index-CEwjBA9q.js} +1 -1
- package/src/assets/web-panel/assets/{index-CL71rIwo.js → index-CFXKbOek.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZ1N07Sp.js → index-CJhREOlG.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0_sKVi7.js → index-COHHQ9cU.js} +1 -1
- package/src/assets/web-panel/assets/{index-DBzBLX-K.js → index-CUmLMFsy.js} +1 -1
- package/src/assets/web-panel/assets/{index-De85D9Pc.js → index-CWgfipcC.js} +1 -1
- package/src/assets/web-panel/assets/{index-x7VYlV41.js → index-C_eUKGYP.js} +1 -1
- package/src/assets/web-panel/assets/{index-hjgrIX5I.js → index-CcVszn2b.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMT8n9vB.js → index-Cod97Urs.js} +3 -3
- package/src/assets/web-panel/assets/{index-DFFzwrNs.js → index-Cwbqh0KZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-YKLCotoo.js → index-D67kIagl.js} +1 -1
- package/src/assets/web-panel/assets/{index-IGGttsCv.js → index-D7cV0tu0.js} +1 -1
- package/src/assets/web-panel/assets/{index-DQor_7NF.js → index-DJvxFyIz.js} +1 -1
- package/src/assets/web-panel/assets/{index-D84BnuFg.js → index-DZl-UXB6.js} +1 -1
- package/src/assets/web-panel/assets/{index-CrnVEEQO.js → index-Dm0byjm1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BER98o4P.js → index-DndNnYc0.js} +1 -1
- package/src/assets/web-panel/assets/{index-DA0-EJKx.js → index-WQUO2vvK.js} +1 -1
- package/src/assets/web-panel/assets/index-WpaYaE3D.js +1 -0
- package/src/assets/web-panel/assets/{index-CExWtm98.js → index-bdaJwTXJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-MKZrJrP_.js → index-ep6JchNG.js} +1 -1
- package/src/assets/web-panel/assets/{index-7QUwcGTg.js → index-jG6h22Qg.js} +1 -1
- package/src/assets/web-panel/assets/{index-BohrJpBf.js → index-ntVQqSML.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DGeU4vPS.js → initDefaultProps-D5UoBRhN.js} +1 -1
- package/src/assets/web-panel/assets/{motion-Dgqj9ab3.js → motion-DltOw0CR.js} +1 -1
- package/src/assets/web-panel/assets/{move-BFyEU6Lr.js → move-_oPkk5zj.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-CueCXXPC.js → mtc-parser-EES8ROCS.js} +1 -1
- package/src/assets/web-panel/assets/{omit-D2pU5OyE.js → omit-B-_39x_M.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CKTf6zD6.js → pickAttrs-l6D0yJHz.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DieJaGCN.js → placementArrow-DIeUK31L.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Bxr-rmU6.js → responsiveObserve-dG4EzUQo.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CDEDk3Se.js → slide-DWFzYGdH.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-icf2eCTx.js → statusUtils-CECdTA5H.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-qKvgmBhE.js → styleChecker-D6kP-mMK.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-1_UhiyJZ.js → useFlexGapSupport-BWfna_rd.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CFGLXDWh.js → useFs-Bb-j6rzo.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-UafOXvQ6.js → usePersonalDataHub-D5OIsYM4.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BfPqSx79.js → vnode-DNpwzFTM.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CXsVcRB6.js → zoom-vNLpFQBW.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-manifest.json +17 -3
- package/src/commands/agent.js +11 -1
- package/src/commands/background-session.js +143 -3
- package/src/commands/browse.js +86 -0
- package/src/commands/cloud.js +232 -0
- package/src/commands/doctor.js +61 -6
- package/src/commands/mcp.js +61 -0
- package/src/commands/plugin.js +63 -1
- package/src/commands/routine.js +367 -0
- package/src/commands/session.js +326 -0
- package/src/data/changelog.json +12 -2
- package/src/harness/jsonl-session-store.js +82 -2
- package/src/harness/session-index.js +295 -0
- package/src/harness/session-mirror.js +142 -0
- package/src/harness/transcript-integrity.js +182 -0
- package/src/index.js +8 -0
- package/src/lib/background-agent-supervisor.js +126 -2
- package/src/lib/channels/channel-manager.js +126 -0
- package/src/lib/channels/telegram-channel.js +112 -0
- package/src/lib/channels/webhook-channel.js +140 -0
- package/src/lib/chrome-connector.js +425 -4
- package/src/lib/cloud/bundle.js +110 -0
- package/src/lib/cloud/cloud-client.js +152 -0
- package/src/lib/computer-use/authorization.js +173 -0
- package/src/lib/computer-use/computer-use-server.js +276 -0
- package/src/lib/computer-use/control-backend.js +150 -0
- package/src/lib/doctor-checkup.js +652 -0
- package/src/lib/jsonl-session-store.js +4 -0
- package/src/lib/lsp/lsp-server-registry.js +48 -21
- package/src/lib/plugin-runtime/governance.js +174 -0
- package/src/lib/pr-link-ledger.js +221 -0
- package/src/lib/remote-control.js +35 -0
- package/src/lib/routine-store.js +352 -0
- package/src/lib/session-store-guard.js +40 -0
- package/src/lib/session-usage.js +318 -2
- package/src/lib/sub-agent-context.js +23 -0
- package/src/lib/web-ui-server.js +120 -7
- package/src/repl/agent-repl.js +592 -61
- package/src/repl/bg-dashboard.js +355 -0
- package/src/repl/fast-mode.js +103 -0
- package/src/repl/fullscreen-tui.js +197 -0
- package/src/repl/remote-approval.js +111 -0
- package/src/repl/slash-command-registry.js +11 -3
- package/src/repl/voice-dictation.js +132 -0
- package/src/runtime/agent-core.js +278 -1
- package/src/runtime/coding-agent-contract-shared.cjs +141 -0
- package/src/runtime/coding-agent-policy.cjs +33 -0
- package/src/runtime/headless-runner.js +71 -7
- package/src/runtime/headless-stream.js +14 -8
- package/src/runtime/policies/agent-policy.js +6 -0
- package/src/assets/web-panel/assets/Artifacts-BbN5V7au.js +0 -1
- package/src/assets/web-panel/assets/Artifacts-CRkepl4t.css +0 -1
- package/src/assets/web-panel/assets/Chat-DDh58PCf.css +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-iFKeNf6Y.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-CIGk7gk-.js +0 -4
- package/src/assets/web-panel/assets/devWarning-BBwWehQ7.js +0 -1
- package/src/assets/web-panel/assets/index-DJ8etkBT.js +0 -1
- package/src/assets/web-panel/assets/index-OeaK-zXZ.js +0 -1
package/src/repl/agent-repl.js
CHANGED
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
appendAssistantMessage,
|
|
48
48
|
appendCompactEvent,
|
|
49
49
|
appendTokenUsage,
|
|
50
|
+
appendToolCallCompact,
|
|
50
51
|
rebuildMessages,
|
|
51
52
|
sessionExists,
|
|
52
53
|
} from "../harness/jsonl-session-store.js";
|
|
@@ -111,6 +112,7 @@ import {
|
|
|
111
112
|
resolveAskIdleTimeoutMs,
|
|
112
113
|
questionWithIdleTimeout,
|
|
113
114
|
} from "./permission-prompt.js";
|
|
115
|
+
import { describeAskContext, raceLocalAndRemote } from "./remote-approval.js";
|
|
114
116
|
import {
|
|
115
117
|
parsePermissionTier,
|
|
116
118
|
parsePermissionModeArg,
|
|
@@ -315,6 +317,25 @@ export async function agentLoop(messages, options) {
|
|
|
315
317
|
),
|
|
316
318
|
);
|
|
317
319
|
} else if (event.type === "tool-result") {
|
|
320
|
+
// 用量归因: persist a compact tool_call record (name + error flag +
|
|
321
|
+
// skill hint — never args, which can carry whole file bodies) so
|
|
322
|
+
// `cc session usage --by tool|mcp` and `cc insights` can aggregate
|
|
323
|
+
// tool use for REPL sessions. Best-effort; opt out with
|
|
324
|
+
// options.persistToolCalls === false.
|
|
325
|
+
if (options.sessionId && options.persistToolCalls !== false) {
|
|
326
|
+
try {
|
|
327
|
+
appendToolCallCompact(options.sessionId, {
|
|
328
|
+
tool: event.tool,
|
|
329
|
+
isError: Boolean(event.error || event.result?.error),
|
|
330
|
+
skill:
|
|
331
|
+
event.tool === "run_skill" && _lastExec?.tool === "run_skill"
|
|
332
|
+
? _lastExec.args?.skill_name
|
|
333
|
+
: undefined,
|
|
334
|
+
});
|
|
335
|
+
} catch (_e) {
|
|
336
|
+
// persistence is best-effort — never break the turn
|
|
337
|
+
}
|
|
338
|
+
}
|
|
318
339
|
if (event.error || event.result?.error) {
|
|
319
340
|
process.stdout.write(
|
|
320
341
|
chalk.red(` Error: ${event.error || event.result?.error}\n`),
|
|
@@ -448,6 +469,11 @@ export async function startAgentRepl(options = {}) {
|
|
|
448
469
|
// gate is wired. The wrapper is installed only when settings customize the
|
|
449
470
|
// map, and only bites while _sessionTier === "auto" (isActive predicate).
|
|
450
471
|
let _autoModeResolved = null;
|
|
472
|
+
// `/remote-control` (批17/18 REPL 收口): paired-device approvals for this
|
|
473
|
+
// interactive session. Holds { bridge, pairing, close } while active; both
|
|
474
|
+
// interactive confirmers below race the terminal against the paired device
|
|
475
|
+
// (first answer wins). null = local-only (byte-identical prompt path).
|
|
476
|
+
let _remoteApproval = null;
|
|
451
477
|
const baseUrl = options.baseUrl || "http://localhost:11434";
|
|
452
478
|
const apiKey = options.apiKey || null;
|
|
453
479
|
// Configured vision model (config.llm.visionModel) — used when a turn carries
|
|
@@ -617,54 +643,79 @@ export async function startAgentRepl(options = {}) {
|
|
|
617
643
|
_autoModeResolved = null; // fail to the plain gate tiers
|
|
618
644
|
}
|
|
619
645
|
if (typeof _approvalGate.setConfirmer === "function") {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
),
|
|
626
|
-
);
|
|
627
|
-
return false;
|
|
628
|
-
}
|
|
629
|
-
await _fireNotification(
|
|
630
|
-
`Permission needed: ${riskLevel || "medium"}-risk ${tool || "run_shell"}${args?.command ? " — " + args.command : ""}`,
|
|
631
|
-
);
|
|
646
|
+
// The local prompt as a cancelable handle so an active /remote-control
|
|
647
|
+
// can race it against a paired device: cancel closes the readline when
|
|
648
|
+
// the device answers first, and `canceled` mutes the stranded
|
|
649
|
+
// idle-timeout leg so it can't print "auto-denied" after the fact.
|
|
650
|
+
const askGateApprovalLocally = ({ tool, args, riskLevel }) => {
|
|
632
651
|
const rlConfirm = readline.createInterface({
|
|
633
652
|
input: process.stdin,
|
|
634
653
|
output: process.stdout,
|
|
635
654
|
});
|
|
655
|
+
let canceled = false;
|
|
636
656
|
const q = (p) => new Promise((res) => rlConfirm.question(p, res));
|
|
637
657
|
const cmd = args?.command ? ` ${args.command}` : "";
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
`\n[ApprovalGate] ${riskLevel || "medium"} risk command:${cmd}\n` +
|
|
642
|
-
` Proceed? [y]es once / [a]lways allow / [N]o: `,
|
|
643
|
-
),
|
|
644
|
-
_askIdleTimeoutMs,
|
|
645
|
-
);
|
|
646
|
-
rlConfirm.close();
|
|
647
|
-
if (res.timedOut) {
|
|
648
|
-
process.stdout.write(
|
|
658
|
+
const promise = (async () => {
|
|
659
|
+
const res = await questionWithIdleTimeout(
|
|
660
|
+
q,
|
|
649
661
|
chalk.yellow(
|
|
650
|
-
`\n
|
|
662
|
+
`\n[ApprovalGate] ${riskLevel || "medium"} risk command:${cmd}\n` +
|
|
663
|
+
` Proceed? [y]es once / [a]lways allow / [N]o: `,
|
|
651
664
|
),
|
|
665
|
+
_askIdleTimeoutMs,
|
|
652
666
|
);
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
if (ans === "a" || ans === "always") {
|
|
657
|
-
const saved = await _persistAlwaysAllow(tool || "run_shell", args);
|
|
658
|
-
if (saved) {
|
|
667
|
+
rlConfirm.close();
|
|
668
|
+
if (canceled) return false; // raced out — the device already decided
|
|
669
|
+
if (res.timedOut) {
|
|
659
670
|
process.stdout.write(
|
|
660
|
-
chalk.
|
|
661
|
-
|
|
671
|
+
chalk.yellow(
|
|
672
|
+
`\n ⏱ no response in ${_askIdleTimeoutMs}ms — auto-denied\n`,
|
|
662
673
|
),
|
|
663
674
|
);
|
|
675
|
+
return false;
|
|
664
676
|
}
|
|
665
|
-
|
|
677
|
+
const ans = res.answer.trim().toLowerCase();
|
|
678
|
+
if (ans === "a" || ans === "always") {
|
|
679
|
+
const saved = await _persistAlwaysAllow(tool || "run_shell", args);
|
|
680
|
+
if (saved) {
|
|
681
|
+
process.stdout.write(
|
|
682
|
+
chalk.green(
|
|
683
|
+
` ✓ always allow: added ${saved.rule} → ${saved.file}\n`,
|
|
684
|
+
),
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
return ans === "y" || ans === "yes";
|
|
690
|
+
})();
|
|
691
|
+
return {
|
|
692
|
+
promise,
|
|
693
|
+
cancel: () => {
|
|
694
|
+
canceled = true;
|
|
695
|
+
rlConfirm.close();
|
|
696
|
+
},
|
|
697
|
+
};
|
|
698
|
+
};
|
|
699
|
+
_approvalGate.setConfirmer(async ({ tool, args, riskLevel }) => {
|
|
700
|
+
if (_dontAskActive()) {
|
|
701
|
+
process.stdout.write(
|
|
702
|
+
chalk.yellow(
|
|
703
|
+
`\n ✕ denied without asking (dontAsk mode): ${riskLevel || "medium"}-risk ${tool || "run_shell"}${args?.command ? " — " + args.command : ""}\n`,
|
|
704
|
+
),
|
|
705
|
+
);
|
|
706
|
+
return false;
|
|
666
707
|
}
|
|
667
|
-
|
|
708
|
+
await _fireNotification(
|
|
709
|
+
`Permission needed: ${riskLevel || "medium"}-risk ${tool || "run_shell"}${args?.command ? " — " + args.command : ""}`,
|
|
710
|
+
);
|
|
711
|
+
const local = askGateApprovalLocally({ tool, args, riskLevel });
|
|
712
|
+
if (!_remoteApproval?.bridge) return local.promise;
|
|
713
|
+
return raceLocalAndRemote({
|
|
714
|
+
bridge: _remoteApproval.bridge,
|
|
715
|
+
ask: describeAskContext({ tool, args, riskLevel }),
|
|
716
|
+
local,
|
|
717
|
+
writeOut: (text) => process.stdout.write(chalk.cyan(text)),
|
|
718
|
+
});
|
|
668
719
|
});
|
|
669
720
|
}
|
|
670
721
|
} catch (_err) {
|
|
@@ -693,45 +744,68 @@ export async function startAgentRepl(options = {}) {
|
|
|
693
744
|
loaded.rules.ask.length +
|
|
694
745
|
loaded.rules.deny.length;
|
|
695
746
|
_permissionRules = total > 0 ? loaded.rules : null;
|
|
696
|
-
//
|
|
697
|
-
// so
|
|
698
|
-
|
|
699
|
-
if (_dontAskActive()) {
|
|
700
|
-
process.stdout.write(
|
|
701
|
-
chalk.yellow(
|
|
702
|
-
`\n ✕ denied without asking (dontAsk mode): ${tool}${rule ? ` (${rule})` : reason ? ` — ${reason}` : ""}\n`,
|
|
703
|
-
),
|
|
704
|
-
);
|
|
705
|
-
return false;
|
|
706
|
-
}
|
|
707
|
-
await _fireNotification(
|
|
708
|
-
`Permission needed: ${tool}${rule ? " (" + rule + ")" : ""}`,
|
|
709
|
-
);
|
|
747
|
+
// Local prompt as a cancelable handle (same shape as the gate confirmer
|
|
748
|
+
// above) so /remote-control can race it against a paired device.
|
|
749
|
+
const askPermissionLocally = ({ tool, args, rule, reason }) => {
|
|
710
750
|
const rl = readline.createInterface({
|
|
711
751
|
input: process.stdin,
|
|
712
752
|
output: process.stdout,
|
|
713
753
|
});
|
|
754
|
+
let canceled = false;
|
|
714
755
|
const q = (p) => new Promise((res) => rl.question(p, res));
|
|
715
756
|
// Picks the right phrasing whether the caller passed a `rule`
|
|
716
757
|
// (settings/hook ask) or a `reason` (destructive-git / sensitive-file
|
|
717
758
|
// guards) — avoids the literal "null" the old template printed.
|
|
718
759
|
const header = buildPermissionPrompt({ tool, args, rule, reason });
|
|
719
|
-
const
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
760
|
+
const promise = (async () => {
|
|
761
|
+
const res = await questionWithIdleTimeout(
|
|
762
|
+
q,
|
|
763
|
+
chalk.yellow(`\n${header}\n Proceed? (y/N) `),
|
|
764
|
+
_askIdleTimeoutMs,
|
|
765
|
+
);
|
|
766
|
+
rl.close();
|
|
767
|
+
if (canceled) return false; // raced out — the device already decided
|
|
768
|
+
if (res.timedOut) {
|
|
769
|
+
process.stdout.write(
|
|
770
|
+
chalk.yellow(
|
|
771
|
+
`\n ⏱ no response in ${_askIdleTimeoutMs}ms — auto-denied\n`,
|
|
772
|
+
),
|
|
773
|
+
);
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
const ans = res.answer.trim().toLowerCase();
|
|
777
|
+
return ans === "y" || ans === "yes";
|
|
778
|
+
})();
|
|
779
|
+
return {
|
|
780
|
+
promise,
|
|
781
|
+
cancel: () => {
|
|
782
|
+
canceled = true;
|
|
783
|
+
rl.close();
|
|
784
|
+
},
|
|
785
|
+
};
|
|
786
|
+
};
|
|
787
|
+
// Confirmer is shared by permission `ask` rules AND hook `ask` decisions,
|
|
788
|
+
// so define it unconditionally (a `hook:` rule label flows through too).
|
|
789
|
+
_permissionConfirm = async ({ tool, args, rule, reason }) => {
|
|
790
|
+
if (_dontAskActive()) {
|
|
726
791
|
process.stdout.write(
|
|
727
792
|
chalk.yellow(
|
|
728
|
-
`\n
|
|
793
|
+
`\n ✕ denied without asking (dontAsk mode): ${tool}${rule ? ` (${rule})` : reason ? ` — ${reason}` : ""}\n`,
|
|
729
794
|
),
|
|
730
795
|
);
|
|
731
796
|
return false;
|
|
732
797
|
}
|
|
733
|
-
|
|
734
|
-
|
|
798
|
+
await _fireNotification(
|
|
799
|
+
`Permission needed: ${tool}${rule ? " (" + rule + ")" : ""}`,
|
|
800
|
+
);
|
|
801
|
+
const local = askPermissionLocally({ tool, args, rule, reason });
|
|
802
|
+
if (!_remoteApproval?.bridge) return local.promise;
|
|
803
|
+
return raceLocalAndRemote({
|
|
804
|
+
bridge: _remoteApproval.bridge,
|
|
805
|
+
ask: describeAskContext({ tool, args, rule, reason }),
|
|
806
|
+
local,
|
|
807
|
+
writeOut: (text) => process.stdout.write(chalk.cyan(text)),
|
|
808
|
+
});
|
|
735
809
|
};
|
|
736
810
|
} catch (_err) {
|
|
737
811
|
if (_err?.code === "CC_MANAGED_SETTINGS_INVALID") throw _err;
|
|
@@ -900,6 +974,60 @@ export async function startAgentRepl(options = {}) {
|
|
|
900
974
|
// Non-critical — SessionManager integration must not block startup
|
|
901
975
|
}
|
|
902
976
|
|
|
977
|
+
// `/remote-control` start/stop (批17 slash 入口 + 批18 REPL 接线): reuses
|
|
978
|
+
// the headless assembly (self-hosted WS on an OS-assigned port + a
|
|
979
|
+
// RemoteApprovalBridge for this session); the interactive difference is
|
|
980
|
+
// purely how the confirmers consume it — they RACE the terminal prompt
|
|
981
|
+
// against the paired device instead of gating on the remote alone.
|
|
982
|
+
const _startRemoteApproval = async () => {
|
|
983
|
+
const { startHeadlessRemoteApproval } =
|
|
984
|
+
await import("../lib/remote-approval-bridge.js");
|
|
985
|
+
const started = await startHeadlessRemoteApproval({
|
|
986
|
+
agentSessionId:
|
|
987
|
+
sessionId || `repl-${process.pid}-${Date.now().toString(36)}`,
|
|
988
|
+
isText: false, // the REPL prints its own pairing lines below
|
|
989
|
+
});
|
|
990
|
+
_remoteApproval = {
|
|
991
|
+
bridge: started.bridge,
|
|
992
|
+
pairing: started.pairing,
|
|
993
|
+
close: started.close,
|
|
994
|
+
};
|
|
995
|
+
logger.log(
|
|
996
|
+
chalk.cyan(
|
|
997
|
+
" remote-control: permission prompts can be answered from a paired device (first answer — terminal or device — wins)",
|
|
998
|
+
),
|
|
999
|
+
);
|
|
1000
|
+
logger.log(` pairing: ${started.pairing.uri}`);
|
|
1001
|
+
try {
|
|
1002
|
+
const { renderQrCode } = await import("../lib/remote-control.js");
|
|
1003
|
+
const qr = await renderQrCode(started.pairing.uri);
|
|
1004
|
+
if (qr) logger.log(qr);
|
|
1005
|
+
} catch (_err) {
|
|
1006
|
+
// QR is progressive enhancement — the URI line is the contract
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
const _stopRemoteApproval = async () => {
|
|
1010
|
+
const active = _remoteApproval;
|
|
1011
|
+
_remoteApproval = null; // confirmers fall back to local-only immediately
|
|
1012
|
+
if (active) await active.close().catch(() => undefined);
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
// --remote-control also applies to interactive sessions. Headless fails
|
|
1016
|
+
// closed when the bridge can't start (approvals would silently fall
|
|
1017
|
+
// closed); interactively the terminal still prompts, so a start failure
|
|
1018
|
+
// degrades to local-only with a warning instead of aborting the session.
|
|
1019
|
+
if (options.remoteControl === true) {
|
|
1020
|
+
try {
|
|
1021
|
+
await _startRemoteApproval();
|
|
1022
|
+
} catch (err) {
|
|
1023
|
+
logger.log(
|
|
1024
|
+
chalk.yellow(
|
|
1025
|
+
` remote-control unavailable (${err.message}) — approvals stay local-only`,
|
|
1026
|
+
),
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
903
1031
|
// --system-prompt replaces the built-in prompt; --append-system-prompt
|
|
904
1032
|
// extends it (parity with the headless runners). The base is kept so an
|
|
905
1033
|
// output-style persona can be swapped in/out at runtime via /output-style.
|
|
@@ -1190,6 +1318,21 @@ export async function startAgentRepl(options = {}) {
|
|
|
1190
1318
|
if (options.sessionId && sessionId) {
|
|
1191
1319
|
try {
|
|
1192
1320
|
if (useJsonl) {
|
|
1321
|
+
// Interactive tamper warning: a broken hash chain means the transcript
|
|
1322
|
+
// was edited outside the store — resume proceeds (the user is present)
|
|
1323
|
+
// but the restored context is flagged untrusted.
|
|
1324
|
+
try {
|
|
1325
|
+
const { verifySession } =
|
|
1326
|
+
await import("../harness/jsonl-session-store.js");
|
|
1327
|
+
const trust = verifySession(sessionId);
|
|
1328
|
+
if (trust.status === "tampered") {
|
|
1329
|
+
logger.warn(
|
|
1330
|
+
`⚠ Transcript integrity check FAILED (${trust.reason}) — restored context is untrusted. Run 'cc session verify ${sessionId}'.`,
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
} catch (_err) {
|
|
1334
|
+
// verification is best-effort in the interactive path
|
|
1335
|
+
}
|
|
1193
1336
|
const rebuilt = rebuildMessages(sessionId);
|
|
1194
1337
|
if (rebuilt.length > 0) {
|
|
1195
1338
|
messages.push(...rebuilt.filter((m) => m.role !== "system"));
|
|
@@ -1305,6 +1448,84 @@ export async function startAgentRepl(options = {}) {
|
|
|
1305
1448
|
// stored cli.theme is untouched — /theme still shows and can change it).
|
|
1306
1449
|
if (_screenReaderMode) _theme = "mono";
|
|
1307
1450
|
applyThemeChalk(_theme, chalk, _chalkBaselineLevel);
|
|
1451
|
+
|
|
1452
|
+
// Fullscreen / no-flicker TUI mode (Claude-Code `/tui` parity). Resolve from
|
|
1453
|
+
// CC_NO_FLICKER (forces fullscreen) or persisted `cli.tuiMode`; switchable at
|
|
1454
|
+
// runtime via `/tui fullscreen|default`. Screen-reader mode forces default —
|
|
1455
|
+
// an alternate screen buffer breaks readers. The heavy renderer lives in
|
|
1456
|
+
// fullscreen-tui.js; here we just track the mode and toggle the alt buffer.
|
|
1457
|
+
const {
|
|
1458
|
+
resolveTuiMode: _resolveTuiMode,
|
|
1459
|
+
renderTuiStatus: _renderTuiStatus,
|
|
1460
|
+
ALT_SCREEN_ENTER: _ALT_ENTER,
|
|
1461
|
+
ALT_SCREEN_LEAVE: _ALT_LEAVE,
|
|
1462
|
+
} = await import("./fullscreen-tui.js");
|
|
1463
|
+
let _tuiMode = "default";
|
|
1464
|
+
try {
|
|
1465
|
+
const { getConfigValue } = await import("../lib/config-manager.js");
|
|
1466
|
+
_tuiMode = _resolveTuiMode({
|
|
1467
|
+
env: process.env.CC_NO_FLICKER,
|
|
1468
|
+
setting: getConfigValue("cli.tuiMode"),
|
|
1469
|
+
});
|
|
1470
|
+
} catch (_e) {
|
|
1471
|
+
_tuiMode = _resolveTuiMode({ env: process.env.CC_NO_FLICKER });
|
|
1472
|
+
}
|
|
1473
|
+
if (_screenReaderMode) _tuiMode = "default";
|
|
1474
|
+
const _setTuiMode = (next) => {
|
|
1475
|
+
if (next === _tuiMode) return;
|
|
1476
|
+
// Enter/leave the alternate screen buffer so the user's scrollback is
|
|
1477
|
+
// preserved either way. Only touch a real TTY.
|
|
1478
|
+
if (process.stdout.isTTY) {
|
|
1479
|
+
process.stdout.write(next === "fullscreen" ? _ALT_ENTER : _ALT_LEAVE);
|
|
1480
|
+
}
|
|
1481
|
+
_tuiMode = next;
|
|
1482
|
+
};
|
|
1483
|
+
if (_tuiMode === "fullscreen" && process.stdout.isTTY) {
|
|
1484
|
+
process.stdout.write(_ALT_ENTER);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Fast mode (Claude-Code `/fast` parity, generalized to a latency profile).
|
|
1488
|
+
// When on, the next turn minimizes reasoning and — unless the user pinned a
|
|
1489
|
+
// model this session — swaps to the provider's low-latency model. Resolved
|
|
1490
|
+
// from config `cli.fastMode`; toggled at runtime via `/fast`. `_modelPinned`
|
|
1491
|
+
// tracks an explicit `/model x` so fast mode never overrides a chosen model.
|
|
1492
|
+
const {
|
|
1493
|
+
parseFastCommand: _parseFastCommand,
|
|
1494
|
+
resolveFastPlan: _resolveFastPlan,
|
|
1495
|
+
renderFastStatus: _renderFastStatus,
|
|
1496
|
+
} = await import("./fast-mode.js");
|
|
1497
|
+
let _fastMode = false;
|
|
1498
|
+
let _modelPinned = false;
|
|
1499
|
+
try {
|
|
1500
|
+
const { getConfigValue } = await import("../lib/config-manager.js");
|
|
1501
|
+
_fastMode = getConfigValue("cli.fastMode") === true;
|
|
1502
|
+
} catch (_e) {
|
|
1503
|
+
/* config optional */
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// Voice dictation (Claude-Code `/voice` parity). Local-first STT priority;
|
|
1507
|
+
// degrades cleanly on headless/SSH. Real capture is a host binding — probed
|
|
1508
|
+
// from config `voice.backends` (map of backend id → true) — so with no
|
|
1509
|
+
// backend bundled, `/voice hold` reports how to enable one rather than
|
|
1510
|
+
// pretending to listen. `voice.allowCloud` gates cloud transcription.
|
|
1511
|
+
const {
|
|
1512
|
+
parseVoiceCommand: _parseVoiceCommand,
|
|
1513
|
+
resolveSttBackend: _resolveSttBackend,
|
|
1514
|
+
detectVoiceEnvironment: _detectVoiceEnvironment,
|
|
1515
|
+
renderVoiceStatus: _renderVoiceStatus,
|
|
1516
|
+
} = await import("./voice-dictation.js");
|
|
1517
|
+
let _voiceMode = "off";
|
|
1518
|
+
let _voiceConfig = { backends: {}, allowCloud: false };
|
|
1519
|
+
try {
|
|
1520
|
+
const { getConfigValue } = await import("../lib/config-manager.js");
|
|
1521
|
+
const v = getConfigValue("voice") || {};
|
|
1522
|
+
_voiceConfig = {
|
|
1523
|
+
backends: v.backends || {},
|
|
1524
|
+
allowCloud: v.allowCloud === true,
|
|
1525
|
+
};
|
|
1526
|
+
} catch (_e) {
|
|
1527
|
+
/* config optional */
|
|
1528
|
+
}
|
|
1308
1529
|
const themedPrompt = (text) => {
|
|
1309
1530
|
const a = promptAccent(_theme);
|
|
1310
1531
|
if (a === "blue") return chalk.blue(text);
|
|
@@ -1349,6 +1570,7 @@ export async function startAgentRepl(options = {}) {
|
|
|
1349
1570
|
"/cd",
|
|
1350
1571
|
"/clear",
|
|
1351
1572
|
"/compact",
|
|
1573
|
+
"/checkup",
|
|
1352
1574
|
"/config",
|
|
1353
1575
|
"/context",
|
|
1354
1576
|
"/copy",
|
|
@@ -1358,6 +1580,7 @@ export async function startAgentRepl(options = {}) {
|
|
|
1358
1580
|
"/effort",
|
|
1359
1581
|
"/exit",
|
|
1360
1582
|
"/export",
|
|
1583
|
+
"/fast",
|
|
1361
1584
|
"/help",
|
|
1362
1585
|
"/hooks",
|
|
1363
1586
|
"/ide",
|
|
@@ -1377,6 +1600,7 @@ export async function startAgentRepl(options = {}) {
|
|
|
1377
1600
|
"/release-notes",
|
|
1378
1601
|
"/reload-plugins",
|
|
1379
1602
|
"/reload-skills",
|
|
1603
|
+
"/remote-control",
|
|
1380
1604
|
"/review",
|
|
1381
1605
|
"/rewind",
|
|
1382
1606
|
"/search",
|
|
@@ -1392,8 +1616,10 @@ export async function startAgentRepl(options = {}) {
|
|
|
1392
1616
|
"/theme",
|
|
1393
1617
|
"/think",
|
|
1394
1618
|
"/todos",
|
|
1619
|
+
"/tui",
|
|
1395
1620
|
"/ultrathink",
|
|
1396
1621
|
"/vim",
|
|
1622
|
+
"/voice",
|
|
1397
1623
|
],
|
|
1398
1624
|
// User/project custom commands (.claude/commands/*.md) join TAB completion
|
|
1399
1625
|
// alongside the built-ins above. Sync + best-effort; the completer
|
|
@@ -1436,6 +1662,37 @@ export async function startAgentRepl(options = {}) {
|
|
|
1436
1662
|
// "close" cleanup instead of a bare exit.
|
|
1437
1663
|
_replRl = rl;
|
|
1438
1664
|
|
|
1665
|
+
// Inbound channels (--channels, gap-2026-07-11 P0#5): external events enter
|
|
1666
|
+
// this session as user turns. rl.emit("line") reuses the REPL's own input
|
|
1667
|
+
// path, so an event landing mid-turn queues behind it (pending-lines), and
|
|
1668
|
+
// the [channel:...] prefix keeps provenance visible while guaranteeing the
|
|
1669
|
+
// injected line can never read as a slash command or shell escape.
|
|
1670
|
+
let _channels = null;
|
|
1671
|
+
if (options.channels) {
|
|
1672
|
+
try {
|
|
1673
|
+
const { startChannels, formatChannelEvent } =
|
|
1674
|
+
await import("../lib/channels/channel-manager.js");
|
|
1675
|
+
let channelsConfig = {};
|
|
1676
|
+
try {
|
|
1677
|
+
channelsConfig =
|
|
1678
|
+
(await import("../lib/config-manager.js")).loadConfig()?.channels ||
|
|
1679
|
+
{};
|
|
1680
|
+
} catch (_err) {
|
|
1681
|
+
// config read is best-effort; channels validate their own inputs
|
|
1682
|
+
}
|
|
1683
|
+
_channels = await startChannels(options.channels, {
|
|
1684
|
+
config: channelsConfig,
|
|
1685
|
+
onEvent: (event) => rl.emit("line", formatChannelEvent(event)),
|
|
1686
|
+
log: (msg) => logger.info(msg),
|
|
1687
|
+
});
|
|
1688
|
+
for (const c of _channels.channels) {
|
|
1689
|
+
logger.info(`channel ready: ${c.describe}`);
|
|
1690
|
+
}
|
|
1691
|
+
} catch (err) {
|
|
1692
|
+
logger.warn(`channels: ${err.message}`);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1439
1696
|
// Vim-mode plumbing: capture readline's OWN keypress listeners now so we can
|
|
1440
1697
|
// suspend them while in NORMAL mode (the engine drives editing then) and
|
|
1441
1698
|
// reattach them for INSERT mode (readline's rich editing/history/completion).
|
|
@@ -1876,6 +2133,15 @@ export async function startAgentRepl(options = {}) {
|
|
|
1876
2133
|
logger.log(
|
|
1877
2134
|
` ${chalk.cyan("/theme")} Color theme (/theme <auto|dark|light|mono>; mono = no color)`,
|
|
1878
2135
|
);
|
|
2136
|
+
logger.log(
|
|
2137
|
+
` ${chalk.cyan("/tui")} Fullscreen no-flicker view (/tui <fullscreen|default>; CC_NO_FLICKER=1)`,
|
|
2138
|
+
);
|
|
2139
|
+
logger.log(
|
|
2140
|
+
` ${chalk.cyan("/fast")} Latency profile — faster+cheaper, less reasoning (/fast on|off|status)`,
|
|
2141
|
+
);
|
|
2142
|
+
logger.log(
|
|
2143
|
+
` ${chalk.cyan("/voice")} Speech dictation — local-first STT (/voice hold|tap|off)`,
|
|
2144
|
+
);
|
|
1879
2145
|
logger.log(
|
|
1880
2146
|
` ${chalk.cyan("/output-style")} Response persona (/output-style <name|list>; explanatory/learning built-in)`,
|
|
1881
2147
|
);
|
|
@@ -1883,7 +2149,7 @@ export async function startAgentRepl(options = {}) {
|
|
|
1883
2149
|
` ${chalk.cyan("/config")} Show config; ${chalk.cyan("/config <key>")} read, ${chalk.cyan("/config <key>=<val>")} set, ${chalk.cyan("/config --help")} keys`,
|
|
1884
2150
|
);
|
|
1885
2151
|
logger.log(
|
|
1886
|
-
` ${chalk.cyan("/doctor")} Session health check (provider/key/IDE/MCP/hooks)`,
|
|
2152
|
+
` ${chalk.cyan("/doctor")} Session health check (provider/key/IDE/MCP/hooks; alias /checkup)`,
|
|
1887
2153
|
);
|
|
1888
2154
|
logger.log(
|
|
1889
2155
|
` ${chalk.cyan("/status")} Environment snapshot (version/model/session/cwd/roots/IDE·MCP·hooks)`,
|
|
@@ -1909,6 +2175,9 @@ export async function startAgentRepl(options = {}) {
|
|
|
1909
2175
|
logger.log(
|
|
1910
2176
|
` ${chalk.cyan("/permissions")} Allow/ask/deny rules; set/cycle tier (/permissions <tier> · Shift+Tab); /permissions denials to review blocked calls`,
|
|
1911
2177
|
);
|
|
2178
|
+
logger.log(
|
|
2179
|
+
` ${chalk.cyan("/remote-control")} Answer permission prompts from a paired phone/web device (on|off|status; alias /rc)`,
|
|
2180
|
+
);
|
|
1912
2181
|
logger.log(
|
|
1913
2182
|
` ${chalk.cyan("/export")} Save this conversation to a Markdown file (/export [path])`,
|
|
1914
2183
|
);
|
|
@@ -2290,6 +2559,7 @@ export async function startAgentRepl(options = {}) {
|
|
|
2290
2559
|
if (arg) {
|
|
2291
2560
|
model = arg;
|
|
2292
2561
|
_curModel = model; // keep the status-line readout in sync
|
|
2562
|
+
_modelPinned = true; // explicit choice — fast mode must not override it
|
|
2293
2563
|
logger.info(`Model: ${chalk.cyan(model)}`);
|
|
2294
2564
|
// Claude-Code 2.1.183 parity: warn when the newly-selected model is a
|
|
2295
2565
|
// provider-retired/deprecated snapshot. Headless paths already warn via
|
|
@@ -2468,6 +2738,149 @@ export async function startAgentRepl(options = {}) {
|
|
|
2468
2738
|
return;
|
|
2469
2739
|
}
|
|
2470
2740
|
|
|
2741
|
+
// `/tui` — fullscreen no-flicker view (Claude-Code parity). `fullscreen`
|
|
2742
|
+
// enters the alternate screen buffer; `default` returns to streaming
|
|
2743
|
+
// scrollback. No arg prints the current mode. Persisted to `cli.tuiMode`.
|
|
2744
|
+
if (trimmed === "/tui" || trimmed.startsWith("/tui ")) {
|
|
2745
|
+
const arg = trimmed.slice("/tui".length).trim().toLowerCase();
|
|
2746
|
+
if (!arg) {
|
|
2747
|
+
logger.log(chalk.gray(_renderTuiStatus(_tuiMode)));
|
|
2748
|
+
prompt();
|
|
2749
|
+
return;
|
|
2750
|
+
}
|
|
2751
|
+
const next = _resolveTuiMode({ arg });
|
|
2752
|
+
if (arg !== "fullscreen" && arg !== "default") {
|
|
2753
|
+
logger.error(
|
|
2754
|
+
chalk.red(`Unknown TUI mode "${arg}". Use: fullscreen | default`),
|
|
2755
|
+
);
|
|
2756
|
+
prompt();
|
|
2757
|
+
return;
|
|
2758
|
+
}
|
|
2759
|
+
if (_screenReaderMode && next === "fullscreen") {
|
|
2760
|
+
logger.info(
|
|
2761
|
+
chalk.gray(
|
|
2762
|
+
"Screen-reader mode is on — staying in default (scrollback) view.",
|
|
2763
|
+
),
|
|
2764
|
+
);
|
|
2765
|
+
prompt();
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
_setTuiMode(next);
|
|
2769
|
+
try {
|
|
2770
|
+
const { setConfigValue } = await import("../lib/config-manager.js");
|
|
2771
|
+
setConfigValue("cli.tuiMode", _tuiMode);
|
|
2772
|
+
} catch (_e) {
|
|
2773
|
+
/* persistence is best-effort */
|
|
2774
|
+
}
|
|
2775
|
+
logger.log(chalk.gray(_renderTuiStatus(_tuiMode)));
|
|
2776
|
+
prompt();
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
// `/fast` — latency profile (Claude-Code parity). on/off/status/toggle.
|
|
2781
|
+
// When on, the next turn minimizes reasoning and swaps to the provider's
|
|
2782
|
+
// low-latency model (unless a model is pinned). Persisted to cli.fastMode.
|
|
2783
|
+
{
|
|
2784
|
+
const fast = _parseFastCommand(trimmed);
|
|
2785
|
+
if (fast) {
|
|
2786
|
+
if (fast.error) {
|
|
2787
|
+
logger.error(chalk.red(fast.error));
|
|
2788
|
+
prompt();
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
if (fast.action === "status") {
|
|
2792
|
+
logger.log(
|
|
2793
|
+
chalk.gray(
|
|
2794
|
+
_renderFastStatus({ enabled: _fastMode, provider, model }),
|
|
2795
|
+
),
|
|
2796
|
+
);
|
|
2797
|
+
prompt();
|
|
2798
|
+
return;
|
|
2799
|
+
}
|
|
2800
|
+
_fastMode =
|
|
2801
|
+
fast.action === "on"
|
|
2802
|
+
? true
|
|
2803
|
+
: fast.action === "off"
|
|
2804
|
+
? false
|
|
2805
|
+
: !_fastMode;
|
|
2806
|
+
try {
|
|
2807
|
+
const { setConfigValue } = await import("../lib/config-manager.js");
|
|
2808
|
+
setConfigValue("cli.fastMode", _fastMode);
|
|
2809
|
+
} catch (_e) {
|
|
2810
|
+
/* persistence is best-effort */
|
|
2811
|
+
}
|
|
2812
|
+
logger.log(
|
|
2813
|
+
chalk.gray(
|
|
2814
|
+
_renderFastStatus({ enabled: _fastMode, provider, model }),
|
|
2815
|
+
),
|
|
2816
|
+
);
|
|
2817
|
+
prompt();
|
|
2818
|
+
return;
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
// `/voice` — speech dictation (Claude-Code parity). hold=push-to-talk,
|
|
2823
|
+
// tap=toggle, off, status. Local-first STT; degrades on headless/SSH or
|
|
2824
|
+
// when no capture backend is configured (voice.backends / voice.allowCloud).
|
|
2825
|
+
{
|
|
2826
|
+
const voice = _parseVoiceCommand(trimmed);
|
|
2827
|
+
if (voice) {
|
|
2828
|
+
if (voice.error) {
|
|
2829
|
+
logger.error(chalk.red(voice.error));
|
|
2830
|
+
prompt();
|
|
2831
|
+
return;
|
|
2832
|
+
}
|
|
2833
|
+
const voiceEnv = _detectVoiceEnvironment({
|
|
2834
|
+
env: process.env,
|
|
2835
|
+
isTTY: Boolean(process.stdin.isTTY),
|
|
2836
|
+
});
|
|
2837
|
+
if (voice.action === "status") {
|
|
2838
|
+
const be = _resolveSttBackend(_voiceConfig).backend;
|
|
2839
|
+
logger.log(
|
|
2840
|
+
chalk.gray(
|
|
2841
|
+
_renderVoiceStatus({
|
|
2842
|
+
mode: _voiceMode,
|
|
2843
|
+
backend: be,
|
|
2844
|
+
env: voiceEnv,
|
|
2845
|
+
}),
|
|
2846
|
+
),
|
|
2847
|
+
);
|
|
2848
|
+
prompt();
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
if (voice.action === "off") {
|
|
2852
|
+
_voiceMode = "off";
|
|
2853
|
+
logger.log(chalk.gray("Voice: off."));
|
|
2854
|
+
prompt();
|
|
2855
|
+
return;
|
|
2856
|
+
}
|
|
2857
|
+
// hold | tap — need a usable environment AND a resolvable backend.
|
|
2858
|
+
if (!voiceEnv.supported) {
|
|
2859
|
+
logger.info(chalk.yellow(`Voice unavailable — ${voiceEnv.reason}.`));
|
|
2860
|
+
prompt();
|
|
2861
|
+
return;
|
|
2862
|
+
}
|
|
2863
|
+
const { backend, reason } = _resolveSttBackend(_voiceConfig);
|
|
2864
|
+
if (!backend) {
|
|
2865
|
+
logger.info(chalk.yellow(`Voice: ${reason}`));
|
|
2866
|
+
prompt();
|
|
2867
|
+
return;
|
|
2868
|
+
}
|
|
2869
|
+
_voiceMode = voice.action;
|
|
2870
|
+
logger.log(
|
|
2871
|
+
chalk.gray(
|
|
2872
|
+
_renderVoiceStatus({
|
|
2873
|
+
mode: _voiceMode,
|
|
2874
|
+
backend,
|
|
2875
|
+
env: voiceEnv,
|
|
2876
|
+
}) + ` (${reason})`,
|
|
2877
|
+
),
|
|
2878
|
+
);
|
|
2879
|
+
prompt();
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
|
|
2471
2884
|
if (trimmed === "/vim" || trimmed.startsWith("/vim ")) {
|
|
2472
2885
|
const arg = trimmed.slice("/vim".length).trim().toLowerCase();
|
|
2473
2886
|
const turnOn = arg === "on" || (arg === "" && !_vimEnabled);
|
|
@@ -2870,6 +3283,18 @@ export async function startAgentRepl(options = {}) {
|
|
|
2870
3283
|
const resumeId = sessionArg.slice(7).trim();
|
|
2871
3284
|
try {
|
|
2872
3285
|
if (useJsonl && sessionExists(resumeId)) {
|
|
3286
|
+
try {
|
|
3287
|
+
const { verifySession } =
|
|
3288
|
+
await import("../harness/jsonl-session-store.js");
|
|
3289
|
+
const trust = verifySession(resumeId);
|
|
3290
|
+
if (trust.status === "tampered") {
|
|
3291
|
+
logger.warn(
|
|
3292
|
+
`⚠ Transcript integrity check FAILED (${trust.reason}) — restored context is untrusted. Run 'cc session verify ${resumeId}'.`,
|
|
3293
|
+
);
|
|
3294
|
+
}
|
|
3295
|
+
} catch (_err) {
|
|
3296
|
+
// verification is best-effort in the interactive path
|
|
3297
|
+
}
|
|
2873
3298
|
const rebuilt = rebuildMessages(resumeId);
|
|
2874
3299
|
messages.length = 1; // keep system prompt
|
|
2875
3300
|
messages.push(...rebuilt.filter((m) => m.role !== "system"));
|
|
@@ -3812,7 +4237,13 @@ export async function startAgentRepl(options = {}) {
|
|
|
3812
4237
|
|
|
3813
4238
|
// `/doctor` — consolidated session-health readout (Claude-Code parity):
|
|
3814
4239
|
// provider/key/IDE/MCP/permissions/hooks in one pass-or-warn view.
|
|
3815
|
-
|
|
4240
|
+
// `/checkup` is the Claude-Code 2.1.205 alias for the same readout.
|
|
4241
|
+
if (
|
|
4242
|
+
trimmed === "/doctor" ||
|
|
4243
|
+
trimmed.startsWith("/doctor ") ||
|
|
4244
|
+
trimmed === "/checkup" ||
|
|
4245
|
+
trimmed.startsWith("/checkup ")
|
|
4246
|
+
) {
|
|
3816
4247
|
let config = {};
|
|
3817
4248
|
try {
|
|
3818
4249
|
config = (await import("../lib/config-manager.js")).loadConfig() || {};
|
|
@@ -4028,6 +4459,57 @@ export async function startAgentRepl(options = {}) {
|
|
|
4028
4459
|
return;
|
|
4029
4460
|
}
|
|
4030
4461
|
|
|
4462
|
+
// `/remote-control` — pair a phone/web device to answer this session's
|
|
4463
|
+
// permission prompts (批17 slash 入口 + 批18 REPL 接线). First answer —
|
|
4464
|
+
// terminal or device — wins; devices get permission.request cards + push.
|
|
4465
|
+
if (
|
|
4466
|
+
trimmed === "/remote-control" ||
|
|
4467
|
+
trimmed.startsWith("/remote-control ") ||
|
|
4468
|
+
trimmed === "/rc" ||
|
|
4469
|
+
trimmed.startsWith("/rc ")
|
|
4470
|
+
) {
|
|
4471
|
+
const arg = trimmed
|
|
4472
|
+
.replace(/^\/(remote-control|rc)/, "")
|
|
4473
|
+
.trim()
|
|
4474
|
+
.toLowerCase();
|
|
4475
|
+
if (arg === "off" || arg === "stop") {
|
|
4476
|
+
if (!_remoteApproval) {
|
|
4477
|
+
logger.info("remote-control is not active.");
|
|
4478
|
+
} else {
|
|
4479
|
+
await _stopRemoteApproval();
|
|
4480
|
+
logger.info(
|
|
4481
|
+
"remote-control stopped — approvals are local-only again.",
|
|
4482
|
+
);
|
|
4483
|
+
}
|
|
4484
|
+
} else if (arg === "status") {
|
|
4485
|
+
if (!_remoteApproval) {
|
|
4486
|
+
logger.info("remote-control: off — start with /remote-control");
|
|
4487
|
+
} else {
|
|
4488
|
+
logger.log(` pairing: ${_remoteApproval.pairing.uri}`);
|
|
4489
|
+
const approvers = await _remoteApproval.bridge.approverCount();
|
|
4490
|
+
logger.log(
|
|
4491
|
+
` paired approvers: ${approvers}${approvers === 0 ? " (scan the pairing URI from the mobile app or web panel)" : ""}`,
|
|
4492
|
+
);
|
|
4493
|
+
}
|
|
4494
|
+
} else if (arg === "" || arg === "on" || arg === "start") {
|
|
4495
|
+
if (_remoteApproval) {
|
|
4496
|
+
logger.info(
|
|
4497
|
+
"remote-control already running — /remote-control status",
|
|
4498
|
+
);
|
|
4499
|
+
} else {
|
|
4500
|
+
try {
|
|
4501
|
+
await _startRemoteApproval();
|
|
4502
|
+
} catch (err) {
|
|
4503
|
+
logger.info(`remote-control failed to start: ${err.message}`);
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
} else {
|
|
4507
|
+
logger.info("Usage: /remote-control [on|off|status] (alias /rc)");
|
|
4508
|
+
}
|
|
4509
|
+
prompt();
|
|
4510
|
+
return;
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4031
4513
|
// `/cost` — running token spend + estimated $ for this session (Claude-Code
|
|
4032
4514
|
// parity). In-memory accumulation, so it works without session persistence.
|
|
4033
4515
|
if (trimmed === "/cost" || trimmed.startsWith("/cost ")) {
|
|
@@ -4470,6 +4952,22 @@ export async function startAgentRepl(options = {}) {
|
|
|
4470
4952
|
}
|
|
4471
4953
|
}
|
|
4472
4954
|
|
|
4955
|
+
// Fast mode (P2#12): minimize reasoning + swap to the provider's
|
|
4956
|
+
// low-latency model for this turn (never over a pinned model). Applied
|
|
4957
|
+
// after auto-selection so it has the last word on latency.
|
|
4958
|
+
let turnThinking = thinking;
|
|
4959
|
+
if (_fastMode) {
|
|
4960
|
+
const fastPlan = _resolveFastPlan({
|
|
4961
|
+
enabled: true,
|
|
4962
|
+
provider,
|
|
4963
|
+
model: activeModel,
|
|
4964
|
+
modelPinned: _modelPinned,
|
|
4965
|
+
});
|
|
4966
|
+
turnThinking = fastPlan.thinking;
|
|
4967
|
+
if (fastPlan.swapped) activeModel = fastPlan.model;
|
|
4968
|
+
logger.info(chalk.gray(`[fast] ${fastPlan.note}`));
|
|
4969
|
+
}
|
|
4970
|
+
|
|
4473
4971
|
try {
|
|
4474
4972
|
process.stdout.write("\n");
|
|
4475
4973
|
const iterationBudget = new IterationBudget({ owner: sessionId });
|
|
@@ -4588,7 +5086,7 @@ export async function startAgentRepl(options = {}) {
|
|
|
4588
5086
|
// turn only (provider/baseUrl/apiKey unchanged, model → vision model).
|
|
4589
5087
|
provider: _visionLlm ? _visionLlm.provider : provider,
|
|
4590
5088
|
model: _visionLlm ? _visionLlm.model : activeModel,
|
|
4591
|
-
thinking,
|
|
5089
|
+
thinking: turnThinking,
|
|
4592
5090
|
thinkingBudget,
|
|
4593
5091
|
baseUrl: _visionLlm ? _visionLlm.baseUrl : baseUrl,
|
|
4594
5092
|
apiKey: _visionLlm ? _visionLlm.apiKey : apiKey,
|
|
@@ -4641,6 +5139,10 @@ export async function startAgentRepl(options = {}) {
|
|
|
4641
5139
|
provider: ue.provider,
|
|
4642
5140
|
model: ue.model,
|
|
4643
5141
|
usage: ue.usage,
|
|
5142
|
+
// 用量归因: sub-agent / isolated-skill usage carries its
|
|
5143
|
+
// attribution frame into the transcript; main-loop events stay
|
|
5144
|
+
// exactly as before (absence ⇒ origin "main").
|
|
5145
|
+
...(ue.attribution ? { attribution: ue.attribution } : {}),
|
|
4644
5146
|
});
|
|
4645
5147
|
} catch (_e) {
|
|
4646
5148
|
/* best-effort */
|
|
@@ -4876,6 +5378,25 @@ export async function startAgentRepl(options = {}) {
|
|
|
4876
5378
|
});
|
|
4877
5379
|
|
|
4878
5380
|
rl.on("close", async () => {
|
|
5381
|
+
// Leave the alternate screen buffer first so the terminal is restored to
|
|
5382
|
+
// the user's scrollback no matter how the REPL exits.
|
|
5383
|
+
if (_tuiMode === "fullscreen" && process.stdout.isTTY) {
|
|
5384
|
+
try {
|
|
5385
|
+
process.stdout.write(_ALT_LEAVE);
|
|
5386
|
+
} catch (_err) {
|
|
5387
|
+
// best-effort restore
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
// Stop inbound channel listeners before anything else — no new external
|
|
5391
|
+
// events may enter a session that is shutting down.
|
|
5392
|
+
if (_channels) {
|
|
5393
|
+
try {
|
|
5394
|
+
_channels.stop();
|
|
5395
|
+
} catch (_err) {
|
|
5396
|
+
// best-effort
|
|
5397
|
+
}
|
|
5398
|
+
_channels = null;
|
|
5399
|
+
}
|
|
4879
5400
|
// settings.json SessionEnd hooks (observe-only) when the REPL exits.
|
|
4880
5401
|
if (_settingsHooks) {
|
|
4881
5402
|
try {
|
|
@@ -4968,6 +5489,16 @@ export async function startAgentRepl(options = {}) {
|
|
|
4968
5489
|
}
|
|
4969
5490
|
}
|
|
4970
5491
|
|
|
5492
|
+
// Tear down /remote-control (bridge + its self-hosted WS server) so the
|
|
5493
|
+
// pairing endpoint doesn't outlive the session.
|
|
5494
|
+
if (_remoteApproval) {
|
|
5495
|
+
try {
|
|
5496
|
+
await _stopRemoteApproval();
|
|
5497
|
+
} catch (_e) {
|
|
5498
|
+
// Non-critical
|
|
5499
|
+
}
|
|
5500
|
+
}
|
|
5501
|
+
|
|
4971
5502
|
// Kill any background run_shell tasks so a backgrounded command (e.g. a
|
|
4972
5503
|
// dev server) doesn't outlive the REPL session.
|
|
4973
5504
|
try {
|