mycode-cli 0.1.0__py3-none-any.whl
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.
- mycode/__init__.py +1 -0
- mycode/cli/__init__.py +1 -0
- mycode/cli/chat.py +650 -0
- mycode/cli/main.py +245 -0
- mycode/cli/render.py +693 -0
- mycode/cli/runtime.py +271 -0
- mycode/cli/theme.py +109 -0
- mycode/core/__init__.py +30 -0
- mycode/core/agent.py +515 -0
- mycode/core/config.py +551 -0
- mycode/core/messages.py +166 -0
- mycode/core/models.py +144 -0
- mycode/core/models_catalog.json +2090 -0
- mycode/core/providers/__init__.py +86 -0
- mycode/core/providers/anthropic_like.py +366 -0
- mycode/core/providers/base.py +351 -0
- mycode/core/providers/gemini.py +321 -0
- mycode/core/providers/openai_chat.py +356 -0
- mycode/core/providers/openai_responses.py +326 -0
- mycode/core/session.py +537 -0
- mycode/core/system_prompt.md +10 -0
- mycode/core/system_prompt.py +319 -0
- mycode/core/tools.py +898 -0
- mycode/server/__init__.py +1 -0
- mycode/server/app.py +52 -0
- mycode/server/deps.py +29 -0
- mycode/server/routers/__init__.py +7 -0
- mycode/server/routers/chat.py +320 -0
- mycode/server/routers/sessions.py +77 -0
- mycode/server/routers/workspaces.py +92 -0
- mycode/server/run_manager.py +195 -0
- mycode/server/schemas.py +66 -0
- mycode/server/static/assets/EditDiff-C1ql7kft.js +12 -0
- mycode/server/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- mycode/server/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- mycode/server/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- mycode/server/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- mycode/server/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- mycode/server/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- mycode/server/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- mycode/server/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- mycode/server/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- mycode/server/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- mycode/server/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- mycode/server/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- mycode/server/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- mycode/server/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- mycode/server/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- mycode/server/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- mycode/server/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- mycode/server/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- mycode/server/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- mycode/server/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- mycode/server/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- mycode/server/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- mycode/server/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- mycode/server/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- mycode/server/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- mycode/server/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- mycode/server/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- mycode/server/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- mycode/server/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- mycode/server/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- mycode/server/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- mycode/server/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- mycode/server/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- mycode/server/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- mycode/server/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- mycode/server/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- mycode/server/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- mycode/server/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- mycode/server/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- mycode/server/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- mycode/server/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- mycode/server/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- mycode/server/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- mycode/server/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- mycode/server/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- mycode/server/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- mycode/server/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- mycode/server/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- mycode/server/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- mycode/server/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- mycode/server/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- mycode/server/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- mycode/server/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- mycode/server/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- mycode/server/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- mycode/server/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- mycode/server/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- mycode/server/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- mycode/server/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- mycode/server/static/assets/abap-BdImnpbu.js +1 -0
- mycode/server/static/assets/actionscript-3-CoDkCxhg.js +1 -0
- mycode/server/static/assets/ada-bCR0ucgS.js +1 -0
- mycode/server/static/assets/andromeeda-C4gqWexZ.js +1 -0
- mycode/server/static/assets/angular-html-DA-rfuFy.js +1 -0
- mycode/server/static/assets/angular-ts-BrjP3tb8.js +1 -0
- mycode/server/static/assets/apache-Pmp26Uib.js +1 -0
- mycode/server/static/assets/apex-D8_7TLub.js +1 -0
- mycode/server/static/assets/apl-CORt7UWP.js +1 -0
- mycode/server/static/assets/applescript-Co6uUVPk.js +1 -0
- mycode/server/static/assets/ara-BRHolxvo.js +1 -0
- mycode/server/static/assets/asciidoc-Ve4PFQV2.js +1 -0
- mycode/server/static/assets/asm-D_Q5rh1f.js +1 -0
- mycode/server/static/assets/astro-HNnZUWAn.js +1 -0
- mycode/server/static/assets/aurora-x-D-2ljcwZ.js +1 -0
- mycode/server/static/assets/auto-render-xntwXHOX.js +261 -0
- mycode/server/static/assets/awk-DMzUqQB5.js +1 -0
- mycode/server/static/assets/ayu-dark-DYE7WIF3.js +1 -0
- mycode/server/static/assets/ayu-light-BA47KaF1.js +1 -0
- mycode/server/static/assets/ayu-mirage-32ctXXKs.js +1 -0
- mycode/server/static/assets/ballerina-BFfxhgS-.js +1 -0
- mycode/server/static/assets/bat-BkioyH1T.js +1 -0
- mycode/server/static/assets/beancount-k_qm7-4y.js +1 -0
- mycode/server/static/assets/berry-uYugtg8r.js +1 -0
- mycode/server/static/assets/bibtex-CHM0blh-.js +1 -0
- mycode/server/static/assets/bicep-Bmn6On1c.js +1 -0
- mycode/server/static/assets/bird2-BIv1doCn.js +1 -0
- mycode/server/static/assets/blade-BjGOyj-B.js +1 -0
- mycode/server/static/assets/bsl-BO_Y6i37.js +1 -0
- mycode/server/static/assets/c-BIGW1oBm.js +1 -0
- mycode/server/static/assets/c3-eo99z4R2.js +1 -0
- mycode/server/static/assets/cadence-Bv_4Rxtq.js +1 -0
- mycode/server/static/assets/cairo-KRGpt6FW.js +1 -0
- mycode/server/static/assets/catppuccin-frappe-DFWUc33u.js +1 -0
- mycode/server/static/assets/catppuccin-latte-C9dUb6Cb.js +1 -0
- mycode/server/static/assets/catppuccin-macchiato-DQyhUUbL.js +1 -0
- mycode/server/static/assets/catppuccin-mocha-D87Tk5Gz.js +1 -0
- mycode/server/static/assets/clarity-D53aC0YG.js +1 -0
- mycode/server/static/assets/clojure-P80f7IUj.js +1 -0
- mycode/server/static/assets/cmake-D1j8_8rp.js +1 -0
- mycode/server/static/assets/cobol-nBiQ_Alo.js +1 -0
- mycode/server/static/assets/codeowners-Bp6g37R7.js +1 -0
- mycode/server/static/assets/codeql-DsOJ9woJ.js +1 -0
- mycode/server/static/assets/coffee-Ch7k5sss.js +1 -0
- mycode/server/static/assets/common-lisp-Cg-RD9OK.js +1 -0
- mycode/server/static/assets/coq-DkFqJrB1.js +1 -0
- mycode/server/static/assets/cpp-CofmeUqb.js +1 -0
- mycode/server/static/assets/crystal-DNxU26gB.js +1 -0
- mycode/server/static/assets/csharp-COcwbKMJ.js +1 -0
- mycode/server/static/assets/css-CLj8gQPS.js +1 -0
- mycode/server/static/assets/csv-fuZLfV_i.js +1 -0
- mycode/server/static/assets/cue-D82EKSYY.js +1 -0
- mycode/server/static/assets/cypher-COkxafJQ.js +1 -0
- mycode/server/static/assets/d-85-TOEBH.js +1 -0
- mycode/server/static/assets/dark-plus-C3mMm8J8.js +1 -0
- mycode/server/static/assets/dart-bE4Kk8sk.js +1 -0
- mycode/server/static/assets/dax-CEL-wOlO.js +1 -0
- mycode/server/static/assets/desktop-BmXAJ9_W.js +1 -0
- mycode/server/static/assets/diff-D97Zzqfu.js +1 -0
- mycode/server/static/assets/docker-BcOcwvcX.js +1 -0
- mycode/server/static/assets/dotenv-Da5cRb03.js +1 -0
- mycode/server/static/assets/dracula-BzJJZx-M.js +1 -0
- mycode/server/static/assets/dracula-soft-BXkSAIEj.js +1 -0
- mycode/server/static/assets/dream-maker-BtqSS_iP.js +1 -0
- mycode/server/static/assets/edge-FbVlp4U3.js +1 -0
- mycode/server/static/assets/elixir-CkH2-t6x.js +1 -0
- mycode/server/static/assets/elm-DbKCFpqz.js +1 -0
- mycode/server/static/assets/emacs-lisp-CXvaQtF9.js +1 -0
- mycode/server/static/assets/erb-BYCe7drp.js +1 -0
- mycode/server/static/assets/erlang-DsQrWhSR.js +1 -0
- mycode/server/static/assets/everforest-dark-BgDCqdQA.js +1 -0
- mycode/server/static/assets/everforest-light-C8M2exoo.js +1 -0
- mycode/server/static/assets/fennel-BYunw83y.js +1 -0
- mycode/server/static/assets/fish-BvzEVeQv.js +1 -0
- mycode/server/static/assets/fluent-C4IJs8-o.js +1 -0
- mycode/server/static/assets/fortran-fixed-form-CkoXwp7k.js +1 -0
- mycode/server/static/assets/fortran-free-form-BxgE0vQu.js +1 -0
- mycode/server/static/assets/fsharp-CXgrBDvD.js +1 -0
- mycode/server/static/assets/gdresource-BOOCDP_w.js +1 -0
- mycode/server/static/assets/gdscript-C5YyOfLZ.js +1 -0
- mycode/server/static/assets/gdshader-DkwncUOv.js +1 -0
- mycode/server/static/assets/genie-D0YGMca9.js +1 -0
- mycode/server/static/assets/gherkin-DyxjwDmM.js +1 -0
- mycode/server/static/assets/git-commit-F4YmCXRG.js +1 -0
- mycode/server/static/assets/git-rebase-r7XF79zn.js +1 -0
- mycode/server/static/assets/github-dark-DHJKELXO.js +1 -0
- mycode/server/static/assets/github-dark-default-Cuk6v7N8.js +1 -0
- mycode/server/static/assets/github-dark-dimmed-DH5Ifo-i.js +1 -0
- mycode/server/static/assets/github-dark-high-contrast-E3gJ1_iC.js +1 -0
- mycode/server/static/assets/github-light-DAi9KRSo.js +1 -0
- mycode/server/static/assets/github-light-default-D7oLnXFd.js +1 -0
- mycode/server/static/assets/github-light-high-contrast-BfjtVDDH.js +1 -0
- mycode/server/static/assets/gleam-BspZqrRM.js +1 -0
- mycode/server/static/assets/glimmer-js-ByusRIyA.js +1 -0
- mycode/server/static/assets/glimmer-ts-BfAWNZQY.js +1 -0
- mycode/server/static/assets/glsl-DplSGwfg.js +1 -0
- mycode/server/static/assets/gn-n2N0HUVH.js +1 -0
- mycode/server/static/assets/gnuplot-DdkO51Og.js +1 -0
- mycode/server/static/assets/go-C27-OAKa.js +1 -0
- mycode/server/static/assets/graphql-ChdNCCLP.js +1 -0
- mycode/server/static/assets/groovy-gcz8RCvz.js +1 -0
- mycode/server/static/assets/gruvbox-dark-hard-CFHQjOhq.js +1 -0
- mycode/server/static/assets/gruvbox-dark-medium-GsRaNv29.js +1 -0
- mycode/server/static/assets/gruvbox-dark-soft-CVdnzihN.js +1 -0
- mycode/server/static/assets/gruvbox-light-hard-CH1njM8p.js +1 -0
- mycode/server/static/assets/gruvbox-light-medium-DRw_LuNl.js +1 -0
- mycode/server/static/assets/gruvbox-light-soft-hJgmCMqR.js +1 -0
- mycode/server/static/assets/hack-i7_Ulhet.js +1 -0
- mycode/server/static/assets/haml-D5jkg6IW.js +1 -0
- mycode/server/static/assets/handlebars-BpdQsYii.js +1 -0
- mycode/server/static/assets/haskell-Df6bDoY_.js +1 -0
- mycode/server/static/assets/haxe-CzTSHFRz.js +1 -0
- mycode/server/static/assets/hcl-BWvSN4gD.js +1 -0
- mycode/server/static/assets/hjson-D5-asLiD.js +1 -0
- mycode/server/static/assets/hlsl-D3lLCCz7.js +1 -0
- mycode/server/static/assets/horizon-BUw7H-hv.js +1 -0
- mycode/server/static/assets/horizon-bright-CUuTKBJd.js +1 -0
- mycode/server/static/assets/houston-DnULxvSX.js +1 -0
- mycode/server/static/assets/html-derivative-DlHx6ybY.js +1 -0
- mycode/server/static/assets/html-pp8916En.js +1 -0
- mycode/server/static/assets/http-jrhK8wxY.js +1 -0
- mycode/server/static/assets/hurl-irOxFIW8.js +1 -0
- mycode/server/static/assets/hxml-Bvhsp5Yf.js +1 -0
- mycode/server/static/assets/hy-DFXneXwc.js +1 -0
- mycode/server/static/assets/imba-DGztddWO.js +1 -0
- mycode/server/static/assets/index-B4e4WQPq.css +1 -0
- mycode/server/static/assets/index-C2xTNJGd.js +203 -0
- mycode/server/static/assets/ini-BEwlwnbL.js +1 -0
- mycode/server/static/assets/java-CylS5w8V.js +1 -0
- mycode/server/static/assets/javascript-wDzz0qaB.js +1 -0
- mycode/server/static/assets/jinja-f2NsQr07.js +1 -0
- mycode/server/static/assets/jison-wvAkD_A8.js +1 -0
- mycode/server/static/assets/json-Cp-IABpG.js +1 -0
- mycode/server/static/assets/json5-C9tS-k6U.js +1 -0
- mycode/server/static/assets/jsonc-Des-eS-w.js +1 -0
- mycode/server/static/assets/jsonl-DcaNXYhu.js +1 -0
- mycode/server/static/assets/jsonnet-DFQXde-d.js +1 -0
- mycode/server/static/assets/jssm-C2t-YnRu.js +1 -0
- mycode/server/static/assets/jsx-g9-lgVsj.js +1 -0
- mycode/server/static/assets/julia-CxzCAyBv.js +1 -0
- mycode/server/static/assets/just-VxiPbLrw.js +1 -0
- mycode/server/static/assets/kanagawa-dragon-CkXjmgJE.js +1 -0
- mycode/server/static/assets/kanagawa-lotus-CfQXZHmo.js +1 -0
- mycode/server/static/assets/kanagawa-wave-DWedfzmr.js +1 -0
- mycode/server/static/assets/katex-DnJR2-55.css +1 -0
- mycode/server/static/assets/kdl-DV7GczEv.js +1 -0
- mycode/server/static/assets/kotlin-BdnUsdx6.js +1 -0
- mycode/server/static/assets/kusto-wEQ09or8.js +1 -0
- mycode/server/static/assets/laserwave-DUszq2jm.js +1 -0
- mycode/server/static/assets/latex-CWtU0Tv5.js +1 -0
- mycode/server/static/assets/lean-BZvkOJ9d.js +1 -0
- mycode/server/static/assets/less-B1dDrJ26.js +1 -0
- mycode/server/static/assets/light-plus-B7mTdjB0.js +1 -0
- mycode/server/static/assets/liquid-C0sCDyMI.js +1 -0
- mycode/server/static/assets/llvm-DjAJT7YJ.js +1 -0
- mycode/server/static/assets/log-2UxHyX5q.js +1 -0
- mycode/server/static/assets/logo-BtOb2qkB.js +1 -0
- mycode/server/static/assets/lua-BaeVxFsk.js +1 -0
- mycode/server/static/assets/luau-C-HG3fhB.js +1 -0
- mycode/server/static/assets/make-CHLpvVh8.js +1 -0
- mycode/server/static/assets/markdown-Cvjx9yec.js +1 -0
- mycode/server/static/assets/marko-DjSrsDqO.js +1 -0
- mycode/server/static/assets/material-theme-D5KoaKCx.js +1 -0
- mycode/server/static/assets/material-theme-darker-BfHTSMKl.js +1 -0
- mycode/server/static/assets/material-theme-lighter-B0m2ddpp.js +1 -0
- mycode/server/static/assets/material-theme-ocean-CyktbL80.js +1 -0
- mycode/server/static/assets/material-theme-palenight-Csfq5Kiy.js +1 -0
- mycode/server/static/assets/matlab-D7o27uSR.js +1 -0
- mycode/server/static/assets/mdc-DTYItulj.js +1 -0
- mycode/server/static/assets/mdx-Cmh6b_Ma.js +1 -0
- mycode/server/static/assets/mermaid-mWjccvbQ.js +1 -0
- mycode/server/static/assets/min-dark-CafNBF8u.js +1 -0
- mycode/server/static/assets/min-light-CTRr51gU.js +1 -0
- mycode/server/static/assets/mipsasm-CKIfxQSi.js +1 -0
- mycode/server/static/assets/mojo-rZm6bMo-.js +1 -0
- mycode/server/static/assets/monokai-D4h5O-jR.js +1 -0
- mycode/server/static/assets/moonbit-_H4v1dQx.js +1 -0
- mycode/server/static/assets/move-IF9eRakj.js +1 -0
- mycode/server/static/assets/narrat-DRg8JJMk.js +1 -0
- mycode/server/static/assets/nextflow-C-mBbutL.js +1 -0
- mycode/server/static/assets/nextflow-groovy-vE_lwT2v.js +1 -0
- mycode/server/static/assets/nginx-BpAMiNFr.js +1 -0
- mycode/server/static/assets/night-owl-C39BiMTA.js +1 -0
- mycode/server/static/assets/night-owl-light-CMTm3GFP.js +1 -0
- mycode/server/static/assets/nim-BIad80T-.js +1 -0
- mycode/server/static/assets/nix-CwoSXNpI.js +1 -0
- mycode/server/static/assets/nord-Ddv68eIx.js +1 -0
- mycode/server/static/assets/nushell-Cz2AlsmD.js +1 -0
- mycode/server/static/assets/objective-c-DXmwc3jG.js +1 -0
- mycode/server/static/assets/objective-cpp-CLxacb5B.js +1 -0
- mycode/server/static/assets/ocaml-C0hk2d4L.js +1 -0
- mycode/server/static/assets/odin-BBf5iR-q.js +1 -0
- mycode/server/static/assets/one-dark-pro-DVMEJ2y_.js +1 -0
- mycode/server/static/assets/one-light-C3Wv6jpd.js +1 -0
- mycode/server/static/assets/openscad-C4EeE6gA.js +1 -0
- mycode/server/static/assets/pascal-D93ZcfNL.js +1 -0
- mycode/server/static/assets/perl-NvoQZIq0.js +1 -0
- mycode/server/static/assets/php-R6g_5hLQ.js +1 -0
- mycode/server/static/assets/pkl-u5AG7uiY.js +1 -0
- mycode/server/static/assets/plastic-3e1v2bzS.js +1 -0
- mycode/server/static/assets/plsql-ChMvpjG-.js +1 -0
- mycode/server/static/assets/po-BTJTHyun.js +1 -0
- mycode/server/static/assets/poimandres-CS3Unz2-.js +1 -0
- mycode/server/static/assets/polar-C0HS_06l.js +1 -0
- mycode/server/static/assets/postcss-CXtECtnM.js +1 -0
- mycode/server/static/assets/powerquery-CEu0bR-o.js +1 -0
- mycode/server/static/assets/powershell-Dpen1YoG.js +1 -0
- mycode/server/static/assets/prisma-Dd19v3D-.js +1 -0
- mycode/server/static/assets/prolog-CbFg5uaA.js +1 -0
- mycode/server/static/assets/proto-C7zT0LnQ.js +1 -0
- mycode/server/static/assets/pug-DKIMFp6K.js +1 -0
- mycode/server/static/assets/puppet-BMWR74SV.js +1 -0
- mycode/server/static/assets/purescript-CklMAg4u.js +1 -0
- mycode/server/static/assets/python-B6aJPvgy.js +1 -0
- mycode/server/static/assets/qml-3beO22l8.js +1 -0
- mycode/server/static/assets/qmldir-C8lEn-DE.js +1 -0
- mycode/server/static/assets/qss-IeuSbFQv.js +1 -0
- mycode/server/static/assets/r-Dspwwk_N.js +1 -0
- mycode/server/static/assets/racket-BqYA7rlc.js +1 -0
- mycode/server/static/assets/raku-DXvB9xmW.js +1 -0
- mycode/server/static/assets/razor-BDqjjVU7.js +1 -0
- mycode/server/static/assets/red-bN70gL4F.js +1 -0
- mycode/server/static/assets/reg-C-SQnVFl.js +1 -0
- mycode/server/static/assets/regexp-CDVJQ6XC.js +1 -0
- mycode/server/static/assets/rel-C3B-1QV4.js +1 -0
- mycode/server/static/assets/riscv-BM1_JUlF.js +1 -0
- mycode/server/static/assets/ron-D8l8udqQ.js +1 -0
- mycode/server/static/assets/rose-pine-dawn-DHQR4-dF.js +1 -0
- mycode/server/static/assets/rose-pine-moon-D4_iv3hh.js +1 -0
- mycode/server/static/assets/rose-pine-qdsjHGoJ.js +1 -0
- mycode/server/static/assets/rosmsg-BJDFO7_C.js +1 -0
- mycode/server/static/assets/rst-CRjBmOyv.js +1 -0
- mycode/server/static/assets/ruby-Wjq7vjNf.js +1 -0
- mycode/server/static/assets/rust-B1yitclQ.js +1 -0
- mycode/server/static/assets/sas-cz2c8ADy.js +1 -0
- mycode/server/static/assets/sass-Cj5Yp3dK.js +1 -0
- mycode/server/static/assets/scala-C151Ov-r.js +1 -0
- mycode/server/static/assets/scheme-C98Dy4si.js +1 -0
- mycode/server/static/assets/scss-D5BDwBP9.js +1 -0
- mycode/server/static/assets/sdbl-DVxCFoDh.js +1 -0
- mycode/server/static/assets/shaderlab-Dg9Lc6iA.js +1 -0
- mycode/server/static/assets/shellscript-Yzrsuije.js +1 -0
- mycode/server/static/assets/shellsession-BADoaaVG.js +1 -0
- mycode/server/static/assets/slack-dark-BthQWCQV.js +1 -0
- mycode/server/static/assets/slack-ochin-DqwNpetd.js +1 -0
- mycode/server/static/assets/smalltalk-BERRCDM3.js +1 -0
- mycode/server/static/assets/snazzy-light-Bw305WKR.js +1 -0
- mycode/server/static/assets/solarized-dark-DXbdFlpD.js +1 -0
- mycode/server/static/assets/solarized-light-L9t79GZl.js +1 -0
- mycode/server/static/assets/solidity-rGO070M0.js +1 -0
- mycode/server/static/assets/soy-8wufbnw4.js +1 -0
- mycode/server/static/assets/sparql-rVzFXLq3.js +1 -0
- mycode/server/static/assets/splunk-BtCnVYZw.js +1 -0
- mycode/server/static/assets/sql-BLtJtn59.js +1 -0
- mycode/server/static/assets/ssh-config-_ykCGR6B.js +1 -0
- mycode/server/static/assets/stata-BH5u7GGu.js +1 -0
- mycode/server/static/assets/stylus-BEDo0Tqx.js +1 -0
- mycode/server/static/assets/surrealql-Bq5Q-fJD.js +1 -0
- mycode/server/static/assets/svelte-Cy7k_4gC.js +1 -0
- mycode/server/static/assets/swift-D82vCrfD.js +1 -0
- mycode/server/static/assets/synthwave-84-CbfX1IO0.js +1 -0
- mycode/server/static/assets/system-verilog-CnnmHF94.js +1 -0
- mycode/server/static/assets/systemd-4A_iFExJ.js +1 -0
- mycode/server/static/assets/talonscript-CkByrt1z.js +1 -0
- mycode/server/static/assets/tasl-QIJgUcNo.js +1 -0
- mycode/server/static/assets/tcl-dwOrl1Do.js +1 -0
- mycode/server/static/assets/templ-DhtptRzy.js +1 -0
- mycode/server/static/assets/terraform-BETggiCN.js +1 -0
- mycode/server/static/assets/tex-idrVyKtj.js +1 -0
- mycode/server/static/assets/tokyo-night-hegEt444.js +1 -0
- mycode/server/static/assets/toml-vGWfd6FD.js +1 -0
- mycode/server/static/assets/ts-tags-DQrlYJgV.js +1 -0
- mycode/server/static/assets/tsv-B_m7g4N7.js +1 -0
- mycode/server/static/assets/tsx-COt5Ahok.js +1 -0
- mycode/server/static/assets/turtle-BsS91CYL.js +1 -0
- mycode/server/static/assets/twig-xg9kU7Mw.js +1 -0
- mycode/server/static/assets/typescript-BPQ3VLAy.js +1 -0
- mycode/server/static/assets/typespec-CAFt9gP4.js +1 -0
- mycode/server/static/assets/typst-DHCkPAjA.js +1 -0
- mycode/server/static/assets/v-BcVCzyr7.js +1 -0
- mycode/server/static/assets/vala-CsfeWuGM.js +1 -0
- mycode/server/static/assets/vb-D17OF-Vu.js +1 -0
- mycode/server/static/assets/verilog-BQ8w6xss.js +1 -0
- mycode/server/static/assets/vesper-DU1UobuO.js +1 -0
- mycode/server/static/assets/vhdl-CeAyd5Ju.js +1 -0
- mycode/server/static/assets/viml-CJc9bBzg.js +1 -0
- mycode/server/static/assets/vitesse-black-Bkuqu6BP.js +1 -0
- mycode/server/static/assets/vitesse-dark-D0r3Knsf.js +1 -0
- mycode/server/static/assets/vitesse-light-CVO1_9PV.js +1 -0
- mycode/server/static/assets/vue-D2xRrEX4.js +1 -0
- mycode/server/static/assets/vue-html-AaS7Mt5G.js +1 -0
- mycode/server/static/assets/vue-vine-BoDAl6tE.js +1 -0
- mycode/server/static/assets/vyper-CDx5xZoG.js +1 -0
- mycode/server/static/assets/wasm-CG6Dc4jp.js +1 -0
- mycode/server/static/assets/wasm-MzD3tlZU.js +1 -0
- mycode/server/static/assets/wenyan-BV7otONQ.js +1 -0
- mycode/server/static/assets/wgsl-Dx-B1_4e.js +1 -0
- mycode/server/static/assets/wikitext-BhOHFoWU.js +1 -0
- mycode/server/static/assets/wit-5i3qLPDT.js +1 -0
- mycode/server/static/assets/wolfram-lXgVvXCa.js +1 -0
- mycode/server/static/assets/xml-sdJ4AIDG.js +1 -0
- mycode/server/static/assets/xsl-CtQFsRM5.js +1 -0
- mycode/server/static/assets/yaml-Buea-lGh.js +1 -0
- mycode/server/static/assets/zenscript-DVFEvuxE.js +1 -0
- mycode/server/static/assets/zig-VOosw3JB.js +1 -0
- mycode/server/static/favicon_slashes.svg +12 -0
- mycode/server/static/index.html +35 -0
- mycode_cli-0.1.0.dist-info/METADATA +186 -0
- mycode_cli-0.1.0.dist-info/RECORD +404 -0
- mycode_cli-0.1.0.dist-info/WHEEL +4 -0
- mycode_cli-0.1.0.dist-info/entry_points.txt +2 -0
- mycode_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
mycode/cli/render.py
ADDED
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
"""Rendering helpers for the terminal CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from difflib import SequenceMatcher
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from rich.console import Console, ConsoleOptions, RenderResult
|
|
12
|
+
from rich.live import Live
|
|
13
|
+
from rich.markdown import CodeBlock as _RichCodeBlock
|
|
14
|
+
from rich.markdown import Heading as _RichHeading
|
|
15
|
+
from rich.markdown import Markdown
|
|
16
|
+
from rich.spinner import Spinner
|
|
17
|
+
from rich.syntax import Syntax
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
from rich.theme import Theme
|
|
21
|
+
|
|
22
|
+
from mycode.core.agent import Agent
|
|
23
|
+
from mycode.core.messages import flatten_message_text
|
|
24
|
+
|
|
25
|
+
from .theme import (
|
|
26
|
+
ACCENT,
|
|
27
|
+
CODE_THEME,
|
|
28
|
+
ERROR,
|
|
29
|
+
ERROR_MARKER,
|
|
30
|
+
MUTED,
|
|
31
|
+
PROMPT_CHAR,
|
|
32
|
+
PROVIDER,
|
|
33
|
+
STATS,
|
|
34
|
+
SUCCESS,
|
|
35
|
+
TERMINAL_THEME,
|
|
36
|
+
THINKING,
|
|
37
|
+
THINKING_SYMBOL,
|
|
38
|
+
TOOL_MARKER,
|
|
39
|
+
TOOL_NAME,
|
|
40
|
+
WARNING,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Override Rich's default inline-code style ("bold cyan on black") to remove
|
|
44
|
+
# the hardcoded background color that clashes with terminal themes.
|
|
45
|
+
_THEME = Theme(
|
|
46
|
+
{
|
|
47
|
+
"markdown.code": "bold blue" if TERMINAL_THEME == "light" else "bold cyan",
|
|
48
|
+
"markdown.code_block": "blue" if TERMINAL_THEME == "light" else "cyan",
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
console = Console(highlight=False, theme=_THEME)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class _LeftHeading(_RichHeading):
|
|
56
|
+
"""Heading variant that left-aligns all heading levels."""
|
|
57
|
+
|
|
58
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
59
|
+
text = self.text
|
|
60
|
+
text.justify = "left"
|
|
61
|
+
if self.tag == "h1":
|
|
62
|
+
yield Text("")
|
|
63
|
+
yield text
|
|
64
|
+
yield Text("")
|
|
65
|
+
elif self.tag == "h2":
|
|
66
|
+
yield Text("")
|
|
67
|
+
yield text
|
|
68
|
+
else:
|
|
69
|
+
yield text
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class _CleanCodeBlock(_RichCodeBlock):
|
|
73
|
+
"""Code block that uses the terminal background instead of the theme background."""
|
|
74
|
+
|
|
75
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
76
|
+
code = str(self.text).rstrip()
|
|
77
|
+
yield Syntax(
|
|
78
|
+
code,
|
|
79
|
+
self.lexer_name,
|
|
80
|
+
theme=self.theme,
|
|
81
|
+
word_wrap=True,
|
|
82
|
+
padding=0,
|
|
83
|
+
background_color="default",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class _LeftMarkdown(Markdown):
|
|
88
|
+
"""Markdown subclass with left-aligned headings and clean code blocks."""
|
|
89
|
+
|
|
90
|
+
elements = {
|
|
91
|
+
**Markdown.elements,
|
|
92
|
+
"heading_open": _LeftHeading,
|
|
93
|
+
"fence": _CleanCodeBlock,
|
|
94
|
+
"code_block": _CleanCodeBlock,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
_TOOL_OUTPUT_MAX_LINES = 5
|
|
99
|
+
|
|
100
|
+
# Maps built-in tool names to the argument key most useful as a one-line preview.
|
|
101
|
+
_TOOL_PREVIEW_KEY: dict[str, str] = {
|
|
102
|
+
"read": "path",
|
|
103
|
+
"write": "path",
|
|
104
|
+
"edit": "path",
|
|
105
|
+
"bash": "command",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def format_local_timestamp(value: str, display_format: str) -> str:
|
|
110
|
+
"""Format an ISO timestamp with a simple local fallback."""
|
|
111
|
+
|
|
112
|
+
if not value:
|
|
113
|
+
return ""
|
|
114
|
+
try:
|
|
115
|
+
timestamp = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
|
116
|
+
return timestamp.astimezone().strftime(display_format)
|
|
117
|
+
except ValueError:
|
|
118
|
+
return value[:16].replace("T", " ")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _format_usage(usage: dict[str, Any]) -> str:
|
|
122
|
+
"""Format token usage into a compact string."""
|
|
123
|
+
input_t = usage.get("input_tokens") or usage.get("prompt_tokens") or 0
|
|
124
|
+
output_t = usage.get("output_tokens") or usage.get("completion_tokens") or 0
|
|
125
|
+
total = input_t + output_t
|
|
126
|
+
if total:
|
|
127
|
+
return f"{total:,} tokens"
|
|
128
|
+
return ""
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TerminalView:
|
|
132
|
+
"""Print static CLI output such as headers, previews, and session lists."""
|
|
133
|
+
|
|
134
|
+
def __init__(self, output: Console | None = None) -> None:
|
|
135
|
+
self.console = output or console
|
|
136
|
+
|
|
137
|
+
def print_header(
|
|
138
|
+
self,
|
|
139
|
+
*,
|
|
140
|
+
provider: str,
|
|
141
|
+
model: str,
|
|
142
|
+
session: dict[str, Any],
|
|
143
|
+
mode: str,
|
|
144
|
+
message_count: int,
|
|
145
|
+
reasoning_effort: str | None = None,
|
|
146
|
+
) -> None:
|
|
147
|
+
"""Print the current session header shown above the interactive chat."""
|
|
148
|
+
|
|
149
|
+
self.console.print()
|
|
150
|
+
|
|
151
|
+
title = session.get("title") or ""
|
|
152
|
+
session_id = str(session.get("id") or "")[:8]
|
|
153
|
+
|
|
154
|
+
line = Text()
|
|
155
|
+
line.append("mycode", style=ACCENT)
|
|
156
|
+
line.append(" · ", style=MUTED)
|
|
157
|
+
line.append(provider, style=PROVIDER)
|
|
158
|
+
line.append(" / ", style=MUTED)
|
|
159
|
+
line.append(model)
|
|
160
|
+
if reasoning_effort:
|
|
161
|
+
line.append(" · ", style=MUTED)
|
|
162
|
+
line.append(reasoning_effort, style=MUTED)
|
|
163
|
+
if session_id:
|
|
164
|
+
line.append(" · ", style=MUTED)
|
|
165
|
+
line.append(session_id, style=MUTED)
|
|
166
|
+
self.console.print(line)
|
|
167
|
+
|
|
168
|
+
if mode == "resumed":
|
|
169
|
+
meta = Text()
|
|
170
|
+
meta.append("resumed", style=WARNING)
|
|
171
|
+
if title and title != "New chat":
|
|
172
|
+
meta.append(" · ", style=MUTED)
|
|
173
|
+
meta.append(title, style=MUTED)
|
|
174
|
+
if message_count:
|
|
175
|
+
meta.append(" · ", style=MUTED)
|
|
176
|
+
meta.append(f"{message_count} msgs", style=MUTED)
|
|
177
|
+
self.console.print(meta)
|
|
178
|
+
|
|
179
|
+
def print_history_preview(self, messages: list[dict[str, Any]]) -> None:
|
|
180
|
+
"""Print recent conversation turns for resumed sessions."""
|
|
181
|
+
|
|
182
|
+
turns = self.history_preview_entries(messages)
|
|
183
|
+
if not turns:
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
self.console.print(Text("recent", style=MUTED))
|
|
187
|
+
for turn in turns:
|
|
188
|
+
self.console.print()
|
|
189
|
+
for kind, content in turn:
|
|
190
|
+
if kind == "user":
|
|
191
|
+
lines = str(content).splitlines() or [""]
|
|
192
|
+
first = Text()
|
|
193
|
+
first.append(f"{PROMPT_CHAR} ", style=ACCENT)
|
|
194
|
+
first.append(lines[0])
|
|
195
|
+
self.console.print(first)
|
|
196
|
+
for line in lines[1:]:
|
|
197
|
+
self.console.print(Text(f" {line}"))
|
|
198
|
+
elif kind == "text":
|
|
199
|
+
self.console.print(_LeftMarkdown(str(content), code_theme=CODE_THEME))
|
|
200
|
+
else:
|
|
201
|
+
name, args = content
|
|
202
|
+
preview = ""
|
|
203
|
+
if isinstance(args, dict) and args:
|
|
204
|
+
key = _TOOL_PREVIEW_KEY.get(name.lower())
|
|
205
|
+
raw = args.get(key) if key else next(iter(args.values()), "")
|
|
206
|
+
preview = str(raw or "")
|
|
207
|
+
if len(preview) > 60:
|
|
208
|
+
preview = preview[:60] + "…"
|
|
209
|
+
|
|
210
|
+
line = Text()
|
|
211
|
+
line.append(f"{TOOL_MARKER} ", style=SUCCESS)
|
|
212
|
+
line.append(name.capitalize(), style=TOOL_NAME)
|
|
213
|
+
if preview:
|
|
214
|
+
line.append(f" {preview}", style=MUTED)
|
|
215
|
+
self.console.print(line)
|
|
216
|
+
|
|
217
|
+
def print_session_list(
|
|
218
|
+
self,
|
|
219
|
+
sessions: list[dict[str, Any]],
|
|
220
|
+
*,
|
|
221
|
+
include_cwd: bool = False,
|
|
222
|
+
current_session_id: str | None = None,
|
|
223
|
+
heading: str = "sessions",
|
|
224
|
+
) -> None:
|
|
225
|
+
"""Print saved sessions in a compact table for selection commands."""
|
|
226
|
+
|
|
227
|
+
if not sessions:
|
|
228
|
+
self.console.print(Text("no sessions found", style=MUTED))
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
self.console.print(Text(f"{heading} ({len(sessions)})", style=MUTED))
|
|
232
|
+
self.console.print()
|
|
233
|
+
|
|
234
|
+
title_limit = 24 if include_cwd else 40
|
|
235
|
+
model_limit = 18 if include_cwd else 24
|
|
236
|
+
cwd_limit = 32 if include_cwd else 48
|
|
237
|
+
|
|
238
|
+
table = Table(box=None, show_header=False, padding=(0, 2, 0, 0), expand=False)
|
|
239
|
+
table.add_column(no_wrap=True) # marker
|
|
240
|
+
table.add_column(no_wrap=True) # index
|
|
241
|
+
table.add_column(no_wrap=True) # session id
|
|
242
|
+
table.add_column(no_wrap=True) # timestamp
|
|
243
|
+
table.add_column() # title
|
|
244
|
+
table.add_column(no_wrap=True) # model
|
|
245
|
+
if include_cwd:
|
|
246
|
+
table.add_column() # cwd
|
|
247
|
+
|
|
248
|
+
for index, session in enumerate(sessions, start=1):
|
|
249
|
+
session_id = str(session.get("id") or "-")
|
|
250
|
+
is_current = bool(current_session_id and session_id == current_session_id)
|
|
251
|
+
|
|
252
|
+
marker = Text("●", style=SUCCESS) if is_current else Text(" ")
|
|
253
|
+
idx = Text(str(index), style=MUTED)
|
|
254
|
+
sid = Text(session_id[:12], style=MUTED)
|
|
255
|
+
ts = Text(self._format_timestamp(str(session.get("updated_at") or "")), style=MUTED)
|
|
256
|
+
title = Text(self._shorten(str(session.get("title") or "New chat"), limit=title_limit))
|
|
257
|
+
|
|
258
|
+
model = str(session.get("model") or "")
|
|
259
|
+
model_text = Text(
|
|
260
|
+
f"[{self._shorten(model, limit=model_limit)}]" if model else "",
|
|
261
|
+
style=MUTED,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
row: list[Any] = [marker, idx, sid, ts, title, model_text]
|
|
265
|
+
if include_cwd:
|
|
266
|
+
cwd = str(session.get("cwd") or "")
|
|
267
|
+
row.append(Text(self._shorten(cwd, limit=cwd_limit), style=MUTED))
|
|
268
|
+
|
|
269
|
+
table.add_row(*row)
|
|
270
|
+
|
|
271
|
+
self.console.print(table)
|
|
272
|
+
|
|
273
|
+
def history_preview_entries(
|
|
274
|
+
self,
|
|
275
|
+
messages: list[dict[str, Any]],
|
|
276
|
+
*,
|
|
277
|
+
limit: int = 3,
|
|
278
|
+
) -> list[list[tuple[str, Any]]]:
|
|
279
|
+
"""Return the last few readable conversation turns for resumed sessions."""
|
|
280
|
+
|
|
281
|
+
turns: list[list[tuple[str, Any]]] = []
|
|
282
|
+
|
|
283
|
+
for message in messages:
|
|
284
|
+
role = message.get("role")
|
|
285
|
+
content = message.get("content")
|
|
286
|
+
|
|
287
|
+
if role == "user":
|
|
288
|
+
if (message.get("meta") or {}).get("synthetic"):
|
|
289
|
+
continue
|
|
290
|
+
# Use the shared flattener so attached file payload blocks stay out
|
|
291
|
+
# of the readable history preview.
|
|
292
|
+
text = flatten_message_text(message, include_thinking=False)
|
|
293
|
+
if not isinstance(content, list):
|
|
294
|
+
text = text or str(content or "").strip()
|
|
295
|
+
if text:
|
|
296
|
+
turns.append([("user", text)])
|
|
297
|
+
continue
|
|
298
|
+
|
|
299
|
+
if role != "assistant":
|
|
300
|
+
continue
|
|
301
|
+
|
|
302
|
+
parts: list[tuple[str, Any]] = []
|
|
303
|
+
if isinstance(content, list):
|
|
304
|
+
for block in content:
|
|
305
|
+
if not isinstance(block, dict):
|
|
306
|
+
continue
|
|
307
|
+
if block.get("type") == "text":
|
|
308
|
+
text = str(block.get("text") or "").strip()
|
|
309
|
+
if text:
|
|
310
|
+
parts.append(("text", text))
|
|
311
|
+
elif block.get("type") == "tool_use":
|
|
312
|
+
parts.append(("tool", (str(block.get("name") or "tool"), block.get("input"))))
|
|
313
|
+
else:
|
|
314
|
+
text = str(content or "").strip()
|
|
315
|
+
if text:
|
|
316
|
+
parts.append(("text", text))
|
|
317
|
+
|
|
318
|
+
if not parts:
|
|
319
|
+
continue
|
|
320
|
+
if not turns:
|
|
321
|
+
turns.append([])
|
|
322
|
+
turns[-1].extend(parts)
|
|
323
|
+
|
|
324
|
+
return turns if limit <= 0 else turns[-limit:]
|
|
325
|
+
|
|
326
|
+
@staticmethod
|
|
327
|
+
def _shorten(value: str, *, limit: int = 96) -> str:
|
|
328
|
+
text = " ".join((value or "").split())
|
|
329
|
+
if len(text) <= limit:
|
|
330
|
+
return text
|
|
331
|
+
return text[: limit - 1] + "…"
|
|
332
|
+
|
|
333
|
+
@staticmethod
|
|
334
|
+
def _format_timestamp(value: str) -> str:
|
|
335
|
+
return format_local_timestamp(value, "%Y-%m-%d %H:%M") or "-"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class ReplyRenderer:
|
|
339
|
+
"""Render one assistant reply, including thinking and tool output."""
|
|
340
|
+
|
|
341
|
+
def __init__(self, output: Console | None = None, *, live_mode: bool = True) -> None:
|
|
342
|
+
self._console = output or console
|
|
343
|
+
self._live_mode = live_mode
|
|
344
|
+
self._live: Live | None = None
|
|
345
|
+
self._reasoning: list[str] = []
|
|
346
|
+
self._text: list[str] = []
|
|
347
|
+
self._text_started = False
|
|
348
|
+
self._printed_static_reasoning = False
|
|
349
|
+
# Timing & stats
|
|
350
|
+
self._response_start_time: float | None = None
|
|
351
|
+
self._thinking_start_time: float | None = None
|
|
352
|
+
self._thinking_collapsed = False
|
|
353
|
+
self._had_prior_output = False
|
|
354
|
+
self._tool_output_count = 0
|
|
355
|
+
self._tool_start_time: float | None = None
|
|
356
|
+
self._tool_name: str = ""
|
|
357
|
+
self._tool_args: dict[str, Any] = {}
|
|
358
|
+
self._tool_buffered = False
|
|
359
|
+
self._usage: dict[str, Any] | None = None
|
|
360
|
+
|
|
361
|
+
async def render(
|
|
362
|
+
self,
|
|
363
|
+
agent: Agent,
|
|
364
|
+
message: str | dict[str, Any],
|
|
365
|
+
*,
|
|
366
|
+
on_persist: Callable[[dict[str, Any]], Awaitable[None]] | None = None,
|
|
367
|
+
) -> int:
|
|
368
|
+
"""Stream one assistant turn to the terminal and return its exit code."""
|
|
369
|
+
|
|
370
|
+
exit_code = 0
|
|
371
|
+
self._response_start_time = time.monotonic()
|
|
372
|
+
|
|
373
|
+
async def _tracking_persist(msg: dict[str, Any]) -> None:
|
|
374
|
+
if msg.get("role") == "assistant":
|
|
375
|
+
meta = msg.get("meta") or {}
|
|
376
|
+
usage = meta.get("usage")
|
|
377
|
+
if usage:
|
|
378
|
+
self._usage = usage
|
|
379
|
+
if on_persist:
|
|
380
|
+
await on_persist(msg)
|
|
381
|
+
|
|
382
|
+
if self._live_mode:
|
|
383
|
+
self._ensure_live()
|
|
384
|
+
|
|
385
|
+
async for event in agent.achat(message, on_persist=_tracking_persist):
|
|
386
|
+
match event.type:
|
|
387
|
+
case "reasoning":
|
|
388
|
+
self.reasoning(event.data.get("delta", ""))
|
|
389
|
+
case "text":
|
|
390
|
+
self.text(event.data.get("delta", ""))
|
|
391
|
+
case "tool_start":
|
|
392
|
+
tool_call = event.data.get("tool_call") or {}
|
|
393
|
+
self.tool_start(tool_call.get("name", ""), tool_call.get("input") or {})
|
|
394
|
+
case "tool_output":
|
|
395
|
+
self.tool_output(event.data.get("output", ""))
|
|
396
|
+
case "tool_done":
|
|
397
|
+
model_text = str(event.data.get("model_text") or "")
|
|
398
|
+
display_text = str(event.data.get("display_text") or "")
|
|
399
|
+
is_error = bool(event.data.get("is_error"))
|
|
400
|
+
self.tool_done(model_text, display_text, is_error=is_error)
|
|
401
|
+
if is_error:
|
|
402
|
+
exit_code = 1
|
|
403
|
+
case "compact":
|
|
404
|
+
self.compact(event.data.get("message", ""))
|
|
405
|
+
case "error":
|
|
406
|
+
exit_code = 1
|
|
407
|
+
self.error(event.data.get("message", ""))
|
|
408
|
+
|
|
409
|
+
self.finish()
|
|
410
|
+
return exit_code
|
|
411
|
+
|
|
412
|
+
def reasoning(self, chunk: str) -> None:
|
|
413
|
+
"""Handle one streamed reasoning chunk from the agent."""
|
|
414
|
+
|
|
415
|
+
if self._thinking_start_time is None:
|
|
416
|
+
self._thinking_start_time = time.monotonic()
|
|
417
|
+
self._reasoning.append(chunk)
|
|
418
|
+
if self._live_mode:
|
|
419
|
+
self._ensure_live()
|
|
420
|
+
self._update()
|
|
421
|
+
|
|
422
|
+
def text(self, chunk: str) -> None:
|
|
423
|
+
"""Handle one streamed assistant text chunk."""
|
|
424
|
+
|
|
425
|
+
self._finalize_reasoning_phase()
|
|
426
|
+
if not self._text_started and self._had_prior_output:
|
|
427
|
+
self._console.print()
|
|
428
|
+
self._text_started = True
|
|
429
|
+
if self._live_mode:
|
|
430
|
+
self._text.append(chunk)
|
|
431
|
+
self._ensure_live()
|
|
432
|
+
self._update()
|
|
433
|
+
elif chunk:
|
|
434
|
+
self._console.print(chunk, end="", markup=False, highlight=False)
|
|
435
|
+
|
|
436
|
+
def tool_start(self, name: str, args: dict[str, Any]) -> None:
|
|
437
|
+
"""Render the start of a tool call."""
|
|
438
|
+
|
|
439
|
+
self._finalize_reasoning_phase()
|
|
440
|
+
self._reset_stream_state()
|
|
441
|
+
if not self._live_mode:
|
|
442
|
+
self._console.print()
|
|
443
|
+
|
|
444
|
+
self._tool_start_time = time.monotonic()
|
|
445
|
+
self._tool_output_count = 0
|
|
446
|
+
self._tool_name = name
|
|
447
|
+
self._tool_args = args
|
|
448
|
+
|
|
449
|
+
# Bash streams output — print header immediately.
|
|
450
|
+
# Other tools are fast — defer to tool_done for single-line display.
|
|
451
|
+
if name.lower() == "bash":
|
|
452
|
+
self._print_tool_header(name, args)
|
|
453
|
+
self._tool_buffered = False
|
|
454
|
+
else:
|
|
455
|
+
self._tool_buffered = True
|
|
456
|
+
|
|
457
|
+
def tool_output(self, line: str) -> None:
|
|
458
|
+
"""Render one streamed output line from a running tool."""
|
|
459
|
+
|
|
460
|
+
if not line:
|
|
461
|
+
return
|
|
462
|
+
self._tool_output_count += 1
|
|
463
|
+
if self._tool_output_count <= _TOOL_OUTPUT_MAX_LINES:
|
|
464
|
+
text = Text(" ", style=MUTED)
|
|
465
|
+
text.append(line, style=MUTED)
|
|
466
|
+
self._console.print(text)
|
|
467
|
+
|
|
468
|
+
def tool_done(self, model_text: str, display_text: str, *, is_error: bool) -> None:
|
|
469
|
+
"""Render the final tool result."""
|
|
470
|
+
shown_text = display_text or model_text
|
|
471
|
+
|
|
472
|
+
elapsed = 0.0
|
|
473
|
+
if self._tool_start_time is not None:
|
|
474
|
+
elapsed = time.monotonic() - self._tool_start_time
|
|
475
|
+
self._tool_start_time = None
|
|
476
|
+
duration = f"{elapsed:.1f}s" if elapsed >= 0.5 else ""
|
|
477
|
+
|
|
478
|
+
if self._tool_buffered:
|
|
479
|
+
# Non-streaming tools (read, write, edit): single-line output
|
|
480
|
+
self._tool_buffered = False
|
|
481
|
+
if is_error:
|
|
482
|
+
self._print_tool_header(self._tool_name, self._tool_args)
|
|
483
|
+
first_line = shown_text.split("\n", 1)[0][:100]
|
|
484
|
+
self._console.print(Text(f" {first_line}", style=ERROR))
|
|
485
|
+
else:
|
|
486
|
+
suffix = self._format_edit_suffix(self._tool_name, self._tool_args)
|
|
487
|
+
self._print_tool_header(self._tool_name, self._tool_args, suffix=suffix)
|
|
488
|
+
else:
|
|
489
|
+
# Bash: streaming tool
|
|
490
|
+
if is_error and self._tool_output_count == 0:
|
|
491
|
+
first_line = shown_text.split("\n", 1)[0][:100]
|
|
492
|
+
self._console.print(Text(f" {first_line}", style=ERROR))
|
|
493
|
+
else:
|
|
494
|
+
parts: list[str] = []
|
|
495
|
+
truncated = self._tool_output_count - _TOOL_OUTPUT_MAX_LINES
|
|
496
|
+
if truncated > 0:
|
|
497
|
+
parts.append(f"+{truncated} lines")
|
|
498
|
+
if duration:
|
|
499
|
+
parts.append(duration)
|
|
500
|
+
if parts:
|
|
501
|
+
self._console.print(Text(f" {' · '.join(parts)}", style=MUTED))
|
|
502
|
+
|
|
503
|
+
self._had_prior_output = True
|
|
504
|
+
|
|
505
|
+
def _print_tool_header(
|
|
506
|
+
self,
|
|
507
|
+
name: str,
|
|
508
|
+
args: dict[str, Any],
|
|
509
|
+
*,
|
|
510
|
+
suffix: Text | None = None,
|
|
511
|
+
) -> None:
|
|
512
|
+
"""Print the ``⏺ Name preview [suffix]`` tool header line."""
|
|
513
|
+
|
|
514
|
+
preview = ""
|
|
515
|
+
if args:
|
|
516
|
+
key = _TOOL_PREVIEW_KEY.get(name.lower())
|
|
517
|
+
raw = args.get(key) if key else next(iter(args.values()), "")
|
|
518
|
+
preview = str(raw or "")
|
|
519
|
+
if len(preview) > 60:
|
|
520
|
+
preview = preview[:60] + "…"
|
|
521
|
+
|
|
522
|
+
text = Text()
|
|
523
|
+
text.append(f"{TOOL_MARKER} ", style=SUCCESS)
|
|
524
|
+
text.append(name.capitalize(), style=TOOL_NAME)
|
|
525
|
+
if preview:
|
|
526
|
+
text.append(f" {preview}", style=MUTED)
|
|
527
|
+
if suffix:
|
|
528
|
+
text.append(" ")
|
|
529
|
+
text.append_text(suffix)
|
|
530
|
+
self._console.print(text)
|
|
531
|
+
|
|
532
|
+
@staticmethod
|
|
533
|
+
def _format_edit_suffix(name: str, args: dict[str, Any]) -> Text | None:
|
|
534
|
+
"""Return the real added/removed line counts for one edit call."""
|
|
535
|
+
|
|
536
|
+
if name.lower() != "edit":
|
|
537
|
+
return None
|
|
538
|
+
|
|
539
|
+
old_text = args.get("oldText")
|
|
540
|
+
new_text = args.get("newText")
|
|
541
|
+
if not isinstance(old_text, str) or not isinstance(new_text, str):
|
|
542
|
+
return None
|
|
543
|
+
|
|
544
|
+
added = 0
|
|
545
|
+
removed = 0
|
|
546
|
+
for tag, old_start, old_end, new_start, new_end in SequenceMatcher(
|
|
547
|
+
None,
|
|
548
|
+
old_text.splitlines(),
|
|
549
|
+
new_text.splitlines(),
|
|
550
|
+
).get_opcodes():
|
|
551
|
+
if tag in {"replace", "delete"}:
|
|
552
|
+
removed += old_end - old_start
|
|
553
|
+
if tag in {"replace", "insert"}:
|
|
554
|
+
added += new_end - new_start
|
|
555
|
+
|
|
556
|
+
suffix = Text()
|
|
557
|
+
suffix.append(f"+{added}", style="green")
|
|
558
|
+
suffix.append(f" −{removed}", style="red")
|
|
559
|
+
return suffix
|
|
560
|
+
|
|
561
|
+
def compact(self, message: str) -> None:
|
|
562
|
+
"""Render a context compaction notification."""
|
|
563
|
+
|
|
564
|
+
self._finalize_reasoning_phase()
|
|
565
|
+
self._reset_stream_state()
|
|
566
|
+
text = Text(f"⟳ {message}", style=MUTED)
|
|
567
|
+
self._console.print(text)
|
|
568
|
+
|
|
569
|
+
def error(self, message: str) -> None:
|
|
570
|
+
"""Render a terminal-visible error message for the current turn."""
|
|
571
|
+
|
|
572
|
+
self._finalize_reasoning_phase()
|
|
573
|
+
self._reset_stream_state()
|
|
574
|
+
text = Text(f"{ERROR_MARKER} ", style=ERROR)
|
|
575
|
+
text.append(message, style=ERROR)
|
|
576
|
+
self._console.print(text)
|
|
577
|
+
|
|
578
|
+
def cancel(self) -> None:
|
|
579
|
+
"""Render a cancellation marker and reset transient state."""
|
|
580
|
+
|
|
581
|
+
self._finalize_reasoning_phase()
|
|
582
|
+
self._reset_stream_state()
|
|
583
|
+
self._console.print(Text("cancelled", style=MUTED))
|
|
584
|
+
|
|
585
|
+
def finish(self) -> None:
|
|
586
|
+
"""Flush the current turn and print timing or token statistics."""
|
|
587
|
+
|
|
588
|
+
self._finalize_reasoning_phase()
|
|
589
|
+
self._reset_stream_state()
|
|
590
|
+
|
|
591
|
+
parts: list[str] = []
|
|
592
|
+
if self._response_start_time is not None:
|
|
593
|
+
elapsed = time.monotonic() - self._response_start_time
|
|
594
|
+
parts.append(f"{elapsed:.1f}s")
|
|
595
|
+
if self._usage:
|
|
596
|
+
token_str = _format_usage(self._usage)
|
|
597
|
+
if token_str:
|
|
598
|
+
parts.append(token_str)
|
|
599
|
+
|
|
600
|
+
if parts:
|
|
601
|
+
self._console.print(Text(" " + " · ".join(parts), style=STATS))
|
|
602
|
+
|
|
603
|
+
if not self._live_mode:
|
|
604
|
+
self._console.print()
|
|
605
|
+
|
|
606
|
+
# -- Internal helpers ----------------------------------------------------
|
|
607
|
+
|
|
608
|
+
def _finalize_reasoning_phase(self) -> None:
|
|
609
|
+
"""Finish the reasoning phase before text, tools, or final output."""
|
|
610
|
+
|
|
611
|
+
if self._live_mode:
|
|
612
|
+
self._collapse_thinking()
|
|
613
|
+
else:
|
|
614
|
+
self._print_static_reasoning()
|
|
615
|
+
|
|
616
|
+
def _collapse_thinking(self) -> None:
|
|
617
|
+
"""In live mode: stop the spinner and print a one-line summary."""
|
|
618
|
+
if self._thinking_collapsed or not self._reasoning:
|
|
619
|
+
return
|
|
620
|
+
self._thinking_collapsed = True
|
|
621
|
+
|
|
622
|
+
if self._live is not None:
|
|
623
|
+
self._live.transient = True
|
|
624
|
+
self._live.stop()
|
|
625
|
+
self._live = None
|
|
626
|
+
|
|
627
|
+
duration = ""
|
|
628
|
+
if self._thinking_start_time is not None:
|
|
629
|
+
elapsed = time.monotonic() - self._thinking_start_time
|
|
630
|
+
duration = f" · {elapsed:.1f}s"
|
|
631
|
+
|
|
632
|
+
self._console.print(Text(f"{THINKING_SYMBOL} thought{duration}", style=THINKING))
|
|
633
|
+
self._had_prior_output = True
|
|
634
|
+
self._reasoning.clear()
|
|
635
|
+
|
|
636
|
+
def _print_static_reasoning(self) -> None:
|
|
637
|
+
"""Non-live mode: print full reasoning content."""
|
|
638
|
+
if self._live_mode or self._printed_static_reasoning or not self._reasoning:
|
|
639
|
+
return
|
|
640
|
+
|
|
641
|
+
duration = ""
|
|
642
|
+
if self._thinking_start_time is not None:
|
|
643
|
+
elapsed = time.monotonic() - self._thinking_start_time
|
|
644
|
+
duration = f" · {elapsed:.1f}s"
|
|
645
|
+
|
|
646
|
+
self._console.print(Text(f"{THINKING_SYMBOL} thinking{duration}", style=THINKING))
|
|
647
|
+
self._console.print("".join(self._reasoning), style="dim")
|
|
648
|
+
self._printed_static_reasoning = True
|
|
649
|
+
self._had_prior_output = True
|
|
650
|
+
|
|
651
|
+
def _ensure_live(self) -> None:
|
|
652
|
+
if self._live is None:
|
|
653
|
+
self._live = Live(self._build_live_renderable(), console=self._console, refresh_per_second=12)
|
|
654
|
+
self._live.start()
|
|
655
|
+
|
|
656
|
+
def _update(self) -> None:
|
|
657
|
+
if self._live is not None:
|
|
658
|
+
self._live.update(self._build_live_renderable())
|
|
659
|
+
|
|
660
|
+
def _reset_stream_state(self) -> None:
|
|
661
|
+
"""Stop live rendering and clear transient buffers for the current phase."""
|
|
662
|
+
|
|
663
|
+
if self._live is not None:
|
|
664
|
+
self._live.stop()
|
|
665
|
+
self._live = None
|
|
666
|
+
self._reasoning.clear()
|
|
667
|
+
self._text.clear()
|
|
668
|
+
self._printed_static_reasoning = False
|
|
669
|
+
self._thinking_collapsed = False
|
|
670
|
+
self._thinking_start_time = None
|
|
671
|
+
|
|
672
|
+
def _build_live_renderable(self):
|
|
673
|
+
"""Build the Rich renderable used while a reply is streaming."""
|
|
674
|
+
|
|
675
|
+
# No content yet: plain spinner
|
|
676
|
+
if not self._reasoning and not self._text:
|
|
677
|
+
return Spinner("dots", style="dim")
|
|
678
|
+
|
|
679
|
+
# Thinking in progress: show rolling preview of reasoning content
|
|
680
|
+
if self._reasoning and not self._text:
|
|
681
|
+
content = " ".join("".join(self._reasoning).split())
|
|
682
|
+
if content:
|
|
683
|
+
preview = content[-80:].strip()
|
|
684
|
+
if len(content) > 80:
|
|
685
|
+
preview = "…" + preview
|
|
686
|
+
return Spinner("dots", text=Text(f" {preview}", style=THINKING), style="dim")
|
|
687
|
+
return Spinner("dots", text=Text(" thinking…", style=THINKING), style="dim")
|
|
688
|
+
|
|
689
|
+
# Text streaming: render as markdown (thinking already collapsed)
|
|
690
|
+
if self._text:
|
|
691
|
+
return _LeftMarkdown("".join(self._text), code_theme=CODE_THEME)
|
|
692
|
+
|
|
693
|
+
return Spinner("dots", style="dim")
|