chainlesschain 0.162.158 → 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 (201) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-BwpTJTN_.js → AIOps-ByIiF5bT.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BtQOoo3l.js → ActionButton-NlgSFZq5.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-Duf_rXZa.js → Analytics-D17cOUcb.js} +3 -3
  5. package/src/assets/web-panel/assets/{AppLayout-XQK3wjvB.js → AppLayout-B4mEZcbl.js} +4 -4
  6. package/src/assets/web-panel/assets/Artifacts-CASsmyQw.js +1 -0
  7. package/src/assets/web-panel/assets/{Audit-DNjOGQLh.js → Audit-DYAia2e2.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-FH9RAfB5.js → BackgroundAgents-C1lvYUSu.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-ZIbrR4-U.js → Backup-B6agK6bA.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-DSt3NaND.js → BaseInput-DM43R2vy.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-CY-wvbjv.js → Chat-Dtdp3eXp.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-BAV9V-t-.js → ChatBubbleRenderer-BL5c_OQ1.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-8JVadE_B.js → Checkbox-D2xf7uZt.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-CN83nPYI.js → Codegen-D3j45jJL.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-xPSvqdN8.js → Col-voPJ9fUZ.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-D78cnIjG.js → Community-DEsyTESF.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-CSw6Csbp.js → Compact-OHV_QXLD.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-E_HhjcYR.js → Compliance-eDTJWC-J.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-DybeQjmm.js → Cowork-BBvy-6d1.js} +3 -3
  20. package/src/assets/web-panel/assets/{Cron-DS0BJeLY.js → Cron-Dw2eoJxu.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-BOwVgkbs.js → Crosschain-DQHms_Bh.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-DRUr9YuX.js → DID-Cyu7Id1c.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-Cht-yUKW.js → Dashboard-sIDzkG92.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-BLr_EJ9X.js → Dropdown-DUW3T2ey.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-Drexuh5W.js → EmailListRenderer-CECELXW8.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-D9Q2Kq63.js → FamilyGuardDashboard-BIZR37ov.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-C-Z1xI-T.js → Federation-BEZIQX84.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-DLHPMkQF.js → FormItemContext-BcdUMznL.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-DxxEdn2r.js → GenericCardRenderer-DsN_fr5H.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-syATcaBt.js → Git-63LhjBx2.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-CYe_r8Zf.js → Governance-CYtQtIcH.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-GWxErB7k.js → Inference-BwR6UUox.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-qQvVCZoi.js → KnowledgeGraph-D9lVNTUA.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-BenAU2W8.js → Logs-C4q5h7Ut.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-7agE3Or1.js → MarkdownRenderer-DexLlXfN.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-D4Jn5fpD.js → Marketplace-Plksliuw.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-DtAwoqiQ.js → McpTools-DZUFfeuG.js} +4 -4
  38. package/src/assets/web-panel/assets/{Memory-BI_O50I8.js → Memory-NoDYt29V.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-CQWE2lqx.js → MobileBridge-CEXGTkdh.js} +1 -1
  40. package/src/assets/web-panel/assets/{MobileProjects-CVtTqgtg.js → MobileProjects-DnbJkbZo.js} +1 -1
  41. package/src/assets/web-panel/assets/{Mtc-Dnr16qsn.js → Mtc-DTAa0cwy.js} +5 -5
  42. package/src/assets/web-panel/assets/{MtcAudit-CQ5Xe4-a.js → MtcAudit-CVV7LnNS.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-Bjr8O-wy.js → Multisig-BqqGrjt_.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-CEe-KirU.js → NLProgramming-C4WP8DRp.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-spKJIKF9.js → Notes-Zo4DJUVI.js} +4 -4
  46. package/src/assets/web-panel/assets/{NotificationSettings-DRp83LFG.js → NotificationSettings-DBcjRaz2.js} +1 -1
  47. package/src/assets/web-panel/assets/OrderTableRenderer-BCQel9bD.js +1 -0
  48. package/src/assets/web-panel/assets/{Organization-BlgB0IYg.js → Organization-BT9WPBOX.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-a_35HqZI.js → Overflow-D6b4bZ3q.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-CSNHCNPC.js → P2P-Cw3OsYx-.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-C8M1JH-1.js → PdhVaultBrowser-C0nyiK2b.js} +5 -5
  52. package/src/assets/web-panel/assets/{Permissions-DmjJi9Ms.js → Permissions-zVBZEB_C.js} +3 -3
  53. package/src/assets/web-panel/assets/{PersonalDataHub-DfuwRyf4.js → PersonalDataHub-BOspsFew.js} +2 -2
  54. package/src/assets/web-panel/assets/{Pipeline-B2-S4ekE.js → Pipeline-CQWfZ4XH.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-B1dOiBLC.js → Privacy-C2oGJ_WL.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-_pNmc-DX.js → ProjectInit-BZMR4qY1.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-DmwG2Ii1.js → ProjectSettings-DAujoP63.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-COATmHxF.js → Projects-DvH_P2Uq.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-C6HSjg1d.js → Providers-D_RGHM-v.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-CrDmS7l1.js → QrScannerModal-Mf6KfL2J.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-CfKvWt-x.js → QuickAsk-5D8URoI_.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-DMa6Kuzv.js → Recommend-Df4kC8Gy.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-DrktUo5x.js → RemoteSession-DGi3f3vO.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-BTPQ1vjL.js → Reputation-jmkGFNWb.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-B6Q7QkMG.js → Row-DP6Pcu3m.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-B1IJhAmX.js → RssFeed-D60hSuPO.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-xbea1kGD.js → Search-0mf1BQUK.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-7rs0cO3F.js → Security-CfZtLFYV.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-0fjixK0S.js → Services-D9zpZLWz.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-B_rVq9Cu.js → Skeleton-D1_44eMD.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-C5uM90b9.js → Skills-cVFa8zkz.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-U4pYCIZH.js → Sla-Cinpzc3L.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-Cdz4yTWx.js → SpeechSettings-bqdiJE_P.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-Me3aetHv.js → SyncSettings-Dgl_SLgj.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-Cf6aNkL_.js → Tasks-DawWAEhy.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-CYatk4tU.js → Templates-Ca0nY651.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-DIMKLw7o.js → Tenant-CgaGEhLm.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-C_cp-uSJ.js → Terminal-D-9EnpRx.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-cUPVNADp.js → TimelineRenderer-CSOxDHZ6.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-CiaEIXVf.js → Tokens-BpwLZpL3.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-DB1WU5lQ.js → Trigger-DgowwBK5.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-Cfht9Ijy.js → Trust-BZutapjE.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-K3P4Vt9S.js → UkeySign-QZPt8_Sp.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-DWjhjXRh.js → VideoEditing-DlQx5UDJ.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-D9-Q1Kan.js → Wallet-ZfdTgtWA.js} +4 -4
  86. package/src/assets/web-panel/assets/{WebAuthn-BDszy-oV.js → WebAuthn-CAp0y_Cr.js} +5 -5
  87. package/src/assets/web-panel/assets/{WorkflowEditor-DIhij4rA.js → WorkflowEditor-BVITZEmr.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-D7VCiKbh.js → chat-CC2tPaNg.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-BvOd9pVn.js → colors-31brc9CA.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-DoqXBiAO.js → compact-item-DLMkC9C-.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-CrKRzUtp.js → createContext-BMtveI6F.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-CB8pPOtd.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-BWAev4WC.js → hasIn-DbD6WHW2.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-B30NvR21.js → index-3oBsXZmF.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-Cx7M7PSF.js → index-ALvoNTRF.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BHOcVYrx.js → index-AaIKG60c.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-BXyl8O23.js → index-BFRLmCR3.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-YGDy7nNq.js → index-BKAB6-6V.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BoYrS_L2.js → index-BOM-VFlv.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Bq_ctRpz.js → index-BPpS-lGb.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-CDGLdn12.js → index-Bbc2JeQG.js} +1 -1
  102. package/src/assets/web-panel/assets/index-BhXiaiev.js +1 -0
  103. package/src/assets/web-panel/assets/{index-ISvPZ05r.js → index-BiRQUoyE.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-B0vGEbJT.js → index-BnFLzejT.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-Bj7EgmOp.js → index-BoO4WEJB.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BZGk8AbZ.js → index-C-cGlCDW.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-BkgnGX7v.js → index-C1PfeI88.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-HwHdTusY.js → index-C21pdBqs.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-Bf9YlbDn.js → index-C7LHNw_N.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-oY45XrkG.js → index-CEXa-4zd.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-KR1-Em9C.js → index-CEwjBA9q.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-C5kRmj3P.js → index-CFXKbOek.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-BN9m4Ux2.js → index-CJhREOlG.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-PDxGjKRu.js → index-COHHQ9cU.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-D23L1V76.js → index-CUmLMFsy.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-zH-tuuGZ.js → index-CWgfipcC.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-CmJ6xKBO.js → index-C_eUKGYP.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CEPHMSih.js → index-CcVszn2b.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-DV-Rfmfx.js → index-Cod97Urs.js} +3 -3
  120. package/src/assets/web-panel/assets/{index-BWWsaimP.js → index-Cwbqh0KZ.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-8g5TJCZ3.js → index-D67kIagl.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-CEXfKSiI.js → index-D7cV0tu0.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CoD31Ndm.js → index-DJvxFyIz.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CSb-tnIo.js → index-DZl-UXB6.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-DvDOpsJB.js → index-Dm0byjm1.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-DlGu7As6.js → index-DndNnYc0.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-gmxqu5Kk.js → index-WQUO2vvK.js} +1 -1
  128. package/src/assets/web-panel/assets/index-WpaYaE3D.js +1 -0
  129. package/src/assets/web-panel/assets/{index-ww1QtLnX.js → index-bdaJwTXJ.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-JzUmJMvn.js → index-ep6JchNG.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BCyP071m.js → index-jG6h22Qg.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-1poMpMU0.js → index-ntVQqSML.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-XW-OR3mL.js → initDefaultProps-D5UoBRhN.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-CeuEHh20.js → motion-DltOw0CR.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-Dva79Keg.js → move-_oPkk5zj.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-C0B6bD2j.js → mtc-parser-EES8ROCS.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-AkZvO-a2.js → omit-B-_39x_M.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-B4r55qGc.js → pickAttrs-l6D0yJHz.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-BLH-z7YE.js → placementArrow-DIeUK31L.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-ksEGZ2Wf.js → responsiveObserve-dG4EzUQo.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-Cth4OD1Z.js → slide-DWFzYGdH.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-9S83ezxw.js → statusUtils-CECdTA5H.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-7yQmQGZ4.js → styleChecker-D6kP-mMK.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-DpGnMmPc.js → useFlexGapSupport-BWfna_rd.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-C4zDZIXH.js → useFs-Bb-j6rzo.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-CoLICdUZ.js → usePersonalDataHub-D5OIsYM4.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-CKyH-35X.js → vnode-DNpwzFTM.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-DRDGit17.js → zoom-vNLpFQBW.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/command-manifest.json +17 -3
  151. package/src/commands/agent.js +7 -0
  152. package/src/commands/background-session.js +143 -3
  153. package/src/commands/browse.js +86 -0
  154. package/src/commands/cloud.js +232 -0
  155. package/src/commands/doctor.js +61 -6
  156. package/src/commands/mcp.js +61 -0
  157. package/src/commands/plugin.js +63 -1
  158. package/src/commands/routine.js +367 -0
  159. package/src/commands/session.js +326 -0
  160. package/src/data/changelog.json +7 -2
  161. package/src/harness/jsonl-session-store.js +82 -2
  162. package/src/harness/session-index.js +295 -0
  163. package/src/harness/session-mirror.js +142 -0
  164. package/src/harness/transcript-integrity.js +182 -0
  165. package/src/index.js +8 -0
  166. package/src/lib/background-agent-supervisor.js +126 -2
  167. package/src/lib/channels/channel-manager.js +126 -0
  168. package/src/lib/channels/telegram-channel.js +112 -0
  169. package/src/lib/channels/webhook-channel.js +140 -0
  170. package/src/lib/chrome-connector.js +386 -0
  171. package/src/lib/cloud/bundle.js +110 -0
  172. package/src/lib/cloud/cloud-client.js +152 -0
  173. package/src/lib/computer-use/authorization.js +173 -0
  174. package/src/lib/computer-use/computer-use-server.js +276 -0
  175. package/src/lib/computer-use/control-backend.js +150 -0
  176. package/src/lib/doctor-checkup.js +652 -0
  177. package/src/lib/jsonl-session-store.js +4 -0
  178. package/src/lib/lsp/lsp-server-registry.js +48 -21
  179. package/src/lib/plugin-runtime/governance.js +174 -0
  180. package/src/lib/pr-link-ledger.js +221 -0
  181. package/src/lib/routine-store.js +352 -0
  182. package/src/lib/session-store-guard.js +40 -0
  183. package/src/lib/session-usage.js +318 -2
  184. package/src/lib/sub-agent-context.js +23 -0
  185. package/src/repl/agent-repl.js +361 -3
  186. package/src/repl/bg-dashboard.js +355 -0
  187. package/src/repl/fast-mode.js +103 -0
  188. package/src/repl/fullscreen-tui.js +197 -0
  189. package/src/repl/slash-command-registry.js +11 -3
  190. package/src/repl/voice-dictation.js +132 -0
  191. package/src/runtime/agent-core.js +242 -1
  192. package/src/runtime/coding-agent-contract-shared.cjs +88 -0
  193. package/src/runtime/coding-agent-policy.cjs +17 -0
  194. package/src/runtime/headless-runner.js +71 -7
  195. package/src/runtime/headless-stream.js +14 -8
  196. package/src/runtime/policies/agent-policy.js +3 -0
  197. package/src/assets/web-panel/assets/Artifacts-DQvJoQ6E.js +0 -1
  198. package/src/assets/web-panel/assets/OrderTableRenderer-h1A0XB2a.js +0 -1
  199. package/src/assets/web-panel/assets/devWarning-nDB--2xr.js +0 -1
  200. package/src/assets/web-panel/assets/index-B1_pRTtB.js +0 -1
  201. package/src/assets/web-panel/assets/index-TMM9FuW-.js +0 -1
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Background-agent dashboard — the interactive agent view behind
3
+ * `cc daemon view` (no id). Claude-Code `claude agents` parity: one live panel
4
+ * over every background session with grouping (Needs input / Working /
5
+ * Completed / Failed / Stopped), peek + reply, attach hand-off, stop, rename,
6
+ * pin, filter, and new-task dispatch.
7
+ *
8
+ * Design:
9
+ * - Pure model/render layer (`groupKey`, `buildDashboardModel`,
10
+ * `renderDashboard`) — deterministic, unit-tested with injected `now`.
11
+ * - `runBgDashboard(deps)` controller — every effect (list/stop/rename/pin/
12
+ * attach/dispatch/log/prompt IO) is an injected dep so tests drive it with
13
+ * fakes; the command layer wires the real supervisor.
14
+ * - Raw ANSI on the alternate screen buffer (no TUI framework dependency);
15
+ * line-input actions (reply/rename/dispatch) temporarily leave the alt
16
+ * screen for a plain readline prompt, then re-enter.
17
+ */
18
+
19
+ import chalk from "chalk";
20
+
21
+ export const GROUP_ORDER = [
22
+ "needs-input",
23
+ "working",
24
+ "completed",
25
+ "failed",
26
+ "stopped",
27
+ ];
28
+
29
+ export const GROUP_TITLES = {
30
+ "needs-input": "Needs input",
31
+ working: "Working",
32
+ completed: "Completed",
33
+ failed: "Failed",
34
+ stopped: "Stopped",
35
+ };
36
+
37
+ /**
38
+ * Group a session: running+idle phase = waiting for a follow-up (the closest
39
+ * analog of "needs input" — the worker finished a turn and a client can
40
+ * reply); running+turn = working; lost workers count as failed.
41
+ */
42
+ export function groupKey(session) {
43
+ const status = session?.status;
44
+ if (status === "running") {
45
+ return session.phase === "idle" ? "needs-input" : "working";
46
+ }
47
+ if (status === "completed") return "completed";
48
+ if (status === "stopped") return "stopped";
49
+ return "failed"; // failed | lost | unknown
50
+ }
51
+
52
+ function formatAge(ms) {
53
+ const n = Math.max(0, Math.round(Number(ms || 0) / 1000));
54
+ if (n < 60) return `${n}s`;
55
+ const m = Math.floor(n / 60);
56
+ if (m < 60) return `${m}m`;
57
+ const h = Math.floor(m / 60);
58
+ return `${h}h${m % 60 ? `${m % 60}m` : ""}`;
59
+ }
60
+
61
+ export const FILTERS = ["all", "active", "needs-input"];
62
+
63
+ function passesFilter(session, filter) {
64
+ if (filter === "active") return session.status === "running";
65
+ if (filter === "needs-input") return groupKey(session) === "needs-input";
66
+ return true;
67
+ }
68
+
69
+ /**
70
+ * Build the render model: grouped rows (pinned first inside each group,
71
+ * then newest first) plus a flat selection list in display order.
72
+ */
73
+ export function buildDashboardModel(sessions, options = {}) {
74
+ const filter = options.filter || "all";
75
+ const now = typeof options.now === "number" ? options.now : Date.now();
76
+ const byGroup = new Map(GROUP_ORDER.map((k) => [k, []]));
77
+ for (const s of sessions || []) {
78
+ if (!s || !passesFilter(s, filter)) continue;
79
+ byGroup.get(groupKey(s))?.push(s);
80
+ }
81
+ for (const list of byGroup.values()) {
82
+ list.sort(
83
+ (a, b) =>
84
+ (b.pinned === true) - (a.pinned === true) ||
85
+ (b.startedAt || 0) - (a.startedAt || 0),
86
+ );
87
+ }
88
+ const groups = GROUP_ORDER.map((key) => ({
89
+ key,
90
+ title: GROUP_TITLES[key],
91
+ rows: byGroup.get(key) || [],
92
+ })).filter((g) => g.rows.length > 0);
93
+ const flat = groups.flatMap((g) => g.rows);
94
+ return { groups, flat, filter, now };
95
+ }
96
+
97
+ function rowLine(session, { selected, now }) {
98
+ const elapsed = session.endedAt
99
+ ? session.endedAt - session.startedAt
100
+ : now - session.startedAt;
101
+ const pin = session.pinned ? chalk.yellow("★ ") : " ";
102
+ const marker = selected ? chalk.cyan("❯ ") : " ";
103
+ const id = String(session.id || "").padEnd(24);
104
+ const title = session.title ? ` ${session.title}` : "";
105
+ const pr = session.pr?.number
106
+ ? chalk.magenta(
107
+ ` #${session.pr.number}${session.pr.state ? ` ${session.pr.state}` : ""}`,
108
+ )
109
+ : "";
110
+ const approvals = Number(session.pendingApprovals)
111
+ ? chalk.yellow(` ⏳${session.pendingApprovals}`)
112
+ : "";
113
+ const ws = session.worktree || session.cwd || "";
114
+ const line = `${marker}${pin}${id} ${formatAge(elapsed).padStart(5)} ${chalk.gray(ws)}${title}${pr}${approvals}`;
115
+ return selected ? chalk.bold(line) : line;
116
+ }
117
+
118
+ /**
119
+ * Render the dashboard to a string (no cursor positioning — the controller
120
+ * clears and repaints the alt screen).
121
+ */
122
+ export function renderDashboard(model, options = {}) {
123
+ const selectedIndex = options.selectedIndex ?? 0;
124
+ const lines = [];
125
+ const total = model.flat.length;
126
+ lines.push(
127
+ chalk.bold(" Background agents") +
128
+ chalk.gray(
129
+ ` ${total} shown · filter: ${model.filter}` +
130
+ (options.message ? ` — ${options.message}` : ""),
131
+ ),
132
+ );
133
+ lines.push("");
134
+ if (total === 0) {
135
+ lines.push(chalk.gray(" No background agents match this filter."));
136
+ lines.push(
137
+ chalk.gray(' Press "d" to dispatch a new background task, "q" to quit.'),
138
+ );
139
+ }
140
+ let index = 0;
141
+ for (const group of model.groups) {
142
+ lines.push(chalk.bold(` ${group.title} (${group.rows.length})`));
143
+ for (const session of group.rows) {
144
+ lines.push(
145
+ rowLine(session, { selected: index === selectedIndex, now: model.now }),
146
+ );
147
+ index++;
148
+ }
149
+ lines.push("");
150
+ }
151
+ if (options.peek) {
152
+ lines.push(chalk.bold(` Peek: ${options.peek.id}`));
153
+ const text = (options.peek.log || "").trimEnd();
154
+ for (const l of text ? text.split("\n").slice(-15) : ["(no output)"]) {
155
+ lines.push(chalk.gray(` │ ${l}`));
156
+ }
157
+ lines.push("");
158
+ }
159
+ lines.push(
160
+ chalk.gray(
161
+ " ↑/↓ select · enter peek · r reply · a attach · s stop · n rename · x pin · d dispatch · f filter · q quit",
162
+ ),
163
+ );
164
+ return lines.join("\n");
165
+ }
166
+
167
+ const ESC = "\x1b";
168
+ const ALT_ENTER = `${ESC}[?1049h${ESC}[?25l`;
169
+ const ALT_EXIT = `${ESC}[?25h${ESC}[?1049l`;
170
+ const CLEAR = `${ESC}[2J${ESC}[H`;
171
+
172
+ /** Default terminal IO — swapped for a fake in tests. */
173
+ export function defaultDashboardIo() {
174
+ return {
175
+ isTTY: Boolean(process.stdout.isTTY && process.stdin.isTTY),
176
+ write: (s) => process.stdout.write(s),
177
+ enterAlt: () => process.stdout.write(ALT_ENTER),
178
+ exitAlt: () => process.stdout.write(ALT_EXIT),
179
+ clear: () => process.stdout.write(CLEAR),
180
+ setRaw: (on) => {
181
+ if (process.stdin.isTTY && process.stdin.setRawMode) {
182
+ process.stdin.setRawMode(on);
183
+ }
184
+ },
185
+ async listenKeys(onKey) {
186
+ const readline = await import("node:readline");
187
+ readline.emitKeypressEvents(process.stdin);
188
+ process.stdin.resume();
189
+ const handler = (str, key) => onKey(str, key || {});
190
+ process.stdin.on("keypress", handler);
191
+ return () => process.stdin.off("keypress", handler);
192
+ },
193
+ async promptLine(question) {
194
+ const readline = await import("node:readline");
195
+ const rl = readline.createInterface({
196
+ input: process.stdin,
197
+ output: process.stdout,
198
+ });
199
+ try {
200
+ return await new Promise((resolve) => rl.question(question, resolve));
201
+ } finally {
202
+ rl.close();
203
+ }
204
+ },
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Interactive dashboard loop. All effects come from deps:
210
+ * listAgents() → session[] readLog(id, lines) → string
211
+ * stopAgent(id) renameAgent(id, title)
212
+ * pinAgent(id, pinned) replyAgent(session, text)
213
+ * attachAgent(session) dispatchAgent(text)
214
+ * io — see defaultDashboardIo() refreshMs, now — timing seams
215
+ */
216
+ export async function runBgDashboard(deps = {}) {
217
+ const io = deps.io || defaultDashboardIo();
218
+ const now = deps.now || (() => Date.now());
219
+ const refreshMs = deps.refreshMs ?? 2000;
220
+ const listAgents = deps.listAgents || (() => []);
221
+
222
+ let selected = 0;
223
+ let filterIndex = 0;
224
+ let peek = null;
225
+ let message = "";
226
+ let model = buildDashboardModel(listAgents(), {
227
+ filter: FILTERS[filterIndex],
228
+ now: now(),
229
+ });
230
+
231
+ const render = () => {
232
+ model = buildDashboardModel(listAgents(), {
233
+ filter: FILTERS[filterIndex],
234
+ now: now(),
235
+ });
236
+ if (selected >= model.flat.length)
237
+ selected = Math.max(0, model.flat.length - 1);
238
+ if (peek && !model.flat.some((s) => s.id === peek.id)) peek = null;
239
+ io.clear();
240
+ io.write(
241
+ renderDashboard(model, { selectedIndex: selected, peek, message }) + "\n",
242
+ );
243
+ };
244
+
245
+ const selectedSession = () => model.flat[selected] || null;
246
+
247
+ // Line-input actions leave the alt screen so readline behaves normally.
248
+ const promptOutside = async (question) => {
249
+ io.setRaw(false);
250
+ io.exitAlt();
251
+ try {
252
+ return await io.promptLine(question);
253
+ } finally {
254
+ io.enterAlt();
255
+ io.setRaw(true);
256
+ }
257
+ };
258
+
259
+ io.enterAlt();
260
+ io.setRaw(true);
261
+ const timer = setInterval(render, refreshMs);
262
+ timer.unref?.();
263
+ render();
264
+
265
+ let stopListening = () => {};
266
+ await new Promise((resolve) => {
267
+ const finish = () => resolve();
268
+ const onKey = async (str, key) => {
269
+ const name = key?.name || str;
270
+ const ctrlC = key?.ctrl && key.name === "c";
271
+ message = "";
272
+ try {
273
+ if (ctrlC || name === "q") return finish();
274
+ if (name === "down" || name === "j") {
275
+ selected = Math.min(model.flat.length - 1, selected + 1);
276
+ } else if (name === "up" || name === "k") {
277
+ selected = Math.max(0, selected - 1);
278
+ } else if (name === "return" || name === "p") {
279
+ const s = selectedSession();
280
+ peek =
281
+ s && peek?.id !== s.id
282
+ ? { id: s.id, log: deps.readLog ? deps.readLog(s.id, 15) : "" }
283
+ : null;
284
+ } else if (name === "f") {
285
+ filterIndex = (filterIndex + 1) % FILTERS.length;
286
+ selected = 0;
287
+ } else if (name === "x") {
288
+ const s = selectedSession();
289
+ if (s && deps.pinAgent) {
290
+ deps.pinAgent(s.id, s.pinned !== true);
291
+ message = s.pinned ? `unpinned ${s.id}` : `pinned ${s.id}`;
292
+ }
293
+ } else if (name === "s") {
294
+ const s = selectedSession();
295
+ if (s && deps.stopAgent) {
296
+ deps.stopAgent(s.id);
297
+ message = `stopped ${s.id}`;
298
+ }
299
+ } else if (name === "n") {
300
+ const s = selectedSession();
301
+ if (s && deps.renameAgent) {
302
+ const title = await promptOutside(`New title for ${s.id}: `);
303
+ if (title && title.trim()) {
304
+ deps.renameAgent(s.id, title.trim());
305
+ message = `renamed ${s.id}`;
306
+ }
307
+ }
308
+ } else if (name === "r") {
309
+ const s = selectedSession();
310
+ if (s && deps.replyAgent) {
311
+ const text = await promptOutside(`Reply to ${s.id}: `);
312
+ if (text && text.trim()) {
313
+ await deps.replyAgent(s, text.trim());
314
+ message = `sent to ${s.id}`;
315
+ }
316
+ }
317
+ } else if (name === "d") {
318
+ if (deps.dispatchAgent) {
319
+ const text = await promptOutside("New background task: ");
320
+ if (text && text.trim()) {
321
+ const id = await deps.dispatchAgent(text.trim());
322
+ message = id ? `dispatched ${id}` : "dispatched";
323
+ }
324
+ }
325
+ } else if (name === "a") {
326
+ const s = selectedSession();
327
+ if (s && deps.attachAgent) {
328
+ // Hand the terminal over to attach, then come back.
329
+ io.setRaw(false);
330
+ io.exitAlt();
331
+ stopListening();
332
+ try {
333
+ await deps.attachAgent(s);
334
+ } finally {
335
+ io.enterAlt();
336
+ io.setRaw(true);
337
+ stopListening = (await io.listenKeys(onKey)) || (() => {});
338
+ }
339
+ }
340
+ }
341
+ } catch (err) {
342
+ message = `error: ${err.message}`;
343
+ }
344
+ render();
345
+ };
346
+ io.listenKeys(onKey).then((stop) => {
347
+ stopListening = stop || (() => {});
348
+ });
349
+ });
350
+
351
+ clearInterval(timer);
352
+ stopListening();
353
+ io.setRaw(false);
354
+ io.exitAlt();
355
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Fast mode — latency profile for the agent REPL (gap-2026-07-11 P2#12).
3
+ *
4
+ * Claude Code's `/fast` binds Opus fast mode. cc is multi-provider, so "fast"
5
+ * is generalized to a latency profile the provider adapter applies to the next
6
+ * turn:
7
+ * - drop extended thinking / shrink the reasoning budget (biggest latency win
8
+ * and provider-agnostic),
9
+ * - optionally swap to the provider's designated low-latency model — but only
10
+ * when the user has NOT pinned a model this session (never override an
11
+ * explicit choice), and always announce it,
12
+ * - surface the cost/quality tradeoff so the user knows what they traded.
13
+ *
14
+ * Pure core (command parse, fast-model table, plan resolution, status/tradeoff
15
+ * rendering). The `/fast` controller in agent-repl owns turn state + config
16
+ * persistence (`cli.fastMode`).
17
+ */
18
+
19
+ /** Provider → designated low-latency model. Only well-known ones. */
20
+ export const KNOWN_FAST_MODELS = Object.freeze({
21
+ anthropic: "claude-haiku-4-5-20251001",
22
+ openai: "gpt-4o-mini",
23
+ deepseek: "deepseek-chat",
24
+ dashscope: "qwen-turbo",
25
+ mistral: "mistral-small-latest",
26
+ gemini: "gemini-2.0-flash",
27
+ });
28
+
29
+ export const FAST_TRADEOFF =
30
+ "Faster + cheaper, but lower reasoning depth — turn it off for hard/multi-step work.";
31
+
32
+ /**
33
+ * Parse `/fast [on|off|status|toggle]`. Returns null when the input is not a
34
+ * `/fast` command, `{ error }` for a bad arg, else `{ action }`. Bare `/fast`
35
+ * toggles. Pure.
36
+ */
37
+ export function parseFastCommand(input) {
38
+ const t = String(input || "").trim();
39
+ if (t !== "/fast" && !t.startsWith("/fast ")) return null;
40
+ const arg = t.slice("/fast".length).trim().toLowerCase();
41
+ if (!arg) return { action: "toggle" };
42
+ if (["on", "enable", "1"].includes(arg)) return { action: "on" };
43
+ if (["off", "disable", "0"].includes(arg)) return { action: "off" };
44
+ if (["status", "state"].includes(arg)) return { action: "status" };
45
+ if (arg === "toggle") return { action: "toggle" };
46
+ return { error: `Unknown /fast option "${arg}". Use: on | off | status` };
47
+ }
48
+
49
+ /**
50
+ * Resolve the effect of the current fast-mode state on the next turn.
51
+ * @param {object} o { enabled, provider, model, modelPinned }
52
+ * modelPinned = the user chose the model this session (`/model x`), so we must
53
+ * not swap it.
54
+ * @returns {{ enabled, thinking, model, swapped, note, tradeoff }}
55
+ * When disabled, returns the inputs unchanged with thinking left null (caller
56
+ * keeps its own thinking state). Pure.
57
+ */
58
+ export function resolveFastPlan({
59
+ enabled = false,
60
+ provider = "",
61
+ model = "",
62
+ modelPinned = false,
63
+ } = {}) {
64
+ if (!enabled) {
65
+ return {
66
+ enabled: false,
67
+ thinking: null,
68
+ model,
69
+ swapped: false,
70
+ note: "",
71
+ tradeoff: "",
72
+ };
73
+ }
74
+ const fast = KNOWN_FAST_MODELS[String(provider).toLowerCase()];
75
+ const canSwap = Boolean(fast) && !modelPinned && fast !== model;
76
+ const nextModel = canSwap ? fast : model;
77
+ let note;
78
+ if (canSwap) {
79
+ note = `Fast mode: low-latency model ${nextModel}, reasoning minimized.`;
80
+ } else if (modelPinned) {
81
+ note = `Fast mode: reasoning minimized (keeping your pinned model ${model}).`;
82
+ } else {
83
+ note = "Fast mode: reasoning minimized.";
84
+ }
85
+ return {
86
+ enabled: true,
87
+ thinking: "off",
88
+ model: nextModel,
89
+ swapped: canSwap,
90
+ note,
91
+ tradeoff: FAST_TRADEOFF,
92
+ };
93
+ }
94
+
95
+ /** Render a `/fast status` line. Pure. */
96
+ export function renderFastStatus({ enabled, provider, model } = {}) {
97
+ if (!enabled) {
98
+ return "Fast mode: off. /fast on for a low-latency profile.";
99
+ }
100
+ const fast = KNOWN_FAST_MODELS[String(provider).toLowerCase()];
101
+ const modelBit = fast ? ` fast model: ${fast};` : "";
102
+ return `Fast mode: on —${modelBit} reasoning minimized. ${FAST_TRADEOFF}`;
103
+ }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Fullscreen / no-flicker TUI helpers (gap-2026-07-11 P1#10).
3
+ *
4
+ * cc's REPL streams to the scrollback buffer with chalk. On long conversations
5
+ * that scrollback grows without bound and every repaint re-emits the whole
6
+ * screen, which flickers and jumps. Claude Code's fullscreen renderer fixes
7
+ * this with the alternate screen buffer + a windowed, diff-based repaint:
8
+ *
9
+ * - `/tui fullscreen` enters the alternate screen buffer (`?1049h`), so the
10
+ * user's scrollback is preserved and the view is a fixed window.
11
+ * - only the messages that fit the viewport are painted; older ones move to
12
+ * "transcript mode" (searchable, scrollable) instead of re-emitting.
13
+ * - repaints diff against the previously painted lines and rewrite only the
14
+ * rows that changed — no full clear, so no flicker.
15
+ * - `CC_NO_FLICKER=1` forces fullscreen on at startup.
16
+ *
17
+ * This module is the pure, testable core (mode resolution, alt-screen escape
18
+ * sequences, message windowing, diff repaint, transcript search, OSC-8
19
+ * hyperlinks). The controller (a thin `/tui` handler in agent-repl) owns the
20
+ * TTY writes and config persistence.
21
+ */
22
+
23
+ const ESC = "\x1b";
24
+
25
+ export const TUI_MODES = Object.freeze(["default", "fullscreen"]);
26
+ export const DEFAULT_TUI_MODE = "default";
27
+
28
+ /** Alternate screen buffer control sequences. */
29
+ export const ALT_SCREEN_ENTER = `${ESC}[?1049h${ESC}[H`;
30
+ export const ALT_SCREEN_LEAVE = `${ESC}[?1049l`;
31
+ export const HIDE_CURSOR = `${ESC}[?25l`;
32
+ export const SHOW_CURSOR = `${ESC}[?25h`;
33
+
34
+ /**
35
+ * Resolve the effective TUI mode. Precedence:
36
+ * explicit arg > CC_NO_FLICKER env > persisted setting > default.
37
+ * `CC_NO_FLICKER=1` forces fullscreen and cannot be overridden by the setting,
38
+ * but an explicit `/tui default` this session still wins (arg is highest).
39
+ * Returns a canonical mode name; unknown args fall through to the next source.
40
+ * Pure.
41
+ */
42
+ export function resolveTuiMode({
43
+ arg = null,
44
+ env = false,
45
+ setting = null,
46
+ } = {}) {
47
+ const canon = (v) => {
48
+ const n = String(v || "")
49
+ .toLowerCase()
50
+ .trim();
51
+ return TUI_MODES.includes(n) ? n : null;
52
+ };
53
+ const fromArg = canon(arg);
54
+ if (fromArg) return fromArg;
55
+ if (envNoFlicker(env)) return "fullscreen";
56
+ const fromSetting = canon(setting);
57
+ if (fromSetting) return fromSetting;
58
+ return DEFAULT_TUI_MODE;
59
+ }
60
+
61
+ /** Whether a CC_NO_FLICKER-style value means "on". Pure. */
62
+ export function envNoFlicker(value) {
63
+ const n = String(value == null ? "" : value)
64
+ .toLowerCase()
65
+ .trim();
66
+ return n === "1" || n === "true" || n === "on" || n === "yes";
67
+ }
68
+
69
+ /**
70
+ * Window a list of message lines to the visible viewport. Everything above the
71
+ * window is "in transcript" (hidden but searchable). The newest lines always
72
+ * stay visible (tail-anchored), leaving `reserveBottom` rows for the prompt.
73
+ * @param {string[]} lines already-rendered transcript lines
74
+ * @param {object} opts { rows, reserveBottom }
75
+ * @returns {{ visible:string[], hiddenCount:number, firstVisible:number }}
76
+ * Pure.
77
+ */
78
+ export function windowLines(lines, { rows = 24, reserveBottom = 2 } = {}) {
79
+ const all = Array.isArray(lines) ? lines : [];
80
+ const capacity = Math.max(1, rows - Math.max(0, reserveBottom));
81
+ if (all.length <= capacity) {
82
+ return { visible: all.slice(), hiddenCount: 0, firstVisible: 0 };
83
+ }
84
+ const firstVisible = all.length - capacity;
85
+ return {
86
+ visible: all.slice(firstVisible),
87
+ hiddenCount: firstVisible,
88
+ firstVisible,
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Compute the minimal cursor-addressed repaint to turn `prev` lines into
94
+ * `next`. Only rows that differ are rewritten (no full-screen clear → no
95
+ * flicker). Rows that shrank get a clear-to-end-of-line. Returns the escape
96
+ * string to write; empty string when nothing changed.
97
+ *
98
+ * `origin` is the 1-based screen row where line 0 is painted.
99
+ * Pure.
100
+ */
101
+ export function diffRepaint(prev, next, { origin = 1 } = {}) {
102
+ const a = Array.isArray(prev) ? prev : [];
103
+ const b = Array.isArray(next) ? next : [];
104
+ const max = Math.max(a.length, b.length);
105
+ let out = "";
106
+ for (let i = 0; i < max; i++) {
107
+ const before = a[i];
108
+ const after = b[i] ?? "";
109
+ if (before === after) continue;
110
+ const row = origin + i;
111
+ // Move to row start, clear the line, write the new content.
112
+ out += `${ESC}[${row};1H${ESC}[2K${after}`;
113
+ }
114
+ return out;
115
+ }
116
+
117
+ /**
118
+ * Search transcript messages for a query. Each message is `{role, text}`.
119
+ * Returns matches with the message index, a 0-based line offset within the
120
+ * message, and a trimmed snippet. Case-insensitive unless `caseSensitive`.
121
+ * Pure.
122
+ */
123
+ export function searchTranscript(
124
+ messages,
125
+ query,
126
+ { caseSensitive = false } = {},
127
+ ) {
128
+ const q = String(query || "");
129
+ if (!q) return [];
130
+ const needle = caseSensitive ? q : q.toLowerCase();
131
+ const matches = [];
132
+ const msgs = Array.isArray(messages) ? messages : [];
133
+ for (let mi = 0; mi < msgs.length; mi++) {
134
+ const text = String(msgs[mi]?.text ?? "");
135
+ const lines = text.split("\n");
136
+ for (let li = 0; li < lines.length; li++) {
137
+ const hay = caseSensitive ? lines[li] : lines[li].toLowerCase();
138
+ const col = hay.indexOf(needle);
139
+ if (col >= 0) {
140
+ matches.push({
141
+ index: mi,
142
+ line: li,
143
+ column: col,
144
+ role: msgs[mi]?.role || "",
145
+ snippet: lines[li].trim().slice(0, 200),
146
+ });
147
+ }
148
+ }
149
+ }
150
+ return matches;
151
+ }
152
+
153
+ /**
154
+ * OSC-8 terminal hyperlink. When the terminal supports it, renders clickable
155
+ * `text` linking to `url`; otherwise degrades to `text (url)` (or just text
156
+ * when they're equal). Pure.
157
+ */
158
+ export function formatHyperlink(text, url, { supported = true } = {}) {
159
+ const label = String(text ?? "");
160
+ const href = String(url ?? "");
161
+ if (!href) return label;
162
+ if (!supported) {
163
+ return label && label !== href ? `${label} (${href})` : href;
164
+ }
165
+ return `${ESC}]8;;${href}${ESC}\\${label || href}${ESC}]8;;${ESC}\\`;
166
+ }
167
+
168
+ /**
169
+ * Whether the environment likely supports OSC-8 hyperlinks. Conservative:
170
+ * honors FORCE_HYPERLINK / NO_HYPERLINK overrides, otherwise trusts known
171
+ * terminals. Pure over the passed env object.
172
+ */
173
+ export function supportsHyperlinks(env = {}) {
174
+ if (envNoFlicker(env.FORCE_HYPERLINK)) return true;
175
+ if (env.NO_HYPERLINK != null && env.NO_HYPERLINK !== "") return false;
176
+ const term = String(env.TERM_PROGRAM || "").toLowerCase();
177
+ if (
178
+ term.includes("iterm") ||
179
+ term.includes("wezterm") ||
180
+ term.includes("vscode") ||
181
+ term.includes("hyper")
182
+ ) {
183
+ return true;
184
+ }
185
+ if (env.WT_SESSION) return true; // Windows Terminal
186
+ if (env.KITTY_WINDOW_ID) return true;
187
+ return false;
188
+ }
189
+
190
+ /** One-line status describing the current TUI mode. Pure. */
191
+ export function renderTuiStatus(mode) {
192
+ const m = TUI_MODES.includes(mode) ? mode : DEFAULT_TUI_MODE;
193
+ if (m === "fullscreen") {
194
+ return "TUI: fullscreen (alternate screen, windowed no-flicker repaint). /tui default to exit.";
195
+ }
196
+ return "TUI: default (streaming scrollback). /tui fullscreen for a no-flicker window.";
197
+ }
@@ -35,7 +35,13 @@ export class SlashCommandRegistry {
35
35
  }
36
36
 
37
37
  getCommand(name) {
38
- return this._commands.get(name);
38
+ const direct = this._commands.get(name);
39
+ if (direct) return direct;
40
+ // Alias resolution: a command registered with aliases answers to them too.
41
+ for (const cmd of this._commands.values()) {
42
+ if (cmd.aliases && cmd.aliases.includes(name)) return cmd;
43
+ }
44
+ return undefined;
39
45
  }
40
46
 
41
47
  getAllCommands() {
@@ -43,7 +49,7 @@ export class SlashCommandRegistry {
43
49
  }
44
50
 
45
51
  hasCommand(name) {
46
- return this._commands.has(name);
52
+ return Boolean(this.getCommand(name));
47
53
  }
48
54
 
49
55
  _registerBuiltinCommands() {
@@ -188,8 +194,10 @@ export class SlashCommandRegistry {
188
194
  ],
189
195
  ];
190
196
 
197
+ // Claude-Code 2.1.205 parity: /checkup is an alias of /doctor.
198
+ const ALIASES = { "/doctor": ["/checkup"] };
191
199
  for (const [name, description, handler] of commands) {
192
- this.register(name, { description, handler });
200
+ this.register(name, { description, handler, aliases: ALIASES[name] });
193
201
  }
194
202
  }
195
203
  }