v1.3.4
Co-Authored-By: Quentin Torroba <quentin.torroba@mistral.ai> Co-Authored-By: Vincent Guilloux <vincent.guilloux@mistral.ai> Co-Authored-By: Luis Cardoso <luis.cardoso@mistral.ai>
This commit is contained in:
parent
4d449be276
commit
add3ab5245
36 changed files with 399 additions and 259 deletions
6
.github/workflows/build-and-upload.yml
vendored
6
.github/workflows/build-and-upload.yml
vendored
|
|
@ -10,6 +10,8 @@ on:
|
|||
jobs:
|
||||
configure:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
|
|
@ -42,6 +44,8 @@ jobs:
|
|||
build-and-upload:
|
||||
needs: configure
|
||||
name: ${{ matrix.os }}-${{ matrix.arch }}
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.configure.outputs.matrix) }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
|
|
@ -131,6 +135,8 @@ jobs:
|
|||
nix-build:
|
||||
needs: configure
|
||||
name: "Nix: ${{ matrix.os }}-${{ matrix.arch }}"
|
||||
permissions:
|
||||
contents: read
|
||||
strategy:
|
||||
matrix: ${{ fromJSON(needs.configure.outputs.matrix) }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
|
|
|
|||
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
|
|
@ -13,7 +13,8 @@ jobs:
|
|||
pre-commit:
|
||||
name: Pre-commit
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
|
|
@ -51,7 +52,8 @@ jobs:
|
|||
tests:
|
||||
name: Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
|
|
@ -85,7 +87,8 @@ jobs:
|
|||
snapshot-tests:
|
||||
name: Snapshot Tests
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
|
|
|
|||
1
.github/workflows/issue-labeler.yml
vendored
1
.github/workflows/issue-labeler.yml
vendored
|
|
@ -8,6 +8,7 @@ jobs:
|
|||
label-component:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
|
|
|
|||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -48,6 +48,8 @@ jobs:
|
|||
name: Release Zed Extension
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'mistralai/mistral-vibe'
|
||||
permissions:
|
||||
contents: read
|
||||
environment:
|
||||
name: zed
|
||||
steps:
|
||||
|
|
|
|||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "1.3.3",
|
||||
"version": "1.3.4",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "ACP Server",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.3.4] - 2026-01-07
|
||||
|
||||
### Fixed
|
||||
|
||||
- markup in blinking messages
|
||||
- safety around Bash and AGENTS.md
|
||||
- explicit permissions to GitHub Actions workflows
|
||||
- improve render performance in long sessions
|
||||
|
||||
## [1.3.3] - 2025-12-26
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
id = "mistral-vibe"
|
||||
name = "Mistral Vibe"
|
||||
description = "Mistral's open-source coding assistant"
|
||||
version = "1.3.3"
|
||||
version = "1.3.4"
|
||||
schema_version = 1
|
||||
authors = ["Mistral AI"]
|
||||
repository = "https://github.com/mistralai/mistral-vibe"
|
||||
|
|
@ -11,25 +11,25 @@ name = "Mistral Vibe"
|
|||
icon = "./icons/mistral_vibe.svg"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.darwin-aarch64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.3.3/vibe-acp-darwin-aarch64-1.3.3.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.3.4/vibe-acp-darwin-aarch64-1.3.4.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.darwin-x86_64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.3.3/vibe-acp-darwin-x86_64-1.3.3.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.3.4/vibe-acp-darwin-x86_64-1.3.4.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.linux-aarch64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.3.3/vibe-acp-linux-aarch64-1.3.3.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.3.4/vibe-acp-linux-aarch64-1.3.4.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.linux-x86_64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.3.3/vibe-acp-linux-x86_64-1.3.3.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.3.4/vibe-acp-linux-x86_64-1.3.4.zip"
|
||||
cmd = "./vibe-acp"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.windows-aarch64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.3.3/vibe-acp-windows-aarch64-1.3.3.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.3.4/vibe-acp-windows-aarch64-1.3.4.zip"
|
||||
cmd = "./vibe-acp.exe"
|
||||
|
||||
[agent_servers.mistral-vibe.targets.windows-x86_64]
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.3.3/vibe-acp-windows-x86_64-1.3.3.zip"
|
||||
archive = "https://github.com/mistralai/mistral-vibe/releases/download/v1.3.4/vibe-acp-windows-x86_64-1.3.4.zip"
|
||||
cmd = "./vibe-acp.exe"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "mistral-vibe"
|
||||
version = "1.3.3"
|
||||
version = "1.3.4"
|
||||
description = "Minimal CLI coding agent by Mistral"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
|
|
@ -44,6 +44,8 @@ dependencies = [
|
|||
"watchfiles>=1.1.1",
|
||||
"pyperclip>=1.11.0",
|
||||
"textual-speedups>=0.2.1",
|
||||
"tree-sitter>=0.25.2",
|
||||
"tree-sitter-bash>=0.25.1",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ class TestACPInitialize:
|
|||
),
|
||||
)
|
||||
assert response.agentInfo == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="1.3.3"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="1.3.4"
|
||||
)
|
||||
|
||||
assert response.authMethods == []
|
||||
|
|
@ -63,7 +63,7 @@ class TestACPInitialize:
|
|||
),
|
||||
)
|
||||
assert response.agentInfo == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="1.3.3"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="1.3.4"
|
||||
)
|
||||
|
||||
assert response.authMethods is not None
|
||||
|
|
|
|||
19
tests/cli/test_no_markup_static.py
Normal file
19
tests/cli/test_no_markup_static.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from textual.markup import MarkupError
|
||||
from textual.visual import visualize
|
||||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
|
||||
|
||||
def test_static_raises_on_invalid_markup() -> None:
|
||||
widget = Static()
|
||||
with pytest.raises(MarkupError):
|
||||
visualize(widget, "[/]", markup=True)
|
||||
|
||||
|
||||
def test_no_markup_static_allows_invalid_markup() -> None:
|
||||
widget = NoMarkupStatic("[/]")
|
||||
assert str(widget.render()) == "[/]"
|
||||
51
uv.lock
generated
51
uv.lock
generated
|
|
@ -661,7 +661,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "mistral-vibe"
|
||||
version = "1.3.3"
|
||||
version = "1.3.4"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "agent-client-protocol" },
|
||||
|
|
@ -680,6 +680,8 @@ dependencies = [
|
|||
{ name = "textual" },
|
||||
{ name = "textual-speedups" },
|
||||
{ name = "tomli-w" },
|
||||
{ name = "tree-sitter" },
|
||||
{ name = "tree-sitter-bash" },
|
||||
{ name = "watchfiles" },
|
||||
]
|
||||
|
||||
|
|
@ -720,6 +722,8 @@ requires-dist = [
|
|||
{ name = "textual", specifier = ">=1.0.0" },
|
||||
{ name = "textual-speedups", specifier = ">=0.2.1" },
|
||||
{ name = "tomli-w", specifier = ">=1.2.0" },
|
||||
{ name = "tree-sitter", specifier = ">=0.25.2" },
|
||||
{ name = "tree-sitter-bash", specifier = ">=0.25.1" },
|
||||
{ name = "watchfiles", specifier = ">=1.1.1" },
|
||||
]
|
||||
|
||||
|
|
@ -1601,6 +1605,51 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.25.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/7c/0350cfc47faadc0d3cf7d8237a4e34032b3014ddf4a12ded9933e1648b55/tree-sitter-0.25.2.tar.gz", hash = "sha256:fe43c158555da46723b28b52e058ad444195afd1db3ca7720c59a254544e9c20", size = 177961, upload-time = "2025-09-25T17:37:59.751Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/9e/20c2a00a862f1c2897a436b17edb774e831b22218083b459d0d081c9db33/tree_sitter-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ddabfff809ffc983fc9963455ba1cecc90295803e06e140a4c83e94c1fa3d960", size = 146941, upload-time = "2025-09-25T17:37:34.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/04/8512e2062e652a1016e840ce36ba1cc33258b0dcc4e500d8089b4054afec/tree_sitter-0.25.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c0c0ab5f94938a23fe81928a21cc0fac44143133ccc4eb7eeb1b92f84748331c", size = 137699, upload-time = "2025-09-25T17:37:36.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/8a/d48c0414db19307b0fb3bb10d76a3a0cbe275bb293f145ee7fba2abd668e/tree_sitter-0.25.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd12d80d91d4114ca097626eb82714618dcdfacd6a5e0955216c6485c350ef99", size = 607125, upload-time = "2025-09-25T17:37:37.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/d1/b95f545e9fc5001b8a78636ef942a4e4e536580caa6a99e73dd0a02e87aa/tree_sitter-0.25.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b43a9e4c89d4d0839de27cd4d6902d33396de700e9ff4c5ab7631f277a85ead9", size = 635418, upload-time = "2025-09-25T17:37:38.922Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/4d/b734bde3fb6f3513a010fa91f1f2875442cdc0382d6a949005cd84563d8f/tree_sitter-0.25.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbb1706407c0e451c4f8cc016fec27d72d4b211fdd3173320b1ada7a6c74c3ac", size = 631250, upload-time = "2025-09-25T17:37:40.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/f2/5f654994f36d10c64d50a192239599fcae46677491c8dd53e7579c35a3e3/tree_sitter-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:6d0302550bbe4620a5dc7649517c4409d74ef18558276ce758419cf09e578897", size = 127156, upload-time = "2025-09-25T17:37:41.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/23/148c468d410efcf0a9535272d81c258d840c27b34781d625f1f627e2e27d/tree_sitter-0.25.2-cp312-cp312-win_arm64.whl", hash = "sha256:0c8b6682cac77e37cfe5cf7ec388844957f48b7bd8d6321d0ca2d852994e10d5", size = 113984, upload-time = "2025-09-25T17:37:42.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/67/67492014ce32729b63d7ef318a19f9cfedd855d677de5773476caf771e96/tree_sitter-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0628671f0de69bb279558ef6b640bcfc97864fe0026d840f872728a86cd6b6cd", size = 146926, upload-time = "2025-09-25T17:37:43.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/9c/a278b15e6b263e86c5e301c82a60923fa7c59d44f78d7a110a89a413e640/tree_sitter-0.25.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f5ddcd3e291a749b62521f71fc953f66f5fd9743973fd6dd962b092773569601", size = 137712, upload-time = "2025-09-25T17:37:44.039Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/9a/423bba15d2bf6473ba67846ba5244b988cd97a4b1ea2b146822162256794/tree_sitter-0.25.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd88fbb0f6c3a0f28f0a68d72df88e9755cf5215bae146f5a1bdc8362b772053", size = 607873, upload-time = "2025-09-25T17:37:45.477Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/4c/b430d2cb43f8badfb3a3fa9d6cd7c8247698187b5674008c9d67b2a90c8e/tree_sitter-0.25.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b878e296e63661c8e124177cc3084b041ba3f5936b43076d57c487822426f614", size = 636313, upload-time = "2025-09-25T17:37:46.68Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/27/5f97098dbba807331d666a0997662e82d066e84b17d92efab575d283822f/tree_sitter-0.25.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d77605e0d353ba3fe5627e5490f0fbfe44141bafa4478d88ef7954a61a848dae", size = 631370, upload-time = "2025-09-25T17:37:47.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/3c/87caaed663fabc35e18dc704cd0e9800a0ee2f22bd18b9cbe7c10799895d/tree_sitter-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:463c032bd02052d934daa5f45d183e0521ceb783c2548501cf034b0beba92c9b", size = 127157, upload-time = "2025-09-25T17:37:48.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/23/f8467b408b7988aff4ea40946a4bd1a2c1a73d17156a9d039bbaff1e2ceb/tree_sitter-0.25.2-cp313-cp313-win_arm64.whl", hash = "sha256:b3f63a1796886249bd22c559a5944d64d05d43f2be72961624278eff0dcc5cb8", size = 113975, upload-time = "2025-09-25T17:37:49.922Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/e3/d9526ba71dfbbe4eba5e51d89432b4b333a49a1e70712aa5590cd22fc74f/tree_sitter-0.25.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:65d3c931013ea798b502782acab986bbf47ba2c452610ab0776cf4a8ef150fc0", size = 146776, upload-time = "2025-09-25T17:37:50.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/97/4bd4ad97f85a23011dd8a535534bb1035c4e0bac1234d58f438e15cff51f/tree_sitter-0.25.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:bda059af9d621918efb813b22fb06b3fe00c3e94079c6143fcb2c565eb44cb87", size = 137732, upload-time = "2025-09-25T17:37:51.877Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/19/1e968aa0b1b567988ed522f836498a6a9529a74aab15f09dd9ac1e41f505/tree_sitter-0.25.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eac4e8e4c7060c75f395feec46421eb61212cb73998dbe004b7384724f3682ab", size = 609456, upload-time = "2025-09-25T17:37:52.925Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/b6/cf08f4f20f4c9094006ef8828555484e842fc468827ad6e56011ab668dbd/tree_sitter-0.25.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:260586381b23be33b6191a07cea3d44ecbd6c01aa4c6b027a0439145fcbc3358", size = 636772, upload-time = "2025-09-25T17:37:54.647Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/e2/d42d55bf56360987c32bc7b16adb06744e425670b823fb8a5786a1cea991/tree_sitter-0.25.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7d2ee1acbacebe50ba0f85fff1bc05e65d877958f00880f49f9b2af38dce1af0", size = 631522, upload-time = "2025-09-25T17:37:55.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/87/af9604ebe275a9345d88c3ace0cf2a1341aa3f8ef49dd9fc11662132df8a/tree_sitter-0.25.2-cp314-cp314-win_amd64.whl", hash = "sha256:4973b718fcadfb04e59e746abfbb0288694159c6aeecd2add59320c03368c721", size = 130864, upload-time = "2025-09-25T17:37:57.453Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/6e/e64621037357acb83d912276ffd30a859ef117f9c680f2e3cb955f47c680/tree_sitter-0.25.2-cp314-cp314-win_arm64.whl", hash = "sha256:b8d4429954a3beb3e844e2872610d2a4800ba4eb42bb1990c6a4b1949b18459f", size = 117470, upload-time = "2025-09-25T17:37:58.431Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-bash"
|
||||
version = "0.25.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/0e/f0108be910f1eef6499eabce517e79fe3b12057280ed398da67ce2426cba/tree_sitter_bash-0.25.1.tar.gz", hash = "sha256:bfc0bdaa77bc1e86e3c6652e5a6e140c40c0a16b84185c2b63ad7cd809b88f14", size = 419703, upload-time = "2025-12-02T17:01:08.849Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/30/8e/37e7364d9c9c58da89e05c510671d8c45818afd7b31c6939ab72f8dc6c04/tree_sitter_bash-0.25.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:0e6235f59e366d220dde7d830196bed597d01e853e44d8ccd1a82c5dd2500acf", size = 194160, upload-time = "2025-12-02T17:00:59.047Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/bb/2d2cfbb1f89aaeb1ec892624f069d92d058d06bb66f16b9ec9fb5873ab60/tree_sitter_bash-0.25.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f4a34a6504c7c5b2a9b8c5c4065531dea19ca2c35026e706cf2eeeebe2c92512", size = 202659, upload-time = "2025-12-02T17:01:00.275Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/f0/1bb25519be27460255d3899db677313cfa1e6306988fbf456a3d7e211bbb/tree_sitter_bash-0.25.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e76c4cfb20b076552406782b7f8c2a3946835993df0a44df006de54b7030c7dc", size = 230596, upload-time = "2025-12-02T17:01:01.759Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/22/9f70bc3d3b942ab9fc0f89c1dc9e087519a3a94f64ae6b7377aae3a7a0f0/tree_sitter_bash-0.25.1-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3f484c4bb8796cde7a87ca351e6116f09653edac0eb3c6d238566359dd28b117", size = 231981, upload-time = "2025-12-02T17:01:02.859Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/c3/f1540e42cd41b323c6821e45e52e1aed6ed386209aad52db996f05703963/tree_sitter_bash-0.25.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5e76af6df46d958c7f5b6d5884c9743218e3902a00ccb493ec92728b1084430b", size = 228364, upload-time = "2025-12-02T17:01:03.997Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/a0/c3050a6277dfcac8c480f514dc4fe49f3f65f0eac68b4702cbaca2584e85/tree_sitter_bash-0.25.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a3332d71c7b7d5f78259b19d02d0ea111fcb82b72712ee4a93aaa5b226d3f0a8", size = 230074, upload-time = "2025-12-02T17:01:05.05Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/0f/203fe6b27211387f4b9ba8c4a321567ca4ded2624dae6ccdbd2b6e940e17/tree_sitter_bash-0.25.1-cp310-abi3-win_amd64.whl", hash = "sha256:52a6802d9218f86278aa3e8b459c3abdad67eed0fde1f9f13aca5b6c634217a6", size = 195574, upload-time = "2025-12-02T17:01:06.412Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/75/4ca1a9fabd8fb5aea78cea70f7837ce4dbf2afae115f62051e5fa99cba1c/tree_sitter_bash-0.25.1-cp310-abi3-win_arm64.whl", hash = "sha256:59115057ec2bae319e8082ff29559861045002964c3431ccb0fc92aa4bc9bccb", size = 191196, upload-time = "2025-12-02T17:01:07.486Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "twine"
|
||||
version = "6.2.0"
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@ from __future__ import annotations
|
|||
from pathlib import Path
|
||||
|
||||
VIBE_ROOT = Path(__file__).parent
|
||||
__version__ = "1.3.3"
|
||||
__version__ = "1.3.4"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from rich import print as rprint
|
|||
|
||||
from vibe import __version__
|
||||
from vibe.core.paths.config_paths import unlock_config_paths
|
||||
from vibe.core.trusted_folders import trusted_folders_manager
|
||||
from vibe.core.trusted_folders import has_trustable_content, trusted_folders_manager
|
||||
from vibe.setup.trusted_folders.trust_folder_dialog import (
|
||||
TrustDialogQuitException,
|
||||
ask_trust_folder,
|
||||
|
|
@ -105,7 +105,7 @@ def parse_arguments() -> argparse.Namespace:
|
|||
|
||||
def check_and_resolve_trusted_folder() -> None:
|
||||
cwd = Path.cwd()
|
||||
if not (cwd / ".vibe").exists() or cwd.resolve() == Path.home().resolve():
|
||||
if not has_trustable_content(cwd) or cwd.resolve() == Path.home().resolve():
|
||||
return
|
||||
|
||||
is_folder_trusted = trusted_folders_manager.is_trusted(cwd)
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ from vibe.cli.textual_ui.widgets.messages import (
|
|||
WarningMessage,
|
||||
)
|
||||
from vibe.cli.textual_ui.widgets.mode_indicator import ModeIndicator
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.cli.textual_ui.widgets.path_display import PathDisplay
|
||||
from vibe.cli.textual_ui.widgets.tools import ToolCallMessage, ToolResultMessage
|
||||
from vibe.cli.textual_ui.widgets.welcome import WelcomeBanner
|
||||
|
|
@ -174,7 +175,7 @@ class VibeApp(App): # noqa: PLR0904
|
|||
yield PathDisplay(
|
||||
self.config.displayed_workdir or self.config.effective_workdir
|
||||
)
|
||||
yield Static(id="spacer")
|
||||
yield NoMarkupStatic(id="spacer")
|
||||
yield ContextProgress()
|
||||
|
||||
async def on_mount(self) -> None:
|
||||
|
|
@ -320,6 +321,24 @@ class VibeApp(App): # noqa: PLR0904
|
|||
|
||||
await self._switch_to_input_app()
|
||||
|
||||
async def on_compact_message_completed(
|
||||
self, message: CompactMessage.Completed
|
||||
) -> None:
|
||||
messages_area = self.query_one("#messages")
|
||||
children = list(messages_area.children)
|
||||
|
||||
try:
|
||||
compact_index = children.index(message.compact_widget)
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if compact_index == 0:
|
||||
return
|
||||
|
||||
with self.batch_update():
|
||||
for widget in children[:compact_index]:
|
||||
await widget.remove()
|
||||
|
||||
def _set_tool_permission_always(
|
||||
self, tool_name: str, save_permanently: bool = False
|
||||
) -> None:
|
||||
|
|
@ -502,31 +521,32 @@ class VibeApp(App): # noqa: PLR0904
|
|||
messages_area = self.query_one("#messages")
|
||||
tool_call_map: dict[str, str] = {}
|
||||
|
||||
for msg in self._loaded_messages:
|
||||
if msg.role == Role.system:
|
||||
continue
|
||||
with self.batch_update():
|
||||
for msg in self._loaded_messages:
|
||||
if msg.role == Role.system:
|
||||
continue
|
||||
|
||||
match msg.role:
|
||||
case Role.user:
|
||||
if msg.content:
|
||||
await messages_area.mount(UserMessage(msg.content))
|
||||
match msg.role:
|
||||
case Role.user:
|
||||
if msg.content:
|
||||
await messages_area.mount(UserMessage(msg.content))
|
||||
|
||||
case Role.assistant:
|
||||
await self._mount_history_assistant_message(
|
||||
msg, messages_area, tool_call_map
|
||||
)
|
||||
|
||||
case Role.tool:
|
||||
tool_name = msg.name or tool_call_map.get(
|
||||
msg.tool_call_id or "", "tool"
|
||||
)
|
||||
await messages_area.mount(
|
||||
ToolResultMessage(
|
||||
tool_name=tool_name,
|
||||
content=msg.content,
|
||||
collapsed=self._tools_collapsed,
|
||||
case Role.assistant:
|
||||
await self._mount_history_assistant_message(
|
||||
msg, messages_area, tool_call_map
|
||||
)
|
||||
|
||||
case Role.tool:
|
||||
tool_name = msg.name or tool_call_map.get(
|
||||
msg.tool_call_id or "", "tool"
|
||||
)
|
||||
await messages_area.mount(
|
||||
ToolResultMessage(
|
||||
tool_name=tool_name,
|
||||
content=msg.content,
|
||||
collapsed=self._tools_collapsed,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
async def _mount_history_assistant_message(
|
||||
self, msg: LLMMessage, messages_area: Widget, tool_call_map: dict[str, str]
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@ from __future__ import annotations
|
|||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.textual_ui.widgets.compact import CompactMessage
|
||||
from vibe.cli.textual_ui.widgets.messages import AssistantMessage, ReasoningMessage
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.cli.textual_ui.widgets.tools import ToolCallMessage, ToolResultMessage
|
||||
from vibe.core.types import (
|
||||
AssistantEvent,
|
||||
|
|
@ -142,9 +141,7 @@ class EventHandler:
|
|||
self.current_compact = None
|
||||
|
||||
async def _handle_unknown_event(self, event: BaseEvent) -> None:
|
||||
await self.mount_callback(
|
||||
Static(str(event), markup=False, classes="unknown-event")
|
||||
)
|
||||
await self.mount_callback(NoMarkupStatic(str(event), classes="unknown-event"))
|
||||
|
||||
def stop_current_tool_call(self) -> None:
|
||||
if self.current_tool_call:
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from textual.containers import Container, Vertical, VerticalScroll
|
|||
from textual.message import Message
|
||||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.cli.textual_ui.widgets.tool_widgets import get_approval_widget
|
||||
from vibe.core.config import VibeConfig
|
||||
|
||||
|
|
@ -67,7 +68,7 @@ class ApprovalApp(Container):
|
|||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="approval-content"):
|
||||
self.title_widget = Static(
|
||||
self.title_widget = NoMarkupStatic(
|
||||
f"⚠ {self.tool_name} command", classes="approval-title"
|
||||
)
|
||||
yield self.title_widget
|
||||
|
|
@ -78,16 +79,16 @@ class ApprovalApp(Container):
|
|||
)
|
||||
yield self.tool_info_container
|
||||
|
||||
yield Static("")
|
||||
yield NoMarkupStatic("")
|
||||
|
||||
for _ in range(3):
|
||||
widget = Static("", classes="approval-option")
|
||||
widget = NoMarkupStatic("", classes="approval-option")
|
||||
self.option_widgets.append(widget)
|
||||
yield widget
|
||||
|
||||
yield Static("")
|
||||
yield NoMarkupStatic("")
|
||||
|
||||
self.help_widget = Static(
|
||||
self.help_widget = NoMarkupStatic(
|
||||
"↑↓ navigate Enter select ESC reject", classes="approval-help"
|
||||
)
|
||||
yield self.help_widget
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ from textual.app import ComposeResult
|
|||
from textual.containers import Horizontal
|
||||
from textual.message import Message
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.history_manager import HistoryManager
|
||||
from vibe.cli.textual_ui.widgets.chat_input.text_area import ChatTextArea, InputMode
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
|
||||
|
||||
class ChatInputBody(Widget):
|
||||
|
|
@ -23,7 +23,7 @@ class ChatInputBody(Widget):
|
|||
def __init__(self, history_file: Path | None = None, **kwargs: Any) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.input_widget: ChatTextArea | None = None
|
||||
self.prompt_widget: Static | None = None
|
||||
self.prompt_widget: NoMarkupStatic | None = None
|
||||
|
||||
if history_file:
|
||||
self.history = HistoryManager(history_file)
|
||||
|
|
@ -34,7 +34,7 @@ class ChatInputBody(Widget):
|
|||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Horizontal():
|
||||
self.prompt_widget = Static(">", id="prompt")
|
||||
self.prompt_widget = NoMarkupStatic(">", id="prompt")
|
||||
yield self.prompt_widget
|
||||
|
||||
self.input_widget = ChatTextArea(placeholder="Ask anything...", id="input")
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from textual.message import Message
|
||||
|
||||
from vibe.cli.textual_ui.widgets.status_message import StatusMessage
|
||||
|
||||
|
||||
class CompactMessage(StatusMessage):
|
||||
class Completed(Message):
|
||||
def __init__(self, compact_widget: CompactMessage) -> None:
|
||||
super().__init__()
|
||||
self.compact_widget = compact_widget
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.add_class("compact-message")
|
||||
|
|
@ -36,6 +43,7 @@ class CompactMessage(StatusMessage):
|
|||
self.old_tokens = old_tokens
|
||||
self.new_tokens = new_tokens
|
||||
self.stop_spinning(success=True)
|
||||
self.post_message(self.Completed(self))
|
||||
|
||||
def set_error(self, error_message: str) -> None:
|
||||
self.error_message = error_message
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from textual.theme import BUILTIN_THEMES
|
|||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.textual_ui.terminal_theme import TERMINAL_THEME_NAME
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vibe.core.config import VibeConfig
|
||||
|
|
@ -85,19 +86,19 @@ class ConfigApp(Container):
|
|||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Vertical(id="config-content"):
|
||||
self.title_widget = Static("Settings", classes="settings-title")
|
||||
self.title_widget = NoMarkupStatic("Settings", classes="settings-title")
|
||||
yield self.title_widget
|
||||
|
||||
yield Static("")
|
||||
yield NoMarkupStatic("")
|
||||
|
||||
for _ in self.settings:
|
||||
widget = Static("", classes="settings-option")
|
||||
widget = NoMarkupStatic("", classes="settings-option")
|
||||
self.setting_widgets.append(widget)
|
||||
yield widget
|
||||
|
||||
yield Static("")
|
||||
yield NoMarkupStatic("")
|
||||
|
||||
self.help_widget = Static(
|
||||
self.help_widget = NoMarkupStatic(
|
||||
"↑↓ navigate Space/Enter toggle ESC exit", classes="settings-help"
|
||||
)
|
||||
yield self.help_widget
|
||||
|
|
@ -143,12 +144,13 @@ class ConfigApp(Container):
|
|||
current: str = self.changes.get(key, setting["value"])
|
||||
|
||||
options: list[str] = setting["options"]
|
||||
new_value = ""
|
||||
try:
|
||||
current_idx = options.index(current)
|
||||
next_idx = (current_idx + 1) % len(options)
|
||||
new_value: str = options[next_idx]
|
||||
new_value = options[next_idx]
|
||||
except (ValueError, IndexError):
|
||||
new_value: str = options[0] if options else current
|
||||
new_value = options[0] if options else current
|
||||
|
||||
self.changes[key] = new_value
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ from dataclasses import dataclass
|
|||
from typing import Any
|
||||
|
||||
from textual.reactive import reactive
|
||||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -13,7 +14,7 @@ class TokenState:
|
|||
current_tokens: int = 0
|
||||
|
||||
|
||||
class ContextProgress(Static):
|
||||
class ContextProgress(NoMarkupStatic):
|
||||
tokens = reactive(TokenState())
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from textual.app import ComposeResult
|
|||
from textual.containers import Horizontal
|
||||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.cli.textual_ui.widgets.spinner import SpinnerMixin, SpinnerType
|
||||
|
||||
|
||||
|
|
@ -55,10 +56,10 @@ class LoadingWidget(SpinnerMixin, Static):
|
|||
self.status = status or self._get_default_status()
|
||||
self.current_color_index = 0
|
||||
self.transition_progress = 0
|
||||
self.char_widgets: list[Static] = []
|
||||
self.ellipsis_widget: Static | None = None
|
||||
self._status_widget: Static | None = None
|
||||
self.hint_widget: Static | None = None
|
||||
self.start_time: float | None = None
|
||||
self._last_elapsed: int = -1
|
||||
|
||||
def _get_easter_egg(self) -> str | None:
|
||||
EASTER_EGG_PROBABILITY = 0.10
|
||||
|
|
@ -85,7 +86,7 @@ class LoadingWidget(SpinnerMixin, Static):
|
|||
|
||||
def set_status(self, status: str) -> None:
|
||||
self.status = self._apply_easter_egg(status)
|
||||
self._rebuild_chars()
|
||||
self._update_animation()
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Horizontal(classes="loading-container"):
|
||||
|
|
@ -94,34 +95,14 @@ class LoadingWidget(SpinnerMixin, Static):
|
|||
)
|
||||
yield self._indicator_widget
|
||||
|
||||
with Horizontal(classes="loading-status"):
|
||||
for char in self.status:
|
||||
widget = Static(char, classes="loading-char")
|
||||
self.char_widgets.append(widget)
|
||||
yield widget
|
||||
self._status_widget = Static("", classes="loading-status")
|
||||
yield self._status_widget
|
||||
|
||||
self.ellipsis_widget = Static("… ", classes="loading-ellipsis")
|
||||
yield self.ellipsis_widget
|
||||
|
||||
self.hint_widget = Static("(0s esc to interrupt)", classes="loading-hint")
|
||||
self.hint_widget = NoMarkupStatic(
|
||||
"(0s esc to interrupt)", classes="loading-hint"
|
||||
)
|
||||
yield self.hint_widget
|
||||
|
||||
def _rebuild_chars(self) -> None:
|
||||
if not self.is_mounted:
|
||||
return
|
||||
|
||||
status_container = self.query_one(".loading-status", Horizontal)
|
||||
|
||||
status_container.remove_children()
|
||||
self.char_widgets.clear()
|
||||
|
||||
for char in self.status:
|
||||
widget = Static(char, classes="loading-char")
|
||||
self.char_widgets.append(widget)
|
||||
status_container.mount(widget)
|
||||
|
||||
self._update_animation()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.start_time = time()
|
||||
self._update_animation()
|
||||
|
|
@ -144,24 +125,26 @@ class LoadingWidget(SpinnerMixin, Static):
|
|||
return next_color
|
||||
return current_color
|
||||
|
||||
def _build_status_text(self) -> str:
|
||||
parts = []
|
||||
for i, char in enumerate(self.status):
|
||||
color = self._get_color_for_position(1 + i)
|
||||
parts.append(f"[{color}]{char}[/]")
|
||||
ellipsis_start = 1 + len(self.status)
|
||||
color_ellipsis = self._get_color_for_position(ellipsis_start)
|
||||
parts.append(f"[{color_ellipsis}]… [/]")
|
||||
return "".join(parts)
|
||||
|
||||
def _update_animation(self) -> None:
|
||||
total_elements = 1 + len(self.char_widgets) + 2
|
||||
total_elements = 1 + len(self.status) + 1
|
||||
|
||||
if self._indicator_widget:
|
||||
spinner_char = self._spinner.next_frame()
|
||||
color = self._get_color_for_position(0)
|
||||
self._indicator_widget.update(f"[{color}]{spinner_char}[/]")
|
||||
|
||||
for i, widget in enumerate(self.char_widgets):
|
||||
position = 1 + i
|
||||
color = self._get_color_for_position(position)
|
||||
widget.update(f"[{color}]{self.status[i]}[/]")
|
||||
|
||||
if self.ellipsis_widget:
|
||||
ellipsis_start = 1 + len(self.status)
|
||||
color_ellipsis = self._get_color_for_position(ellipsis_start)
|
||||
color_space = self._get_color_for_position(ellipsis_start + 1)
|
||||
self.ellipsis_widget.update(f"[{color_ellipsis}]…[/][{color_space}] [/]")
|
||||
if self._status_widget:
|
||||
self._status_widget.update(self._build_status_text())
|
||||
|
||||
self.transition_progress += 1
|
||||
if self.transition_progress > total_elements:
|
||||
|
|
@ -172,4 +155,6 @@ class LoadingWidget(SpinnerMixin, Static):
|
|||
|
||||
if self.hint_widget and self.start_time is not None:
|
||||
elapsed = int(time() - self.start_time)
|
||||
self.hint_widget.update(f"({elapsed}s esc to interrupt)")
|
||||
if elapsed != self._last_elapsed:
|
||||
self._last_elapsed = elapsed
|
||||
self.hint_widget.update(f"({elapsed}s esc to interrupt)")
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ from textual.containers import Horizontal, Vertical
|
|||
from textual.widgets import Markdown, Static
|
||||
from textual.widgets._markdown import MarkdownStream
|
||||
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.cli.textual_ui.widgets.spinner import SpinnerMixin, SpinnerType
|
||||
|
||||
|
||||
class NonSelectableStatic(Static):
|
||||
class NonSelectableStatic(NoMarkupStatic):
|
||||
@property
|
||||
def text_selection(self) -> None:
|
||||
return None
|
||||
|
|
@ -42,7 +43,7 @@ class UserMessage(Static):
|
|||
def compose(self) -> ComposeResult:
|
||||
with Horizontal(classes="user-message-container"):
|
||||
yield NonSelectableStatic("> ", classes="user-message-prompt")
|
||||
yield Static(self._content, markup=False, classes="user-message-content")
|
||||
yield NoMarkupStatic(self._content, classes="user-message-content")
|
||||
if self._pending:
|
||||
self.add_class("pending")
|
||||
|
||||
|
|
@ -137,8 +138,8 @@ class ReasoningMessage(SpinnerMixin, StreamingMessageBase):
|
|||
self._spinner.current_frame(), classes="reasoning-indicator"
|
||||
)
|
||||
yield self._indicator_widget
|
||||
self._status_text_widget = Static(
|
||||
self.SPINNING_TEXT, markup=False, classes="reasoning-collapsed-text"
|
||||
self._status_text_widget = NoMarkupStatic(
|
||||
self.SPINNING_TEXT, classes="reasoning-collapsed-text"
|
||||
)
|
||||
yield self._status_text_widget
|
||||
self._triangle_widget = NonSelectableStatic(
|
||||
|
|
@ -204,9 +205,8 @@ class InterruptMessage(Static):
|
|||
def compose(self) -> ComposeResult:
|
||||
with Horizontal(classes="interrupt-container"):
|
||||
yield ExpandingBorder(classes="interrupt-border")
|
||||
yield Static(
|
||||
yield NoMarkupStatic(
|
||||
"Interrupted · What should Vibe do instead?",
|
||||
markup=False,
|
||||
classes="interrupt-content",
|
||||
)
|
||||
|
||||
|
|
@ -223,18 +223,20 @@ class BashOutputMessage(Static):
|
|||
def compose(self) -> ComposeResult:
|
||||
with Vertical(classes="bash-output-container"):
|
||||
with Horizontal(classes="bash-cwd-line"):
|
||||
yield Static(self._cwd, markup=False, classes="bash-cwd")
|
||||
yield Static("", classes="bash-cwd-spacer")
|
||||
yield NoMarkupStatic(self._cwd, classes="bash-cwd")
|
||||
yield NoMarkupStatic("", classes="bash-cwd-spacer")
|
||||
if self._exit_code == 0:
|
||||
yield Static("✓", classes="bash-exit-success")
|
||||
yield NoMarkupStatic("✓", classes="bash-exit-success")
|
||||
else:
|
||||
yield Static("✗", classes="bash-exit-failure")
|
||||
yield Static(f" ({self._exit_code})", classes="bash-exit-code")
|
||||
yield NoMarkupStatic("✗", classes="bash-exit-failure")
|
||||
yield NoMarkupStatic(
|
||||
f" ({self._exit_code})", classes="bash-exit-code"
|
||||
)
|
||||
with Horizontal(classes="bash-command-line"):
|
||||
yield Static("> ", classes="bash-chevron")
|
||||
yield Static(self._command, markup=False, classes="bash-command")
|
||||
yield Static("", classes="bash-command-spacer")
|
||||
yield Static(self._output, markup=False, classes="bash-output")
|
||||
yield NoMarkupStatic("> ", classes="bash-chevron")
|
||||
yield NoMarkupStatic(self._command, classes="bash-command")
|
||||
yield NoMarkupStatic("", classes="bash-command-spacer")
|
||||
yield NoMarkupStatic(self._output, classes="bash-output")
|
||||
|
||||
|
||||
class ErrorMessage(Static):
|
||||
|
|
@ -248,8 +250,8 @@ class ErrorMessage(Static):
|
|||
def compose(self) -> ComposeResult:
|
||||
with Horizontal(classes="error-container"):
|
||||
yield ExpandingBorder(classes="error-border")
|
||||
self._content_widget = Static(
|
||||
self._get_text(), markup=False, classes="error-content"
|
||||
self._content_widget = NoMarkupStatic(
|
||||
self._get_text(), classes="error-content"
|
||||
)
|
||||
yield self._content_widget
|
||||
|
||||
|
|
@ -278,4 +280,4 @@ class WarningMessage(Static):
|
|||
with Horizontal(classes="warning-container"):
|
||||
if self._show_border:
|
||||
yield ExpandingBorder(classes="warning-border")
|
||||
yield Static(self._message, markup=False, classes="warning-content")
|
||||
yield NoMarkupStatic(self._message, classes="warning-content")
|
||||
|
|
|
|||
11
vibe/cli/textual_ui/widgets/no_markup_static.py
Normal file
11
vibe/cli/textual_ui/widgets/no_markup_static.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from textual.visual import VisualType
|
||||
from textual.widgets import Static
|
||||
|
||||
|
||||
class NoMarkupStatic(Static):
|
||||
def __init__(self, content: VisualType = "", **kwargs: Any) -> None:
|
||||
super().__init__(content, markup=False, **kwargs)
|
||||
|
|
@ -2,10 +2,10 @@ from __future__ import annotations
|
|||
|
||||
from pathlib import Path
|
||||
|
||||
from textual.widgets import Static
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
|
||||
|
||||
class PathDisplay(Static):
|
||||
class PathDisplay(NoMarkupStatic):
|
||||
def __init__(self, path: Path | str) -> None:
|
||||
super().__init__()
|
||||
self.can_focus = False
|
||||
|
|
|
|||
|
|
@ -147,3 +147,8 @@ class SpinnerMixin:
|
|||
self._indicator_widget.add_class("error")
|
||||
if self._status_text_widget and self.COMPLETED_TEXT:
|
||||
self._status_text_widget.update(self.COMPLETED_TEXT)
|
||||
|
||||
def on_unmount(self) -> None:
|
||||
if self._spinner_timer:
|
||||
self._spinner_timer.stop()
|
||||
self._spinner_timer = None
|
||||
|
|
|
|||
|
|
@ -7,16 +7,17 @@ from textual.containers import Horizontal
|
|||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.textual_ui.widgets.messages import NonSelectableStatic
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.cli.textual_ui.widgets.spinner import SpinnerMixin, SpinnerType
|
||||
|
||||
|
||||
class StatusMessage(SpinnerMixin, Static):
|
||||
class StatusMessage(SpinnerMixin, NoMarkupStatic):
|
||||
SPINNER_TYPE: ClassVar[SpinnerType] = SpinnerType.LINE
|
||||
|
||||
def __init__(self, initial_text: str = "", **kwargs: Any) -> None:
|
||||
self._initial_text = initial_text
|
||||
self._indicator_widget: Static | None = None
|
||||
self._text_widget: Static | None = None
|
||||
self._text_widget: NoMarkupStatic | None = None
|
||||
self.success = True
|
||||
self.init_spinner()
|
||||
super().__init__(**kwargs)
|
||||
|
|
@ -24,14 +25,10 @@ class StatusMessage(SpinnerMixin, Static):
|
|||
def compose(self) -> ComposeResult:
|
||||
with Horizontal():
|
||||
self._indicator_widget = NonSelectableStatic(
|
||||
self._spinner.current_frame(),
|
||||
markup=False,
|
||||
classes="status-indicator-icon",
|
||||
self._spinner.current_frame(), classes="status-indicator-icon"
|
||||
)
|
||||
yield self._indicator_widget
|
||||
self._text_widget = Static(
|
||||
"", markup=False, classes="status-indicator-text"
|
||||
)
|
||||
self._text_widget = NoMarkupStatic("", classes="status-indicator-text")
|
||||
yield self._text_widget
|
||||
|
||||
def on_mount(self) -> None:
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from textual.app import ComposeResult
|
|||
from textual.containers import Vertical
|
||||
from textual.widgets import Markdown, Static
|
||||
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.cli.textual_ui.widgets.utils import DEFAULT_TOOL_SHORTCUT, TOOL_SHORTCUTS
|
||||
from vibe.core.tools.builtins.bash import BashArgs, BashResult
|
||||
from vibe.core.tools.builtins.grep import GrepArgs, GrepResult
|
||||
|
|
@ -51,15 +52,15 @@ def parse_search_replace_to_diff(content: str) -> list[str]:
|
|||
def render_diff_line(line: str) -> Static:
|
||||
"""Render a single diff line with appropriate styling."""
|
||||
if line.startswith("---") or line.startswith("+++"):
|
||||
return Static(line, markup=False, classes="diff-header")
|
||||
return NoMarkupStatic(line, classes="diff-header")
|
||||
elif line.startswith("-"):
|
||||
return Static(line, markup=False, classes="diff-removed")
|
||||
return NoMarkupStatic(line, classes="diff-removed")
|
||||
elif line.startswith("+"):
|
||||
return Static(line, markup=False, classes="diff-added")
|
||||
return NoMarkupStatic(line, classes="diff-added")
|
||||
elif line.startswith("@@"):
|
||||
return Static(line, markup=False, classes="diff-range")
|
||||
return NoMarkupStatic(line, classes="diff-range")
|
||||
else:
|
||||
return Static(line, markup=False, classes="diff-context")
|
||||
return NoMarkupStatic(line, classes="diff-context")
|
||||
|
||||
|
||||
class ToolApprovalWidget[TArgs: BaseModel](Vertical):
|
||||
|
|
@ -80,10 +81,8 @@ class ToolApprovalWidget[TArgs: BaseModel](Vertical):
|
|||
if len(value_str) > MAX_MSG_SIZE:
|
||||
hidden = len(value_str) - MAX_MSG_SIZE
|
||||
value_str = value_str[:MAX_MSG_SIZE] + f"… ({hidden} more characters)"
|
||||
yield Static(
|
||||
f"{field_name}: {value_str}",
|
||||
markup=False,
|
||||
classes="approval-description",
|
||||
yield NoMarkupStatic(
|
||||
f"{field_name}: {value_str}", classes="approval-description"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -115,9 +114,9 @@ class ToolResultWidget[TResult: BaseModel](Static):
|
|||
def _header(self) -> ComposeResult:
|
||||
"""Yield the standard header. Subclasses can call this then add content."""
|
||||
if self.collapsed:
|
||||
yield Static(f"{self.message} {self._hint()}", markup=False)
|
||||
yield NoMarkupStatic(f"{self.message} {self._hint()}")
|
||||
else:
|
||||
yield Static(self.message, markup=False)
|
||||
yield NoMarkupStatic(self.message)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Default: show message and optionally result fields."""
|
||||
|
|
@ -127,10 +126,8 @@ class ToolResultWidget[TResult: BaseModel](Static):
|
|||
for field_name in type(self.result).model_fields:
|
||||
value = getattr(self.result, field_name)
|
||||
if value is not None and value not in ("", []):
|
||||
yield Static(
|
||||
f"{field_name}: {value}",
|
||||
markup=False,
|
||||
classes="tool-result-detail",
|
||||
yield NoMarkupStatic(
|
||||
f"{field_name}: {value}", classes="tool-result-detail"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -144,24 +141,18 @@ class BashResultWidget(ToolResultWidget[BashResult]):
|
|||
yield from self._header()
|
||||
if self.collapsed or not self.result:
|
||||
return
|
||||
yield Static(
|
||||
f"returncode: {self.result.returncode}",
|
||||
markup=False,
|
||||
classes="tool-result-detail",
|
||||
yield NoMarkupStatic(
|
||||
f"returncode: {self.result.returncode}", classes="tool-result-detail"
|
||||
)
|
||||
if self.result.stdout:
|
||||
sep = "\n" if "\n" in self.result.stdout else " "
|
||||
yield Static(
|
||||
f"stdout:{sep}{self.result.stdout}",
|
||||
markup=False,
|
||||
classes="tool-result-detail",
|
||||
yield NoMarkupStatic(
|
||||
f"stdout:{sep}{self.result.stdout}", classes="tool-result-detail"
|
||||
)
|
||||
if self.result.stderr:
|
||||
sep = "\n" if "\n" in self.result.stderr else " "
|
||||
yield Static(
|
||||
f"stderr:{sep}{self.result.stderr}",
|
||||
markup=False,
|
||||
classes="tool-result-detail",
|
||||
yield NoMarkupStatic(
|
||||
f"stderr:{sep}{self.result.stderr}", classes="tool-result-detail"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -170,10 +161,8 @@ class WriteFileApprovalWidget(ToolApprovalWidget[WriteFileArgs]):
|
|||
path = Path(self.args.path)
|
||||
file_extension = path.suffix.lstrip(".") or "text"
|
||||
|
||||
yield Static(
|
||||
f"File: {self.args.path}", markup=False, classes="approval-description"
|
||||
)
|
||||
yield Static("")
|
||||
yield NoMarkupStatic(f"File: {self.args.path}", classes="approval-description")
|
||||
yield NoMarkupStatic("")
|
||||
yield Markdown(f"```{file_extension}\n{self.args.content}\n```")
|
||||
|
||||
|
||||
|
|
@ -182,26 +171,22 @@ class WriteFileResultWidget(ToolResultWidget[WriteFileResult]):
|
|||
yield from self._header()
|
||||
if self.collapsed or not self.result:
|
||||
return
|
||||
yield Static(
|
||||
f"Path: {self.result.path}", markup=False, classes="tool-result-detail"
|
||||
)
|
||||
yield Static(
|
||||
f"Bytes: {self.result.bytes_written}",
|
||||
markup=False,
|
||||
classes="tool-result-detail",
|
||||
yield NoMarkupStatic(f"Path: {self.result.path}", classes="tool-result-detail")
|
||||
yield NoMarkupStatic(
|
||||
f"Bytes: {self.result.bytes_written}", classes="tool-result-detail"
|
||||
)
|
||||
if self.result.content:
|
||||
yield Static("")
|
||||
yield NoMarkupStatic("")
|
||||
ext = Path(self.result.path).suffix.lstrip(".") or "text"
|
||||
yield Markdown(f"```{ext}\n{_truncate_lines(self.result.content, 10)}\n```")
|
||||
|
||||
|
||||
class SearchReplaceApprovalWidget(ToolApprovalWidget[SearchReplaceArgs]):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static(
|
||||
f"File: {self.args.file_path}", markup=False, classes="approval-description"
|
||||
yield NoMarkupStatic(
|
||||
f"File: {self.args.file_path}", classes="approval-description"
|
||||
)
|
||||
yield Static("")
|
||||
yield NoMarkupStatic("")
|
||||
|
||||
diff_lines = parse_search_replace_to_diff(self.args.content)
|
||||
for line in diff_lines:
|
||||
|
|
@ -213,37 +198,30 @@ class SearchReplaceResultWidget(ToolResultWidget[SearchReplaceResult]):
|
|||
yield from self._header()
|
||||
if self.collapsed or not self.result:
|
||||
return
|
||||
yield Static(
|
||||
f"File: {self.result.file}", markup=False, classes="tool-result-detail"
|
||||
)
|
||||
yield Static(
|
||||
yield NoMarkupStatic(f"File: {self.result.file}", classes="tool-result-detail")
|
||||
yield NoMarkupStatic(
|
||||
f"Blocks applied: {self.result.blocks_applied}",
|
||||
markup=False,
|
||||
classes="tool-result-detail",
|
||||
)
|
||||
yield Static(
|
||||
f"Lines changed: {self.result.lines_changed}",
|
||||
markup=False,
|
||||
classes="tool-result-detail",
|
||||
yield NoMarkupStatic(
|
||||
f"Lines changed: {self.result.lines_changed}", classes="tool-result-detail"
|
||||
)
|
||||
for warning in self.result.warnings:
|
||||
yield Static(f"⚠ {warning}", markup=False, classes="tool-result-warning")
|
||||
yield NoMarkupStatic(f"⚠ {warning}", classes="tool-result-warning")
|
||||
if self.result.content:
|
||||
yield Static("")
|
||||
yield NoMarkupStatic("")
|
||||
for line in parse_search_replace_to_diff(self.result.content):
|
||||
yield render_diff_line(line)
|
||||
|
||||
|
||||
class TodoApprovalWidget(ToolApprovalWidget[TodoArgs]):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static(
|
||||
f"Action: {self.args.action}", markup=False, classes="approval-description"
|
||||
yield NoMarkupStatic(
|
||||
f"Action: {self.args.action}", classes="approval-description"
|
||||
)
|
||||
if self.args.todos:
|
||||
yield Static(
|
||||
f"Todos: {len(self.args.todos)} items",
|
||||
markup=False,
|
||||
classes="approval-description",
|
||||
yield NoMarkupStatic(
|
||||
f"Todos: {len(self.args.todos)} items", classes="approval-description"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -252,13 +230,13 @@ class TodoResultWidget(ToolResultWidget[TodoResult]):
|
|||
|
||||
def compose(self) -> ComposeResult:
|
||||
if self.collapsed:
|
||||
yield Static(f"{self.message} {self._hint()}", markup=False)
|
||||
yield NoMarkupStatic(f"{self.message} {self._hint()}")
|
||||
else:
|
||||
yield Static(f"{self.message} {self._hint()}", markup=False)
|
||||
yield Static("")
|
||||
yield NoMarkupStatic(f"{self.message} {self._hint()}")
|
||||
yield NoMarkupStatic("")
|
||||
|
||||
if not self.result or not self.result.todos:
|
||||
yield Static("No todos", markup=False, classes="todo-empty")
|
||||
yield NoMarkupStatic("No todos", classes="todo-empty")
|
||||
return
|
||||
|
||||
# Group todos by status
|
||||
|
|
@ -280,8 +258,8 @@ class TodoResultWidget(ToolResultWidget[TodoResult]):
|
|||
for status in ["in_progress", "pending", "completed", "cancelled"]:
|
||||
for todo in by_status[status]:
|
||||
icon = self._get_status_icon(status)
|
||||
yield Static(
|
||||
f"{icon} {todo.content}", markup=False, classes=f"todo-{status}"
|
||||
yield NoMarkupStatic(
|
||||
f"{icon} {todo.content}", classes=f"todo-{status}"
|
||||
)
|
||||
|
||||
def _get_status_icon(self, status: str) -> str:
|
||||
|
|
@ -291,20 +269,14 @@ class TodoResultWidget(ToolResultWidget[TodoResult]):
|
|||
|
||||
class ReadFileApprovalWidget(ToolApprovalWidget[ReadFileArgs]):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static(
|
||||
f"path: {self.args.path}", markup=False, classes="approval-description"
|
||||
)
|
||||
yield NoMarkupStatic(f"path: {self.args.path}", classes="approval-description")
|
||||
if self.args.offset > 0:
|
||||
yield Static(
|
||||
f"offset: {self.args.offset}",
|
||||
markup=False,
|
||||
classes="approval-description",
|
||||
yield NoMarkupStatic(
|
||||
f"offset: {self.args.offset}", classes="approval-description"
|
||||
)
|
||||
if self.args.limit is not None:
|
||||
yield Static(
|
||||
f"limit: {self.args.limit}",
|
||||
markup=False,
|
||||
classes="approval-description",
|
||||
yield NoMarkupStatic(
|
||||
f"limit: {self.args.limit}", classes="approval-description"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -314,32 +286,26 @@ class ReadFileResultWidget(ToolResultWidget[ReadFileResult]):
|
|||
if self.collapsed:
|
||||
return
|
||||
if self.result:
|
||||
yield Static(
|
||||
f"Path: {self.result.path}", markup=False, classes="tool-result-detail"
|
||||
yield NoMarkupStatic(
|
||||
f"Path: {self.result.path}", classes="tool-result-detail"
|
||||
)
|
||||
for warning in self.warnings:
|
||||
yield Static(f"⚠ {warning}", markup=False, classes="tool-result-warning")
|
||||
yield NoMarkupStatic(f"⚠ {warning}", classes="tool-result-warning")
|
||||
if self.result and self.result.content:
|
||||
yield Static("")
|
||||
yield NoMarkupStatic("")
|
||||
ext = Path(self.result.path).suffix.lstrip(".") or "text"
|
||||
yield Markdown(f"```{ext}\n{_truncate_lines(self.result.content, 10)}\n```")
|
||||
|
||||
|
||||
class GrepApprovalWidget(ToolApprovalWidget[GrepArgs]):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static(
|
||||
f"pattern: {self.args.pattern}",
|
||||
markup=False,
|
||||
classes="approval-description",
|
||||
)
|
||||
yield Static(
|
||||
f"path: {self.args.path}", markup=False, classes="approval-description"
|
||||
yield NoMarkupStatic(
|
||||
f"pattern: {self.args.pattern}", classes="approval-description"
|
||||
)
|
||||
yield NoMarkupStatic(f"path: {self.args.path}", classes="approval-description")
|
||||
if self.args.max_matches is not None:
|
||||
yield Static(
|
||||
f"max_matches: {self.args.max_matches}",
|
||||
markup=False,
|
||||
classes="approval-description",
|
||||
yield NoMarkupStatic(
|
||||
f"max_matches: {self.args.max_matches}", classes="approval-description"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -349,9 +315,9 @@ class GrepResultWidget(ToolResultWidget[GrepResult]):
|
|||
if self.collapsed:
|
||||
return
|
||||
for warning in self.warnings:
|
||||
yield Static(f"⚠ {warning}", markup=False, classes="tool-result-warning")
|
||||
yield NoMarkupStatic(f"⚠ {warning}", classes="tool-result-warning")
|
||||
if self.result and self.result.matches:
|
||||
yield Static("")
|
||||
yield NoMarkupStatic("")
|
||||
yield Markdown(f"```\n{_truncate_lines(self.result.matches, 30)}\n```")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from textual.containers import Horizontal, Vertical
|
|||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.textual_ui.widgets.messages import ExpandingBorder
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.cli.textual_ui.widgets.status_message import StatusMessage
|
||||
from vibe.cli.textual_ui.widgets.tool_widgets import get_result_widget
|
||||
from vibe.cli.textual_ui.widgets.utils import DEFAULT_TOOL_SHORTCUT, TOOL_SHORTCUTS
|
||||
|
|
@ -99,11 +100,11 @@ class ToolResultMessage(Static):
|
|||
self.add_class("error-text")
|
||||
if self.collapsed:
|
||||
await self._content_container.mount(
|
||||
Static(f"Error. {self._hint()}", markup=False)
|
||||
NoMarkupStatic(f"Error. {self._hint()}")
|
||||
)
|
||||
else:
|
||||
await self._content_container.mount(
|
||||
Static(f"Error: {self._event.error}", markup=False)
|
||||
NoMarkupStatic(f"Error: {self._event.error}")
|
||||
)
|
||||
return
|
||||
|
||||
|
|
@ -112,11 +113,11 @@ class ToolResultMessage(Static):
|
|||
reason = self._event.skip_reason or "User skipped"
|
||||
if self.collapsed:
|
||||
await self._content_container.mount(
|
||||
Static(f"Skipped. {self._hint()}", markup=False)
|
||||
NoMarkupStatic(f"Skipped. {self._hint()}")
|
||||
)
|
||||
else:
|
||||
await self._content_container.mount(
|
||||
Static(f"Skipped: {reason}", markup=False)
|
||||
NoMarkupStatic(f"Skipped: {reason}")
|
||||
)
|
||||
return
|
||||
|
||||
|
|
@ -146,15 +147,15 @@ class ToolResultMessage(Static):
|
|||
|
||||
if self.collapsed:
|
||||
await self._content_container.mount(
|
||||
Static(f"{self._tool_name} completed {self._hint()}", markup=False)
|
||||
NoMarkupStatic(f"{self._tool_name} completed {self._hint()}")
|
||||
)
|
||||
return
|
||||
|
||||
if self._content:
|
||||
await self._content_container.mount(Static(self._content, markup=False))
|
||||
await self._content_container.mount(NoMarkupStatic(self._content))
|
||||
else:
|
||||
await self._content_container.mount(
|
||||
Static(f"{self._tool_name} completed.", markup=False)
|
||||
NoMarkupStatic(f"{self._tool_name} completed.")
|
||||
)
|
||||
|
||||
async def set_collapsed(self, collapsed: bool) -> None:
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ from vibe.core.paths.global_paths import GLOBAL_ENV_FILE, SESSION_LOG_DIR
|
|||
from vibe.core.prompts import SystemPrompt
|
||||
from vibe.core.tools.base import BaseToolConfig
|
||||
|
||||
PROJECT_DOC_FILENAMES = ["AGENTS.md", "VIBE.md", ".vibe.md"]
|
||||
|
||||
|
||||
def load_api_keys_from_env() -> None:
|
||||
if GLOBAL_ENV_FILE.path.is_file():
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ import sys
|
|||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from vibe.core.config import PROJECT_DOC_FILENAMES
|
||||
from vibe.core.llm.format import get_active_tool_classes
|
||||
from vibe.core.paths.config_paths import INSTRUCTIONS_FILE
|
||||
from vibe.core.prompts import UtilityPrompt
|
||||
from vibe.core.trusted_folders import TRUSTABLE_FILENAMES, trusted_folders_manager
|
||||
from vibe.core.utils import is_dangerous_directory, is_windows
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -30,7 +30,9 @@ def _load_user_instructions() -> str:
|
|||
|
||||
|
||||
def _load_project_doc(workdir: Path, max_bytes: int) -> str:
|
||||
for name in PROJECT_DOC_FILENAMES:
|
||||
if not trusted_folders_manager.is_trusted(workdir):
|
||||
return ""
|
||||
for name in TRUSTABLE_FILENAMES:
|
||||
path = workdir / name
|
||||
try:
|
||||
return path.read_text("utf-8", errors="ignore")[:max_bytes]
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from functools import lru_cache
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import sys
|
||||
from typing import ClassVar, Literal, final
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from tree_sitter import Language, Node, Parser
|
||||
import tree_sitter_bash as tsbash
|
||||
|
||||
from vibe.core.tools.base import (
|
||||
BaseTool,
|
||||
|
|
@ -19,6 +21,37 @@ from vibe.core.tools.base import (
|
|||
from vibe.core.utils import is_windows
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _get_parser() -> Parser:
|
||||
return Parser(Language(tsbash.language()))
|
||||
|
||||
|
||||
def _extract_commands(command: str) -> list[str]:
|
||||
parser = _get_parser()
|
||||
tree = parser.parse(command.encode("utf-8"))
|
||||
|
||||
commands: list[str] = []
|
||||
|
||||
def find_commands(node: Node) -> None:
|
||||
if node.type == "command":
|
||||
parts = []
|
||||
for child in node.children:
|
||||
if (
|
||||
child.type
|
||||
in {"command_name", "word", "string", "raw_string", "concatenation"}
|
||||
and child.text is not None
|
||||
):
|
||||
parts.append(child.text.decode("utf-8"))
|
||||
if parts:
|
||||
commands.append(" ".join(parts))
|
||||
|
||||
for child in node.children:
|
||||
find_commands(child)
|
||||
|
||||
find_commands(tree.root_node)
|
||||
return commands
|
||||
|
||||
|
||||
def _get_subprocess_encoding() -> str:
|
||||
if sys.platform == "win32":
|
||||
# Windows console uses OEM code page (e.g., cp850, cp1252)
|
||||
|
|
@ -167,9 +200,10 @@ class Bash(BaseTool[BashArgs, BashResult, BashToolConfig, BaseToolState]):
|
|||
description: ClassVar[str] = "Run a one-off bash command and capture its output."
|
||||
|
||||
def check_allowlist_denylist(self, args: BashArgs) -> ToolPermission | None:
|
||||
command_parts = re.split(r"(?:&&|\|\||;|\|)", args.command)
|
||||
command_parts = [part.strip() for part in command_parts if part.strip()]
|
||||
if is_windows():
|
||||
return None
|
||||
|
||||
command_parts = _extract_commands(args.command)
|
||||
if not command_parts:
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,17 @@ import tomli_w
|
|||
|
||||
from vibe.core.paths.global_paths import TRUSTED_FOLDERS_FILE
|
||||
|
||||
TRUSTABLE_FILENAMES = ["AGENTS.md", "VIBE.md", ".vibe.md"]
|
||||
|
||||
|
||||
def has_trustable_content(path: Path) -> bool:
|
||||
if (path / ".vibe").exists():
|
||||
return True
|
||||
for name in TRUSTABLE_FILENAMES:
|
||||
if (path / name).exists():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class TrustedFoldersManager:
|
||||
def __init__(self) -> None:
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from textual.validation import Length
|
|||
from textual.widgets import Input, Link, Static
|
||||
|
||||
from vibe.cli.clipboard import copy_selection_to_clipboard
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.core.config import VibeConfig
|
||||
from vibe.core.paths.global_paths import GLOBAL_ENV_FILE
|
||||
from vibe.setup.onboarding.base import OnboardingScreen
|
||||
|
|
@ -48,10 +49,10 @@ class ApiKeyScreen(OnboardingScreen):
|
|||
return
|
||||
|
||||
help_url, help_name = PROVIDER_HELP[self.provider.name]
|
||||
yield Static(f"Grab your {provider_name} API key from the {help_name}:")
|
||||
yield NoMarkupStatic(f"Grab your {provider_name} API key from the {help_name}:")
|
||||
yield Center(
|
||||
Horizontal(
|
||||
Static("→ ", classes="link-chevron"),
|
||||
NoMarkupStatic("→ ", classes="link-chevron"),
|
||||
Link(help_url, url=help_url),
|
||||
classes="link-row",
|
||||
)
|
||||
|
|
@ -60,7 +61,7 @@ class ApiKeyScreen(OnboardingScreen):
|
|||
def _compose_config_docs(self) -> ComposeResult:
|
||||
yield Static("[dim]Learn more about Vibe configuration:[/]")
|
||||
yield Horizontal(
|
||||
Static("→ ", classes="link-chevron"),
|
||||
NoMarkupStatic("→ ", classes="link-chevron"),
|
||||
Link(CONFIG_DOCS_URL, url=CONFIG_DOCS_URL),
|
||||
classes="link-row",
|
||||
)
|
||||
|
|
@ -76,17 +77,17 @@ class ApiKeyScreen(OnboardingScreen):
|
|||
)
|
||||
|
||||
with Vertical(id="api-key-outer"):
|
||||
yield Static("", classes="spacer")
|
||||
yield Center(Static("One last thing...", id="api-key-title"))
|
||||
yield NoMarkupStatic("", classes="spacer")
|
||||
yield Center(NoMarkupStatic("One last thing...", id="api-key-title"))
|
||||
with Center():
|
||||
with Vertical(id="api-key-content"):
|
||||
yield from self._compose_provider_link(provider_name)
|
||||
yield Static(
|
||||
yield NoMarkupStatic(
|
||||
"...and paste it below to finish the setup:", id="paste-hint"
|
||||
)
|
||||
yield Center(Horizontal(self.input_widget, id="input-box"))
|
||||
yield Static("", id="feedback")
|
||||
yield Static("", classes="spacer")
|
||||
yield NoMarkupStatic("", id="feedback")
|
||||
yield NoMarkupStatic("", classes="spacer")
|
||||
yield Vertical(
|
||||
Vertical(*self._compose_config_docs(), id="config-docs-group"),
|
||||
id="config-docs-section",
|
||||
|
|
@ -96,7 +97,7 @@ class ApiKeyScreen(OnboardingScreen):
|
|||
self.input_widget.focus()
|
||||
|
||||
def on_input_changed(self, event: Input.Changed) -> None:
|
||||
feedback = self.query_one("#feedback", Static)
|
||||
feedback = self.query_one("#feedback", NoMarkupStatic)
|
||||
input_box = self.query_one("#input-box")
|
||||
|
||||
if event.validation_result is None:
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from textual.theme import BUILTIN_THEMES
|
|||
from textual.widgets import Markdown, Static
|
||||
|
||||
from vibe.cli.textual_ui.terminal_theme import TERMINAL_THEME_NAME
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.core.config import VibeConfig
|
||||
from vibe.setup.onboarding.base import OnboardingScreen
|
||||
|
||||
|
|
@ -67,19 +68,19 @@ class ThemeSelectionScreen(OnboardingScreen):
|
|||
|
||||
def _compose_theme_list(self) -> ComposeResult:
|
||||
for _ in range(VISIBLE_NEIGHBORS * 2 + 1):
|
||||
widget = Static("", classes="theme-item")
|
||||
widget = NoMarkupStatic("", classes="theme-item")
|
||||
self._theme_widgets.append(widget)
|
||||
yield widget
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with Center(id="theme-outer"):
|
||||
with Vertical(id="theme-content"):
|
||||
yield Static("Select your preferred theme", id="theme-title")
|
||||
yield NoMarkupStatic("Select your preferred theme", id="theme-title")
|
||||
yield Center(
|
||||
Horizontal(
|
||||
Static("Navigate ↑ ↓", id="nav-hint"),
|
||||
NoMarkupStatic("Navigate ↑ ↓", id="nav-hint"),
|
||||
Vertical(*self._compose_theme_list(), id="theme-list"),
|
||||
Static("Press Enter \u21b5", id="enter-hint"),
|
||||
NoMarkupStatic("Press Enter \u21b5", id="enter-hint"),
|
||||
id="theme-row",
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from textual.containers import Center, Vertical
|
|||
from textual.timer import Timer
|
||||
from textual.widgets import Static
|
||||
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.setup.onboarding.base import OnboardingScreen
|
||||
|
||||
WELCOME_PREFIX = "Welcome to "
|
||||
|
|
@ -68,7 +69,7 @@ class WelcomeScreen(OnboardingScreen):
|
|||
with Center():
|
||||
yield Static("", id="welcome-text")
|
||||
with Center():
|
||||
yield Static("", id="enter-hint", classes="hidden")
|
||||
yield NoMarkupStatic("", id="enter-hint", classes="hidden")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self._welcome_text = self.query_one("#welcome-text", Static)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from vibe.cli.textual_ui.terminal_theme import (
|
|||
TERMINAL_THEME_NAME,
|
||||
capture_terminal_theme,
|
||||
)
|
||||
from vibe.cli.textual_ui.widgets.no_markup_static import NoMarkupStatic
|
||||
from vibe.core.paths.global_paths import GLOBAL_CONFIG_FILE, TRUSTED_FOLDERS_FILE
|
||||
|
||||
|
||||
|
|
@ -50,14 +51,14 @@ class TrustFolderDialog(CenterMiddle):
|
|||
|
||||
def compose(self) -> ComposeResult:
|
||||
with CenterMiddle(id="trust-dialog"):
|
||||
yield Static("⚠ Trust this folder?", id="trust-dialog-title")
|
||||
yield Static(
|
||||
yield NoMarkupStatic("⚠ Trust this folder?", id="trust-dialog-title")
|
||||
yield NoMarkupStatic(
|
||||
str(self.folder_path),
|
||||
id="trust-dialog-path",
|
||||
classes="trust-dialog-path",
|
||||
)
|
||||
yield Static(
|
||||
"A .vibe/ directory was found here. Should Vibe load custom configuration and tools from it?",
|
||||
yield NoMarkupStatic(
|
||||
"Files that can modify your Mistral Vibe setup were found here. Do you trust this folder?",
|
||||
id="trust-dialog-message",
|
||||
classes="trust-dialog-message",
|
||||
)
|
||||
|
|
@ -65,13 +66,17 @@ class TrustFolderDialog(CenterMiddle):
|
|||
with Horizontal(id="trust-options-container"):
|
||||
options = ["Yes", "No"]
|
||||
for idx, text in enumerate(options):
|
||||
widget = Static(f" {idx + 1}. {text}", classes="trust-option")
|
||||
widget = NoMarkupStatic(
|
||||
f" {idx + 1}. {text}", classes="trust-option"
|
||||
)
|
||||
self.option_widgets.append(widget)
|
||||
yield widget
|
||||
|
||||
yield Static("← → navigate Enter select", classes="trust-dialog-help")
|
||||
yield NoMarkupStatic(
|
||||
"← → navigate Enter select", classes="trust-dialog-help"
|
||||
)
|
||||
|
||||
yield Static(
|
||||
yield NoMarkupStatic(
|
||||
f"Setting will be saved in: {TRUSTED_FOLDERS_FILE.path}",
|
||||
id="trust-dialog-save-info",
|
||||
classes="trust-dialog-save-info",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue