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/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""mycode — minimal coding agent."""
|
mycode/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI package for mycode."""
|
mycode/cli/chat.py
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
"""Interactive terminal chat for the CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import html
|
|
7
|
+
import re
|
|
8
|
+
import shlex
|
|
9
|
+
from base64 import b64encode
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from prompt_toolkit import PromptSession
|
|
14
|
+
from prompt_toolkit.application import Application, get_app
|
|
15
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
16
|
+
from prompt_toolkit.formatted_text import ANSI
|
|
17
|
+
from prompt_toolkit.history import FileHistory
|
|
18
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
19
|
+
from prompt_toolkit.keys import Keys
|
|
20
|
+
from prompt_toolkit.layout import Layout
|
|
21
|
+
from prompt_toolkit.widgets import RadioList
|
|
22
|
+
from rich.text import Text
|
|
23
|
+
|
|
24
|
+
from mycode.core.agent import Agent
|
|
25
|
+
from mycode.core.config import resolve_mycode_home
|
|
26
|
+
from mycode.core.messages import build_message, image_block, text_block
|
|
27
|
+
from mycode.core.session import SessionStore
|
|
28
|
+
from mycode.core.tools import detect_image_mime_type, resolve_path
|
|
29
|
+
|
|
30
|
+
from .render import ReplyRenderer, TerminalView, format_local_timestamp
|
|
31
|
+
from .runtime import (
|
|
32
|
+
REASONING_EFFORT_OPTIONS,
|
|
33
|
+
append_session_message,
|
|
34
|
+
clone_agent,
|
|
35
|
+
get_provider_option,
|
|
36
|
+
list_model_options,
|
|
37
|
+
list_provider_options,
|
|
38
|
+
supports_reasoning_effort,
|
|
39
|
+
update_agent_runtime,
|
|
40
|
+
update_reasoning_effort,
|
|
41
|
+
)
|
|
42
|
+
from .theme import MUTED, PROMPT_CHAR, TERMINAL_THEME, TOOL_MARKER
|
|
43
|
+
|
|
44
|
+
_PROMPT = ANSI(f"\033[1m\033[34m{PROMPT_CHAR}\033[0m ")
|
|
45
|
+
|
|
46
|
+
_COMMAND_HELP = (
|
|
47
|
+
("/clear", "Clear conversation"),
|
|
48
|
+
("/new", "New session"),
|
|
49
|
+
("/resume", "Switch session"),
|
|
50
|
+
("/rewind", "Rewind to a previous message"),
|
|
51
|
+
("/provider", "Switch provider"),
|
|
52
|
+
("/model", "Switch model"),
|
|
53
|
+
("/effort", "Set reasoning effort"),
|
|
54
|
+
("/q", "Quit"),
|
|
55
|
+
)
|
|
56
|
+
_SLASH_COMMANDS = tuple(command for command, _ in _COMMAND_HELP)
|
|
57
|
+
# Only treat `@path` as a reference when it starts a standalone token.
|
|
58
|
+
_AT_PATH_RE = re.compile(r"(?<!\S)@(?:(?P<quote>['\"])(?P<quoted>[^'\"]*)|(?P<plain>[^\s'\"]*))$")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Style for the focused row in the inline selector.
|
|
62
|
+
_FOCUSED_STYLE = "bold blue" if TERMINAL_THEME == "light" else "bold cyan"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class _InlineRadioList[T](RadioList):
|
|
66
|
+
"""Arrow-key list that shows > on the focused item and exits on Enter."""
|
|
67
|
+
|
|
68
|
+
def _handle_enter(self) -> None:
|
|
69
|
+
# Only called by Enter/Space (not arrows), so safe to exit.
|
|
70
|
+
self.current_value = self.values[self._selected_index][0]
|
|
71
|
+
get_app().exit(result=self.current_value)
|
|
72
|
+
|
|
73
|
+
def _get_text_fragments(self):
|
|
74
|
+
# Override rendering: show > based on focus, not checked state.
|
|
75
|
+
result: list[tuple[str, str]] = []
|
|
76
|
+
for i, (_value, text) in enumerate(self.values):
|
|
77
|
+
focused = i == self._selected_index
|
|
78
|
+
style = _FOCUSED_STYLE if focused else ""
|
|
79
|
+
result.append((style, "> " if focused else " "))
|
|
80
|
+
result.append((style, str(text)))
|
|
81
|
+
result.append(("", "\n"))
|
|
82
|
+
result.pop() # remove trailing newline
|
|
83
|
+
return result
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def choose[T](options: list[tuple[T, str]], *, default: T | None = None) -> T | None:
|
|
87
|
+
"""Inline arrow-key selector. Returns the selected value or None on cancel."""
|
|
88
|
+
|
|
89
|
+
radio = _InlineRadioList(
|
|
90
|
+
values=options,
|
|
91
|
+
default=default,
|
|
92
|
+
show_scrollbar=False,
|
|
93
|
+
show_cursor=False,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
kb = KeyBindings()
|
|
97
|
+
|
|
98
|
+
@kb.add("c-c")
|
|
99
|
+
@kb.add("escape")
|
|
100
|
+
def _cancel(event) -> None:
|
|
101
|
+
event.app.exit(result=None)
|
|
102
|
+
|
|
103
|
+
app: Application[T | None] = Application(
|
|
104
|
+
layout=Layout(radio),
|
|
105
|
+
key_bindings=kb,
|
|
106
|
+
full_screen=False,
|
|
107
|
+
)
|
|
108
|
+
return await app.run_async()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class _PromptCompleter(Completer):
|
|
112
|
+
"""Complete slash commands and explicit `@path` references for the prompt."""
|
|
113
|
+
|
|
114
|
+
_COMMANDS = dict(_COMMAND_HELP)
|
|
115
|
+
|
|
116
|
+
def __init__(self, *, cwd: str | None = None) -> None:
|
|
117
|
+
self._cwd = cwd
|
|
118
|
+
|
|
119
|
+
def get_completions(self, document, complete_event):
|
|
120
|
+
text_before_cursor = document.text_before_cursor
|
|
121
|
+
text = text_before_cursor.lstrip()
|
|
122
|
+
if self._cwd:
|
|
123
|
+
match = _AT_PATH_RE.search(text_before_cursor)
|
|
124
|
+
if match:
|
|
125
|
+
quote = str(match.group("quote") or "")
|
|
126
|
+
query = str(match.group("quoted") or match.group("plain") or "")
|
|
127
|
+
# Complete only real paths under the current working directory.
|
|
128
|
+
if query == "~":
|
|
129
|
+
base_prefix = "~/"
|
|
130
|
+
partial = ""
|
|
131
|
+
base_dir = Path("~").expanduser()
|
|
132
|
+
elif query.endswith("/"):
|
|
133
|
+
base_prefix = query
|
|
134
|
+
partial = ""
|
|
135
|
+
base_dir = Path(resolve_path(query or ".", cwd=self._cwd))
|
|
136
|
+
else:
|
|
137
|
+
head, sep, tail = query.rpartition("/")
|
|
138
|
+
base_prefix = f"{head}{sep}" if sep else ""
|
|
139
|
+
partial = tail if sep else query
|
|
140
|
+
base_dir = Path(resolve_path(base_prefix or ".", cwd=self._cwd))
|
|
141
|
+
|
|
142
|
+
if base_dir.is_dir():
|
|
143
|
+
for entry in sorted(base_dir.iterdir(), key=lambda item: (not item.is_dir(), item.name.lower())):
|
|
144
|
+
if partial and not entry.name.startswith(partial):
|
|
145
|
+
continue
|
|
146
|
+
candidate = f"{base_prefix}{entry.name}{'/' if entry.is_dir() else ''}"
|
|
147
|
+
replacement = "@" + shlex.quote(candidate)
|
|
148
|
+
if quote:
|
|
149
|
+
replacement = f"@{quote}{candidate}"
|
|
150
|
+
if not entry.is_dir():
|
|
151
|
+
replacement += quote
|
|
152
|
+
yield Completion(
|
|
153
|
+
replacement,
|
|
154
|
+
start_position=-len(match.group(0)),
|
|
155
|
+
display="@" + candidate,
|
|
156
|
+
display_meta="dir" if entry.is_dir() else "file",
|
|
157
|
+
)
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
if not text.startswith("/"):
|
|
161
|
+
return
|
|
162
|
+
for cmd, desc in self._COMMANDS.items():
|
|
163
|
+
if cmd.startswith(text) and cmd != text:
|
|
164
|
+
yield Completion(cmd, start_position=-len(text), display_meta=desc)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _rewrite_pasted_file_paths(text: str) -> str | None:
|
|
168
|
+
"""Rewrite pasted file paths into explicit `@path` references."""
|
|
169
|
+
|
|
170
|
+
normalized = text.replace("\r\n", "\n").replace("\r", "\n").strip()
|
|
171
|
+
if not normalized:
|
|
172
|
+
return None
|
|
173
|
+
try:
|
|
174
|
+
tokens = shlex.split(normalized, posix=True)
|
|
175
|
+
except ValueError:
|
|
176
|
+
return None
|
|
177
|
+
if not tokens:
|
|
178
|
+
return None
|
|
179
|
+
paths = [Path(token).expanduser() for token in tokens]
|
|
180
|
+
if not all(path.is_file() for path in paths):
|
|
181
|
+
return None
|
|
182
|
+
return " ".join(f"@{shlex.quote(str(path))}" for path in paths)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _build_chat_key_bindings() -> KeyBindings:
|
|
186
|
+
"""Build key bindings for the main chat prompt."""
|
|
187
|
+
kb = KeyBindings()
|
|
188
|
+
|
|
189
|
+
kb.add("c-l")(lambda event: event.app.renderer.clear())
|
|
190
|
+
|
|
191
|
+
# In multiline mode the default Enter inserts a newline; override it to submit.
|
|
192
|
+
kb.add("enter", eager=True)(lambda event: event.current_buffer.validate_and_handle())
|
|
193
|
+
|
|
194
|
+
# Esc+Enter (Meta+Enter) inserts a newline for multiline input.
|
|
195
|
+
kb.add("escape", "enter")(lambda event: event.current_buffer.insert_text("\n"))
|
|
196
|
+
|
|
197
|
+
@kb.add(Keys.BracketedPaste, eager=True)
|
|
198
|
+
def _handle_bracketed_paste(event) -> None:
|
|
199
|
+
pasted = event.data.replace("\r\n", "\n").replace("\r", "\n")
|
|
200
|
+
event.current_buffer.insert_text(_rewrite_pasted_file_paths(pasted) or pasted)
|
|
201
|
+
|
|
202
|
+
return kb
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def history_file_path() -> str:
|
|
206
|
+
"""Return the path used by prompt-toolkit to store CLI history."""
|
|
207
|
+
|
|
208
|
+
path = resolve_mycode_home() / "cli_history"
|
|
209
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
return str(path)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class TerminalChat:
|
|
214
|
+
"""Own the interactive TUI session, including slash commands and rendering."""
|
|
215
|
+
|
|
216
|
+
def __init__(
|
|
217
|
+
self,
|
|
218
|
+
*,
|
|
219
|
+
agent: Agent,
|
|
220
|
+
store: SessionStore,
|
|
221
|
+
session_id: str,
|
|
222
|
+
view: TerminalView | None = None,
|
|
223
|
+
) -> None:
|
|
224
|
+
self.agent = agent
|
|
225
|
+
self.store = store
|
|
226
|
+
self.session_id = session_id
|
|
227
|
+
self.view = view or TerminalView()
|
|
228
|
+
self.prompt_session = PromptSession(
|
|
229
|
+
history=FileHistory(history_file_path()),
|
|
230
|
+
completer=_PromptCompleter(cwd=self.agent.cwd),
|
|
231
|
+
key_bindings=_build_chat_key_bindings(),
|
|
232
|
+
multiline=True,
|
|
233
|
+
prompt_continuation=" ",
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
async def run(self) -> None:
|
|
237
|
+
"""Run the interactive chat loop until the user exits the terminal UI."""
|
|
238
|
+
|
|
239
|
+
prefill = ""
|
|
240
|
+
while True:
|
|
241
|
+
self.view.console.print()
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
user_input = await self.prompt_session.prompt_async(_PROMPT, default=prefill)
|
|
245
|
+
except KeyboardInterrupt:
|
|
246
|
+
prefill = ""
|
|
247
|
+
continue
|
|
248
|
+
except EOFError:
|
|
249
|
+
self.view.console.print("\n[dim]bye[/dim]")
|
|
250
|
+
return
|
|
251
|
+
finally:
|
|
252
|
+
prefill = ""
|
|
253
|
+
|
|
254
|
+
user_input = user_input.strip()
|
|
255
|
+
if not user_input:
|
|
256
|
+
continue
|
|
257
|
+
|
|
258
|
+
result = await self._handle_command(user_input)
|
|
259
|
+
if result == "exit":
|
|
260
|
+
return
|
|
261
|
+
if isinstance(result, str):
|
|
262
|
+
# Command wants to prefill the next prompt (e.g. /rewind).
|
|
263
|
+
prefill = result
|
|
264
|
+
continue
|
|
265
|
+
if result:
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
self.view.console.print()
|
|
269
|
+
renderer = ReplyRenderer(self.view.console)
|
|
270
|
+
user_message = self._build_user_message(user_input)
|
|
271
|
+
try:
|
|
272
|
+
await renderer.render(self.agent, user_message, on_persist=self._persist_message)
|
|
273
|
+
except (KeyboardInterrupt, asyncio.CancelledError):
|
|
274
|
+
self.agent.cancel()
|
|
275
|
+
renderer.cancel()
|
|
276
|
+
# Python 3.11+: uncancel the task so the loop can continue after Ctrl+C.
|
|
277
|
+
task = asyncio.current_task()
|
|
278
|
+
if task is not None:
|
|
279
|
+
try:
|
|
280
|
+
task.uncancel()
|
|
281
|
+
except AttributeError:
|
|
282
|
+
pass # Python < 3.11
|
|
283
|
+
|
|
284
|
+
def _build_user_message(self, text: str) -> dict[str, Any]:
|
|
285
|
+
"""Build one user message with the raw prompt first, then resolved attachments.
|
|
286
|
+
|
|
287
|
+
Text files are appended as extra text blocks in their final provider-facing
|
|
288
|
+
form. Images are appended as image blocks. Only explicit `@path` tokens
|
|
289
|
+
that resolve to real files are attached.
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
blocks = [text_block(text)]
|
|
293
|
+
try:
|
|
294
|
+
tokens = shlex.split(text.replace("\r\n", "\n").replace("\r", "\n"), posix=True)
|
|
295
|
+
except ValueError:
|
|
296
|
+
return build_message("user", blocks)
|
|
297
|
+
|
|
298
|
+
seen: set[str] = set()
|
|
299
|
+
for token in tokens:
|
|
300
|
+
if not token.startswith("@") or token == "@":
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
path = Path(resolve_path(token[1:], cwd=self.agent.cwd))
|
|
304
|
+
if not path.is_file():
|
|
305
|
+
continue
|
|
306
|
+
|
|
307
|
+
path_text = str(path)
|
|
308
|
+
if path_text in seen:
|
|
309
|
+
continue
|
|
310
|
+
seen.add(path_text)
|
|
311
|
+
|
|
312
|
+
image_mime_type = detect_image_mime_type(path)
|
|
313
|
+
if image_mime_type:
|
|
314
|
+
image_data = b64encode(path.read_bytes()).decode("utf-8")
|
|
315
|
+
blocks.append(image_block(image_data, mime_type=image_mime_type, name=path.name))
|
|
316
|
+
continue
|
|
317
|
+
|
|
318
|
+
# Reuse the existing read tool so attached text files follow the same
|
|
319
|
+
# UTF-8, truncation, and long-line rules as agent-initiated reads.
|
|
320
|
+
result = self.agent.tools.read(path=path_text)
|
|
321
|
+
if result.is_error:
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
blocks.append(
|
|
325
|
+
text_block(
|
|
326
|
+
f'<file name="{html.escape(path_text, quote=True)}">\n{result.model_text}\n</file>',
|
|
327
|
+
meta={"attachment": True, "path": path_text},
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
return build_message("user", blocks)
|
|
332
|
+
|
|
333
|
+
async def _persist_message(self, message: dict[str, Any]) -> None:
|
|
334
|
+
"""Persist one streamed message into the active session."""
|
|
335
|
+
|
|
336
|
+
await append_session_message(self.store, self.session_id, message, agent=self.agent)
|
|
337
|
+
|
|
338
|
+
async def _handle_command(self, text: str) -> str | bool:
|
|
339
|
+
"""Handle a slash command. Returns "exit" to quit, True if consumed, False otherwise."""
|
|
340
|
+
|
|
341
|
+
# Non-slash exit aliases.
|
|
342
|
+
if text in ("exit", "quit"):
|
|
343
|
+
self.view.console.print("[dim]bye[/dim]")
|
|
344
|
+
return "exit"
|
|
345
|
+
|
|
346
|
+
if not text.startswith("/"):
|
|
347
|
+
return False
|
|
348
|
+
|
|
349
|
+
command, _, argument = text.partition(" ")
|
|
350
|
+
argument = argument.strip()
|
|
351
|
+
matches = [candidate for candidate in _SLASH_COMMANDS if candidate.startswith(command)]
|
|
352
|
+
if len(matches) == 1:
|
|
353
|
+
command = matches[0]
|
|
354
|
+
|
|
355
|
+
match command:
|
|
356
|
+
case "/q":
|
|
357
|
+
self.view.console.print("[dim]bye[/dim]")
|
|
358
|
+
return "exit"
|
|
359
|
+
case "/c" | "/clear":
|
|
360
|
+
await self.store.clear_session(self.session_id)
|
|
361
|
+
self.agent.clear()
|
|
362
|
+
self.view.console.print(f"[green]{TOOL_MARKER}[/green] [dim]cleared[/dim]")
|
|
363
|
+
case "/new":
|
|
364
|
+
await self._start_new_session()
|
|
365
|
+
case "/rewind":
|
|
366
|
+
prefill = await self._rewind()
|
|
367
|
+
if prefill:
|
|
368
|
+
return prefill
|
|
369
|
+
case "/resume":
|
|
370
|
+
await self._resume_session()
|
|
371
|
+
case "/provider":
|
|
372
|
+
if argument:
|
|
373
|
+
await self._apply_provider_change(argument)
|
|
374
|
+
else:
|
|
375
|
+
await self._switch_provider()
|
|
376
|
+
case "/model":
|
|
377
|
+
if argument:
|
|
378
|
+
await self._apply_model_change(argument)
|
|
379
|
+
else:
|
|
380
|
+
await self._switch_model()
|
|
381
|
+
case "/effort":
|
|
382
|
+
if argument:
|
|
383
|
+
self._apply_effort_change(argument)
|
|
384
|
+
else:
|
|
385
|
+
await self._switch_effort()
|
|
386
|
+
case _:
|
|
387
|
+
self._print_help()
|
|
388
|
+
|
|
389
|
+
return True
|
|
390
|
+
|
|
391
|
+
def _print_help(self) -> None:
|
|
392
|
+
commands = [
|
|
393
|
+
("/c, /clear", "Clear conversation"),
|
|
394
|
+
("/new", "New session"),
|
|
395
|
+
("/resume", "Switch session"),
|
|
396
|
+
("/rewind", "Rewind to a previous message"),
|
|
397
|
+
("/provider [name]", "Switch provider"),
|
|
398
|
+
("/model [name]", "Switch model"),
|
|
399
|
+
("/effort [level]", "Set reasoning effort"),
|
|
400
|
+
("/q", "Quit"),
|
|
401
|
+
]
|
|
402
|
+
self.view.console.print()
|
|
403
|
+
for cmd, desc in commands:
|
|
404
|
+
line = Text()
|
|
405
|
+
line.append(f" {cmd:<20}", style="bold")
|
|
406
|
+
line.append(desc, style=MUTED)
|
|
407
|
+
self.view.console.print(line)
|
|
408
|
+
|
|
409
|
+
def _print_runtime_status(self, action: str, value: str, *, changed: bool) -> None:
|
|
410
|
+
"""Print the result of a runtime-only change."""
|
|
411
|
+
|
|
412
|
+
if changed:
|
|
413
|
+
self.view.console.print(f"[green]{TOOL_MARKER}[/green] [dim]{action} →[/dim] {value}")
|
|
414
|
+
return
|
|
415
|
+
self.view.console.print(f"[green]{TOOL_MARKER}[/green] [dim]already using[/dim] {value}")
|
|
416
|
+
|
|
417
|
+
def _supports_effort_or_warn(self) -> bool:
|
|
418
|
+
"""Return whether the current model supports reasoning effort."""
|
|
419
|
+
|
|
420
|
+
if supports_reasoning_effort(self.agent):
|
|
421
|
+
return True
|
|
422
|
+
self.view.console.print("[dim]current model does not support reasoning effort[/dim]")
|
|
423
|
+
return False
|
|
424
|
+
|
|
425
|
+
async def _start_new_session(self) -> None:
|
|
426
|
+
"""Start a fresh session while keeping the current runtime settings."""
|
|
427
|
+
|
|
428
|
+
data = self.store.draft_session(
|
|
429
|
+
None,
|
|
430
|
+
provider=self.agent.provider,
|
|
431
|
+
model=self.agent.model,
|
|
432
|
+
cwd=self.agent.cwd,
|
|
433
|
+
api_base=self.agent.api_base,
|
|
434
|
+
)
|
|
435
|
+
session = data.get("session") or {}
|
|
436
|
+
self.session_id = str(session.get("id") or "")
|
|
437
|
+
self.agent = clone_agent(self.agent, store=self.store, session_id=self.session_id, messages=[])
|
|
438
|
+
self.view.print_header(
|
|
439
|
+
provider=self.agent.provider,
|
|
440
|
+
model=self.agent.model,
|
|
441
|
+
session=session,
|
|
442
|
+
mode="new",
|
|
443
|
+
message_count=0,
|
|
444
|
+
reasoning_effort=self.agent.reasoning_effort,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
async def _rewind(self) -> str | None:
|
|
448
|
+
"""Rewind the conversation to a chosen user message.
|
|
449
|
+
|
|
450
|
+
Shows an interactive selector of all real user text messages.
|
|
451
|
+
Selecting one truncates the in-memory conversation to the slice before
|
|
452
|
+
that user message index and appends a rewind marker to the session log.
|
|
453
|
+
Returns the original message text to prefill the next prompt.
|
|
454
|
+
"""
|
|
455
|
+
messages = self.agent.messages
|
|
456
|
+
if not messages:
|
|
457
|
+
self.view.console.print("[dim]nothing to rewind[/dim]")
|
|
458
|
+
return None
|
|
459
|
+
|
|
460
|
+
# Collect real user text messages (skip synthetic compact summaries
|
|
461
|
+
# and tool-result-only user messages).
|
|
462
|
+
user_turns: list[tuple[int, str]] = [] # (message_index, full_text)
|
|
463
|
+
for i, msg in enumerate(messages):
|
|
464
|
+
if msg.get("role") != "user":
|
|
465
|
+
continue
|
|
466
|
+
if (msg.get("meta") or {}).get("synthetic"):
|
|
467
|
+
continue
|
|
468
|
+
blocks = msg.get("content") or []
|
|
469
|
+
text = ""
|
|
470
|
+
for b in blocks:
|
|
471
|
+
if isinstance(b, dict) and b.get("type") == "text" and b.get("text"):
|
|
472
|
+
text = str(b["text"]).strip()
|
|
473
|
+
break
|
|
474
|
+
if text:
|
|
475
|
+
user_turns.append((i, text))
|
|
476
|
+
|
|
477
|
+
if not user_turns:
|
|
478
|
+
self.view.console.print("[dim]no user messages to rewind to[/dim]")
|
|
479
|
+
return None
|
|
480
|
+
|
|
481
|
+
# Build selector options — most recent first.
|
|
482
|
+
options: list[tuple[int, str]] = []
|
|
483
|
+
for msg_index, text in reversed(user_turns):
|
|
484
|
+
preview = text.replace("\n", " ")[:60]
|
|
485
|
+
if len(text) > 60:
|
|
486
|
+
preview += "..."
|
|
487
|
+
options.append((msg_index, preview))
|
|
488
|
+
|
|
489
|
+
selected = await choose(options)
|
|
490
|
+
if selected is None:
|
|
491
|
+
return None
|
|
492
|
+
|
|
493
|
+
# Look up the full text of the selected message for prefill.
|
|
494
|
+
original_text = ""
|
|
495
|
+
for msg_index, text in user_turns:
|
|
496
|
+
if msg_index == selected:
|
|
497
|
+
original_text = text
|
|
498
|
+
break
|
|
499
|
+
|
|
500
|
+
# Persist the rewind event and truncate in-memory messages.
|
|
501
|
+
await self.store.append_rewind(self.session_id, selected)
|
|
502
|
+
self.agent.messages = messages[:selected]
|
|
503
|
+
|
|
504
|
+
self.view.console.print(f"[green]{TOOL_MARKER}[/green] [dim]rewound[/dim]")
|
|
505
|
+
if self.agent.messages:
|
|
506
|
+
self.view.print_history_preview(self.agent.messages)
|
|
507
|
+
else:
|
|
508
|
+
self.view.console.print("[dim]conversation is now empty[/dim]")
|
|
509
|
+
|
|
510
|
+
return original_text
|
|
511
|
+
|
|
512
|
+
async def _resume_session(self) -> None:
|
|
513
|
+
"""Switch to another saved session in the current workspace."""
|
|
514
|
+
|
|
515
|
+
sessions = await self.store.list_sessions(cwd=self.agent.cwd)
|
|
516
|
+
sessions = [s for s in sessions if s.get("id") != self.session_id]
|
|
517
|
+
if not sessions:
|
|
518
|
+
self.view.console.print("[dim]no other sessions in this workspace[/dim]")
|
|
519
|
+
return
|
|
520
|
+
|
|
521
|
+
options: list[tuple[dict[str, Any], str]] = []
|
|
522
|
+
for s in sessions:
|
|
523
|
+
title = str(s.get("title") or "New chat")[:40]
|
|
524
|
+
ts = format_local_timestamp(str(s.get("updated_at") or ""), "%m-%d %H:%M")
|
|
525
|
+
label = f"{title} {ts}" if ts else title
|
|
526
|
+
options.append((s, label))
|
|
527
|
+
|
|
528
|
+
session = await choose(options)
|
|
529
|
+
if session is None:
|
|
530
|
+
return
|
|
531
|
+
|
|
532
|
+
self.session_id = str(session.get("id") or "")
|
|
533
|
+
data = await self.store.load_session(self.session_id)
|
|
534
|
+
if not data:
|
|
535
|
+
self.view.console.print("[red]failed to load session[/red]")
|
|
536
|
+
return
|
|
537
|
+
messages = data.get("messages") or []
|
|
538
|
+
loaded_session = data.get("session") or session
|
|
539
|
+
self.agent = clone_agent(self.agent, store=self.store, session_id=self.session_id, messages=messages)
|
|
540
|
+
self.view.print_header(
|
|
541
|
+
provider=self.agent.provider,
|
|
542
|
+
model=self.agent.model,
|
|
543
|
+
session=loaded_session,
|
|
544
|
+
mode="resumed",
|
|
545
|
+
message_count=len(messages),
|
|
546
|
+
reasoning_effort=self.agent.reasoning_effort,
|
|
547
|
+
)
|
|
548
|
+
self.view.print_history_preview(messages)
|
|
549
|
+
|
|
550
|
+
async def _switch_provider(self) -> None:
|
|
551
|
+
"""Prompt for a configured provider and apply it to the active agent."""
|
|
552
|
+
|
|
553
|
+
options = list_provider_options(self.agent.settings)
|
|
554
|
+
current = get_provider_option(self.agent.settings, provider=self.agent.provider, api_base=self.agent.api_base)
|
|
555
|
+
|
|
556
|
+
choices: list[tuple[str, str]] = []
|
|
557
|
+
for option in options:
|
|
558
|
+
models = " ".join(option.models[:3])
|
|
559
|
+
if len(option.models) > 3:
|
|
560
|
+
models += f" +{len(option.models) - 3}"
|
|
561
|
+
choices.append((option.name, f"{option.name} {models}"))
|
|
562
|
+
|
|
563
|
+
selected = await choose(choices, default=current.name if current else None)
|
|
564
|
+
if selected is not None:
|
|
565
|
+
await self._apply_provider_change(selected)
|
|
566
|
+
|
|
567
|
+
async def _switch_model(self) -> None:
|
|
568
|
+
"""Prompt for a model supported by the current provider runtime."""
|
|
569
|
+
|
|
570
|
+
models = list_model_options(
|
|
571
|
+
self.agent.settings,
|
|
572
|
+
provider=self.agent.provider,
|
|
573
|
+
api_base=self.agent.api_base,
|
|
574
|
+
current_model=self.agent.model,
|
|
575
|
+
)
|
|
576
|
+
if not models:
|
|
577
|
+
self.view.console.print("[dim]no configured models for the current provider[/dim]")
|
|
578
|
+
return
|
|
579
|
+
|
|
580
|
+
choices = [(m, m) for m in models]
|
|
581
|
+
selected = await choose(choices, default=self.agent.model)
|
|
582
|
+
if selected is not None:
|
|
583
|
+
await self._apply_model_change(selected)
|
|
584
|
+
|
|
585
|
+
async def _apply_provider_change(self, provider_name: str) -> None:
|
|
586
|
+
"""Switch the active provider, keeping session history unchanged."""
|
|
587
|
+
|
|
588
|
+
try:
|
|
589
|
+
changed = await update_agent_runtime(
|
|
590
|
+
self.agent,
|
|
591
|
+
provider_name=provider_name,
|
|
592
|
+
model=None,
|
|
593
|
+
)
|
|
594
|
+
except ValueError as exc:
|
|
595
|
+
self.view.console.print(f"[red]{exc}[/red]")
|
|
596
|
+
return
|
|
597
|
+
|
|
598
|
+
label = f"{self.agent.provider} / {self.agent.model}"
|
|
599
|
+
if self.agent.reasoning_effort:
|
|
600
|
+
label += f" [effort: {self.agent.reasoning_effort}]"
|
|
601
|
+
self._print_runtime_status("provider/model", label, changed=changed)
|
|
602
|
+
|
|
603
|
+
async def _apply_model_change(self, model_name: str) -> None:
|
|
604
|
+
"""Switch the active model for the current provider runtime."""
|
|
605
|
+
|
|
606
|
+
current = get_provider_option(self.agent.settings, provider=self.agent.provider, api_base=self.agent.api_base)
|
|
607
|
+
provider_name = current.name if current else self.agent.provider
|
|
608
|
+
|
|
609
|
+
try:
|
|
610
|
+
changed = await update_agent_runtime(
|
|
611
|
+
self.agent,
|
|
612
|
+
provider_name=provider_name,
|
|
613
|
+
model=model_name,
|
|
614
|
+
)
|
|
615
|
+
except ValueError as exc:
|
|
616
|
+
self.view.console.print(f"[red]{exc}[/red]")
|
|
617
|
+
return
|
|
618
|
+
|
|
619
|
+
self._print_runtime_status("model", self.agent.model, changed=changed)
|
|
620
|
+
|
|
621
|
+
async def _switch_effort(self) -> None:
|
|
622
|
+
"""Prompt for a reasoning effort level."""
|
|
623
|
+
|
|
624
|
+
if not self._supports_effort_or_warn():
|
|
625
|
+
return
|
|
626
|
+
|
|
627
|
+
current = self.agent.reasoning_effort or "auto"
|
|
628
|
+
choices = [(o, o) for o in REASONING_EFFORT_OPTIONS]
|
|
629
|
+
selected = await choose(choices, default=current)
|
|
630
|
+
if selected is not None:
|
|
631
|
+
self._apply_effort_change(selected)
|
|
632
|
+
|
|
633
|
+
def _apply_effort_change(self, effort: str) -> None:
|
|
634
|
+
"""Apply a reasoning effort change to the active agent."""
|
|
635
|
+
|
|
636
|
+
if not self._supports_effort_or_warn():
|
|
637
|
+
return
|
|
638
|
+
|
|
639
|
+
cleaned = effort.strip().lower()
|
|
640
|
+
if cleaned in ("auto", ""):
|
|
641
|
+
resolved = None
|
|
642
|
+
elif cleaned in REASONING_EFFORT_OPTIONS:
|
|
643
|
+
resolved = cleaned
|
|
644
|
+
else:
|
|
645
|
+
self.view.console.print(f"[red]unknown effort: {effort}[/red]")
|
|
646
|
+
return
|
|
647
|
+
|
|
648
|
+
changed = update_reasoning_effort(self.agent, resolved)
|
|
649
|
+
display = resolved or "default"
|
|
650
|
+
self._print_runtime_status("effort", display, changed=changed)
|