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.
- package/package.json +1 -1
- package/src/assets/web-panel/assets/{AIOps-BwpTJTN_.js → AIOps-ByIiF5bT.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-BtQOoo3l.js → ActionButton-NlgSFZq5.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-Duf_rXZa.js → Analytics-D17cOUcb.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-XQK3wjvB.js → AppLayout-B4mEZcbl.js} +4 -4
- package/src/assets/web-panel/assets/Artifacts-CASsmyQw.js +1 -0
- package/src/assets/web-panel/assets/{Audit-DNjOGQLh.js → Audit-DYAia2e2.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-FH9RAfB5.js → BackgroundAgents-C1lvYUSu.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-ZIbrR4-U.js → Backup-B6agK6bA.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-DSt3NaND.js → BaseInput-DM43R2vy.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CY-wvbjv.js → Chat-Dtdp3eXp.js} +5 -5
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-BAV9V-t-.js → ChatBubbleRenderer-BL5c_OQ1.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-8JVadE_B.js → Checkbox-D2xf7uZt.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-CN83nPYI.js → Codegen-D3j45jJL.js} +1 -1
- package/src/assets/web-panel/assets/{Col-xPSvqdN8.js → Col-voPJ9fUZ.js} +1 -1
- package/src/assets/web-panel/assets/{Community-D78cnIjG.js → Community-DEsyTESF.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-CSw6Csbp.js → Compact-OHV_QXLD.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-E_HhjcYR.js → Compliance-eDTJWC-J.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-DybeQjmm.js → Cowork-BBvy-6d1.js} +3 -3
- package/src/assets/web-panel/assets/{Cron-DS0BJeLY.js → Cron-Dw2eoJxu.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-BOwVgkbs.js → Crosschain-DQHms_Bh.js} +1 -1
- package/src/assets/web-panel/assets/{DID-DRUr9YuX.js → DID-Cyu7Id1c.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-Cht-yUKW.js → Dashboard-sIDzkG92.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-BLr_EJ9X.js → Dropdown-DUW3T2ey.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-Drexuh5W.js → EmailListRenderer-CECELXW8.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-D9Q2Kq63.js → FamilyGuardDashboard-BIZR37ov.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-C-Z1xI-T.js → Federation-BEZIQX84.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-DLHPMkQF.js → FormItemContext-BcdUMznL.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DxxEdn2r.js → GenericCardRenderer-DsN_fr5H.js} +1 -1
- package/src/assets/web-panel/assets/{Git-syATcaBt.js → Git-63LhjBx2.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-CYe_r8Zf.js → Governance-CYtQtIcH.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-GWxErB7k.js → Inference-BwR6UUox.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-qQvVCZoi.js → KnowledgeGraph-D9lVNTUA.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-BenAU2W8.js → Logs-C4q5h7Ut.js} +2 -2
- package/src/assets/web-panel/assets/{MarkdownRenderer-7agE3Or1.js → MarkdownRenderer-DexLlXfN.js} +1 -1
- package/src/assets/web-panel/assets/{Marketplace-D4Jn5fpD.js → Marketplace-Plksliuw.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DtAwoqiQ.js → McpTools-DZUFfeuG.js} +4 -4
- package/src/assets/web-panel/assets/{Memory-BI_O50I8.js → Memory-NoDYt29V.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-CQWE2lqx.js → MobileBridge-CEXGTkdh.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-CVtTqgtg.js → MobileProjects-DnbJkbZo.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-Dnr16qsn.js → Mtc-DTAa0cwy.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-CQ5Xe4-a.js → MtcAudit-CVV7LnNS.js} +2 -2
- package/src/assets/web-panel/assets/{Multisig-Bjr8O-wy.js → Multisig-BqqGrjt_.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-CEe-KirU.js → NLProgramming-C4WP8DRp.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-spKJIKF9.js → Notes-Zo4DJUVI.js} +4 -4
- package/src/assets/web-panel/assets/{NotificationSettings-DRp83LFG.js → NotificationSettings-DBcjRaz2.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-BCQel9bD.js +1 -0
- package/src/assets/web-panel/assets/{Organization-BlgB0IYg.js → Organization-BT9WPBOX.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-a_35HqZI.js → Overflow-D6b4bZ3q.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-CSNHCNPC.js → P2P-Cw3OsYx-.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-C8M1JH-1.js → PdhVaultBrowser-C0nyiK2b.js} +5 -5
- package/src/assets/web-panel/assets/{Permissions-DmjJi9Ms.js → Permissions-zVBZEB_C.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-DfuwRyf4.js → PersonalDataHub-BOspsFew.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-B2-S4ekE.js → Pipeline-CQWfZ4XH.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-B1dOiBLC.js → Privacy-C2oGJ_WL.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-_pNmc-DX.js → ProjectInit-BZMR4qY1.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DmwG2Ii1.js → ProjectSettings-DAujoP63.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-COATmHxF.js → Projects-DvH_P2Uq.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-C6HSjg1d.js → Providers-D_RGHM-v.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-CrDmS7l1.js → QrScannerModal-Mf6KfL2J.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-CfKvWt-x.js → QuickAsk-5D8URoI_.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DMa6Kuzv.js → Recommend-Df4kC8Gy.js} +1 -1
- package/src/assets/web-panel/assets/{RemoteSession-DrktUo5x.js → RemoteSession-DGi3f3vO.js} +2 -2
- package/src/assets/web-panel/assets/{Reputation-BTPQ1vjL.js → Reputation-jmkGFNWb.js} +1 -1
- package/src/assets/web-panel/assets/{Row-B6Q7QkMG.js → Row-DP6Pcu3m.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-B1IJhAmX.js → RssFeed-D60hSuPO.js} +3 -3
- package/src/assets/web-panel/assets/{Search-xbea1kGD.js → Search-0mf1BQUK.js} +1 -1
- package/src/assets/web-panel/assets/{Security-7rs0cO3F.js → Security-CfZtLFYV.js} +4 -4
- package/src/assets/web-panel/assets/{Services-0fjixK0S.js → Services-D9zpZLWz.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-B_rVq9Cu.js → Skeleton-D1_44eMD.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-C5uM90b9.js → Skills-cVFa8zkz.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-U4pYCIZH.js → Sla-Cinpzc3L.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-Cdz4yTWx.js → SpeechSettings-bqdiJE_P.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-Me3aetHv.js → SyncSettings-Dgl_SLgj.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-Cf6aNkL_.js → Tasks-DawWAEhy.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-CYatk4tU.js → Templates-Ca0nY651.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-DIMKLw7o.js → Tenant-CgaGEhLm.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-C_cp-uSJ.js → Terminal-D-9EnpRx.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-cUPVNADp.js → TimelineRenderer-CSOxDHZ6.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-CiaEIXVf.js → Tokens-BpwLZpL3.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-DB1WU5lQ.js → Trigger-DgowwBK5.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-Cfht9Ijy.js → Trust-BZutapjE.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-K3P4Vt9S.js → UkeySign-QZPt8_Sp.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-DWjhjXRh.js → VideoEditing-DlQx5UDJ.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-D9-Q1Kan.js → Wallet-ZfdTgtWA.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-BDszy-oV.js → WebAuthn-CAp0y_Cr.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-DIhij4rA.js → WorkflowEditor-BVITZEmr.js} +1 -1
- package/src/assets/web-panel/assets/{chat-D7VCiKbh.js → chat-CC2tPaNg.js} +1 -1
- package/src/assets/web-panel/assets/{colors-BvOd9pVn.js → colors-31brc9CA.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-DoqXBiAO.js → compact-item-DLMkC9C-.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-CrKRzUtp.js → createContext-BMtveI6F.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CB8pPOtd.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BWAev4WC.js → hasIn-DbD6WHW2.js} +1 -1
- package/src/assets/web-panel/assets/{index-B30NvR21.js → index-3oBsXZmF.js} +1 -1
- package/src/assets/web-panel/assets/{index-Cx7M7PSF.js → index-ALvoNTRF.js} +1 -1
- package/src/assets/web-panel/assets/{index-BHOcVYrx.js → index-AaIKG60c.js} +1 -1
- package/src/assets/web-panel/assets/{index-BXyl8O23.js → index-BFRLmCR3.js} +1 -1
- package/src/assets/web-panel/assets/{index-YGDy7nNq.js → index-BKAB6-6V.js} +1 -1
- package/src/assets/web-panel/assets/{index-BoYrS_L2.js → index-BOM-VFlv.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bq_ctRpz.js → index-BPpS-lGb.js} +1 -1
- package/src/assets/web-panel/assets/{index-CDGLdn12.js → index-Bbc2JeQG.js} +1 -1
- package/src/assets/web-panel/assets/index-BhXiaiev.js +1 -0
- package/src/assets/web-panel/assets/{index-ISvPZ05r.js → index-BiRQUoyE.js} +1 -1
- package/src/assets/web-panel/assets/{index-B0vGEbJT.js → index-BnFLzejT.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bj7EgmOp.js → index-BoO4WEJB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZGk8AbZ.js → index-C-cGlCDW.js} +1 -1
- package/src/assets/web-panel/assets/{index-BkgnGX7v.js → index-C1PfeI88.js} +1 -1
- package/src/assets/web-panel/assets/{index-HwHdTusY.js → index-C21pdBqs.js} +1 -1
- package/src/assets/web-panel/assets/{index-Bf9YlbDn.js → index-C7LHNw_N.js} +1 -1
- package/src/assets/web-panel/assets/{index-oY45XrkG.js → index-CEXa-4zd.js} +1 -1
- package/src/assets/web-panel/assets/{index-KR1-Em9C.js → index-CEwjBA9q.js} +1 -1
- package/src/assets/web-panel/assets/{index-C5kRmj3P.js → index-CFXKbOek.js} +1 -1
- package/src/assets/web-panel/assets/{index-BN9m4Ux2.js → index-CJhREOlG.js} +1 -1
- package/src/assets/web-panel/assets/{index-PDxGjKRu.js → index-COHHQ9cU.js} +1 -1
- package/src/assets/web-panel/assets/{index-D23L1V76.js → index-CUmLMFsy.js} +1 -1
- package/src/assets/web-panel/assets/{index-zH-tuuGZ.js → index-CWgfipcC.js} +1 -1
- package/src/assets/web-panel/assets/{index-CmJ6xKBO.js → index-C_eUKGYP.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEPHMSih.js → index-CcVszn2b.js} +1 -1
- package/src/assets/web-panel/assets/{index-DV-Rfmfx.js → index-Cod97Urs.js} +3 -3
- package/src/assets/web-panel/assets/{index-BWWsaimP.js → index-Cwbqh0KZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-8g5TJCZ3.js → index-D67kIagl.js} +1 -1
- package/src/assets/web-panel/assets/{index-CEXfKSiI.js → index-D7cV0tu0.js} +1 -1
- package/src/assets/web-panel/assets/{index-CoD31Ndm.js → index-DJvxFyIz.js} +1 -1
- package/src/assets/web-panel/assets/{index-CSb-tnIo.js → index-DZl-UXB6.js} +1 -1
- package/src/assets/web-panel/assets/{index-DvDOpsJB.js → index-Dm0byjm1.js} +1 -1
- package/src/assets/web-panel/assets/{index-DlGu7As6.js → index-DndNnYc0.js} +1 -1
- package/src/assets/web-panel/assets/{index-gmxqu5Kk.js → index-WQUO2vvK.js} +1 -1
- package/src/assets/web-panel/assets/index-WpaYaE3D.js +1 -0
- package/src/assets/web-panel/assets/{index-ww1QtLnX.js → index-bdaJwTXJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-JzUmJMvn.js → index-ep6JchNG.js} +1 -1
- package/src/assets/web-panel/assets/{index-BCyP071m.js → index-jG6h22Qg.js} +1 -1
- package/src/assets/web-panel/assets/{index-1poMpMU0.js → index-ntVQqSML.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-XW-OR3mL.js → initDefaultProps-D5UoBRhN.js} +1 -1
- package/src/assets/web-panel/assets/{motion-CeuEHh20.js → motion-DltOw0CR.js} +1 -1
- package/src/assets/web-panel/assets/{move-Dva79Keg.js → move-_oPkk5zj.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-C0B6bD2j.js → mtc-parser-EES8ROCS.js} +1 -1
- package/src/assets/web-panel/assets/{omit-AkZvO-a2.js → omit-B-_39x_M.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-B4r55qGc.js → pickAttrs-l6D0yJHz.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-BLH-z7YE.js → placementArrow-DIeUK31L.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-ksEGZ2Wf.js → responsiveObserve-dG4EzUQo.js} +1 -1
- package/src/assets/web-panel/assets/{slide-Cth4OD1Z.js → slide-DWFzYGdH.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-9S83ezxw.js → statusUtils-CECdTA5H.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-7yQmQGZ4.js → styleChecker-D6kP-mMK.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-DpGnMmPc.js → useFlexGapSupport-BWfna_rd.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-C4zDZIXH.js → useFs-Bb-j6rzo.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-CoLICdUZ.js → usePersonalDataHub-D5OIsYM4.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-CKyH-35X.js → vnode-DNpwzFTM.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-DRDGit17.js → zoom-vNLpFQBW.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-manifest.json +17 -3
- package/src/commands/agent.js +7 -0
- package/src/commands/background-session.js +143 -3
- package/src/commands/browse.js +86 -0
- package/src/commands/cloud.js +232 -0
- package/src/commands/doctor.js +61 -6
- package/src/commands/mcp.js +61 -0
- package/src/commands/plugin.js +63 -1
- package/src/commands/routine.js +367 -0
- package/src/commands/session.js +326 -0
- package/src/data/changelog.json +7 -2
- package/src/harness/jsonl-session-store.js +82 -2
- package/src/harness/session-index.js +295 -0
- package/src/harness/session-mirror.js +142 -0
- package/src/harness/transcript-integrity.js +182 -0
- package/src/index.js +8 -0
- package/src/lib/background-agent-supervisor.js +126 -2
- package/src/lib/channels/channel-manager.js +126 -0
- package/src/lib/channels/telegram-channel.js +112 -0
- package/src/lib/channels/webhook-channel.js +140 -0
- package/src/lib/chrome-connector.js +386 -0
- package/src/lib/cloud/bundle.js +110 -0
- package/src/lib/cloud/cloud-client.js +152 -0
- package/src/lib/computer-use/authorization.js +173 -0
- package/src/lib/computer-use/computer-use-server.js +276 -0
- package/src/lib/computer-use/control-backend.js +150 -0
- package/src/lib/doctor-checkup.js +652 -0
- package/src/lib/jsonl-session-store.js +4 -0
- package/src/lib/lsp/lsp-server-registry.js +48 -21
- package/src/lib/plugin-runtime/governance.js +174 -0
- package/src/lib/pr-link-ledger.js +221 -0
- package/src/lib/routine-store.js +352 -0
- package/src/lib/session-store-guard.js +40 -0
- package/src/lib/session-usage.js +318 -2
- package/src/lib/sub-agent-context.js +23 -0
- package/src/repl/agent-repl.js +361 -3
- package/src/repl/bg-dashboard.js +355 -0
- package/src/repl/fast-mode.js +103 -0
- package/src/repl/fullscreen-tui.js +197 -0
- package/src/repl/slash-command-registry.js +11 -3
- package/src/repl/voice-dictation.js +132 -0
- package/src/runtime/agent-core.js +242 -1
- package/src/runtime/coding-agent-contract-shared.cjs +88 -0
- package/src/runtime/coding-agent-policy.cjs +17 -0
- package/src/runtime/headless-runner.js +71 -7
- package/src/runtime/headless-stream.js +14 -8
- package/src/runtime/policies/agent-policy.js +3 -0
- package/src/assets/web-panel/assets/Artifacts-DQvJoQ6E.js +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-h1A0XB2a.js +0 -1
- package/src/assets/web-panel/assets/devWarning-nDB--2xr.js +0 -1
- package/src/assets/web-panel/assets/index-B1_pRTtB.js +0 -1
- 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
|
-
|
|
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.
|
|
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
|
}
|