@iowarp/clio-coder 0.4.6 → 0.4.7
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/.claude-plugin/marketplace.json +283 -0
- package/CHANGELOG.md +50 -0
- package/CONTRIBUTING.md +102 -29
- package/NOTICE +8 -0
- package/README.md +37 -5
- package/dist/{acp-UVEVWSDL.js → acp-W3Y3TRHD.js} +12 -12
- package/dist/{agents-VWY37DTK.js → agents-J4ICNLQ3.js} +52 -52
- package/dist/assets/codewiki.json +1 -1
- package/dist/assets/tui-notices/get-east-asian-width-LICENSE +9 -0
- package/dist/assets/tui-notices/marked-LICENSE +44 -0
- package/dist/assets/tui-notices/pi-tui-LICENSE +21 -0
- package/dist/{auth-PCV2XQDV.js → auth-X2UJUMOC.js} +24 -24
- package/dist/{builtins-PD652MTK.js → builtins-XLXWWSRF.js} +6 -6
- package/dist/{chunk-DZK63TZJ.js → chunk-24S3DN22.js} +336 -612
- package/dist/{chunk-NTK3WTHM.js → chunk-24TOGHPO.js} +2 -2
- package/dist/{chunk-M6TAYK66.js → chunk-2O2LVQQK.js} +11 -4
- package/dist/{chunk-OQM5IVJY.js → chunk-34XHRVKG.js} +3 -3
- package/dist/{chunk-LY4N4TTL.js → chunk-3LXJOICS.js} +2 -2
- package/dist/{chunk-3BJGMT2D.js → chunk-3XE4NRLF.js} +1 -1
- package/dist/{chunk-N2OKBREN.js → chunk-4MFBBY2Z.js} +3 -3
- package/dist/{chunk-5VTJP36Q.js → chunk-4N2RZ4TO.js} +2 -2
- package/dist/{chunk-WNGF5AJX.js → chunk-4QKAITQT.js} +3 -3
- package/dist/chunk-4UHVS5T7.js +207 -0
- package/dist/{chunk-GLYBJ4RQ.js → chunk-5AJPUR72.js} +6 -6
- package/dist/{chunk-5CY4Q6QI.js → chunk-5EQO5NWP.js} +105 -3
- package/dist/{chunk-6HAO6X5B.js → chunk-65BGHOUR.js} +8 -5
- package/dist/{chunk-6VRRJT7L.js → chunk-6CA2BHOR.js} +2 -2
- package/dist/{chunk-HL26ADLH.js → chunk-6CB2IL4N.js} +2 -1
- package/dist/{chunk-E7RF4KBO.js → chunk-6LRLOJHQ.js} +3 -3
- package/dist/{chunk-HXHFDHB6.js → chunk-6TAATXXH.js} +6 -6
- package/dist/{chunk-P6M7RKMI.js → chunk-6YHPAAZR.js} +3 -3
- package/dist/{chunk-5QRX7SLS.js → chunk-7EEGBKV4.js} +7 -7
- package/dist/{chunk-BS4GAMUB.js → chunk-A67NR5AT.js} +2 -2
- package/dist/{chunk-WJIMKGD6.js → chunk-BAU36233.js} +8 -8
- package/dist/{chunk-PZN2LQY6.js → chunk-BO4XV5VX.js} +4 -4
- package/dist/{chunk-I2K2NCZQ.js → chunk-BQQ5MXUC.js} +6 -6
- package/dist/{chunk-X3BI7HTV.js → chunk-C3G5PNUO.js} +21 -21
- package/dist/{chunk-SRZOW5L2.js → chunk-C6GEXX4L.js} +1 -1
- package/dist/{chunk-6RYDAVQT.js → chunk-CE64C5K5.js} +219 -64
- package/dist/chunk-CP3HIR5H.js +2119 -0
- package/dist/{chunk-BBV5TKE6.js → chunk-CPXGJI2A.js} +2 -2
- package/dist/{chunk-IGLVVNEG.js → chunk-CULVKYTK.js} +4 -4
- package/dist/{chunk-DXLUVUD4.js → chunk-D2T66PDV.js} +5 -5
- package/dist/{chunk-NF3BK5WR.js → chunk-DACXIVVF.js} +5 -5
- package/dist/{chunk-IKRWLEDA.js → chunk-DLIYANB7.js} +20 -15
- package/dist/{chunk-RGZNQ5CT.js → chunk-DO7M3LSX.js} +9 -9
- package/dist/{chunk-2QPI3TR3.js → chunk-DVJL77SO.js} +2 -2
- package/dist/{chunk-ODUTYZIU.js → chunk-EUEXGXXR.js} +5 -5
- package/dist/{chunk-WKUYQBCG.js → chunk-EUZRMP4Y.js} +18 -16
- package/dist/{chunk-JMPL7NUB.js → chunk-G4LGUTOE.js} +4 -4
- package/dist/{chunk-CBSKDFWL.js → chunk-GEVQNST4.js} +2 -2
- package/dist/{chunk-NNWJYUNO.js → chunk-GFQ7P2UF.js} +7 -7
- package/dist/{chunk-SXQKNKRA.js → chunk-GI5RRMAH.js} +14 -14
- package/dist/{chunk-HPTJF6DG.js → chunk-GVZFCJHE.js} +336 -83
- package/dist/{chunk-ACCKVSHU.js → chunk-GXIWHXDB.js} +3 -3
- package/dist/{chunk-LUL5Z66S.js → chunk-HOX4YZ2K.js} +6 -5
- package/dist/chunk-HRAKUPIH.js +57 -0
- package/dist/{chunk-F5Z7EWMN.js → chunk-IJMW64DX.js} +50 -50
- package/dist/{chunk-QNWRHOMF.js → chunk-IMOD2YPV.js} +16 -9
- package/dist/{chunk-HZ2T2YED.js → chunk-IQCNVLO6.js} +28 -8
- package/dist/{chunk-HZFBVU5X.js → chunk-ISNCCX42.js} +8 -8
- package/dist/{chunk-7JSSKXG2.js → chunk-IZYTT3CD.js} +7 -3
- package/dist/chunk-KKEYGFX4.js +915 -0
- package/dist/{chunk-HHVJNVUJ.js → chunk-KS7ETA3P.js} +2 -2
- package/dist/{chunk-BLTEAUCB.js → chunk-L3Z5ANCV.js} +7 -7
- package/dist/{chunk-4VMV6OS6.js → chunk-LBFQMYS3.js} +2 -2
- package/dist/{chunk-URBGEOH3.js → chunk-M6Z2V6YU.js} +5 -5
- package/dist/{chunk-BT7HGDOX.js → chunk-MBJO5VNZ.js} +12 -12
- package/dist/chunk-MNQN3X6Y.js +595 -0
- package/dist/{chunk-3QKXHCGS.js → chunk-MZHXENL7.js} +5 -5
- package/dist/chunk-MZNWZUJM.js +1801 -0
- package/dist/{chunk-7YJWEF2L.js → chunk-N37QC5FV.js} +5 -5
- package/dist/{chunk-TJFDL26W.js → chunk-NCCQ6DVS.js} +7 -7
- package/dist/{chunk-P5JMBPDJ.js → chunk-NO7J45X6.js} +162 -47
- package/dist/chunk-OH3YYA3B.js +15 -0
- package/dist/{chunk-22PQ4TX4.js → chunk-P4DL2LM5.js} +16 -77
- package/dist/{chunk-LIN2K3EL.js → chunk-PFLOULP3.js} +4 -4
- package/dist/{chunk-QXFBABFJ.js → chunk-QKWL7SYE.js} +11 -11
- package/dist/chunk-RFCKPS2V.js +26 -0
- package/dist/{chunk-ZS43Y2FT.js → chunk-RTLNFZMD.js} +12 -4
- package/dist/{chunk-U7H5WRG5.js → chunk-RXFPB32E.js} +8 -6
- package/dist/{chunk-Q7NNUHQV.js → chunk-SEDI5K5B.js} +16 -16
- package/dist/{chunk-WV6TDAPN.js → chunk-SELG4SUR.js} +7 -7
- package/dist/{chunk-3WBALRQH.js → chunk-SER7RRAE.js} +27 -832
- package/dist/{chunk-K2HL3GBL.js → chunk-T4TY5GG3.js} +165 -15
- package/dist/chunk-T7QQBNLH.js +691 -0
- package/dist/{chunk-6OII3LOK.js → chunk-TLTGZTXI.js} +3 -3
- package/dist/chunk-TZ6UVP2H.js +637 -0
- package/dist/{chunk-3JLFK5MW.js → chunk-TZHN5ITX.js} +13 -41
- package/dist/{chunk-VANN3GDP.js → chunk-U3TPUXOQ.js} +399 -234
- package/dist/{chunk-VHMX7SXF.js → chunk-UA3W4FGC.js} +6 -6
- package/dist/{chunk-3O2ZAUGQ.js → chunk-UA6W4XFJ.js} +2 -2
- package/dist/chunk-UWD3JD5X.js +709 -0
- package/dist/chunk-V4ZKOKRB.js +13433 -0
- package/dist/{chunk-NC4KV7EL.js → chunk-VCRVEDUC.js} +5 -5
- package/dist/{chunk-4DU2TVI4.js → chunk-VMATUZB6.js} +12 -10
- package/dist/{chunk-J2FYEQ5L.js → chunk-VTYHHQ57.js} +2 -2
- package/dist/chunk-WB7POC6M.js +891 -0
- package/dist/{chunk-3HQVBIKD.js → chunk-WBVKKTUT.js} +2 -2
- package/dist/{chunk-EHOSNVVM.js → chunk-WLHX3MM5.js} +6 -2
- package/dist/{chunk-24T2YZ7A.js → chunk-WN552PKN.js} +553 -298
- package/dist/{chunk-4U4TKZFR.js → chunk-WT5TRMO4.js} +51 -26
- package/dist/{chunk-XWMSPJYN.js → chunk-WURGVOZJ.js} +56 -12
- package/dist/{chunk-7WUZEB2M.js → chunk-XR3XJN2M.js} +2 -2
- package/dist/chunk-XW2UZICE.js +372 -0
- package/dist/{chunk-2AKYFREM.js → chunk-XZ6UFQ52.js} +11 -11
- package/dist/chunk-Y3JU5RPK.js +8 -0
- package/dist/chunk-YKRJTYNP.js +1143 -0
- package/dist/{chunk-PWWSIB2A.js → chunk-ZGIGW4ZG.js} +5 -5
- package/dist/cli/index.js +34 -36
- package/dist/{clio-QQCMTUZE.js → clio-L2P7S2R2.js} +9 -9
- package/dist/{code-nav-GUPM6766.js → code-nav-B5LNX4KU.js} +25 -25
- package/dist/{components-TNIKXCBH.js → components-63MMQKUT.js} +6 -6
- package/dist/{config-IIM7YOD3.js → config-YXARKGTX.js} +70 -67
- package/dist/{configure-4ZM5HWJX.js → configure-LMVQJ46J.js} +47 -35
- package/dist/{context-OL5P3HEP.js → context-7HJEWTTP.js} +87 -44
- package/dist/{context-44BZLJ3J.js → context-XSZKKFBW.js} +14 -14
- package/dist/{context-AIDVQKMW.js → context-YG2LZDYZ.js} +60 -58
- package/dist/{context-clear-OQV76MYP.js → context-clear-AC5NXHM2.js} +57 -55
- package/dist/{context-index-HP2JFUWA.js → context-index-KKTPRV5G.js} +3 -3
- package/dist/{context-map-INTXD54D.js → context-map-T7ROR5D5.js} +4 -4
- package/dist/{context-working-set-ZUW3EGLS.js → context-working-set-F4Q4J3NS.js} +12 -10
- package/dist/{detail-UXPWRANG.js → detail-H73BEF2N.js} +58 -56
- package/dist/{dispatch-runner-P3ZW6G77.js → dispatch-runner-3RY5FSJC.js} +74 -72
- package/dist/{doctor-JJVGPZWT.js → doctor-EQYJOGUG.js} +44 -30
- package/dist/{eval-V2RTN3UH.js → eval-4P7O6CCE.js} +109 -46
- package/dist/{evidence-35PYWGSO.js → evidence-D6GUSYJW.js} +57 -55
- package/dist/{evidence-CIGGKI6J.js → evidence-O76QF7TZ.js} +62 -59
- package/dist/{evolve-33SM3NJP.js → evolve-H47BCETS.js} +58 -56
- package/dist/{extensions-YVRHX6ZI.js → extensions-NNL3ZOCU.js} +98 -20
- package/dist/{fleet-LFO7SIPC.js → fleet-KMNS6NOM.js} +106 -99
- package/dist/{fleet-commands-RLK72GU3.js → fleet-commands-DOWSYCBX.js} +14 -14
- package/dist/{fleet-decisions-3SBSCTCW.js → fleet-decisions-S3PTZV2D.js} +9 -9
- package/dist/{fleet-graph-JZIBCGMW.js → fleet-graph-F2DE2TME.js} +26 -24
- package/dist/{fleet-inspect-2L5YGGZJ.js → fleet-inspect-EN2Q6IVB.js} +60 -57
- package/dist/{fleet-preflight-QRSHIIDE.js → fleet-preflight-FRLETDD3.js} +3 -3
- package/dist/{fleet-validate-LIAVKDUU.js → fleet-validate-B2XDLS57.js} +28 -29
- package/dist/{fleet-verify-JIK3OQZR.js → fleet-verify-4JKUWGJ7.js} +57 -55
- package/dist/{fleet-view-IWHYGQKK.js → fleet-view-BHJFW26B.js} +60 -57
- package/dist/{init-NJEDOTQW.js → init-XAT5N5YM.js} +79 -74
- package/dist/interop-FVECHFKG.js +246 -0
- package/dist/{inventory-74UL7UHU.js → inventory-7KFQGLCV.js} +60 -57
- package/dist/library-NY2XOT75.js +492 -0
- package/dist/library-QAPAO23R.js +306 -0
- package/dist/library-import-5Y6MAS3R.js +183 -0
- package/dist/library-inventory-JT6HWITQ.js +56 -0
- package/dist/library-validation-EQUH25DC.js +42 -0
- package/dist/{memory-I76I5VUH.js → memory-DYVTTCP6.js} +58 -56
- package/dist/{models-D4YDIQZQ.js → models-MASFJ6Y2.js} +48 -36
- package/dist/{monitor-NKVD55HX.js → monitor-YMP4NXJJ.js} +64 -61
- package/dist/{orchestrator-I6OATVPK.js → orchestrator-AFQRRTEI.js} +3685 -1841
- package/dist/{panes-7BTYH3XQ.js → panes-D3Y2KFG5.js} +4 -4
- package/dist/{panes-WPOMEMRH.js → panes-NICTHQCV.js} +7 -7
- package/dist/{paths-J3KN7UAO.js → paths-PSFAUIGO.js} +5 -5
- package/dist/{preload-EJAWJEIO.js → preload-5Q7VXRFJ.js} +57 -55
- package/dist/{reset-4XMQKDNZ.js → reset-7NKZK7JN.js} +9 -9
- package/dist/{resources-TVYVXK3Q.js → resources-NILSIZAT.js} +94 -38
- package/dist/{run-OWQNNSJU.js → run-BPZGEBZV.js} +121 -105
- package/dist/{share-NPZZMUXG.js → share-EZ6ERBMH.js} +31 -28
- package/dist/skills-BTVVNYVM.js +100 -0
- package/dist/{skills-eval-J5TCKSJA.js → skills-eval-UY34MNK7.js} +110 -72
- package/dist/{skills-inventory-7II4DM57.js → skills-inventory-JDHSIA7I.js} +30 -25
- package/dist/{slash-commands-MDSDSWYF.js → slash-commands-K4FNQ2AT.js} +49 -46
- package/dist/{steer-S7D7IKPU.js → steer-2B3EQGFH.js} +4 -4
- package/dist/{support-5VAEC32W.js → support-L2SOL3IE.js} +7 -7
- package/dist/{targets-KILZEOCE.js → targets-44VKGFLN.js} +64 -58
- package/dist/{tasks-R6P6OV22.js → tasks-3KODKFU7.js} +12 -12
- package/dist/{terminal-lease-LZCG2KN3.js → terminal-lease-LXA7MMPO.js} +32 -19
- package/dist/{tools-6OCQ6SVB.js → tools-3UB7TEVR.js} +7 -7
- package/dist/{trace-JHLXWUMN.js → trace-TAMIU4OJ.js} +5 -4
- package/dist/{tui-primitives-RCY2HN7G.js → tui-primitives-A6MUW32V.js} +3 -2
- package/dist/{uninstall-FXYMFFRT.js → uninstall-QYVRPEM4.js} +9 -9
- package/dist/{upgrade-D2OZH6IQ.js → upgrade-7PMKM2JH.js} +15 -16
- package/dist/{usage-PIASNUS4.js → usage-6EQV7EQK.js} +70 -67
- package/dist/{verifiers-LTHUY2PX.js → verifiers-JLITWSHU.js} +15 -15
- package/dist/{verify-HYF7KBVE.js → verify-T7NAEVIG.js} +13 -13
- package/dist/{web-fetch-MQN4FH3X.js → web-fetch-W4TNW4Q7.js} +4 -4
- package/dist/{wiki-generate-RK7JQ5NP.js → wiki-generate-T2TJQJUA.js} +81 -76
- package/dist/{with-panes-OFTNAR4Y.js → with-panes-CHFC7HGI.js} +4 -4
- package/dist/worker/entry.js +283 -76
- package/docs/README.md +43 -0
- package/docs/architecture/architecture.md +8 -7
- package/docs/architecture/artifact-versions.md +3 -3
- package/docs/architecture/context-engine.md +35 -1
- package/docs/architecture/library.md +32 -0
- package/docs/architecture/middleware-and-components.md +1 -1
- package/docs/architecture/model-catalog.md +21 -1
- package/docs/architecture/pi-boundary.md +18 -0
- package/docs/architecture/prompt-envelope-and-tools.md +5 -5
- package/docs/architecture/safety-model.md +21 -2
- package/docs/architecture/session-lifecycle.md +12 -1
- package/docs/architecture/tui-design.md +108 -9
- package/docs/architecture/worker-context.md +106 -0
- package/docs/guide/authoring-plugins.md +160 -0
- package/docs/guide/built-in-agents.md +5 -5
- package/docs/guide/commands-and-modes.md +161 -56
- package/docs/guide/configuration-and-targets.md +1 -1
- package/docs/guide/configuration-reference.md +27 -34
- package/docs/guide/environment-variables.md +2 -2
- package/docs/guide/exit-codes-and-output.md +1 -1
- package/docs/guide/extensions-and-sharing.md +93 -53
- package/docs/guide/fleet-dispatch.md +31 -4
- package/docs/guide/glossary.md +1 -1
- package/docs/guide/harness-extensions.md +291 -0
- package/docs/guide/installation-and-lifecycle.md +9 -9
- package/docs/guide/interop.md +243 -0
- package/docs/guide/panes-and-files.md +3 -3
- package/docs/guide/plugins.md +80 -0
- package/docs/guide/resource-library.md +111 -45
- package/docs/guide/skills-marketplace.md +35 -72
- package/docs/guide/tool-usage.md +19 -7
- package/docs/guide/troubleshooting.md +1 -1
- package/docs/process/development-pipeline.md +14 -14
- package/docs/process/documentation-coverage.md +28 -1
- package/docs/process/documentation-guide.md +15 -7
- package/docs/process/eval-runner.md +8 -3
- package/docs/process/performance-methodology.md +1 -1
- package/docs/process/release-cut-checklist.md +4 -0
- package/docs/process/scientific-validation.md +23 -0
- package/evals/scalar-decision-machinery.ts +1 -1
- package/examples/extensions/README.md +67 -0
- package/examples/extensions/lab-status/clio-coder-extension.json +14 -0
- package/examples/extensions/lab-status/extension.mjs +68 -0
- package/examples/extensions/lab-status/jobs.synthetic.json +9 -0
- package/examples/extensions/measurements/clio-coder-extension.json +28 -0
- package/examples/extensions/measurements/summarize.cjs +23 -0
- package/library/README.md +109 -0
- package/library/_authoring/templates/agent/README.md +28 -0
- package/library/_authoring/templates/agent/agents/benchmark-scout.md +30 -0
- package/library/_authoring/templates/agent/plugin.json +23 -0
- package/library/_authoring/templates/fleet/README.md +29 -0
- package/library/_authoring/templates/fleet/fleets/pipeline-review.md +31 -0
- package/library/_authoring/templates/fleet/plugin.json +23 -0
- package/library/_authoring/templates/plugin/README.md +37 -0
- package/library/_authoring/templates/plugin/agents/data-curator.md +31 -0
- package/library/_authoring/templates/plugin/assets/curation-guide.md +22 -0
- package/library/_authoring/templates/plugin/plugin.json +42 -0
- package/library/_authoring/templates/plugin/prompts/inspect-dataset.md +15 -0
- package/library/_authoring/templates/plugin/skills/dataset-curation/SKILL.md +36 -0
- package/library/_authoring/templates/plugin/skills/dataset-curation/evals.md +17 -0
- package/library/_authoring/templates/prompt/README.md +28 -0
- package/library/_authoring/templates/prompt/plugin.json +23 -0
- package/library/_authoring/templates/prompt/prompts/paper-summary.md +26 -0
- package/library/_authoring/templates/skill/README.md +24 -0
- package/library/_authoring/templates/skill/SKILL.md +39 -0
- package/library/_authoring/templates/skill/evals.md +30 -0
- package/library/_authoring/templates/skill/plugin.json +23 -0
- package/library/agents/README.md +7 -0
- package/library/fleets/README.md +7 -0
- package/library/plugins/materio/README.md +137 -0
- package/library/plugins/materio/ai.iowarp.clio/agents/materio-lab-definer.md +208 -0
- package/library/plugins/materio/ai.iowarp.clio/agents/materio-literature-reviewer.md +308 -0
- package/library/plugins/materio/ai.iowarp.clio/agents/materio-research-explorer.md +249 -0
- package/library/plugins/materio/ai.iowarp.clio/agents/materio-task-executor.md +416 -0
- package/library/plugins/materio/ai.iowarp.clio/agents/materio-task-verifier.md +60 -0
- package/library/plugins/materio/ai.iowarp.clio/agents/materio-workflow-planner.md +238 -0
- package/library/plugins/materio/ai.iowarp.clio/fleets/materio-execute-task.md +50 -0
- package/library/plugins/materio/ai.iowarp.clio/prompts/materio/add-task.md +66 -0
- package/library/plugins/materio/ai.iowarp.clio/prompts/materio/archive-task.md +65 -0
- package/library/plugins/materio/ai.iowarp.clio/prompts/materio/checkpoint.md +26 -0
- package/library/plugins/materio/ai.iowarp.clio/prompts/materio/define-research-tasks.md +230 -0
- package/library/plugins/materio/ai.iowarp.clio/prompts/materio/define-virtual-lab.md +181 -0
- package/library/plugins/materio/ai.iowarp.clio/prompts/materio/execute-task.md +292 -0
- package/library/plugins/materio/ai.iowarp.clio/prompts/materio/help.md +129 -0
- package/library/plugins/materio/ai.iowarp.clio/prompts/materio/identify-research.md +206 -0
- package/library/plugins/materio/ai.iowarp.clio/prompts/materio/literature-review.md +201 -0
- package/library/plugins/materio/ai.iowarp.clio/prompts/materio/pause-research.md +12 -0
- package/library/plugins/materio/ai.iowarp.clio/prompts/materio/progress.md +105 -0
- package/library/plugins/materio/ai.iowarp.clio/prompts/materio/remove-task.md +63 -0
- package/library/plugins/materio/ai.iowarp.clio/prompts/materio/resume-research.md +64 -0
- package/library/plugins/materio/ai.iowarp.clio/prompts/materio/settings.md +16 -0
- package/library/plugins/materio/ai.iowarp.clio/prompts/materio/status.md +92 -0
- package/library/plugins/materio/ai.iowarp.clio/prompts/materio/upload-data.md +104 -0
- package/library/plugins/materio/ai.iowarp.clio/prompts/materio/wtfp.md +145 -0
- package/library/plugins/materio/ai.iowarp.clio/references/execution.md +56 -0
- package/library/plugins/materio/ai.iowarp.portability/provenance.json +9 -0
- package/library/plugins/materio/assets/actions/add-task.md +86 -0
- package/library/plugins/materio/assets/actions/archive-task.md +84 -0
- package/library/plugins/materio/assets/actions/checkpoint.md +47 -0
- package/library/plugins/materio/assets/actions/define-research-tasks.md +228 -0
- package/library/plugins/materio/assets/actions/define-virtual-lab.md +179 -0
- package/library/plugins/materio/assets/actions/execute-task.md +290 -0
- package/library/plugins/materio/assets/actions/help.md +140 -0
- package/library/plugins/materio/assets/actions/identify-research.md +204 -0
- package/library/plugins/materio/assets/actions/literature-review.md +199 -0
- package/library/plugins/materio/assets/actions/pause-research.md +34 -0
- package/library/plugins/materio/assets/actions/progress.md +125 -0
- package/library/plugins/materio/assets/actions/remove-task.md +82 -0
- package/library/plugins/materio/assets/actions/resume-research.md +84 -0
- package/library/plugins/materio/assets/actions/settings.md +38 -0
- package/library/plugins/materio/assets/actions/status.md +112 -0
- package/library/plugins/materio/assets/actions/upload-data.md +123 -0
- package/library/plugins/materio/assets/actions/wtfp.md +179 -0
- package/library/plugins/materio/assets/references/research-domains.md +134 -0
- package/library/plugins/materio/assets/references/research-policy.md +77 -0
- package/library/plugins/materio/assets/references/traditional-workflows.md +146 -0
- package/library/plugins/materio/assets/scripts/check_physics.py +271 -0
- package/library/plugins/materio/assets/scripts/check_scripts.py +248 -0
- package/library/plugins/materio/assets/scripts/project_plugin.py +165 -0
- package/library/plugins/materio/assets/scripts/research_state.py +400 -0
- package/library/plugins/materio/assets/scripts/verify_citations.py +616 -0
- package/library/plugins/materio/assets/templates/RESEARCH.md +39 -0
- package/library/plugins/materio/assets/templates/VIRTUAL-LAB.md +120 -0
- package/library/plugins/materio/assets/templates/WORKFLOW.md +42 -0
- package/library/plugins/materio/assets/templates/config.json +5 -0
- package/library/plugins/materio/evals/scripts.yaml +29 -0
- package/library/plugins/materio/plugin.json +530 -0
- package/library/plugins/materio/skills/materio-lab-definer/SKILL.md +74 -0
- package/library/plugins/materio/skills/materio-literature-reviewer/SKILL.md +64 -0
- package/library/plugins/materio/skills/materio-research-explorer/SKILL.md +74 -0
- package/library/plugins/materio/skills/materio-task-executor/SKILL.md +214 -0
- package/library/plugins/materio/skills/materio-task-verifier/SKILL.md +46 -0
- package/library/plugins/materio/skills/materio-workflow-planner/SKILL.md +79 -0
- package/library/plugins/materio/tests/test_check_physics.py +139 -0
- package/library/plugins/materio/tests/test_check_scripts.py +140 -0
- package/library/plugins/materio/tests/test_research_state.py +291 -0
- package/library/plugins/materio/tests/test_verify_citations.py +337 -0
- package/library/prompts/README.md +7 -0
- package/library/registry.yaml +921 -0
- package/library/skills/README.md +120 -0
- package/{skills → library/skills}/coding/ast-grep/SKILL.md +1 -1
- package/{skills → library/skills}/coding/ast-grep/evals.md +1 -1
- package/library/skills/coding/ast-grep/plugin.json +23 -0
- package/{skills → library/skills}/coding/coding-standards/SKILL.md +1 -1
- package/{skills → library/skills}/coding/coding-standards/evals.md +1 -1
- package/library/skills/coding/coding-standards/plugin.json +23 -0
- package/{skills → library/skills}/coding/prototype/SKILL.md +1 -1
- package/{skills → library/skills}/coding/prototype/evals.md +1 -1
- package/library/skills/coding/prototype/plugin.json +23 -0
- package/{skills → library/skills}/coding/tdd/SKILL.md +1 -1
- package/{skills → library/skills}/coding/tdd/evals.md +1 -1
- package/library/skills/coding/tdd/plugin.json +23 -0
- package/{skills → library/skills}/context/context-handoff/SKILL.md +1 -1
- package/{skills → library/skills}/context/context-handoff/evals.md +1 -1
- package/library/skills/context/context-handoff/plugin.json +23 -0
- package/{skills → library/skills}/context/context-prime/SKILL.md +1 -1
- package/{skills → library/skills}/context/context-prime/evals.md +1 -1
- package/library/skills/context/context-prime/plugin.json +23 -0
- package/{skills → library/skills}/git/branch-closeout/SKILL.md +1 -1
- package/library/skills/git/branch-closeout/plugin.json +23 -0
- package/{skills → library/skills}/git/file-ticket/SKILL.md +1 -1
- package/library/skills/git/file-ticket/plugin.json +23 -0
- package/{skills → library/skills}/git/fix-issue/SKILL.md +1 -1
- package/library/skills/git/fix-issue/plugin.json +23 -0
- package/{skills → library/skills}/git/resolve-merge-conflicts/SKILL.md +1 -1
- package/{skills → library/skills}/git/resolve-merge-conflicts/evals.md +1 -1
- package/library/skills/git/resolve-merge-conflicts/plugin.json +23 -0
- package/{skills → library/skills}/git/ship/SKILL.md +1 -1
- package/library/skills/git/ship/plugin.json +23 -0
- package/{skills → library/skills}/git/worktree-create/SKILL.md +1 -1
- package/library/skills/git/worktree-create/plugin.json +23 -0
- package/{skills → library/skills}/git/worktree-merge/SKILL.md +1 -1
- package/library/skills/git/worktree-merge/plugin.json +23 -0
- package/{skills → library/skills}/meta/clio-coder-dev/SKILL.md +1 -1
- package/{skills → library/skills}/meta/clio-coder-dev/evals.md +1 -1
- package/library/skills/meta/clio-coder-dev/plugin.json +23 -0
- package/{skills → library/skills}/meta/clio-coder-test/SKILL.md +1 -1
- package/{skills → library/skills}/meta/clio-coder-test/evals.md +1 -1
- package/library/skills/meta/clio-coder-test/plugin.json +23 -0
- package/{skills → library/skills}/meta/credentials/SKILL.md +1 -1
- package/{skills → library/skills}/meta/credentials/evals.md +1 -1
- package/library/skills/meta/credentials/plugin.json +23 -0
- package/library/skills/meta/find-skills/SKILL.md +85 -0
- package/library/skills/meta/find-skills/evals.md +65 -0
- package/library/skills/meta/find-skills/plugin.json +23 -0
- package/{skills → library/skills}/meta/herdr/SKILL.md +1 -1
- package/{skills → library/skills}/meta/herdr/evals.md +1 -1
- package/library/skills/meta/herdr/plugin.json +23 -0
- package/{skills → library/skills}/meta/skill-craft/SKILL.md +23 -9
- package/{skills → library/skills}/meta/skill-craft/evals.md +11 -3
- package/library/skills/meta/skill-craft/plugin.json +23 -0
- package/{skills → library/skills}/planning/archify/SKILL.md +13 -13
- package/library/skills/planning/archify/plugin.json +23 -0
- package/{skills → library/skills}/planning/architecture/SKILL.md +1 -1
- package/{skills → library/skills}/planning/architecture/evals.md +1 -1
- package/library/skills/planning/architecture/plugin.json +23 -0
- package/{skills → library/skills}/planning/backlog/SKILL.md +1 -1
- package/{skills → library/skills}/planning/backlog/evals.md +1 -1
- package/library/skills/planning/backlog/plugin.json +23 -0
- package/{skills → library/skills}/planning/prd/SKILL.md +1 -1
- package/{skills → library/skills}/planning/prd/evals.md +1 -1
- package/library/skills/planning/prd/plugin.json +23 -0
- package/{skills → library/skills}/planning/product-intent/SKILL.md +1 -1
- package/{skills → library/skills}/planning/product-intent/evals.md +1 -1
- package/library/skills/planning/product-intent/plugin.json +23 -0
- package/{skills → library/skills}/planning/tech-spec/SKILL.md +1 -1
- package/{skills → library/skills}/planning/tech-spec/evals.md +1 -1
- package/library/skills/planning/tech-spec/plugin.json +23 -0
- package/{skills → library/skills}/registry.yaml +7 -7
- package/{skills → library/skills}/remote.yaml +1 -1
- package/{skills → library/skills}/research/arxiv-literature/SKILL.md +1 -1
- package/{skills → library/skills}/research/arxiv-literature/evals.md +1 -1
- package/library/skills/research/arxiv-literature/plugin.json +23 -0
- package/{skills → library/skills}/research/experiment-protocol/SKILL.md +1 -1
- package/{skills → library/skills}/research/experiment-protocol/evals.md +1 -1
- package/library/skills/research/experiment-protocol/plugin.json +23 -0
- package/{skills → library/skills}/research/scientific-debugging/SKILL.md +1 -1
- package/{skills → library/skills}/research/scientific-debugging/evals.md +1 -1
- package/library/skills/research/scientific-debugging/plugin.json +23 -0
- package/{skills → library/skills}/research/scientific-modernization/SKILL.md +1 -1
- package/{skills → library/skills}/research/scientific-modernization/evals.md +1 -1
- package/library/skills/research/scientific-modernization/plugin.json +23 -0
- package/{skills → library/skills}/skill-marketplace.json +37 -37
- package/{skills → library/skills}/workflow/cut-it/SKILL.md +1 -1
- package/{skills → library/skills}/workflow/cut-it/evals.md +1 -1
- package/library/skills/workflow/cut-it/plugin.json +23 -0
- package/{skills → library/skills}/workflow/design-council/SKILL.md +1 -1
- package/{skills → library/skills}/workflow/design-council/evals.md +1 -1
- package/library/skills/workflow/design-council/plugin.json +23 -0
- package/{skills → library/skills}/workflow/grill-me/SKILL.md +1 -1
- package/{skills → library/skills}/workflow/grill-me/evals.md +1 -1
- package/library/skills/workflow/grill-me/plugin.json +23 -0
- package/{skills → library/skills}/workflow/workflow-distiller/SKILL.md +14 -11
- package/{skills → library/skills}/workflow/workflow-distiller/evals.md +14 -6
- package/library/skills/workflow/workflow-distiller/plugin.json +23 -0
- package/package.json +12 -5
- package/src/cli/agents.ts +1 -1
- package/src/cli/config-inspect.ts +1 -1
- package/src/cli/configure.ts +2 -2
- package/src/cli/eval.ts +40 -6
- package/src/cli/extensions.ts +74 -8
- package/src/cli/fleet-preflight.ts +2 -0
- package/src/cli/fleet.ts +2 -0
- package/src/cli/index.ts +1 -3
- package/src/cli/interop-inspect.ts +61 -33
- package/src/cli/interop.ts +79 -9
- package/src/cli/library-import.ts +139 -0
- package/src/cli/library.ts +418 -120
- package/src/cli/run.ts +26 -9
- package/src/cli/skills-eval.ts +43 -6
- package/src/cli/skills-inventory.ts +6 -2
- package/src/cli/skills.ts +44 -491
- package/src/core/bus-events.ts +5 -0
- package/src/core/defaults.ts +1 -1
- package/src/core/recent-models.ts +2 -2
- package/src/core/session-routing.ts +3 -3
- package/src/core/state-file-lock.ts +1 -1
- package/src/core/tool-names.ts +5 -0
- package/src/domains/agents/extension.ts +42 -6
- package/src/domains/agents/fleet-commands.ts +92 -1
- package/src/domains/agents/fleet-contract.ts +87 -19
- package/src/domains/agents/recipe.ts +5 -4
- package/src/domains/agents/registry.ts +80 -24
- package/src/domains/config/keybindings.ts +155 -95
- package/src/domains/context/worker/contract.ts +91 -0
- package/src/domains/context/worker/pressure.ts +120 -0
- package/src/domains/context/worker/recall.ts +62 -0
- package/src/domains/context/worker/select.ts +164 -0
- package/src/domains/context/worker/snapshot.ts +35 -0
- package/src/domains/context/worker/store.ts +16 -0
- package/src/domains/context/working-set/replay/synthetic.ts +2 -1
- package/src/domains/dispatch/active-route-planner.ts +2 -0
- package/src/domains/dispatch/backoff.ts +1 -0
- package/src/domains/dispatch/code-step.ts +12 -2
- package/src/domains/dispatch/contract.ts +3 -0
- package/src/domains/dispatch/execution-plan.ts +2 -0
- package/src/domains/dispatch/extension.ts +109 -27
- package/src/domains/dispatch/fleet-plan.ts +10 -0
- package/src/domains/dispatch/fleet-run.ts +1 -0
- package/src/domains/dispatch/orphan-recovery.ts +1 -0
- package/src/domains/dispatch/receipt-integrity.ts +4 -0
- package/src/domains/dispatch/state.ts +1 -0
- package/src/domains/dispatch/types.ts +5 -0
- package/src/domains/dispatch/validation.ts +12 -0
- package/src/domains/dispatch/workspace-checkpoint.ts +1 -1
- package/src/domains/evidence/failure-attribution.ts +1 -0
- package/src/domains/evidence/trust-status.ts +22 -3
- package/src/domains/extensions/command-schema.ts +145 -0
- package/src/domains/extensions/command-tools.ts +15 -0
- package/src/domains/extensions/compatibility.ts +7 -0
- package/src/domains/extensions/contract.ts +0 -4
- package/src/domains/extensions/discovery.ts +69 -205
- package/src/domains/extensions/extension.ts +0 -4
- package/src/domains/extensions/index.ts +1 -5
- package/src/domains/extensions/manager.ts +1 -6
- package/src/domains/extensions/operator-commands.ts +29 -0
- package/src/domains/extensions/operator-runtime.ts +483 -0
- package/src/domains/extensions/public-api.ts +53 -0
- package/src/domains/extensions/runtime-child.mjs +114 -0
- package/src/domains/extensions/runtime-process.ts +306 -0
- package/src/domains/extensions/runtime-schema.ts +146 -0
- package/src/domains/extensions/{resources.ts → snapshot-access.ts} +1 -17
- package/src/domains/extensions/snapshot.ts +4 -32
- package/src/domains/extensions/state.ts +5 -83
- package/src/domains/extensions/types.ts +33 -34
- package/src/domains/interop/adopt.ts +546 -0
- package/src/domains/interop/detect.ts +50 -12
- package/src/domains/interop/foreign.ts +554 -0
- package/src/domains/interop/import.ts +277 -0
- package/src/domains/interop/index.ts +24 -0
- package/src/domains/interop/install.ts +6 -0
- package/src/domains/interop/inventory.ts +351 -0
- package/src/domains/interop/projection.ts +360 -0
- package/src/domains/interop/registry.ts +106 -3
- package/src/domains/interop/types.ts +51 -0
- package/src/domains/lifecycle/migrations/index.ts +0 -2
- package/src/domains/lifecycle/naming-resources.ts +1 -1
- package/src/domains/middleware/marketplace-offer.ts +1 -1
- package/src/domains/mux/contract.ts +1 -1
- package/src/domains/plugins/catalog.ts +204 -0
- package/src/domains/plugins/contract.ts +24 -0
- package/src/domains/plugins/discovery.ts +316 -0
- package/src/domains/plugins/extension.ts +40 -0
- package/src/domains/plugins/index.ts +77 -0
- package/src/domains/plugins/integrity.ts +6 -0
- package/src/domains/plugins/manifest.ts +3 -0
- package/src/domains/plugins/resources.ts +102 -0
- package/src/domains/plugins/state.ts +753 -0
- package/src/domains/plugins/types.ts +217 -0
- package/src/domains/prompts/extension.ts +9 -2
- package/src/domains/prompts/fragments/operating/skills.md +10 -10
- package/src/domains/resources/common-loader.ts +9 -6
- package/src/domains/resources/index.ts +67 -0
- package/src/domains/resources/library-actions.ts +836 -0
- package/src/domains/resources/library-evals.ts +38 -0
- package/src/domains/resources/library-inventory.ts +1048 -0
- package/src/domains/resources/library-types.ts +50 -0
- package/src/domains/resources/library-validation.ts +634 -0
- package/src/domains/resources/library.ts +437 -156
- package/src/domains/resources/package-references.ts +66 -0
- package/src/domains/resources/prompts/loader.ts +75 -41
- package/src/domains/resources/skills/install.ts +2 -2
- package/src/domains/resources/skills/loader.ts +77 -22
- package/src/domains/resources/skills/marketplace.ts +48 -57
- package/src/domains/resources/skills/promotion.ts +2 -2
- package/src/domains/resources/skills/provenance-pin.ts +17 -4
- package/src/domains/safety/action-classifier.ts +2 -2
- package/src/domains/safety/default-path-policy.ts +3 -1
- package/src/domains/safety/policy-engine.ts +1 -1
- package/src/domains/safety/protected-artifacts.ts +21 -5
- package/src/domains/safety/skill-authority.ts +5 -2
- package/src/domains/share/archive.ts +1 -1
- package/src/engine/apis/openai-completions.ts +52 -2
- package/src/engine/notices/pi-tui-LICENSE +21 -0
- package/src/engine/tui.ts +3 -0
- package/src/engine/worker-runtime.ts +58 -14
- package/src/engine/worker-tools.ts +5 -0
- package/src/entry/orchestrator.ts +26 -5
- package/src/entry/plugin-reload.ts +18 -0
- package/src/interactive/application-controller.ts +67 -21
- package/src/interactive/chat-loop.ts +32 -5
- package/src/interactive/chat-panel.ts +5 -1
- package/src/interactive/clio-editor.ts +53 -21
- package/src/interactive/command-output.ts +56 -1
- package/src/interactive/editor-submit.ts +65 -28
- package/src/interactive/fleet-run-preview.ts +2 -0
- package/src/interactive/follow-up-queue-panel.ts +3 -3
- package/src/interactive/footer/dashboard.ts +4 -1
- package/src/interactive/footer/notifications.ts +2 -2
- package/src/interactive/footer/widgets.ts +2 -2
- package/src/interactive/interactive-application.ts +209 -21
- package/src/interactive/interactive-input-runtime.ts +254 -7
- package/src/interactive/interactive-presentation.ts +41 -12
- package/src/interactive/interactive-slash-runtime.ts +37 -8
- package/src/interactive/keybinding-manager.ts +156 -79
- package/src/interactive/keyboard-owner.ts +37 -0
- package/src/interactive/leader-key.ts +49 -102
- package/src/interactive/leader-menu.ts +45 -0
- package/src/interactive/overlay-ask-user-lifecycle.ts +9 -1
- package/src/interactive/overlay-frame.ts +15 -0
- package/src/interactive/overlay-key-routing.ts +46 -47
- package/src/interactive/overlay-lifecycle.ts +30 -15
- package/src/interactive/overlay-resource-openers.ts +56 -46
- package/src/interactive/overlays/ask-user.ts +630 -556
- package/src/interactive/overlays/extension-panel.ts +70 -0
- package/src/interactive/overlays/extensions.ts +28 -5
- package/src/interactive/overlays/fleet-run-approval.ts +7 -3
- package/src/interactive/overlays/help-reference.ts +89 -40
- package/src/interactive/overlays/interop.ts +122 -3
- package/src/interactive/overlays/keybinding-detail.ts +2 -2
- package/src/interactive/overlays/library-lifecycle.ts +90 -0
- package/src/interactive/overlays/library-model.ts +574 -0
- package/src/interactive/overlays/library-review.ts +557 -0
- package/src/interactive/overlays/library-tabs.ts +3 -15
- package/src/interactive/overlays/library.ts +602 -0
- package/src/interactive/overlays/list-overlay.ts +218 -39
- package/src/interactive/overlays/model-selector.ts +28 -25
- package/src/interactive/overlays/session-selector.ts +9 -3
- package/src/interactive/overlays/settings.ts +25 -18
- package/src/interactive/overlays/tree-selector.ts +16 -11
- package/src/interactive/permission-hint.ts +13 -0
- package/src/interactive/permission-overlay.ts +77 -28
- package/src/interactive/prompt-source-label.ts +21 -0
- package/src/interactive/renderers/reference-card.ts +35 -0
- package/src/interactive/renderers/worker-entry.ts +35 -3
- package/src/interactive/slash-autocomplete.ts +99 -1
- package/src/interactive/slash-commands.ts +390 -67
- package/src/interactive/slash-spec.ts +0 -1
- package/src/interactive/terminal-lease.ts +44 -12
- package/src/interactive/turn-queues.ts +8 -7
- package/src/interactive/turn-runtime.ts +1 -1
- package/src/interactive/view/view-overlay.ts +19 -14
- package/src/interactive/welcome-dashboard.ts +525 -408
- package/src/tools/bootstrap.ts +8 -0
- package/src/tools/builtin-tool-catalog.ts +7 -2
- package/src/tools/context/index.ts +58 -5
- package/src/tools/context/library.ts +465 -0
- package/src/tools/context/surface.ts +20 -6
- package/src/tools/core-bootstrap.ts +3 -0
- package/src/tools/dispatch-admission.ts +24 -1
- package/src/tools/dispatch-arguments.ts +8 -0
- package/src/tools/dispatch-plan.ts +32 -0
- package/src/tools/dispatch-schema.ts +31 -1
- package/src/tools/dispatch-types.ts +3 -0
- package/src/tools/dispatch.ts +1 -1
- package/src/tools/harness-extensions.ts +169 -0
- package/src/tools/observation.ts +4 -0
- package/src/tools/policy.ts +7 -1
- package/src/tools/registry.ts +19 -1
- package/src/tools/worker-evidence.ts +5 -0
- package/src/worker/context-seed.ts +192 -0
- package/src/worker/entry.ts +1 -0
- package/src/worker/spec-contract.ts +8 -1
- package/dist/chunk-3UW3KU4A.js +0 -21
- package/dist/chunk-536HXRFE.js +0 -1257
- package/dist/chunk-B3MOMLJ4.js +0 -433
- package/dist/chunk-CXQ37WJH.js +0 -45
- package/dist/chunk-KTFSFCVN.js +0 -46
- package/dist/chunk-QCOV5DFY.js +0 -911
- package/dist/chunk-QRFXEO6Y.js +0 -17
- package/dist/chunk-VAHLPXPY.js +0 -399
- package/dist/interop-RVCAYECG.js +0 -115
- package/dist/library-CTKZA7K3.js +0 -226
- package/dist/skills-OXFEAWMT.js +0 -510
- package/skills/README.md +0 -418
- package/skills/meta/find-skills/SKILL.md +0 -78
- package/skills/meta/find-skills/evals.md +0 -47
- package/src/domains/extensions/resource-path.ts +0 -27
- package/src/domains/lifecycle/migrations/2026-09-01-extension-install-digests.ts +0 -27
- package/src/interactive/overlays/agents.ts +0 -70
- package/src/interactive/overlays/library-install-confirm.ts +0 -152
- package/src/interactive/overlays/prompts.ts +0 -89
- package/src/interactive/overlays/skills-hub.ts +0 -615
- /package/{skills → library/skills}/coding/ast-grep/references/rule_reference.md +0 -0
- /package/{skills → library/skills}/coding/prototype/references/LOGIC.md +0 -0
- /package/{skills → library/skills}/coding/prototype/references/UI.md +0 -0
- /package/{skills → library/skills}/coding/tdd/references/mocking.md +0 -0
- /package/{skills → library/skills}/coding/tdd/references/tests.md +0 -0
- /package/{skills → library/skills}/context/context-handoff/scripts/new-handoff.sh +0 -0
- /package/{skills → library/skills}/git/branch-closeout/evals.md +0 -0
- /package/{skills → library/skills}/git/branch-closeout/references/closeout-checklist.md +0 -0
- /package/{skills → library/skills}/git/file-ticket/assets/issue-template.md +0 -0
- /package/{skills → library/skills}/git/file-ticket/evals.md +0 -0
- /package/{skills → library/skills}/git/file-ticket/references/issue-discovery.md +0 -0
- /package/{skills → library/skills}/git/fix-issue/evals.md +0 -0
- /package/{skills → library/skills}/git/fix-issue/references/diagnosis-and-rca.md +0 -0
- /package/{skills → library/skills}/git/resolve-merge-conflicts/references/conflict-matrix.md +0 -0
- /package/{skills → library/skills}/git/ship/assets/pr-template.md +0 -0
- /package/{skills → library/skills}/git/ship/evals.md +0 -0
- /package/{skills → library/skills}/git/ship/references/remote-and-branch-policy.md +0 -0
- /package/{skills → library/skills}/git/worktree-create/evals.md +0 -0
- /package/{skills → library/skills}/git/worktree-create/references/worktree-setup.md +0 -0
- /package/{skills → library/skills}/git/worktree-merge/evals.md +0 -0
- /package/{skills → library/skills}/git/worktree-merge/references/merge-strategies.md +0 -0
- /package/{skills → library/skills}/meta/clio-coder-test/references/harness.md +0 -0
- /package/{skills → library/skills}/meta/clio-coder-test/references/test-map.md +0 -0
- /package/{skills → library/skills}/planning/archify/evals.md +0 -0
|
@@ -5,10 +5,11 @@ import {
|
|
|
5
5
|
} from "../../domains/safety/decision-presentation.js";
|
|
6
6
|
import {
|
|
7
7
|
type Component,
|
|
8
|
+
Editor,
|
|
9
|
+
getKeybindings,
|
|
8
10
|
Input,
|
|
9
11
|
matchesKey,
|
|
10
12
|
type OverlayHandle,
|
|
11
|
-
type OverlayOptions,
|
|
12
13
|
type SelectItem,
|
|
13
14
|
SelectList,
|
|
14
15
|
type TUI,
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
wrapTextWithAnsi,
|
|
18
19
|
} from "../../engine/tui.js";
|
|
19
20
|
import type { AskUserAnswer, AskUserQuestion, AskUserResult } from "../../tools/ask-user.js";
|
|
20
|
-
import {
|
|
21
|
+
import { cancelledAskUserResult } from "../../tools/ask-user.js";
|
|
21
22
|
import {
|
|
22
23
|
buildHint,
|
|
23
24
|
DEFAULT_SELECT_THEME,
|
|
@@ -25,7 +26,7 @@ import {
|
|
|
25
26
|
type OverlayEscVerb,
|
|
26
27
|
showClioOverlayFrame,
|
|
27
28
|
} from "../overlay-frame.js";
|
|
28
|
-
import { type ClioToken, clioTheme, dotSep, GLYPH, screenTitle } from "../theme/index.js";
|
|
29
|
+
import { type ClioToken, clioTheme, dotSep, editorTheme, GLYPH, rule, screenTitle } from "../theme/index.js";
|
|
29
30
|
import { nextContentScrollOffset, type ViewScrollAction } from "../view/view-overlay.js";
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -40,81 +41,35 @@ export const ASK_USER_DECISION_TITLE = DEFAULT_ASK_USER_PRESENTATION.title;
|
|
|
40
41
|
export const ASK_USER_WAITING_TITLE = "Ask User";
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
|
-
*
|
|
44
|
+
* One box for every request shape.
|
|
44
45
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* long to read in a bar.
|
|
53
|
-
* - `interview` is the full surface with the round strip and the answer ledger,
|
|
54
|
-
* for a round of several questions or any round after the first.
|
|
55
|
-
*/
|
|
56
|
-
export type AskUserSurface = "compact" | "panel" | "interview";
|
|
57
|
-
|
|
58
|
-
/** Options a single question may carry and still be answered in the bar. */
|
|
59
|
-
export const ASK_USER_COMPACT_MAX_OPTIONS = 3;
|
|
60
|
-
|
|
61
|
-
/** The surface an overlay waits on before its first round decides one. */
|
|
62
|
-
export const ASK_USER_DEFAULT_SURFACE: AskUserSurface = "panel";
|
|
63
|
-
|
|
64
|
-
interface AskUserSurfaceGeometry {
|
|
65
|
-
/** Box width in columns; zero fills the row. The frame clamps to the terminal. */
|
|
66
|
-
width: number;
|
|
67
|
-
/** Rows the body may use, before the borders. Zero means every row the frame has. */
|
|
68
|
-
maxInnerRows: number;
|
|
69
|
-
anchor: NonNullable<OverlayOptions["anchor"]>;
|
|
70
|
-
margin: NonNullable<OverlayOptions["margin"]>;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Where each surface sits and how big it is allowed to get.
|
|
75
|
-
*
|
|
76
|
-
* The compact bar anchors to the bottom because a decision that has taken the
|
|
77
|
-
* keyboard should appear where the operator was already typing, not in the
|
|
78
|
-
* middle of the transcript. It covers the composer, which is honest: the
|
|
79
|
-
* composer accepts nothing while a decision is pending.
|
|
80
|
-
*
|
|
81
|
-
* Every surface's margin is one row top and bottom, which is what
|
|
82
|
-
* `ASK_USER_FRAME_AND_MARGIN_ROWS` counts alongside the two border rows.
|
|
46
|
+
* Three surfaces used to answer three request shapes: a fixed 68-column bar for
|
|
47
|
+
* a short question, a fixed 88-column panel for a longer option list, and a
|
|
48
|
+
* full-screen workspace for an interview. The bar cut Daisy's four-part research
|
|
49
|
+
* question to two parts at every terminal width, and the workspace put twelve
|
|
50
|
+
* empty rows between the question and its options. The box now sizes itself to
|
|
51
|
+
* what it has to say, sits above the composer where the operator was typing,
|
|
52
|
+
* and takes a readable measure from the terminal rather than a constant.
|
|
83
53
|
*/
|
|
84
|
-
export const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// terminal into a centered box with twenty blank rows around it, so the
|
|
89
|
-
// interview takes every row the frame has and lays its own chrome out inside
|
|
90
|
-
// them. The one-row margin keeps the border off the top row and the footer.
|
|
91
|
-
interview: { width: 0, maxInnerRows: 0, anchor: "center", margin: 1 },
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const ASK_USER_FRAME_ROWS = 2;
|
|
54
|
+
export const ASK_USER_MAX_BOX_WIDTH = 100;
|
|
55
|
+
export const ASK_USER_MIN_BOX_WIDTH = 40;
|
|
56
|
+
const ASK_USER_MARGIN = { top: 1, right: 2, bottom: 1, left: 2 };
|
|
57
|
+
/** Two border rows plus the one-row margin above and below. */
|
|
95
58
|
const ASK_USER_FRAME_AND_MARGIN_ROWS = 4;
|
|
96
|
-
const
|
|
97
|
-
/**
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
59
|
+
const MIN_INNER_ROWS = 6;
|
|
60
|
+
/** The question region keeps at least this many rows before the options window shrinks. */
|
|
61
|
+
const MIN_QUESTION_ROWS = 3;
|
|
62
|
+
/** Below this width an option's description goes under its label instead of beside it. */
|
|
63
|
+
const STACKED_OPTION_WIDTH = 56;
|
|
64
|
+
const MAX_LABEL_COLUMN = 30;
|
|
65
|
+
const MIN_LABEL_COLUMN = 12;
|
|
102
66
|
const ELLIPSIS = "…";
|
|
67
|
+
const CONTINUATION_INDENT = " ";
|
|
68
|
+
const TEXT_ASKING_DESCRIPTION = "opens a text field for your answer";
|
|
103
69
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
* `priorRounds` is what makes an interview an interview. The tool's `mode`
|
|
108
|
-
* cannot answer this: `single_question` is the mode an interview asks its
|
|
109
|
-
* rounds in, and a first single question is exactly the confirmation that
|
|
110
|
-
* should not take the screen. A round that follows an answered one has captured
|
|
111
|
-
* answers to keep on screen, which is the full surface's whole job.
|
|
112
|
-
*/
|
|
113
|
-
function askUserSurface(questions: ReadonlyArray<AskUserQuestion>, priorRounds = 0): AskUserSurface {
|
|
114
|
-
if (priorRounds > 0 || questions.length !== 1) return "interview";
|
|
115
|
-
const question = questions[0];
|
|
116
|
-
if (!question) return "interview";
|
|
117
|
-
return (question.options?.length ?? 0) <= ASK_USER_COMPACT_MAX_OPTIONS ? "compact" : "panel";
|
|
70
|
+
export function askUserBoxWidth(columns: number): number {
|
|
71
|
+
if (!Number.isFinite(columns) || columns <= 0) return ASK_USER_MAX_BOX_WIDTH;
|
|
72
|
+
return Math.max(ASK_USER_MIN_BOX_WIDTH, Math.min(ASK_USER_MAX_BOX_WIDTH, Math.floor(columns) - 4));
|
|
118
73
|
}
|
|
119
74
|
|
|
120
75
|
export interface OpenAskUserOverlayDeps {
|
|
@@ -134,6 +89,7 @@ type InterviewPhase = "waiting" | "asking" | "closed";
|
|
|
134
89
|
interface QuestionState {
|
|
135
90
|
mode: Mode;
|
|
136
91
|
selected: Set<number>;
|
|
92
|
+
committedSelected: Set<number>;
|
|
137
93
|
customAnswer: string;
|
|
138
94
|
inputValue: string;
|
|
139
95
|
answer: string;
|
|
@@ -148,11 +104,11 @@ interface QuestionState {
|
|
|
148
104
|
focusedValue?: string;
|
|
149
105
|
}
|
|
150
106
|
|
|
151
|
-
interface AskUserOverlayViewDeps extends OpenAskUserOverlayDeps {
|
|
107
|
+
export interface AskUserOverlayViewDeps extends OpenAskUserOverlayDeps {
|
|
152
108
|
getTerminalRows: () => number;
|
|
153
109
|
requestRender: () => void;
|
|
154
|
-
/**
|
|
155
|
-
|
|
110
|
+
/** The engine, for the multi-line answer editor. Absent, the field is a one-line input. */
|
|
111
|
+
tui?: TUI;
|
|
156
112
|
}
|
|
157
113
|
|
|
158
114
|
function questionHasOptions(question: AskUserQuestion): boolean {
|
|
@@ -167,6 +123,7 @@ function createQuestionState(question: AskUserQuestion): QuestionState {
|
|
|
167
123
|
return {
|
|
168
124
|
mode: initialMode(question),
|
|
169
125
|
selected: new Set<number>(),
|
|
126
|
+
committedSelected: new Set<number>(),
|
|
170
127
|
customAnswer: "",
|
|
171
128
|
inputValue: "",
|
|
172
129
|
answer: "",
|
|
@@ -174,11 +131,15 @@ function createQuestionState(question: AskUserQuestion): QuestionState {
|
|
|
174
131
|
};
|
|
175
132
|
}
|
|
176
133
|
|
|
177
|
-
function
|
|
178
|
-
|
|
134
|
+
function normalizedLabel(label: string): string {
|
|
135
|
+
return label
|
|
179
136
|
.toLowerCase()
|
|
180
|
-
.replace(/[^a-z0-9]+/g, " ")
|
|
137
|
+
.replace(/[^a-z0-9']+/g, " ")
|
|
181
138
|
.trim();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isOtherOption(label: string): boolean {
|
|
142
|
+
const normalized = normalizedLabel(label);
|
|
182
143
|
return (
|
|
183
144
|
normalized === "other" ||
|
|
184
145
|
normalized === "custom" ||
|
|
@@ -187,6 +148,23 @@ function isOtherOption(label: string): boolean {
|
|
|
187
148
|
);
|
|
188
149
|
}
|
|
189
150
|
|
|
151
|
+
/**
|
|
152
|
+
* An option whose label says the operator is about to type.
|
|
153
|
+
*
|
|
154
|
+
* wtf-MS interviews offer "Provided details" beside "help me explore"; the
|
|
155
|
+
* candidates round offers "Combine or edit; I'll describe". Choosing such an
|
|
156
|
+
* option used to record the bare label, and the model then had a route with no
|
|
157
|
+
* details on it and asked again. Enter on one of these opens the text field
|
|
158
|
+
* with the label kept, the same path `t` takes.
|
|
159
|
+
*/
|
|
160
|
+
export function optionAsksForText(label: string): boolean {
|
|
161
|
+
if (isOtherOption(label)) return true;
|
|
162
|
+
const normalized = normalizedLabel(label);
|
|
163
|
+
return /\b(provided? details|i'?ll (describe|type|explain|specify|provide|write)|i (will|can) (describe|type|explain|specify|provide|write)|let me (describe|type|explain|specify)|type (it|my|the)|describe (it|my|the|below))\b/u.test(
|
|
164
|
+
normalized,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
190
168
|
function optionItems(question: AskUserQuestion, selected: ReadonlySet<number>): SelectItem[] {
|
|
191
169
|
const options = question.options ?? [];
|
|
192
170
|
const explicitOtherIndex = options.findIndex((option) => isOtherOption(option.label));
|
|
@@ -204,12 +182,15 @@ function optionItems(question: AskUserQuestion, selected: ReadonlySet<number>):
|
|
|
204
182
|
label,
|
|
205
183
|
};
|
|
206
184
|
if (option.description) item.description = option.description;
|
|
185
|
+
else if (optionAsksForText(option.label)) item.description = TEXT_ASKING_DESCRIPTION;
|
|
207
186
|
items.push(item);
|
|
208
187
|
}
|
|
209
188
|
if (explicitOtherIndex === -1) {
|
|
189
|
+
// The implicit choice is one word on screen; its description says what it
|
|
190
|
+
// does, and the tool's longer label is what the answer records.
|
|
210
191
|
items.push({
|
|
211
192
|
value: "other",
|
|
212
|
-
label: question.multi_select === true ?
|
|
193
|
+
label: question.multi_select === true ? "[ ] Other" : "Other",
|
|
213
194
|
description: "type your answer",
|
|
214
195
|
});
|
|
215
196
|
}
|
|
@@ -250,8 +231,8 @@ function wrapLabeledValue(prefix: string, value: string, width: number): string[
|
|
|
250
231
|
const safeWidth = Math.max(1, width);
|
|
251
232
|
const prefixWidth = visibleWidth(prefix);
|
|
252
233
|
const valueWidth = Math.max(1, safeWidth - prefixWidth);
|
|
253
|
-
return wrapTextWithAnsi(value, valueWidth).map(
|
|
254
|
-
|
|
234
|
+
return wrapTextWithAnsi(value, valueWidth).map(
|
|
235
|
+
(line, index) => `${index === 0 ? prefix : " ".repeat(prefixWidth)}${line}`,
|
|
255
236
|
);
|
|
256
237
|
}
|
|
257
238
|
|
|
@@ -259,25 +240,248 @@ function compactTitle(question: AskUserQuestion): string {
|
|
|
259
240
|
return (question.header ?? question.question).replace(/\s+/g, " ").trim();
|
|
260
241
|
}
|
|
261
242
|
|
|
243
|
+
const BOLD_SPAN = /\*\*([^*\n]+?)\*\*/gu;
|
|
244
|
+
const LIST_MARKER = /^(\s*)(\d{1,2}[.)]|[-*•])\s+(.*)$/u;
|
|
245
|
+
|
|
246
|
+
function inlineMarkdown(text: string): string {
|
|
247
|
+
const theme = clioTheme();
|
|
248
|
+
return text.replace(BOLD_SPAN, (_match, inner: string) => theme.paint(inner, { bold: true }));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The question, wrapped to the width and never cut.
|
|
253
|
+
*
|
|
254
|
+
* A model writes a question the way it writes prose: `**Domain**` for emphasis
|
|
255
|
+
* and `1.` lists for the parts it wants answered. The old body rendered the
|
|
256
|
+
* asterisks literally and wrapped a list item's second row back to column zero,
|
|
257
|
+
* where it read as a new paragraph. Bold spans render bold, list items hang
|
|
258
|
+
* under their marker, and blank lines survive one deep so paragraphs stay apart.
|
|
259
|
+
*/
|
|
260
|
+
export function formatAskUserQuestion(text: string, width: number): string[] {
|
|
261
|
+
const safeWidth = Math.max(4, width);
|
|
262
|
+
const lines: string[] = [];
|
|
263
|
+
let previousBlank = true;
|
|
264
|
+
for (const raw of text.replace(/\r\n?/g, "\n").split("\n")) {
|
|
265
|
+
const line = raw.replace(/\t/g, " ").trimEnd();
|
|
266
|
+
if (line.trim().length === 0) {
|
|
267
|
+
if (!previousBlank) lines.push("");
|
|
268
|
+
previousBlank = true;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
previousBlank = false;
|
|
272
|
+
const list = LIST_MARKER.exec(line);
|
|
273
|
+
if (list) {
|
|
274
|
+
const lead = list[1] ?? "";
|
|
275
|
+
const marker = (list[2] ?? "").replace(/^[-*]$/u, "•");
|
|
276
|
+
const rest = list[3] ?? "";
|
|
277
|
+
const prefix = `${lead}${marker} `;
|
|
278
|
+
const indent = " ".repeat(visibleWidth(prefix));
|
|
279
|
+
const wrapped = wrapTextWithAnsi(inlineMarkdown(rest), Math.max(4, safeWidth - indent.length));
|
|
280
|
+
wrapped.forEach((part, index) => {
|
|
281
|
+
lines.push(`${index === 0 ? prefix : indent}${part}`);
|
|
282
|
+
});
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
// An indented line is a continuation the author laid out by hand, such as
|
|
286
|
+
// the "In: … · Out: …" row under a candidate; its wrapped rows keep the
|
|
287
|
+
// indent so they read as part of it rather than as a new paragraph.
|
|
288
|
+
const lead = /^\s*/u.exec(line)?.[0] ?? "";
|
|
289
|
+
const wrapped = wrapTextWithAnsi(inlineMarkdown(line.slice(lead.length)), Math.max(4, safeWidth - lead.length));
|
|
290
|
+
lines.push(...wrapped.map((part) => `${lead}${part}`));
|
|
291
|
+
}
|
|
292
|
+
while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
293
|
+
return lines;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** The typed answer box: a small editor whose chrome is a caption above and a key hint below. */
|
|
297
|
+
class AnswerEditor extends Editor {
|
|
298
|
+
caption = "";
|
|
299
|
+
|
|
300
|
+
constructor(tui: TUI) {
|
|
301
|
+
super(tui, editorTheme(clioTheme()), { paddingX: 0 });
|
|
302
|
+
// The overlay forwards keys itself, so the engine never focuses this
|
|
303
|
+
// editor; the flag is what draws the cursor.
|
|
304
|
+
this.focused = true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
protected override renderTopBorder(width: number): string {
|
|
308
|
+
const theme = clioTheme();
|
|
309
|
+
return fitLine(theme.fg("dim", this.caption), width);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
protected override renderBottomBorder(width: number, hiddenLineCount: number): string {
|
|
313
|
+
if (hiddenLineCount <= 0) return "";
|
|
314
|
+
return fitLine(clioTheme().fg("dim", `${GLYPH.down} ${hiddenLineCount} more rows`), width);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** One shape over the two text controls, so the view never asks which it holds. */
|
|
319
|
+
interface TextControl {
|
|
320
|
+
render(width: number): string[];
|
|
321
|
+
handleInput(data: string): void;
|
|
322
|
+
applyEdit?(operation: "undo"): void;
|
|
323
|
+
getText(): string;
|
|
324
|
+
setText(text: string): void;
|
|
325
|
+
invalidate(): void;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
class InputTextControl implements TextControl {
|
|
329
|
+
constructor(
|
|
330
|
+
private readonly input: Input,
|
|
331
|
+
private readonly caption: string,
|
|
332
|
+
) {}
|
|
333
|
+
|
|
334
|
+
render(width: number): string[] {
|
|
335
|
+
const theme = clioTheme();
|
|
336
|
+
const rows = this.input
|
|
337
|
+
.render(width)
|
|
338
|
+
.map((line) =>
|
|
339
|
+
fitLine(line.startsWith("> ") ? `${theme.fg("accent", `${GLYPH.cursor} `)}${line.slice(2)}` : line, width),
|
|
340
|
+
);
|
|
341
|
+
return [fitLine(theme.fg("dim", this.caption), width), ...rows];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
applyEdit(operation: "undo"): void {
|
|
345
|
+
this.input.applyEdit(operation);
|
|
346
|
+
}
|
|
347
|
+
handleInput(data: string): void {
|
|
348
|
+
this.input.handleInput(data);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
getText(): string {
|
|
352
|
+
return this.input.getValue();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
setText(text: string): void {
|
|
356
|
+
this.input.setValue(text);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
invalidate(): void {
|
|
360
|
+
this.input.invalidate();
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
interface OptionRowLayout {
|
|
365
|
+
/** Rows per item, in item order. */
|
|
366
|
+
rows: string[][];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Every option, with its whole description.
|
|
371
|
+
*
|
|
372
|
+
* A description is what the operator is choosing between: "+Latency,
|
|
373
|
+
* +Simplicity. Good for: Financial, Auth" is the trade-off, and the old table
|
|
374
|
+
* cut it to its first clause on every row the cursor was not on. Every row now
|
|
375
|
+
* wraps its description beside the label, or under it when the box is too
|
|
376
|
+
* narrow for two columns, and the row carries no key: the footer does.
|
|
377
|
+
*/
|
|
378
|
+
function layoutOptionRows(items: ReadonlyArray<SelectItem>, focusedIndex: number, width: number): OptionRowLayout {
|
|
379
|
+
const theme = clioTheme();
|
|
380
|
+
const safeWidth = Math.max(8, width);
|
|
381
|
+
const stacked = safeWidth < STACKED_OPTION_WIDTH;
|
|
382
|
+
const widestLabel = items.reduce((max, item) => Math.max(max, visibleWidth(item.label)), 0);
|
|
383
|
+
const labelColumn = Math.max(
|
|
384
|
+
MIN_LABEL_COLUMN,
|
|
385
|
+
Math.min(MAX_LABEL_COLUMN, Math.floor(safeWidth * 0.42), widestLabel + 2),
|
|
386
|
+
);
|
|
387
|
+
const rows: string[][] = [];
|
|
388
|
+
for (let index = 0; index < items.length; index += 1) {
|
|
389
|
+
const item = items[index];
|
|
390
|
+
if (!item) continue;
|
|
391
|
+
const focused = index === focusedIndex;
|
|
392
|
+
const prefix = focused ? theme.fg("accent", `${GLYPH.cursor} `) : " ";
|
|
393
|
+
const description = item.description?.replace(/[\r\n]+/g, " ").trim() ?? "";
|
|
394
|
+
const paint = (text: string): string => (focused ? theme.style("accent", text, { bold: true }) : text);
|
|
395
|
+
const describe = (text: string): string => (focused ? text : theme.fg("muted", text));
|
|
396
|
+
if (description.length === 0) {
|
|
397
|
+
rows.push(
|
|
398
|
+
wrapTextWithAnsi(paint(item.label), Math.max(4, safeWidth - 2)).map(
|
|
399
|
+
(line, at) => `${at === 0 ? prefix : " "}${line}`,
|
|
400
|
+
),
|
|
401
|
+
);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (stacked || visibleWidth(item.label) > labelColumn - 1) {
|
|
405
|
+
const labelRows = wrapTextWithAnsi(paint(item.label), Math.max(4, safeWidth - 2)).map(
|
|
406
|
+
(line, at) => `${at === 0 ? prefix : " "}${line}`,
|
|
407
|
+
);
|
|
408
|
+
const descriptionRows = wrapTextWithAnsi(
|
|
409
|
+
describe(description),
|
|
410
|
+
Math.max(4, safeWidth - CONTINUATION_INDENT.length),
|
|
411
|
+
).map((line) => `${CONTINUATION_INDENT}${line}`);
|
|
412
|
+
rows.push([...labelRows, ...descriptionRows]);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const spacing = " ".repeat(Math.max(1, labelColumn - visibleWidth(item.label)));
|
|
416
|
+
const descriptionWidth = Math.max(4, safeWidth - 2 - labelColumn);
|
|
417
|
+
const wrapped = wrapTextWithAnsi(describe(description), descriptionWidth);
|
|
418
|
+
const indent = " ".repeat(2 + labelColumn);
|
|
419
|
+
rows.push([
|
|
420
|
+
`${prefix}${paint(item.label)}${spacing}${wrapped[0] ?? ""}`,
|
|
421
|
+
...wrapped.slice(1).map((line) => `${indent}${line}`),
|
|
422
|
+
]);
|
|
423
|
+
}
|
|
424
|
+
return { rows };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* The rows of a windowed list: every option when they fit, otherwise a run of
|
|
429
|
+
* whole options around the focused one, with a position marker. An option is
|
|
430
|
+
* never split across the window edge, so a description is read whole or not
|
|
431
|
+
* at all, and one arrow key brings any hidden option back.
|
|
432
|
+
*/
|
|
433
|
+
function windowOptionRows(layout: OptionRowLayout, focusedIndex: number, rowBudget: number, width: number): string[] {
|
|
434
|
+
const theme = clioTheme();
|
|
435
|
+
const total = layout.rows.reduce((sum, rows) => sum + rows.length, 0);
|
|
436
|
+
if (total <= rowBudget) return layout.rows.flat();
|
|
437
|
+
const budget = Math.max(1, rowBudget - 1);
|
|
438
|
+
const focusedRows = layout.rows[focusedIndex]?.length ?? 1;
|
|
439
|
+
let start = focusedIndex;
|
|
440
|
+
let end = focusedIndex + 1;
|
|
441
|
+
let used = focusedRows;
|
|
442
|
+
// Grow around the focus, later options first so the reading order continues
|
|
443
|
+
// downward, then earlier ones.
|
|
444
|
+
let grew = true;
|
|
445
|
+
while (grew) {
|
|
446
|
+
grew = false;
|
|
447
|
+
const after = layout.rows[end]?.length;
|
|
448
|
+
if (after !== undefined && used + after <= budget) {
|
|
449
|
+
used += after;
|
|
450
|
+
end += 1;
|
|
451
|
+
grew = true;
|
|
452
|
+
}
|
|
453
|
+
const before = layout.rows[start - 1]?.length;
|
|
454
|
+
if (before !== undefined && used + before <= budget) {
|
|
455
|
+
used += before;
|
|
456
|
+
start -= 1;
|
|
457
|
+
grew = true;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const visible = layout.rows.slice(start, end).flat();
|
|
461
|
+
const marker = ` (${focusedIndex + 1}/${layout.rows.length})${start > 0 ? ` ${GLYPH.up}` : ""}${end < layout.rows.length ? ` ${GLYPH.down}` : ""}`;
|
|
462
|
+
return [...visible, fitLine(theme.fg("dim", marker), width)];
|
|
463
|
+
}
|
|
464
|
+
|
|
262
465
|
class AskUserOverlayView implements Component {
|
|
263
466
|
private phase: InterviewPhase = "waiting";
|
|
264
|
-
private surface: AskUserSurface = ASK_USER_DEFAULT_SURFACE;
|
|
265
467
|
private index = 0;
|
|
266
468
|
private status = "";
|
|
267
469
|
private questions: ReadonlyArray<AskUserQuestion> = [];
|
|
268
470
|
private states: QuestionState[] = [];
|
|
269
471
|
private history: AskUserAnswer[] = [];
|
|
472
|
+
/** Rounds answered so far; the strip and header count these, not answers. */
|
|
473
|
+
private roundsAnswered = 0;
|
|
270
474
|
private list: SelectList | null = null;
|
|
271
|
-
private
|
|
475
|
+
private text: TextControl | null = null;
|
|
272
476
|
private resolveCurrent: ((result: AskUserResult) => void) | null = null;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
private
|
|
277
|
-
|
|
278
|
-
private
|
|
279
|
-
|
|
280
|
-
private
|
|
477
|
+
private optionDetailRows: string[] = [];
|
|
478
|
+
/** Rows of the question region already scrolled past. */
|
|
479
|
+
private questionScroll = 0;
|
|
480
|
+
private questionOverflows = false;
|
|
481
|
+
private questionRegionRows = 1;
|
|
482
|
+
private questionTotalRows = 0;
|
|
483
|
+
private ledgerExpanded = false;
|
|
484
|
+
private detailsExpanded = false;
|
|
281
485
|
private presentation: DecisionPresentation = DEFAULT_ASK_USER_PRESENTATION;
|
|
282
486
|
|
|
283
487
|
constructor(private readonly deps: AskUserOverlayViewDeps) {}
|
|
@@ -295,11 +499,9 @@ class AskUserOverlayView implements Component {
|
|
|
295
499
|
this.presentation = presentation;
|
|
296
500
|
this.states = this.questions.map((question) => createQuestionState(question));
|
|
297
501
|
this.list = null;
|
|
298
|
-
this.
|
|
299
|
-
this.
|
|
300
|
-
|
|
301
|
-
// the box this round is about to be drawn in.
|
|
302
|
-
this.setSurface(askUserSurface(this.questions, this.history.length));
|
|
502
|
+
this.text = null;
|
|
503
|
+
this.detailsExpanded = false;
|
|
504
|
+
this.resetQuestionScroll();
|
|
303
505
|
this.rebuildControl();
|
|
304
506
|
this.deps.requestRender();
|
|
305
507
|
return new Promise<AskUserResult>((resolve) => {
|
|
@@ -325,10 +527,6 @@ class AskUserOverlayView implements Component {
|
|
|
325
527
|
return this.phase === "asking";
|
|
326
528
|
}
|
|
327
529
|
|
|
328
|
-
currentSurface(): AskUserSurface {
|
|
329
|
-
return this.surface;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
530
|
decisionTitle(): string {
|
|
333
531
|
return this.presentation.title;
|
|
334
532
|
}
|
|
@@ -339,29 +537,33 @@ class AskUserOverlayView implements Component {
|
|
|
339
537
|
|
|
340
538
|
invalidate(): void {
|
|
341
539
|
this.list?.invalidate();
|
|
342
|
-
this.
|
|
540
|
+
this.text?.invalidate();
|
|
343
541
|
}
|
|
344
542
|
|
|
543
|
+
get keyboardScope(): "edit" | "review" {
|
|
544
|
+
return this.currentState()?.mode === "text" ? "edit" : "review";
|
|
545
|
+
}
|
|
546
|
+
undoInput(): boolean {
|
|
547
|
+
if (this.currentState()?.mode !== "text") return false;
|
|
548
|
+
this.text?.applyEdit?.("undo");
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
345
551
|
handleInput(data: string): void {
|
|
346
552
|
if (this.phase !== "asking") return;
|
|
347
553
|
const question = this.currentQuestion();
|
|
348
554
|
const state = this.currentState();
|
|
349
555
|
if (!question || !state) return;
|
|
350
556
|
|
|
351
|
-
// Scrolling the
|
|
557
|
+
// Scrolling the question must not cost the operator their place in the
|
|
352
558
|
// options, so the region has keys of its own that neither control claims.
|
|
353
559
|
if (this.handleScrollInput(data, state.mode)) return;
|
|
354
560
|
|
|
355
561
|
if (state.mode === "text") {
|
|
356
|
-
if (
|
|
357
|
-
this.
|
|
562
|
+
if (matchesKey(data, "escape")) {
|
|
563
|
+
this.escapeFromTextInput(question, state);
|
|
358
564
|
return;
|
|
359
565
|
}
|
|
360
|
-
|
|
361
|
-
this.goToRelativeQuestion(1);
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
this.input?.handleInput(data);
|
|
566
|
+
this.text?.handleInput(data);
|
|
365
567
|
return;
|
|
366
568
|
}
|
|
367
569
|
|
|
@@ -385,17 +587,25 @@ class AskUserOverlayView implements Component {
|
|
|
385
587
|
this.chooseFocusedAndOpenText(question, state);
|
|
386
588
|
return;
|
|
387
589
|
}
|
|
590
|
+
if (data === "a" && this.ledgerEntries().length > 0) {
|
|
591
|
+
this.ledgerExpanded = !this.ledgerExpanded;
|
|
592
|
+
this.deps.requestRender();
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (data === "?") {
|
|
596
|
+
this.detailsExpanded = !this.detailsExpanded;
|
|
597
|
+
this.deps.requestRender();
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
388
600
|
this.list?.handleInput(data);
|
|
389
601
|
}
|
|
390
602
|
|
|
391
603
|
/**
|
|
392
|
-
* The body,
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
* compact bar rests on: whatever the question and its options come to, the
|
|
398
|
-
* body never returns more rows than its surface owns.
|
|
604
|
+
* The body, top to bottom: where the round stands, the question, its
|
|
605
|
+
* options or the answer field, the status, and the answers so far. The
|
|
606
|
+
* question region is the one part that scrolls; everything the operator has
|
|
607
|
+
* to act on stays on screen at its full row cost, and the box draws only the
|
|
608
|
+
* rows it needs.
|
|
399
609
|
*/
|
|
400
610
|
render(width: number): string[] {
|
|
401
611
|
const safeWidth = Math.max(1, width);
|
|
@@ -404,102 +614,59 @@ class AskUserOverlayView implements Component {
|
|
|
404
614
|
const question = this.currentQuestion();
|
|
405
615
|
if (!question) return [clioTheme().fg("muted", "No questions.")];
|
|
406
616
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* The interview, as a workspace with fixed chrome and a scrolling middle.
|
|
430
|
-
*
|
|
431
|
-
* The round strip and the question header hold the top; the status line and
|
|
432
|
-
* the active control hold the bottom, at their full row cost, so the thing the
|
|
433
|
-
* operator has to act on is never the thing that gets clipped. The question
|
|
434
|
-
* text and the answer ledger share whatever is left and scroll through it.
|
|
435
|
-
* Everything is re-derived per render, so a resize is just the next frame.
|
|
436
|
-
*/
|
|
437
|
-
private renderInterview(question: AskUserQuestion, width: number, maxRows: number): string[] {
|
|
438
|
-
const theme = clioTheme();
|
|
439
|
-
const controlLines = this.renderControlLines(width);
|
|
440
|
-
const statusLines = this.status.length > 0 ? ["", ...wrapTextWithAnsi(theme.fg("dim", this.status), width)] : [];
|
|
441
|
-
const headerLine = this.renderQuestionHeader(question, width);
|
|
442
|
-
const headerLines = headerLine.length > 0 ? [headerLine] : [];
|
|
443
|
-
const stripLines = [this.renderQuestionStrip(width), ""];
|
|
444
|
-
const content = [
|
|
445
|
-
...wrapTextWithAnsi(question.question, width),
|
|
446
|
-
"",
|
|
447
|
-
...this.renderDecisionContext(width),
|
|
448
|
-
...this.renderAnswerLedger(width),
|
|
449
|
-
];
|
|
617
|
+
const strip = this.renderQuestionStrip(safeWidth);
|
|
618
|
+
const header = this.renderQuestionHeader(question, safeWidth);
|
|
619
|
+
const details = this.renderDecisionContext(safeWidth);
|
|
620
|
+
const status = this.status.length > 0 ? wrapTextWithAnsi(clioTheme().fg("dim", this.status), safeWidth) : [];
|
|
621
|
+
const body = formatAskUserQuestion(question.question, safeWidth);
|
|
622
|
+
const fixedTop = [...strip, ...header, ...details];
|
|
623
|
+
// The control is sized after the question has claimed its minimum, so a
|
|
624
|
+
// long option list cannot push a short question off the box.
|
|
625
|
+
const controlBudget = Math.max(
|
|
626
|
+
4,
|
|
627
|
+
maxRows - fixedTop.length - 1 - status.length - Math.min(body.length, MIN_QUESTION_ROWS) - 1,
|
|
628
|
+
);
|
|
629
|
+
this.optionDetailRows = [];
|
|
630
|
+
const control = this.renderControlLines(safeWidth, controlBudget);
|
|
631
|
+
body.push(...this.optionDetailRows);
|
|
632
|
+
const ledgerBudget = Math.max(
|
|
633
|
+
1,
|
|
634
|
+
maxRows - fixedTop.length - 1 - control.length - status.length - Math.min(body.length, MIN_QUESTION_ROWS),
|
|
635
|
+
);
|
|
636
|
+
const ledger = this.renderAnswerLedger(safeWidth, ledgerBudget);
|
|
637
|
+
const fixed = fixedTop.length + 1 + control.length + status.length + ledger.length;
|
|
450
638
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
let bottom = [...statusLines, "", ...controlLines];
|
|
456
|
-
const region = (): number => maxRows - top.length - bottom.length;
|
|
457
|
-
if (region() < 1) top = [...headerLines];
|
|
458
|
-
if (region() < 1) top = [];
|
|
459
|
-
if (region() < 1) bottom = ["", ...controlLines];
|
|
460
|
-
if (region() < 1) bottom = [...controlLines];
|
|
461
|
-
const regionRows = Math.max(1, region());
|
|
462
|
-
|
|
463
|
-
const overflows = content.length > regionRows;
|
|
464
|
-
this.contentOverflows = overflows;
|
|
465
|
-
this.contentTotalRows = content.length;
|
|
466
|
-
// One row of the region pays for the indicator when there is more than the
|
|
467
|
-
// region holds, so the marker never covers a row it is reporting on.
|
|
639
|
+
const regionRows = Math.max(1, Math.min(body.length, maxRows - fixed));
|
|
640
|
+
const overflows = body.length > regionRows;
|
|
641
|
+
this.questionOverflows = overflows;
|
|
642
|
+
this.questionTotalRows = body.length;
|
|
468
643
|
const viewRows = overflows ? Math.max(1, regionRows - 1) : regionRows;
|
|
469
|
-
this.
|
|
470
|
-
this.
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const middle = [...visible];
|
|
474
|
-
if (overflows) middle.push(fitLine(theme.fg("dim", this.scrollIndicator(content.length, viewRows)), width));
|
|
475
|
-
while (middle.length < regionRows) middle.push("");
|
|
644
|
+
this.questionRegionRows = viewRows;
|
|
645
|
+
this.questionScroll = Math.max(0, Math.min(this.questionScroll, Math.max(0, body.length - viewRows)));
|
|
646
|
+
const visible = body.slice(this.questionScroll, this.questionScroll + viewRows);
|
|
647
|
+
if (overflows) visible.push(fitLine(clioTheme().fg("dim", this.scrollIndicator(body.length, viewRows)), safeWidth));
|
|
476
648
|
|
|
477
|
-
return [...
|
|
649
|
+
return [...fixedTop, ...visible, "", ...control, ...status, ...ledger].slice(0, maxRows);
|
|
478
650
|
}
|
|
479
651
|
|
|
480
652
|
private scrollIndicator(total: number, viewRows: number): string {
|
|
481
|
-
const above = this.
|
|
653
|
+
const above = this.questionScroll;
|
|
482
654
|
const below = Math.max(0, total - viewRows - above);
|
|
483
655
|
const parts: string[] = [];
|
|
484
|
-
if (above > 0) parts.push(
|
|
485
|
-
if (below > 0) parts.push(
|
|
486
|
-
parts.push(
|
|
656
|
+
if (above > 0) parts.push(`${GLYPH.up} ${above} more`);
|
|
657
|
+
if (below > 0) parts.push(`${GLYPH.down} ${below} more`);
|
|
658
|
+
parts.push("PgUp/PgDn");
|
|
487
659
|
return parts.join(" ");
|
|
488
660
|
}
|
|
489
661
|
|
|
490
|
-
/** True when the key belonged to the
|
|
662
|
+
/** True when the key belonged to the question region rather than to a control. */
|
|
491
663
|
private handleScrollInput(data: string, mode: Mode): boolean {
|
|
492
|
-
if (this.
|
|
664
|
+
if (!this.questionOverflows) return false;
|
|
493
665
|
const action = this.scrollAction(data, mode);
|
|
494
666
|
if (!action) return false;
|
|
495
|
-
const next = nextContentScrollOffset(
|
|
496
|
-
|
|
497
|
-
this.
|
|
498
|
-
this.contentRegionHeight,
|
|
499
|
-
action,
|
|
500
|
-
);
|
|
501
|
-
if (next !== this.contentScrollOffset) {
|
|
502
|
-
this.contentScrollOffset = next;
|
|
667
|
+
const next = nextContentScrollOffset(this.questionScroll, this.questionTotalRows, this.questionRegionRows, action);
|
|
668
|
+
if (next !== this.questionScroll) {
|
|
669
|
+
this.questionScroll = next;
|
|
503
670
|
this.deps.requestRender();
|
|
504
671
|
}
|
|
505
672
|
return true;
|
|
@@ -515,99 +682,83 @@ class AskUserOverlayView implements Component {
|
|
|
515
682
|
return null;
|
|
516
683
|
}
|
|
517
684
|
|
|
518
|
-
private
|
|
519
|
-
this.
|
|
685
|
+
private resetQuestionScroll(): void {
|
|
686
|
+
this.questionScroll = 0;
|
|
520
687
|
}
|
|
521
688
|
|
|
522
689
|
footerHint(): string {
|
|
523
|
-
if (this.phase !== "asking")
|
|
524
|
-
return buildHint([]);
|
|
525
|
-
}
|
|
690
|
+
if (this.phase !== "asking") return buildHint([]);
|
|
526
691
|
const question = this.currentQuestion();
|
|
527
692
|
const state = this.currentState();
|
|
528
693
|
if (!question || !state) return buildHint([]);
|
|
529
694
|
const recordAnswer =
|
|
530
695
|
this.presentation.requiredActions.find((action) => action.id === "record-answer")?.label.toLowerCase() ??
|
|
531
696
|
"record answer";
|
|
697
|
+
const scroll: HintEntry[] = this.questionOverflows ? [{ key: "PgUp/PgDn", verb: "scroll" }] : [];
|
|
532
698
|
if (state.mode === "text") {
|
|
533
699
|
// Esc means two different things depending on what is behind the field.
|
|
534
700
|
// A question with options has a list to fall back to, so the key goes
|
|
535
701
|
// back; a question without one has nothing behind it and the key still
|
|
536
702
|
// leaves the interview. The footer says which one this is.
|
|
537
703
|
const escapeVerb: OverlayEscVerb = questionHasOptions(question) ? "back" : "close";
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
{ key: "Enter", verb: recordAnswer },
|
|
542
|
-
{ key: "Alt+Left/Right", verb: "question" },
|
|
543
|
-
],
|
|
544
|
-
escapeVerb,
|
|
545
|
-
)
|
|
546
|
-
: this.withScrollHint([{ key: "Enter", verb: recordAnswer }], escapeVerb);
|
|
704
|
+
const entries: HintEntry[] = [{ key: "Enter", verb: recordAnswer }];
|
|
705
|
+
if (this.deps.tui) entries.push({ key: getKeybindings().getKeys("tui.input.newLine").join("/"), verb: "newline" });
|
|
706
|
+
return buildHint([...entries, ...scroll], escapeVerb);
|
|
547
707
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
708
|
+
const entries: HintEntry[] = [];
|
|
709
|
+
if (this.questions.length > 1) entries.push({ key: "Left/Right", verb: "question" });
|
|
710
|
+
if (question.multi_select === true) entries.push({ key: "Space", verb: "toggle" });
|
|
551
711
|
// `t` is on every select footer because the operator cannot tell from a
|
|
552
712
|
// label whether the option needs a figure attached until they read it.
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
{ key: "Space", verb: "toggle" },
|
|
559
|
-
addText,
|
|
560
|
-
{ key: "Enter", verb: recordAnswer },
|
|
561
|
-
])
|
|
562
|
-
: this.withScrollHint([{ key: "Space", verb: "toggle" }, addText, { key: "Enter", verb: recordAnswer }]);
|
|
563
|
-
}
|
|
564
|
-
return this.questions.length > 1
|
|
565
|
-
? this.withScrollHint([{ key: "Left/Right", verb: "question" }, addText, { key: "Enter", verb: recordAnswer }])
|
|
566
|
-
: this.withScrollHint([addText, { key: "Enter", verb: recordAnswer }]);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
private renderDecisionContext(width: number): string[] {
|
|
570
|
-
const theme = clioTheme();
|
|
571
|
-
const tier = theme.style(this.presentation.semanticToken, this.presentation.tierLabel, { bold: true });
|
|
572
|
-
const requested = theme.fg("dim", `requested by ${this.presentation.requestedByCopy}`);
|
|
573
|
-
const effect = `${theme.fg("dim", "Effect:")} ${theme.fg("muted", this.presentation.authorizationCopy)}`;
|
|
574
|
-
return [
|
|
575
|
-
fitLine(`${tier}${dotSep(theme)}${requested}`, width),
|
|
576
|
-
...wrapTextWithAnsi(effect, width).map((line) => fitLine(line, width)),
|
|
577
|
-
...wrapTextWithAnsi(theme.fg("muted", this.presentation.reversibilityCopy), width).map((line) =>
|
|
578
|
-
fitLine(line, width),
|
|
579
|
-
),
|
|
580
|
-
];
|
|
713
|
+
entries.push({ key: "t", verb: "add text", short: "text" });
|
|
714
|
+
entries.push({ key: "Enter", verb: recordAnswer });
|
|
715
|
+
if (this.ledgerEntries().length > 0) entries.push({ key: "a", verb: "answers" });
|
|
716
|
+
entries.push({ key: "?", verb: "details" });
|
|
717
|
+
return buildHint([...entries, ...scroll]);
|
|
581
718
|
}
|
|
582
719
|
|
|
583
720
|
/**
|
|
584
|
-
* The
|
|
585
|
-
*
|
|
586
|
-
*
|
|
721
|
+
* The classified context, folded.
|
|
722
|
+
*
|
|
723
|
+
* Tier, requester, effect, and reversibility used to take four rows above
|
|
724
|
+
* every question, and for a local conversational answer they say nothing the
|
|
725
|
+
* title ("Answer a question") does not. They render on `?`; a tier that is
|
|
726
|
+
* not the default one keeps its one-line identity on screen because that is
|
|
727
|
+
* the row that says the answer reaches outside the workspace.
|
|
587
728
|
*/
|
|
588
|
-
private
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
729
|
+
private renderDecisionContext(width: number): string[] {
|
|
730
|
+
const theme = clioTheme();
|
|
731
|
+
const lines: string[] = [];
|
|
732
|
+
const defaultTier = this.presentation.tier === DEFAULT_ASK_USER_PRESENTATION.tier;
|
|
733
|
+
if (!defaultTier || this.detailsExpanded) {
|
|
734
|
+
const tier = theme.style(this.presentation.semanticToken, this.presentation.tierLabel, { bold: true });
|
|
735
|
+
const requested = theme.fg("dim", `requested by ${this.presentation.requestedByCopy}`);
|
|
736
|
+
lines.push(fitLine(`${tier}${dotSep(theme)}${requested}`, width));
|
|
737
|
+
}
|
|
738
|
+
if (this.detailsExpanded) {
|
|
739
|
+
lines.push(
|
|
740
|
+
...wrapTextWithAnsi(
|
|
741
|
+
`${theme.fg("dim", "Effect:")} ${theme.fg("muted", this.presentation.authorizationCopy)}`,
|
|
742
|
+
width,
|
|
743
|
+
),
|
|
744
|
+
...wrapTextWithAnsi(theme.fg("muted", this.presentation.reversibilityCopy), width),
|
|
745
|
+
"",
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
return lines;
|
|
597
749
|
}
|
|
598
750
|
|
|
599
751
|
private finish(result: AskUserResult): void {
|
|
600
752
|
const resolve = this.resolveCurrent;
|
|
601
753
|
this.resolveCurrent = null;
|
|
602
754
|
this.list = null;
|
|
603
|
-
this.
|
|
755
|
+
this.text = null;
|
|
604
756
|
this.status = "";
|
|
605
|
-
if (result.cancelled !== true)
|
|
757
|
+
if (result.cancelled !== true) {
|
|
758
|
+
this.history.push(...result.answers);
|
|
759
|
+
this.roundsAnswered += 1;
|
|
760
|
+
}
|
|
606
761
|
if (this.phase !== "closed") this.phase = "waiting";
|
|
607
|
-
// A round that follows an answered one is an interview, so the box grows
|
|
608
|
-
// here rather than at the next `begin`. The waiting frame between rounds
|
|
609
|
-
// is then already the size of the round it is waiting for.
|
|
610
|
-
if (this.history.length > 0) this.setSurface("interview");
|
|
611
762
|
this.deps.requestRender();
|
|
612
763
|
resolve?.(result);
|
|
613
764
|
}
|
|
@@ -615,56 +766,27 @@ class AskUserOverlayView implements Component {
|
|
|
615
766
|
private renderWaiting(width: number, maxRows: number): string[] {
|
|
616
767
|
const theme = clioTheme();
|
|
617
768
|
const lines = [
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
...wrapTextWithAnsi(
|
|
621
|
-
theme.fg(
|
|
769
|
+
fitLine(
|
|
770
|
+
`${screenTitle(theme, "Interview")}${dotSep(theme)}${theme.fg(
|
|
622
771
|
"muted",
|
|
623
|
-
this.history.length > 0
|
|
624
|
-
|
|
625
|
-
: "Waiting for Clio to prepare the interview.",
|
|
626
|
-
),
|
|
772
|
+
this.history.length > 0 ? "answer sent · waiting for the next question" : "waiting for the first question",
|
|
773
|
+
)}`,
|
|
627
774
|
width,
|
|
628
775
|
),
|
|
629
776
|
];
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
for (let index = 0; index < this.history.length; index += 1) {
|
|
633
|
-
const answer = this.history[index];
|
|
634
|
-
if (!answer) continue;
|
|
635
|
-
lines.push(...wrapLabeledValue(`${theme.fg("dim", `${index + 1}.`)} `, theme.fg("muted", answer.answer), width));
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
return lines.map((line) => fitLine(line, width)).slice(0, maxRows);
|
|
777
|
+
const ledger = this.renderAnswerLedger(width, Math.max(1, maxRows - lines.length));
|
|
778
|
+
return [...lines, ...ledger].slice(0, maxRows);
|
|
639
779
|
}
|
|
640
780
|
|
|
641
781
|
/**
|
|
642
|
-
* Rows the body may draw
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
* the two border rows and both clamp to the terminal the same way. That is
|
|
646
|
-
* what keeps `fitBody`'s "… N more rows" off the compact bar: the body never
|
|
647
|
-
* hands the frame more than the frame is prepared to draw.
|
|
782
|
+
* Rows the body may draw. The frame budgets the same number from the live
|
|
783
|
+
* terminal, so the body never hands the frame more than it can draw and the
|
|
784
|
+
* frame's "… N more rows" cut never fires on a decision.
|
|
648
785
|
*/
|
|
649
786
|
private maxInnerRows(): number {
|
|
650
|
-
const cap = ASK_USER_SURFACE_GEOMETRY[this.surface].maxInnerRows;
|
|
651
787
|
const rows = this.deps.getTerminalRows();
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
if (!Number.isFinite(rows) || rows <= 0) return cap > 0 ? cap : 1;
|
|
655
|
-
const available = Math.max(1, Math.floor(rows) - ASK_USER_FRAME_AND_MARGIN_ROWS);
|
|
656
|
-
return cap > 0 ? Math.min(cap, available) : available;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
private maxVisibleOptions(): number {
|
|
660
|
-
const inner = this.maxInnerRows();
|
|
661
|
-
// The interview's option list is budgeted off the live row count rather
|
|
662
|
-
// than a fixed heuristic: the strip, header, blank, status, blank and one
|
|
663
|
-
// row of question are what the control shares the workspace with.
|
|
664
|
-
if (this.surface === "interview") {
|
|
665
|
-
return Math.max(MIN_VISIBLE_OPTIONS, Math.min(MAX_VISIBLE_OPTIONS, inner - INTERVIEW_CHROME_ROWS));
|
|
666
|
-
}
|
|
667
|
-
return Math.max(1, Math.min(MAX_VISIBLE_OPTIONS, inner - 4));
|
|
788
|
+
if (!Number.isFinite(rows) || rows <= 0) return 40;
|
|
789
|
+
return Math.max(MIN_INNER_ROWS, Math.floor(rows) - ASK_USER_FRAME_AND_MARGIN_ROWS);
|
|
668
790
|
}
|
|
669
791
|
|
|
670
792
|
private currentQuestion(): AskUserQuestion | null {
|
|
@@ -683,14 +805,6 @@ class AskUserOverlayView implements Component {
|
|
|
683
805
|
return this.questions.length > 1 && matchesKey(data, "right");
|
|
684
806
|
}
|
|
685
807
|
|
|
686
|
-
private isTextModePreviousKey(data: string): boolean {
|
|
687
|
-
return this.questions.length > 1 && (matchesKey(data, "alt+left") || matchesKey(data, "ctrl+left"));
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
private isTextModeNextKey(data: string): boolean {
|
|
691
|
-
return this.questions.length > 1 && (matchesKey(data, "alt+right") || matchesKey(data, "ctrl+right"));
|
|
692
|
-
}
|
|
693
|
-
|
|
694
808
|
/**
|
|
695
809
|
* The key that adds typed text to the option under the cursor.
|
|
696
810
|
*
|
|
@@ -711,212 +825,141 @@ class AskUserOverlayView implements Component {
|
|
|
711
825
|
private chooseFocusedAndOpenText(question: AskUserQuestion, state: QuestionState): void {
|
|
712
826
|
const current = this.list?.getSelectedItem();
|
|
713
827
|
const optionIndex = current && current.value !== "other" ? optionIndexFromValue(current.value) : null;
|
|
828
|
+
let label: string | undefined;
|
|
829
|
+
if (optionIndex === null && !question.multi_select) state.selected = new Set();
|
|
714
830
|
if (optionIndex !== null) {
|
|
715
831
|
if (question.multi_select === true) state.selected.add(optionIndex);
|
|
716
832
|
else state.selected = new Set<number>([optionIndex]);
|
|
717
833
|
state.focusedValue = `option:${optionIndex}`;
|
|
834
|
+
label = question.options?.[optionIndex]?.label;
|
|
718
835
|
}
|
|
719
|
-
this.openTextInput(
|
|
836
|
+
this.openTextInput(label ? `Your answer, with "${label}"` : "Your answer");
|
|
720
837
|
}
|
|
721
838
|
|
|
722
839
|
/**
|
|
723
|
-
* The row above the question,
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
* confirmation. A one-question surface shows the question's own header if it
|
|
727
|
-
* has one and skips the row entirely if it does not.
|
|
840
|
+
* The row above the question: its header, and the round for a phased
|
|
841
|
+
* interview. `Question 1/1` was a counter over a set of one, printed on every
|
|
842
|
+
* confirmation; a round of several questions names them in the strip instead.
|
|
728
843
|
*/
|
|
729
|
-
private renderQuestionHeader(question: AskUserQuestion, width: number): string {
|
|
844
|
+
private renderQuestionHeader(question: AskUserQuestion, width: number): string[] {
|
|
730
845
|
const theme = clioTheme();
|
|
731
846
|
const parts: string[] = [];
|
|
732
|
-
if (this.questions.length > 1) parts.push(theme.fg("dim", `Question ${this.index + 1}/${this.questions.length}`));
|
|
733
847
|
if (question.header) parts.push(screenTitle(theme, question.header));
|
|
848
|
+
if (this.questions.length <= 1 && this.roundsAnswered > 0) {
|
|
849
|
+
parts.push(theme.fg("dim", `Round ${this.roundsAnswered + 1}`));
|
|
850
|
+
}
|
|
734
851
|
if (this.currentState()?.answer.trim()) parts.push(theme.fg("muted", "answered"));
|
|
735
|
-
return parts.length > 0 ?
|
|
852
|
+
return parts.length > 0 ? wrapTextWithAnsi(parts.join(dotSep(theme)), width) : [];
|
|
736
853
|
}
|
|
737
854
|
|
|
738
|
-
|
|
855
|
+
/**
|
|
856
|
+
* The questions of this round, by name.
|
|
857
|
+
*
|
|
858
|
+
* Four fixed slots at 80 columns left three characters of each header
|
|
859
|
+
* ("Con…", "Sam…"), which is the strip saying nothing. The names now flow on
|
|
860
|
+
* one row and wrap to a second when they must; a question that has been
|
|
861
|
+
* answered carries a check, the active one carries the cursor.
|
|
862
|
+
*/
|
|
863
|
+
private renderQuestionStrip(width: number): string[] {
|
|
739
864
|
const theme = clioTheme();
|
|
740
|
-
|
|
741
|
-
// A single-question interview round is the phased shape: one question per
|
|
742
|
-
// round, several rounds. Its position is the round number, since there is
|
|
743
|
-
// no set of siblings to draw a strip across.
|
|
744
|
-
if (total <= 1) {
|
|
745
|
-
const round = theme.fg("dim", `Round ${this.history.length + 1}`);
|
|
746
|
-
return fitLine(`${screenTitle(theme, "Interview")}${dotSep(theme)}${round}`, width);
|
|
747
|
-
}
|
|
748
|
-
const gap = " ";
|
|
749
|
-
const slotWidth = Math.max(10, Math.floor((width - visibleWidth(gap) * (total - 1)) / total));
|
|
865
|
+
if (this.questions.length <= 1) return [];
|
|
750
866
|
const parts = this.questions.map((question, index) => {
|
|
751
867
|
const state = this.states[index];
|
|
752
868
|
const active = index === this.index;
|
|
753
|
-
const
|
|
754
|
-
const
|
|
869
|
+
const answered = Boolean(state?.answer.trim());
|
|
870
|
+
const mark = answered ? `${theme.fg("success", GLYPH.ok)} ` : "";
|
|
871
|
+
const name = `Q${index + 1} ${compactTitle(question)}`;
|
|
755
872
|
const title = active
|
|
756
|
-
? theme.
|
|
757
|
-
: theme.fg("muted",
|
|
758
|
-
return
|
|
873
|
+
? `${theme.fg("accent", GLYPH.cursor)} ${theme.style("accent", name, { bold: true })}`
|
|
874
|
+
: theme.fg(answered ? "muted" : "dim", name);
|
|
875
|
+
return `${mark}${title}`;
|
|
759
876
|
});
|
|
760
|
-
|
|
877
|
+
const round = this.roundsAnswered > 0 ? [theme.fg("dim", `Round ${this.roundsAnswered + 1}`)] : [];
|
|
878
|
+
const strip = wrapTextWithAnsi([...round, ...parts].join(dotSep(theme)), width).slice(0, 2);
|
|
879
|
+
return [...strip, ""];
|
|
761
880
|
}
|
|
762
881
|
|
|
763
|
-
private renderControlLines(width: number): string[] {
|
|
882
|
+
private renderControlLines(width: number, rowBudget: number): string[] {
|
|
764
883
|
this.ensureControl();
|
|
765
884
|
const state = this.currentState();
|
|
766
|
-
if (state?.mode === "text") return this.
|
|
767
|
-
return this.renderSelectControl(width);
|
|
885
|
+
if (state?.mode === "text") return this.text?.render(width) ?? [""];
|
|
886
|
+
return this.renderSelectControl(width, rowBudget);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/** Earlier rounds first, then this round's answered siblings. */
|
|
890
|
+
private ledgerEntries(): Array<{ label: string; answer: string }> {
|
|
891
|
+
const entries: Array<{ label: string; answer: string }> = [];
|
|
892
|
+
this.history.forEach((answer, index) => {
|
|
893
|
+
entries.push({ label: `${index + 1}.`, answer: answer.answer });
|
|
894
|
+
});
|
|
895
|
+
if (this.questions.length > 1) {
|
|
896
|
+
this.states.forEach((state, index) => {
|
|
897
|
+
const answer = state.answer.trim();
|
|
898
|
+
if (answer.length > 0 && index !== this.index) entries.push({ label: `Q${index + 1}`, answer });
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
return entries;
|
|
768
902
|
}
|
|
769
903
|
|
|
770
904
|
/**
|
|
771
|
-
*
|
|
905
|
+
* What the interview has captured, folded to one row.
|
|
772
906
|
*
|
|
773
|
-
* The ledger used to
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
907
|
+
* The full ledger used to sit between the question and its options and grow
|
|
908
|
+
* by the whole typed answer every round, so by the fourth question the
|
|
909
|
+
* options were twenty rows below the sentence they answered. The ledger is
|
|
910
|
+
* one dim row under the options until `a` opens it.
|
|
777
911
|
*/
|
|
778
|
-
private renderAnswerLedger(width: number): string[] {
|
|
912
|
+
private renderAnswerLedger(width: number, rowBudget: number): string[] {
|
|
779
913
|
const theme = clioTheme();
|
|
914
|
+
const entries = this.ledgerEntries();
|
|
915
|
+
if (entries.length === 0) return [];
|
|
916
|
+
const count = `${entries.length} earlier answer${entries.length === 1 ? "" : "s"}`;
|
|
917
|
+
if (!this.ledgerExpanded) return ["", fitLine(theme.fg("dim", `${count} · a to review`), width)];
|
|
780
918
|
const rows: string[] = [];
|
|
781
|
-
for (
|
|
782
|
-
|
|
783
|
-
if (!answer) continue;
|
|
784
|
-
rows.push(...wrapLabeledValue(`${theme.fg("dim", `${index + 1}.`)} `, theme.fg("muted", answer.answer), width));
|
|
785
|
-
}
|
|
786
|
-
// A round of one question needs no per-question roll: the strip already
|
|
787
|
-
// names the round and the question is on screen above this.
|
|
788
|
-
if (this.questions.length > 1) {
|
|
789
|
-
for (let index = 0; index < this.questions.length; index += 1) {
|
|
790
|
-
const answer = this.states[index]?.answer.trim();
|
|
791
|
-
const value = answer && answer.length > 0 ? theme.fg("muted", answer) : theme.fg("dim", "pending");
|
|
792
|
-
rows.push(...wrapLabeledValue(`${theme.fg("dim", `Q${index + 1}`)} `, value, width));
|
|
793
|
-
}
|
|
919
|
+
for (const entry of entries) {
|
|
920
|
+
rows.push(...wrapLabeledValue(`${theme.fg("dim", entry.label)} `, theme.fg("muted", entry.answer), width));
|
|
794
921
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
fitLine(
|
|
803
|
-
|
|
922
|
+
const head = ["", fitLine(rule(theme, width, { left: "Answers", leftToken: "dim" }), width)];
|
|
923
|
+
const budget = Math.max(1, rowBudget - head.length);
|
|
924
|
+
if (rows.length <= budget) return [...head, ...rows];
|
|
925
|
+
const kept = rows.slice(0, Math.max(0, budget - 1));
|
|
926
|
+
return [
|
|
927
|
+
...head,
|
|
928
|
+
...kept,
|
|
929
|
+
fitLine(theme.fg("dim", `${GLYPH.ellipsis} ${rows.length - kept.length} more on /decisions`), width),
|
|
930
|
+
];
|
|
804
931
|
}
|
|
805
932
|
|
|
806
|
-
private renderSelectControl(width: number): string[] {
|
|
933
|
+
private renderSelectControl(width: number, rowBudget: number): string[] {
|
|
807
934
|
const question = this.currentQuestion();
|
|
808
935
|
const state = this.currentState();
|
|
809
936
|
const selectedItem = this.list?.getSelectedItem();
|
|
810
937
|
if (!question || !state || !selectedItem) return [""];
|
|
811
|
-
const theme = clioTheme();
|
|
812
938
|
const items = optionItems(question, state.selected);
|
|
813
|
-
const
|
|
939
|
+
const focusedIndex = Math.max(
|
|
814
940
|
0,
|
|
815
941
|
items.findIndex((item) => item.value === selectedItem.value),
|
|
816
942
|
);
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
const focusedRows = this.renderOptionRow(
|
|
824
|
-
items[selectedIndex] as SelectItem,
|
|
825
|
-
true,
|
|
826
|
-
width,
|
|
827
|
-
primaryColumnWidth,
|
|
828
|
-
acceptKey,
|
|
829
|
-
).length;
|
|
830
|
-
const budget = Math.max(1, this.maxVisibleOptions() - (focusedRows - 1));
|
|
831
|
-
const visibleCount = Math.min(budget, Math.max(1, items.length));
|
|
832
|
-
const start = Math.max(0, Math.min(selectedIndex - Math.floor(visibleCount / 2), items.length - visibleCount));
|
|
833
|
-
const end = Math.min(items.length, start + visibleCount);
|
|
834
|
-
const lines: string[] = [];
|
|
835
|
-
|
|
836
|
-
for (let index = start; index < end; index += 1) {
|
|
837
|
-
const item = items[index];
|
|
838
|
-
if (!item) continue;
|
|
839
|
-
lines.push(...this.renderOptionRow(item, index === selectedIndex, width, primaryColumnWidth, acceptKey));
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
if (start > 0 || end < items.length) {
|
|
843
|
-
lines.push(fitLine(theme.fg("dim", ` (${selectedIndex + 1}/${items.length})`), width));
|
|
844
|
-
}
|
|
845
|
-
return lines;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
private primaryColumnWidth(items: ReadonlyArray<SelectItem>, width: number): number {
|
|
849
|
-
const widest = items.reduce((max, item) => Math.max(max, visibleWidth(item.label) + 2), 0);
|
|
850
|
-
const bounded = Math.max(24, Math.min(38, widest));
|
|
851
|
-
return Math.max(1, Math.min(bounded, Math.max(1, width - 4)));
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
/**
|
|
855
|
-
* One option, as the rows it needs.
|
|
856
|
-
*
|
|
857
|
-
* The focused option's description wraps under its own column and is never
|
|
858
|
-
* cut: a description is what the operator is choosing between, and half of
|
|
859
|
-
* one reads as the whole thing. An unfocused row stays a one-line table cell
|
|
860
|
-
* whose ellipsis says there is more, which the operator reads by moving to
|
|
861
|
-
* it.
|
|
862
|
-
*/
|
|
863
|
-
private renderOptionRow(
|
|
864
|
-
item: SelectItem,
|
|
865
|
-
selected: boolean,
|
|
866
|
-
width: number,
|
|
867
|
-
primaryColumnWidth: number,
|
|
868
|
-
acceptKey: string,
|
|
869
|
-
): string[] {
|
|
870
|
-
const theme = clioTheme();
|
|
871
|
-
const prefix = selected ? theme.fg("accent", `${GLYPH.cursor} `) : " ";
|
|
872
|
-
// The key that answers rides on the focused row. An operator whose eyes are
|
|
873
|
-
// already on the option they want should not have to travel to the footer
|
|
874
|
-
// to learn what commits it.
|
|
875
|
-
const affordance = selected ? ` ${theme.style("accent", `[${acceptKey}]`, { bold: true })}` : "";
|
|
876
|
-
const available = Math.max(1, width - 2 - visibleWidth(affordance));
|
|
877
|
-
const description = item.description?.replace(/[\r\n]+/g, " ").trim();
|
|
878
|
-
|
|
879
|
-
if (!description || width <= 40) {
|
|
880
|
-
const label = truncateToWidth(item.label, available, ELLIPSIS, false);
|
|
881
|
-
const body = selected ? theme.style("accent", label, { bold: true }) : label;
|
|
882
|
-
return [fitLine(`${prefix}${body}${affordance}`, width)];
|
|
883
|
-
}
|
|
884
|
-
|
|
885
|
-
const labelWidth = Math.max(1, Math.min(primaryColumnWidth - 2, available - 4));
|
|
886
|
-
const label = truncateToWidth(item.label, labelWidth, ELLIPSIS, false);
|
|
887
|
-
const spacing = " ".repeat(Math.max(1, primaryColumnWidth - visibleWidth(label)));
|
|
888
|
-
const descriptionWidth = Math.max(1, available - visibleWidth(label) - visibleWidth(spacing));
|
|
889
|
-
if (!selected) {
|
|
890
|
-
const desc = truncateToWidth(description, descriptionWidth, ELLIPSIS, false);
|
|
891
|
-
return [fitLine(`${prefix}${label}${theme.fg("muted", `${spacing}${desc}`)}`, width)];
|
|
892
|
-
}
|
|
893
|
-
// A gutter this narrow wraps a sentence into slivers and breaks words, so
|
|
894
|
-
// below the floor the description drops to its own full-width lines under
|
|
895
|
-
// the label instead of squeezing into the second column.
|
|
896
|
-
if (descriptionWidth < MIN_DESCRIPTION_COLUMN) {
|
|
897
|
-
const indent = " ";
|
|
898
|
-
const body = theme.style("accent", label, { bold: true });
|
|
899
|
-
return [
|
|
900
|
-
fitLine(`${prefix}${body}${affordance}`, width),
|
|
901
|
-
...wrapTextWithAnsi(description, Math.max(1, width - indent.length)).map((line) =>
|
|
902
|
-
fitLine(`${indent}${theme.style("accent", line, { bold: true })}`, width),
|
|
903
|
-
),
|
|
943
|
+
const layout = layoutOptionRows(items, focusedIndex, width);
|
|
944
|
+
if ((layout.rows[focusedIndex]?.length ?? 0) >= rowBudget) {
|
|
945
|
+
this.optionDetailRows = [
|
|
946
|
+
"",
|
|
947
|
+
...wrapTextWithAnsi(`Option details: ${selectedItem.label}`, width),
|
|
948
|
+
...wrapTextWithAnsi(selectedItem.description ?? "", width),
|
|
904
949
|
];
|
|
950
|
+
layout.rows[focusedIndex] = wrapTextWithAnsi(
|
|
951
|
+
`${GLYPH.cursor} ${selectedItem.label} (details above; PgUp/PgDn)`,
|
|
952
|
+
width,
|
|
953
|
+
);
|
|
905
954
|
}
|
|
906
|
-
|
|
907
|
-
const head = wrapped[0] ?? "";
|
|
908
|
-
const indent = " ".repeat(visibleWidth(prefix) + visibleWidth(label) + visibleWidth(spacing));
|
|
909
|
-
return [
|
|
910
|
-
fitLine(`${prefix}${theme.style("accent", `${label}${spacing}${head}`, { bold: true })}${affordance}`, width),
|
|
911
|
-
...wrapped.slice(1).map((line) => fitLine(`${indent}${theme.style("accent", line, { bold: true })}`, width)),
|
|
912
|
-
];
|
|
955
|
+
return windowOptionRows(layout, focusedIndex, rowBudget, width);
|
|
913
956
|
}
|
|
914
957
|
|
|
915
958
|
private ensureControl(): void {
|
|
916
959
|
const question = this.currentQuestion();
|
|
917
960
|
const state = this.currentState();
|
|
918
961
|
if (!question || !state) return;
|
|
919
|
-
if (state.mode === "text" && !this.
|
|
962
|
+
if (state.mode === "text" && !this.text) this.rebuildTextInput(question, state, "Your answer");
|
|
920
963
|
if (state.mode === "select" && !this.list) this.rebuildSelectList(question, state);
|
|
921
964
|
}
|
|
922
965
|
|
|
@@ -924,59 +967,67 @@ class AskUserOverlayView implements Component {
|
|
|
924
967
|
const question = this.currentQuestion();
|
|
925
968
|
const state = this.currentState();
|
|
926
969
|
if (!question || !state) {
|
|
927
|
-
this.
|
|
970
|
+
this.text = null;
|
|
928
971
|
this.list = null;
|
|
929
972
|
return;
|
|
930
973
|
}
|
|
931
|
-
if (state.mode === "text") this.rebuildTextInput(question, state);
|
|
974
|
+
if (state.mode === "text") this.rebuildTextInput(question, state, "Your answer");
|
|
932
975
|
else this.rebuildSelectList(question, state);
|
|
933
976
|
}
|
|
934
977
|
|
|
935
|
-
private
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
978
|
+
private submitText(question: AskUserQuestion, state: QuestionState, value: string): void {
|
|
979
|
+
const answer = value.trim();
|
|
980
|
+
if (answer.length === 0) {
|
|
981
|
+
this.status = "Enter an answer or press Esc to cancel.";
|
|
982
|
+
this.deps.requestRender();
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
state.inputValue = answer;
|
|
986
|
+
// Verbatim, before any joining or trimming the display line does. The
|
|
987
|
+
// typed text is the answer; the one-line `answer` is a rendering of it.
|
|
988
|
+
state.rawValue = value;
|
|
989
|
+
if (questionHasOptions(question) && question.multi_select === true) {
|
|
990
|
+
state.customAnswer = answer;
|
|
991
|
+
this.commitCurrentAnswer();
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
state.customAnswer = questionHasOptions(question) ? answer : "";
|
|
995
|
+
// The chosen labels and the typed text compose, so a question answered
|
|
996
|
+
// with an option that says "I will type it" keeps both. With nothing
|
|
997
|
+
// selected, which is the implicit Other path, this is the text alone.
|
|
998
|
+
state.answer = questionHasOptions(question)
|
|
999
|
+
? answerText(question, state.selected, state.customAnswer)
|
|
1000
|
+
: answer.replace(/\s*\n\s*/g, " ");
|
|
1001
|
+
this.finishIfCompleteOrAdvance();
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
private rebuildTextInput(question: AskUserQuestion, state: QuestionState, caption: string): void {
|
|
1005
|
+
const initial = state.inputValue || state.customAnswer || state.answer;
|
|
1006
|
+
if (this.deps.tui) {
|
|
1007
|
+
const editor = new AnswerEditor(this.deps.tui);
|
|
1008
|
+
editor.caption = caption;
|
|
1009
|
+
editor.setText(initial);
|
|
1010
|
+
editor.onSubmit = (value) => this.submitText(question, state, value);
|
|
1011
|
+
this.text = editor;
|
|
1012
|
+
} else {
|
|
1013
|
+
const input = new Input();
|
|
1014
|
+
input.setValue(initial);
|
|
1015
|
+
input.onSubmit = (value) => this.submitText(question, state, value);
|
|
1016
|
+
this.text = new InputTextControl(input, caption);
|
|
1017
|
+
}
|
|
963
1018
|
this.list = null;
|
|
964
1019
|
}
|
|
965
1020
|
|
|
966
1021
|
private rebuildSelectList(question: AskUserQuestion, state: QuestionState): void {
|
|
967
1022
|
const items = optionItems(question, state.selected);
|
|
968
|
-
const activeList = new SelectList(
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
{
|
|
973
|
-
minPrimaryColumnWidth: 24,
|
|
974
|
-
maxPrimaryColumnWidth: 38,
|
|
975
|
-
},
|
|
976
|
-
);
|
|
1023
|
+
const activeList = new SelectList(items, Math.max(1, items.length), DEFAULT_SELECT_THEME, {
|
|
1024
|
+
minPrimaryColumnWidth: MIN_LABEL_COLUMN,
|
|
1025
|
+
maxPrimaryColumnWidth: MAX_LABEL_COLUMN,
|
|
1026
|
+
});
|
|
977
1027
|
activeList.onSelect = (item) => {
|
|
978
1028
|
if (item.value === "other") {
|
|
979
|
-
|
|
1029
|
+
if (!question.multi_select) state.selected = new Set();
|
|
1030
|
+
this.openTextInput("Your answer");
|
|
980
1031
|
return;
|
|
981
1032
|
}
|
|
982
1033
|
const optionIndex = optionIndexFromValue(item.value);
|
|
@@ -985,6 +1036,11 @@ class AskUserOverlayView implements Component {
|
|
|
985
1036
|
this.toggleSelectionIndex(question, state, optionIndex);
|
|
986
1037
|
return;
|
|
987
1038
|
}
|
|
1039
|
+
const label = question.options?.[optionIndex]?.label ?? item.label;
|
|
1040
|
+
if (optionAsksForText(label)) {
|
|
1041
|
+
this.chooseFocusedAndOpenText(question, state);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
988
1044
|
state.selected = new Set<number>([optionIndex]);
|
|
989
1045
|
// Choosing a plain option is a label-only answer, so the typed text
|
|
990
1046
|
// clears with the choice: it was given for a label the operator has
|
|
@@ -1004,7 +1060,7 @@ class AskUserOverlayView implements Component {
|
|
|
1004
1060
|
};
|
|
1005
1061
|
const selectedIndex = this.preferredSelectedIndex(question, state, items);
|
|
1006
1062
|
if (selectedIndex >= 0) activeList.setSelectedIndex(selectedIndex);
|
|
1007
|
-
this.
|
|
1063
|
+
this.text = null;
|
|
1008
1064
|
this.list = activeList;
|
|
1009
1065
|
}
|
|
1010
1066
|
|
|
@@ -1049,6 +1105,7 @@ class AskUserOverlayView implements Component {
|
|
|
1049
1105
|
return;
|
|
1050
1106
|
}
|
|
1051
1107
|
state.inputValue = "";
|
|
1108
|
+
state.selected = new Set(state.committedSelected);
|
|
1052
1109
|
state.mode = "select";
|
|
1053
1110
|
this.status = "";
|
|
1054
1111
|
this.rebuildSelectList(question, state);
|
|
@@ -1076,14 +1133,14 @@ class AskUserOverlayView implements Component {
|
|
|
1076
1133
|
state.mode = "select";
|
|
1077
1134
|
}
|
|
1078
1135
|
|
|
1079
|
-
private openTextInput(
|
|
1136
|
+
private openTextInput(caption: string): void {
|
|
1080
1137
|
const state = this.currentState();
|
|
1081
1138
|
const question = this.currentQuestion();
|
|
1082
1139
|
if (!state || !question) return;
|
|
1083
1140
|
this.syncActiveControl();
|
|
1084
1141
|
state.mode = "text";
|
|
1085
|
-
this.status =
|
|
1086
|
-
this.rebuildTextInput(question, state);
|
|
1142
|
+
this.status = "";
|
|
1143
|
+
this.rebuildTextInput(question, state, caption);
|
|
1087
1144
|
this.deps.requestRender();
|
|
1088
1145
|
}
|
|
1089
1146
|
|
|
@@ -1091,7 +1148,7 @@ class AskUserOverlayView implements Component {
|
|
|
1091
1148
|
const current = this.list?.getSelectedItem();
|
|
1092
1149
|
if (!current) return;
|
|
1093
1150
|
if (current.value === "other") {
|
|
1094
|
-
this.openTextInput("
|
|
1151
|
+
this.openTextInput("Your answer");
|
|
1095
1152
|
return;
|
|
1096
1153
|
}
|
|
1097
1154
|
const optionIndex = optionIndexFromValue(current.value);
|
|
@@ -1111,7 +1168,7 @@ class AskUserOverlayView implements Component {
|
|
|
1111
1168
|
private commitMultiSelectOrOpenOther(state: QuestionState): void {
|
|
1112
1169
|
const current = this.list?.getSelectedItem();
|
|
1113
1170
|
if (current?.value === "other") {
|
|
1114
|
-
this.openTextInput("
|
|
1171
|
+
this.openTextInput("Your answer");
|
|
1115
1172
|
return;
|
|
1116
1173
|
}
|
|
1117
1174
|
const optionIndex = current ? optionIndexFromValue(current.value) : null;
|
|
@@ -1138,6 +1195,8 @@ class AskUserOverlayView implements Component {
|
|
|
1138
1195
|
|
|
1139
1196
|
private finishIfCompleteOrAdvance(): void {
|
|
1140
1197
|
this.syncActiveControl();
|
|
1198
|
+
const current = this.currentState();
|
|
1199
|
+
if (current) current.committedSelected = new Set(current.selected);
|
|
1141
1200
|
if (this.allAnswered()) {
|
|
1142
1201
|
this.finish({ answers: this.answers() });
|
|
1143
1202
|
return;
|
|
@@ -1145,7 +1204,7 @@ class AskUserOverlayView implements Component {
|
|
|
1145
1204
|
const next = this.nextUnansweredIndex();
|
|
1146
1205
|
if (next !== null) {
|
|
1147
1206
|
this.index = next;
|
|
1148
|
-
this.
|
|
1207
|
+
this.resetQuestionScroll();
|
|
1149
1208
|
const nextState = this.currentState();
|
|
1150
1209
|
const nextQuestion = this.currentQuestion();
|
|
1151
1210
|
if (nextState && nextQuestion && !nextState.answer.trim()) nextState.mode = initialMode(nextQuestion);
|
|
@@ -1179,7 +1238,7 @@ class AskUserOverlayView implements Component {
|
|
|
1179
1238
|
const state = this.states[index];
|
|
1180
1239
|
const answer = state?.answer.trim();
|
|
1181
1240
|
if (!question || !state || !answer || answer.length === 0) continue;
|
|
1182
|
-
const options = selectedOptionLabels(question, state.
|
|
1241
|
+
const options = selectedOptionLabels(question, state.committedSelected);
|
|
1183
1242
|
answers.push({
|
|
1184
1243
|
question: question.question,
|
|
1185
1244
|
answer,
|
|
@@ -1196,7 +1255,7 @@ class AskUserOverlayView implements Component {
|
|
|
1196
1255
|
this.parkTextMode();
|
|
1197
1256
|
this.index = (this.index + delta + this.questions.length) % this.questions.length;
|
|
1198
1257
|
this.status = "";
|
|
1199
|
-
this.
|
|
1258
|
+
this.resetQuestionScroll();
|
|
1200
1259
|
this.rebuildControl();
|
|
1201
1260
|
this.deps.requestRender();
|
|
1202
1261
|
}
|
|
@@ -1208,69 +1267,84 @@ class AskUserOverlayView implements Component {
|
|
|
1208
1267
|
const current = this.list.getSelectedItem();
|
|
1209
1268
|
if (current) state.focusedValue = current.value;
|
|
1210
1269
|
}
|
|
1211
|
-
if (this.
|
|
1270
|
+
if (this.text) state.inputValue = this.text.getText();
|
|
1212
1271
|
}
|
|
1213
1272
|
}
|
|
1214
1273
|
|
|
1215
1274
|
/**
|
|
1216
|
-
*
|
|
1275
|
+
* The view without a frame, for layout tests: it renders the body rows the
|
|
1276
|
+
* frame would wrap, at the width and row count the test names.
|
|
1277
|
+
*/
|
|
1278
|
+
export interface AskUserViewForTesting {
|
|
1279
|
+
ask(questions: ReadonlyArray<AskUserQuestion>, presentation?: DecisionPresentation): Promise<AskUserResult>;
|
|
1280
|
+
render(width: number): string[];
|
|
1281
|
+
handleInput(data: string): void;
|
|
1282
|
+
footerHint(): string;
|
|
1283
|
+
cancel(): void;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
export function createAskUserViewForTesting(deps: {
|
|
1287
|
+
rows: number;
|
|
1288
|
+
tui?: TUI;
|
|
1289
|
+
onCancel?: () => void;
|
|
1290
|
+
}): AskUserViewForTesting {
|
|
1291
|
+
const view = new AskUserOverlayView({
|
|
1292
|
+
onCancel: deps.onCancel ?? (() => {}),
|
|
1293
|
+
getTerminalRows: () => deps.rows,
|
|
1294
|
+
requestRender: () => {},
|
|
1295
|
+
...(deps.tui ? { tui: deps.tui } : {}),
|
|
1296
|
+
});
|
|
1297
|
+
return {
|
|
1298
|
+
ask: (questions, presentation) => view.begin(questions, presentation),
|
|
1299
|
+
render: (width) => view.render(width),
|
|
1300
|
+
handleInput: (data) => view.handleInput(data),
|
|
1301
|
+
footerHint: () => view.footerHint(),
|
|
1302
|
+
cancel: () => view.cancel(),
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/**
|
|
1307
|
+
* Show the ask_user overlay.
|
|
1217
1308
|
*
|
|
1218
|
-
*
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1221
|
-
* the request actually changes; the new frame is shown before the old one is
|
|
1222
|
-
* hidden so the keyboard passes straight across instead of returning to the
|
|
1223
|
-
* composer for a frame.
|
|
1309
|
+
* One frame for the whole interview: it sits above the composer, takes its
|
|
1310
|
+
* width from the terminal, and its height from what the round has to say. A
|
|
1311
|
+
* shape change between rounds is just the next render; nothing is remounted.
|
|
1224
1312
|
*/
|
|
1225
1313
|
export function openAskUserOverlay(tui: TUI, deps: OpenAskUserOverlayDeps): AskUserOverlaySession {
|
|
1226
|
-
let handle: OverlayHandle | null = null;
|
|
1227
|
-
let mounted: AskUserSurface | null = null;
|
|
1228
1314
|
let closed = false;
|
|
1229
1315
|
|
|
1230
1316
|
const view = new AskUserOverlayView({
|
|
1231
1317
|
...deps,
|
|
1232
1318
|
getTerminalRows: () => tui.terminal?.rows ?? 0,
|
|
1233
1319
|
requestRender: () => tui.requestRender(),
|
|
1234
|
-
|
|
1320
|
+
tui,
|
|
1235
1321
|
});
|
|
1236
1322
|
|
|
1237
|
-
const
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
// Not derived from the title: this modal swaps between a waiting title
|
|
1249
|
-
// and a classified decision title without ever changing hands.
|
|
1250
|
-
markerId: "ask-user",
|
|
1251
|
-
title: () => (view.isDecisionPending() ? view.decisionTitle() : ASK_USER_WAITING_TITLE),
|
|
1252
|
-
tone: () => (view.isDecisionPending() ? view.decisionTone() : undefined),
|
|
1253
|
-
footerHint: () => view.footerHint(),
|
|
1254
|
-
});
|
|
1255
|
-
mounted = surface;
|
|
1256
|
-
previous?.hide();
|
|
1257
|
-
};
|
|
1258
|
-
|
|
1259
|
-
mount(view.currentSurface());
|
|
1323
|
+
const handle = showClioOverlayFrame(tui, view, {
|
|
1324
|
+
anchor: "bottom-center",
|
|
1325
|
+
width: askUserBoxWidth(tui.terminal?.columns ?? 0),
|
|
1326
|
+
margin: ASK_USER_MARGIN,
|
|
1327
|
+
// Not derived from the title: this modal swaps between a waiting title
|
|
1328
|
+
// and a classified decision title without ever changing hands.
|
|
1329
|
+
markerId: "ask-user",
|
|
1330
|
+
title: () => (view.isDecisionPending() ? view.decisionTitle() : ASK_USER_WAITING_TITLE),
|
|
1331
|
+
tone: () => (view.isDecisionPending() ? view.decisionTone() : undefined),
|
|
1332
|
+
footerHint: () => view.footerHint(),
|
|
1333
|
+
});
|
|
1260
1334
|
|
|
1261
1335
|
const close = (): void => {
|
|
1336
|
+
if (closed) return;
|
|
1262
1337
|
closed = true;
|
|
1263
1338
|
view.close();
|
|
1264
|
-
handle
|
|
1265
|
-
handle = null;
|
|
1339
|
+
handle.hide();
|
|
1266
1340
|
};
|
|
1267
1341
|
return {
|
|
1268
|
-
setHidden: (hidden) => handle
|
|
1269
|
-
isHidden: () => handle
|
|
1270
|
-
focus: () => handle
|
|
1271
|
-
unfocus: (options) => (options ? handle
|
|
1272
|
-
isFocused: () => handle
|
|
1273
|
-
getBounds: () => handle
|
|
1342
|
+
setHidden: (hidden) => handle.setHidden(hidden),
|
|
1343
|
+
isHidden: () => (closed ? true : handle.isHidden()),
|
|
1344
|
+
focus: () => handle.focus(),
|
|
1345
|
+
unfocus: (options) => (options ? handle.unfocus(options) : handle.unfocus()),
|
|
1346
|
+
isFocused: () => (closed ? false : handle.isFocused()),
|
|
1347
|
+
getBounds: () => (closed ? undefined : handle.getBounds()),
|
|
1274
1348
|
ask: (questions, presentation) => view.begin(questions, presentation),
|
|
1275
1349
|
cancel: () => view.cancel(),
|
|
1276
1350
|
close,
|