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.
Files changed (212) hide show
  1. package/README.md +1 -1
  2. package/package.json +1 -1
  3. package/src/assets/web-panel/.build-hash +1 -1
  4. package/src/assets/web-panel/assets/{AIOps-CfMP2Oud.js → AIOps-ByIiF5bT.js} +1 -1
  5. package/src/assets/web-panel/assets/{ActionButton-Zu0N8BQL.js → ActionButton-NlgSFZq5.js} +1 -1
  6. package/src/assets/web-panel/assets/{Analytics-DZTwj5T_.js → Analytics-D17cOUcb.js} +3 -3
  7. package/src/assets/web-panel/assets/{AppLayout-CVFJh47v.js → AppLayout-B4mEZcbl.js} +5 -5
  8. package/src/assets/web-panel/assets/Artifacts-CASsmyQw.js +1 -0
  9. package/src/assets/web-panel/assets/Artifacts-CTVt96zW.css +1 -0
  10. package/src/assets/web-panel/assets/{Audit-BMfiLj7T.js → Audit-DYAia2e2.js} +1 -1
  11. package/src/assets/web-panel/assets/{BackgroundAgents-BslPCdGF.js → BackgroundAgents-C1lvYUSu.js} +1 -1
  12. package/src/assets/web-panel/assets/{Backup-CQ_fMC1r.js → Backup-B6agK6bA.js} +1 -1
  13. package/src/assets/web-panel/assets/{BaseInput-CsFsARiC.js → BaseInput-DM43R2vy.js} +1 -1
  14. package/src/assets/web-panel/assets/{Chat-CwfBobco.js → Chat-Dtdp3eXp.js} +6 -6
  15. package/src/assets/web-panel/assets/Chat-KSGPtU0L.css +1 -0
  16. package/src/assets/web-panel/assets/{ChatBubbleRenderer-K6Nf3xeN.js → ChatBubbleRenderer-BL5c_OQ1.js} +1 -1
  17. package/src/assets/web-panel/assets/{Checkbox-DkSyjjPp.js → Checkbox-D2xf7uZt.js} +1 -1
  18. package/src/assets/web-panel/assets/{Codegen-B7Fx3zZ6.js → Codegen-D3j45jJL.js} +1 -1
  19. package/src/assets/web-panel/assets/{Col-Dzfqn8Ep.js → Col-voPJ9fUZ.js} +1 -1
  20. package/src/assets/web-panel/assets/{Community-DhHPdzMM.js → Community-DEsyTESF.js} +1 -1
  21. package/src/assets/web-panel/assets/{Compact-Ct18_8y7.js → Compact-OHV_QXLD.js} +1 -1
  22. package/src/assets/web-panel/assets/{Compliance-CY64IqBi.js → Compliance-eDTJWC-J.js} +1 -1
  23. package/src/assets/web-panel/assets/{Cowork-BYSmWlUl.js → Cowork-BBvy-6d1.js} +2 -2
  24. package/src/assets/web-panel/assets/{Cron-_4xtP3Ug.js → Cron-Dw2eoJxu.js} +2 -2
  25. package/src/assets/web-panel/assets/{Crosschain-B4qY-Ycr.js → Crosschain-DQHms_Bh.js} +1 -1
  26. package/src/assets/web-panel/assets/{DID-CkCzWkx1.js → DID-Cyu7Id1c.js} +2 -2
  27. package/src/assets/web-panel/assets/{Dashboard-C1scT7Ke.js → Dashboard-sIDzkG92.js} +2 -2
  28. package/src/assets/web-panel/assets/{Dropdown-DBnQ2EWd.js → Dropdown-DUW3T2ey.js} +1 -1
  29. package/src/assets/web-panel/assets/{EmailListRenderer-C9JhOTq0.js → EmailListRenderer-CECELXW8.js} +1 -1
  30. package/src/assets/web-panel/assets/{FamilyGuardDashboard-CWXv5RUv.js → FamilyGuardDashboard-BIZR37ov.js} +1 -1
  31. package/src/assets/web-panel/assets/{Federation-fYdcbz9W.js → Federation-BEZIQX84.js} +1 -1
  32. package/src/assets/web-panel/assets/{FormItemContext-CJqOT3Yj.js → FormItemContext-BcdUMznL.js} +1 -1
  33. package/src/assets/web-panel/assets/{GenericCardRenderer-DGCsHc5K.js → GenericCardRenderer-DsN_fr5H.js} +1 -1
  34. package/src/assets/web-panel/assets/{Git-EH2K-H0Q.js → Git-63LhjBx2.js} +2 -2
  35. package/src/assets/web-panel/assets/{Governance-DJn1X2pR.js → Governance-CYtQtIcH.js} +1 -1
  36. package/src/assets/web-panel/assets/{Inference-Dch1E8qd.js → Inference-BwR6UUox.js} +1 -1
  37. package/src/assets/web-panel/assets/{KnowledgeGraph-QIur8j8-.js → KnowledgeGraph-D9lVNTUA.js} +1 -1
  38. package/src/assets/web-panel/assets/{Logs-DUgHldPg.js → Logs-C4q5h7Ut.js} +2 -2
  39. package/src/assets/web-panel/assets/MarkdownRenderer-DbzhFwvz.css +1 -0
  40. package/src/assets/web-panel/assets/MarkdownRenderer-DexLlXfN.js +1 -0
  41. package/src/assets/web-panel/assets/{Marketplace-DafrSfvd.js → Marketplace-Plksliuw.js} +1 -1
  42. package/src/assets/web-panel/assets/{McpTools-D2xRbfJx.js → McpTools-DZUFfeuG.js} +3 -3
  43. package/src/assets/web-panel/assets/{Memory-C6RbVHAo.js → Memory-NoDYt29V.js} +2 -2
  44. package/src/assets/web-panel/assets/{MobileBridge-BPtVBKqn.js → MobileBridge-CEXGTkdh.js} +1 -1
  45. package/src/assets/web-panel/assets/{MobileProjects-Cuw832cr.js → MobileProjects-DnbJkbZo.js} +1 -1
  46. package/src/assets/web-panel/assets/{Mtc-D6O9fWFa.js → Mtc-DTAa0cwy.js} +5 -5
  47. package/src/assets/web-panel/assets/{MtcAudit-CWZcXIH1.js → MtcAudit-CVV7LnNS.js} +4 -4
  48. package/src/assets/web-panel/assets/{Multisig-DhIBxTFd.js → Multisig-BqqGrjt_.js} +3 -3
  49. package/src/assets/web-panel/assets/{NLProgramming-wkBFS33e.js → NLProgramming-C4WP8DRp.js} +1 -1
  50. package/src/assets/web-panel/assets/{Notes-B09FxncA.js → Notes-Zo4DJUVI.js} +3 -3
  51. package/src/assets/web-panel/assets/{NotificationSettings-oQwtH6-9.js → NotificationSettings-DBcjRaz2.js} +1 -1
  52. package/src/assets/web-panel/assets/OrderTableRenderer-BCQel9bD.js +1 -0
  53. package/src/assets/web-panel/assets/{Organization-CsF-NIjz.js → Organization-BT9WPBOX.js} +4 -4
  54. package/src/assets/web-panel/assets/{Overflow-bLxv9Dqi.js → Overflow-D6b4bZ3q.js} +1 -1
  55. package/src/assets/web-panel/assets/{P2P-DVwQtm4q.js → P2P-Cw3OsYx-.js} +2 -2
  56. package/src/assets/web-panel/assets/{PdhVaultBrowser-Bgic3QKm.js → PdhVaultBrowser-C0nyiK2b.js} +4 -4
  57. package/src/assets/web-panel/assets/{Permissions-as80-9ep.js → Permissions-zVBZEB_C.js} +3 -3
  58. package/src/assets/web-panel/assets/{PersonalDataHub-BGmUw36a.js → PersonalDataHub-BOspsFew.js} +2 -2
  59. package/src/assets/web-panel/assets/{Pipeline-BM96zBxG.js → Pipeline-CQWfZ4XH.js} +1 -1
  60. package/src/assets/web-panel/assets/{Privacy-BBOzBSWe.js → Privacy-C2oGJ_WL.js} +1 -1
  61. package/src/assets/web-panel/assets/{ProjectInit-GHX41V28.js → ProjectInit-BZMR4qY1.js} +2 -2
  62. package/src/assets/web-panel/assets/{ProjectSettings-DraqzxKD.js → ProjectSettings-DAujoP63.js} +2 -2
  63. package/src/assets/web-panel/assets/{Projects-BzzdUml4.js → Projects-DvH_P2Uq.js} +1 -1
  64. package/src/assets/web-panel/assets/{Providers-CSeYNAtW.js → Providers-D_RGHM-v.js} +1 -1
  65. package/src/assets/web-panel/assets/{QrScannerModal-DlzBCdXh.js → QrScannerModal-Mf6KfL2J.js} +1 -1
  66. package/src/assets/web-panel/assets/{QuickAsk-p8iB7od_.js → QuickAsk-5D8URoI_.js} +1 -1
  67. package/src/assets/web-panel/assets/{Recommend-DjOVXacd.js → Recommend-Df4kC8Gy.js} +1 -1
  68. package/src/assets/web-panel/assets/RemoteSession-DGi3f3vO.js +4 -0
  69. package/src/assets/web-panel/assets/{Reputation-CjT3M0Di.js → Reputation-jmkGFNWb.js} +1 -1
  70. package/src/assets/web-panel/assets/{Row-tw8m6T0c.js → Row-DP6Pcu3m.js} +1 -1
  71. package/src/assets/web-panel/assets/{RssFeed-B47KBx_f.js → RssFeed-D60hSuPO.js} +3 -3
  72. package/src/assets/web-panel/assets/{Search-D2sx1PBe.js → Search-0mf1BQUK.js} +1 -1
  73. package/src/assets/web-panel/assets/{Security-JmtL8g7D.js → Security-CfZtLFYV.js} +3 -3
  74. package/src/assets/web-panel/assets/{Services-BxrNqjin.js → Services-D9zpZLWz.js} +2 -2
  75. package/src/assets/web-panel/assets/{Skeleton-udk8L18t.js → Skeleton-D1_44eMD.js} +1 -1
  76. package/src/assets/web-panel/assets/{Skills-DooLT9of.js → Skills-cVFa8zkz.js} +1 -1
  77. package/src/assets/web-panel/assets/{Sla-ITVZE1xI.js → Sla-Cinpzc3L.js} +1 -1
  78. package/src/assets/web-panel/assets/{SpeechSettings-DCkfqY1n.js → SpeechSettings-bqdiJE_P.js} +1 -1
  79. package/src/assets/web-panel/assets/{SyncSettings-CkNlgFjM.js → SyncSettings-Dgl_SLgj.js} +2 -2
  80. package/src/assets/web-panel/assets/{Tasks-BMAoyCM_.js → Tasks-DawWAEhy.js} +1 -1
  81. package/src/assets/web-panel/assets/{Templates-BnrFTlrz.js → Templates-Ca0nY651.js} +1 -1
  82. package/src/assets/web-panel/assets/{Tenant-BjNsy3-i.js → Tenant-CgaGEhLm.js} +1 -1
  83. package/src/assets/web-panel/assets/{Terminal-C0gxLL7V.js → Terminal-D-9EnpRx.js} +2 -2
  84. package/src/assets/web-panel/assets/{TimelineRenderer-CUtj_KHA.js → TimelineRenderer-CSOxDHZ6.js} +1 -1
  85. package/src/assets/web-panel/assets/{Tokens-DOg61zlq.js → Tokens-BpwLZpL3.js} +1 -1
  86. package/src/assets/web-panel/assets/{Trigger-D4ldCewj.js → Trigger-DgowwBK5.js} +1 -1
  87. package/src/assets/web-panel/assets/{Trust-3lk_Zd8j.js → Trust-BZutapjE.js} +1 -1
  88. package/src/assets/web-panel/assets/{UkeySign-kWhFR6Li.js → UkeySign-QZPt8_Sp.js} +1 -1
  89. package/src/assets/web-panel/assets/{VideoEditing-D3GOFVWg.js → VideoEditing-DlQx5UDJ.js} +1 -1
  90. package/src/assets/web-panel/assets/{Wallet-DzrZfBuh.js → Wallet-ZfdTgtWA.js} +4 -4
  91. package/src/assets/web-panel/assets/{WebAuthn-CyVJVp_g.js → WebAuthn-CAp0y_Cr.js} +5 -5
  92. package/src/assets/web-panel/assets/{WorkflowEditor-i_b0wi9b.js → WorkflowEditor-BVITZEmr.js} +1 -1
  93. package/src/assets/web-panel/assets/{chat-DwPCK8I7.js → chat-CC2tPaNg.js} +1 -1
  94. package/src/assets/web-panel/assets/{colors-D0vaVqZU.js → colors-31brc9CA.js} +1 -1
  95. package/src/assets/web-panel/assets/{compact-item-Don7nGW2.js → compact-item-DLMkC9C-.js} +1 -1
  96. package/src/assets/web-panel/assets/{createContext-D84R5qy2.js → createContext-BMtveI6F.js} +1 -1
  97. package/src/assets/web-panel/assets/devWarning-CB8pPOtd.js +1 -0
  98. package/src/assets/web-panel/assets/{hasIn-BeUqVVKU.js → hasIn-DbD6WHW2.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-m9bn0F1p.js → index-3oBsXZmF.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-CfYRv-zA.js → index-ALvoNTRF.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-4MkfPLGv.js → index-AaIKG60c.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-LMo9bBLJ.js → index-BFRLmCR3.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-wesQQ2cH.js → index-BKAB6-6V.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CzT56x-_.js → index-BOM-VFlv.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-L73NrmlW.js → index-BPpS-lGb.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-WRw1pM5E.js → index-Bbc2JeQG.js} +1 -1
  107. package/src/assets/web-panel/assets/index-BhXiaiev.js +1 -0
  108. package/src/assets/web-panel/assets/{index-Br3SOdER.js → index-BiRQUoyE.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-D8r7WKk-.js → index-BnFLzejT.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-CAdCTTBC.js → index-BoO4WEJB.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BlfTf0ka.js → index-C-cGlCDW.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-B57al0Z5.js → index-C1PfeI88.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-D4b5XqIy.js → index-C21pdBqs.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-CnnxzIx0.js → index-C7LHNw_N.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BNpJf-sf.js → index-CEXa-4zd.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DT40zd7K.js → index-CEwjBA9q.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CL71rIwo.js → index-CFXKbOek.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-BZ1N07Sp.js → index-CJhREOlG.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-C0_sKVi7.js → index-COHHQ9cU.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-DBzBLX-K.js → index-CUmLMFsy.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-De85D9Pc.js → index-CWgfipcC.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-x7VYlV41.js → index-C_eUKGYP.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-hjgrIX5I.js → index-CcVszn2b.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-DMT8n9vB.js → index-Cod97Urs.js} +3 -3
  125. package/src/assets/web-panel/assets/{index-DFFzwrNs.js → index-Cwbqh0KZ.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-YKLCotoo.js → index-D67kIagl.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-IGGttsCv.js → index-D7cV0tu0.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-DQor_7NF.js → index-DJvxFyIz.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-D84BnuFg.js → index-DZl-UXB6.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-CrnVEEQO.js → index-Dm0byjm1.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BER98o4P.js → index-DndNnYc0.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-DA0-EJKx.js → index-WQUO2vvK.js} +1 -1
  133. package/src/assets/web-panel/assets/index-WpaYaE3D.js +1 -0
  134. package/src/assets/web-panel/assets/{index-CExWtm98.js → index-bdaJwTXJ.js} +1 -1
  135. package/src/assets/web-panel/assets/{index-MKZrJrP_.js → index-ep6JchNG.js} +1 -1
  136. package/src/assets/web-panel/assets/{index-7QUwcGTg.js → index-jG6h22Qg.js} +1 -1
  137. package/src/assets/web-panel/assets/{index-BohrJpBf.js → index-ntVQqSML.js} +1 -1
  138. package/src/assets/web-panel/assets/{initDefaultProps-DGeU4vPS.js → initDefaultProps-D5UoBRhN.js} +1 -1
  139. package/src/assets/web-panel/assets/{motion-Dgqj9ab3.js → motion-DltOw0CR.js} +1 -1
  140. package/src/assets/web-panel/assets/{move-BFyEU6Lr.js → move-_oPkk5zj.js} +1 -1
  141. package/src/assets/web-panel/assets/{mtc-parser-CueCXXPC.js → mtc-parser-EES8ROCS.js} +1 -1
  142. package/src/assets/web-panel/assets/{omit-D2pU5OyE.js → omit-B-_39x_M.js} +1 -1
  143. package/src/assets/web-panel/assets/{pickAttrs-CKTf6zD6.js → pickAttrs-l6D0yJHz.js} +1 -1
  144. package/src/assets/web-panel/assets/{placementArrow-DieJaGCN.js → placementArrow-DIeUK31L.js} +1 -1
  145. package/src/assets/web-panel/assets/{responsiveObserve-Bxr-rmU6.js → responsiveObserve-dG4EzUQo.js} +1 -1
  146. package/src/assets/web-panel/assets/{slide-CDEDk3Se.js → slide-DWFzYGdH.js} +1 -1
  147. package/src/assets/web-panel/assets/{statusUtils-icf2eCTx.js → statusUtils-CECdTA5H.js} +1 -1
  148. package/src/assets/web-panel/assets/{styleChecker-qKvgmBhE.js → styleChecker-D6kP-mMK.js} +1 -1
  149. package/src/assets/web-panel/assets/{useFlexGapSupport-1_UhiyJZ.js → useFlexGapSupport-BWfna_rd.js} +1 -1
  150. package/src/assets/web-panel/assets/{useFs-CFGLXDWh.js → useFs-Bb-j6rzo.js} +1 -1
  151. package/src/assets/web-panel/assets/{usePersonalDataHub-UafOXvQ6.js → usePersonalDataHub-D5OIsYM4.js} +1 -1
  152. package/src/assets/web-panel/assets/{vnode-BfPqSx79.js → vnode-DNpwzFTM.js} +1 -1
  153. package/src/assets/web-panel/assets/{zoom-CXsVcRB6.js → zoom-vNLpFQBW.js} +1 -1
  154. package/src/assets/web-panel/index.html +1 -1
  155. package/src/command-manifest.json +17 -3
  156. package/src/commands/agent.js +11 -1
  157. package/src/commands/background-session.js +143 -3
  158. package/src/commands/browse.js +86 -0
  159. package/src/commands/cloud.js +232 -0
  160. package/src/commands/doctor.js +61 -6
  161. package/src/commands/mcp.js +61 -0
  162. package/src/commands/plugin.js +63 -1
  163. package/src/commands/routine.js +367 -0
  164. package/src/commands/session.js +326 -0
  165. package/src/data/changelog.json +12 -2
  166. package/src/harness/jsonl-session-store.js +82 -2
  167. package/src/harness/session-index.js +295 -0
  168. package/src/harness/session-mirror.js +142 -0
  169. package/src/harness/transcript-integrity.js +182 -0
  170. package/src/index.js +8 -0
  171. package/src/lib/background-agent-supervisor.js +126 -2
  172. package/src/lib/channels/channel-manager.js +126 -0
  173. package/src/lib/channels/telegram-channel.js +112 -0
  174. package/src/lib/channels/webhook-channel.js +140 -0
  175. package/src/lib/chrome-connector.js +425 -4
  176. package/src/lib/cloud/bundle.js +110 -0
  177. package/src/lib/cloud/cloud-client.js +152 -0
  178. package/src/lib/computer-use/authorization.js +173 -0
  179. package/src/lib/computer-use/computer-use-server.js +276 -0
  180. package/src/lib/computer-use/control-backend.js +150 -0
  181. package/src/lib/doctor-checkup.js +652 -0
  182. package/src/lib/jsonl-session-store.js +4 -0
  183. package/src/lib/lsp/lsp-server-registry.js +48 -21
  184. package/src/lib/plugin-runtime/governance.js +174 -0
  185. package/src/lib/pr-link-ledger.js +221 -0
  186. package/src/lib/remote-control.js +35 -0
  187. package/src/lib/routine-store.js +352 -0
  188. package/src/lib/session-store-guard.js +40 -0
  189. package/src/lib/session-usage.js +318 -2
  190. package/src/lib/sub-agent-context.js +23 -0
  191. package/src/lib/web-ui-server.js +120 -7
  192. package/src/repl/agent-repl.js +592 -61
  193. package/src/repl/bg-dashboard.js +355 -0
  194. package/src/repl/fast-mode.js +103 -0
  195. package/src/repl/fullscreen-tui.js +197 -0
  196. package/src/repl/remote-approval.js +111 -0
  197. package/src/repl/slash-command-registry.js +11 -3
  198. package/src/repl/voice-dictation.js +132 -0
  199. package/src/runtime/agent-core.js +278 -1
  200. package/src/runtime/coding-agent-contract-shared.cjs +141 -0
  201. package/src/runtime/coding-agent-policy.cjs +33 -0
  202. package/src/runtime/headless-runner.js +71 -7
  203. package/src/runtime/headless-stream.js +14 -8
  204. package/src/runtime/policies/agent-policy.js +6 -0
  205. package/src/assets/web-panel/assets/Artifacts-BbN5V7au.js +0 -1
  206. package/src/assets/web-panel/assets/Artifacts-CRkepl4t.css +0 -1
  207. package/src/assets/web-panel/assets/Chat-DDh58PCf.css +0 -1
  208. package/src/assets/web-panel/assets/OrderTableRenderer-iFKeNf6Y.js +0 -1
  209. package/src/assets/web-panel/assets/RemoteSession-CIGk7gk-.js +0 -4
  210. package/src/assets/web-panel/assets/devWarning-BBwWehQ7.js +0 -1
  211. package/src/assets/web-panel/assets/index-DJ8etkBT.js +0 -1
  212. package/src/assets/web-panel/assets/index-OeaK-zXZ.js +0 -1
@@ -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
- _approvalGate.setConfirmer(async ({ tool, args, riskLevel }) => {
621
- if (_dontAskActive()) {
622
- process.stdout.write(
623
- chalk.yellow(
624
- `\n ✕ denied without asking (dontAsk mode): ${riskLevel || "medium"}-risk ${tool || "run_shell"}${args?.command ? " " + args.command : ""}\n`,
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 res = await questionWithIdleTimeout(
639
- q,
640
- chalk.yellow(
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 no response in ${_askIdleTimeoutMs}ms — auto-denied\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
- return false;
654
- }
655
- const ans = res.answer.trim().toLowerCase();
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.green(
661
- ` always allow: added ${saved.rule} ${saved.file}\n`,
671
+ chalk.yellow(
672
+ `\n no response in ${_askIdleTimeoutMs}ms auto-denied\n`,
662
673
  ),
663
674
  );
675
+ return false;
664
676
  }
665
- return true;
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
- return ans === "y" || ans === "yes";
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
- // Confirmer is shared by permission `ask` rules AND hook `ask` decisions,
697
- // so define it unconditionally (a `hook:` rule label flows through too).
698
- _permissionConfirm = async ({ tool, args, rule, reason }) => {
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 res = await questionWithIdleTimeout(
720
- q,
721
- chalk.yellow(`\n${header}\n Proceed? (y/N) `),
722
- _askIdleTimeoutMs,
723
- );
724
- rl.close();
725
- if (res.timedOut) {
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 no response in ${_askIdleTimeoutMs}msauto-denied\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
- const ans = res.answer.trim().toLowerCase();
734
- return ans === "y" || ans === "yes";
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
- if (trimmed === "/doctor" || trimmed.startsWith("/doctor ")) {
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 {