Compare commits

..

1 Commits

Author SHA1 Message Date
Thorsten Ball
96cfc0e224 Fix assistant hint flickering to action name
This is an attempt to fix #21029 but it doesn't 100% fix it: we only
hide the message if the binding isn't available instead of showing the
action name.

What I don't understand yet is why the binding isn't available in the
first place when switching the tab, even though the editor is
`focused()`, `within_focused()`, `contains_focused()`.

I do think not showing anything is better than showing the action name
though, since that's too long.
2024-11-22 07:44:04 +01:00
646 changed files with 15339 additions and 48144 deletions

View File

@@ -13,12 +13,6 @@ rustflags = ["-C", "link-arg=-fuse-ld=mold"]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
[target.aarch64-apple-darwin]
rustflags = ["-C", "link-args=-Objc -all_load"]
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-args=-Objc -all_load"]
# This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes
[target.'cfg(target_os = "windows")']
rustflags = ["--cfg", "windows_slim_errors"]

View File

@@ -3,6 +3,15 @@ export default {
const url = new URL(request.url);
url.hostname = "docs-anw.pages.dev";
// These pages were removed, but may still be served due to Cloudflare's
// [asset retention](https://developers.cloudflare.com/pages/configuration/serving-pages/#asset-retention).
if (
url.pathname === "/docs/assistant/context-servers" ||
url.pathname === "/docs/assistant/model-context-protocol"
) {
return await fetch("https://zed.dev/404");
}
let res = await fetch(url, request);
if (res.status === 404) {

View File

@@ -26,8 +26,8 @@ body:
required: true
- type: textarea
attributes:
label: If applicable, add screenshots or screencasts of the incorrect state / behavior
description: Drag images / videos into the text input below
label: If applicable, add mockups / screenshots to help explain present your vision of the feature
description: Drag issues into the text input below
validations:
required: false
- type: textarea

View File

@@ -1,4 +1,3 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
blank_issues_enabled: false
contact_links:
- name: Language Request

View File

@@ -113,12 +113,6 @@ jobs:
script/check-licenses
script/generate-licenses /tmp/zed_licenses_output
- name: Check for new vulnerable dependencies
if: github.event_name == 'pull_request'
uses: actions/dependency-review-action@3b139cfc5fae8b618d3eae3675e383bb1769c019 # v4
with:
license-check: false
- name: Run tests
uses: ./.github/actions/run_tests
@@ -129,9 +123,7 @@ jobs:
run: |
cargo build --workspace --bins --all-features
cargo check -p gpui --features "macos-blade"
cargo check -p workspace
cargo build -p remote_server
script/check-rust-livekit-macos
linux_tests:
timeout-minutes: 60
@@ -163,10 +155,8 @@ jobs:
- name: Run tests
uses: ./.github/actions/run_tests
- name: Build other binaries and features
run: |
cargo build -p zed
cargo check -p workspace
- name: Build Zed
run: cargo build -p zed
build_remote_server:
timeout-minutes: 60

View File

@@ -8,11 +8,11 @@ on:
jobs:
update_top_ranking_issues:
runs-on: ubuntu-latest
if: github.repository == 'zed-industries/zed'
if: github.repository_owner == 'zed-industries'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up uv
uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
uses: astral-sh/setup-uv@2e657c127d5b1635d5a8e3fa40e0ac50a5bf6992 # v3
with:
version: "latest"
enable-cache: true

View File

@@ -8,11 +8,11 @@ on:
jobs:
update_top_ranking_issues:
runs-on: ubuntu-latest
if: github.repository == 'zed-industries/zed'
if: github.repository_owner == 'zed-industries'
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Set up uv
uses: astral-sh/setup-uv@caf0cab7a618c569241d31dcd442f54681755d39 # v3
uses: astral-sh/setup-uv@2e657c127d5b1635d5a8e3fa40e0ac50a5bf6992 # v3
with:
version: "latest"
enable-cache: true

View File

@@ -37,28 +37,28 @@ jobs:
mdbook build ./docs --dest-dir=../target/deploy/docs/
- name: Deploy Docs
uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy target/deploy --project-name=docs
- name: Deploy Install
uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: r2 object put -f script/install.sh zed-open-source-website-assets/install.sh
- name: Deploy Docs Workers
uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy .cloudflare/docs-proxy/src/worker.js
- name: Deploy Install Workers
uses: cloudflare/wrangler-action@6d58852c35a27e6034745c5d0bc373d739014f7f # v3
uses: cloudflare/wrangler-action@05f17c4a695b4d94b57b59997562c6a4624c64e4 # v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

View File

@@ -140,7 +140,7 @@ jobs:
name: Create a Linux *.tar.gz bundle for ARM
if: github.repository_owner == 'zed-industries'
runs-on:
- buildjet-16vcpu-ubuntu-2204-arm
- hosted-linux-arm-1
needs: tests
env:
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}

1
.gitignore vendored
View File

@@ -1,5 +1,4 @@
/.direnv
.envrc
.idea
**/target
**/cargo-target

2222
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,10 +5,8 @@ members = [
"crates/anthropic",
"crates/assets",
"crates/assistant",
"crates/assistant2",
"crates/assistant_slash_command",
"crates/assistant_tool",
"crates/assistant_tools",
"crates/audio",
"crates/auto_update",
"crates/auto_update_ui",
@@ -23,8 +21,7 @@ members = [
"crates/collections",
"crates/command_palette",
"crates/command_palette_hooks",
"crates/context_server",
"crates/context_server_settings",
"crates/context_servers",
"crates/copilot",
"crates/db",
"crates/diagnostics",
@@ -58,16 +55,13 @@ members = [
"crates/install_cli",
"crates/journal",
"crates/language",
"crates/language_extension",
"crates/language_model",
"crates/language_model_selector",
"crates/language_models",
"crates/language_selector",
"crates/language_tools",
"crates/languages",
"crates/livekit_client",
"crates/livekit_client_macos",
"crates/livekit_server",
"crates/live_kit_client",
"crates/live_kit_server",
"crates/lsp",
"crates/markdown",
"crates/markdown_preview",
@@ -122,7 +116,6 @@ members = [
"crates/terminal_view",
"crates/text",
"crates/theme",
"crates/theme_extension",
"crates/theme_importer",
"crates/theme_selector",
"crates/time_format",
@@ -141,8 +134,6 @@ members = [
"crates/worktree",
"crates/zed",
"crates/zed_actions",
"crates/zeta",
"crates/git_ui",
#
# Extensions
@@ -193,10 +184,8 @@ ai = { path = "crates/ai" }
anthropic = { path = "crates/anthropic" }
assets = { path = "crates/assets" }
assistant = { path = "crates/assistant" }
assistant2 = { path = "crates/assistant2" }
assistant_slash_command = { path = "crates/assistant_slash_command" }
assistant_tool = { path = "crates/assistant_tool" }
assistant_tools = { path = "crates/assistant_tools" }
audio = { path = "crates/audio" }
auto_update = { path = "crates/auto_update" }
auto_update_ui = { path = "crates/auto_update_ui" }
@@ -211,8 +200,7 @@ collab_ui = { path = "crates/collab_ui" }
collections = { path = "crates/collections" }
command_palette = { path = "crates/command_palette" }
command_palette_hooks = { path = "crates/command_palette_hooks" }
context_server = { path = "crates/context_server" }
context_server_settings = { path = "crates/context_server_settings" }
context_servers = { path = "crates/context_servers" }
copilot = { path = "crates/copilot" }
db = { path = "crates/db" }
diagnostics = { path = "crates/diagnostics" }
@@ -228,13 +216,10 @@ fs = { path = "crates/fs" }
fsevent = { path = "crates/fsevent" }
fuzzy = { path = "crates/fuzzy" }
git = { path = "crates/git" }
git_ui = { path = "crates/git_ui" }
git_hosting_providers = { path = "crates/git_hosting_providers" }
go_to_line = { path = "crates/go_to_line" }
google_ai = { path = "crates/google_ai" }
gpui = { path = "crates/gpui", default-features = false, features = [
"http_client",
] }
gpui = { path = "crates/gpui", default-features = false, features = ["http_client"]}
gpui_macros = { path = "crates/gpui_macros" }
html_to_markdown = { path = "crates/html_to_markdown" }
http_client = { path = "crates/http_client" }
@@ -245,16 +230,13 @@ inline_completion_button = { path = "crates/inline_completion_button" }
install_cli = { path = "crates/install_cli" }
journal = { path = "crates/journal" }
language = { path = "crates/language" }
language_extension = { path = "crates/language_extension" }
language_model = { path = "crates/language_model" }
language_model_selector = { path = "crates/language_model_selector" }
language_models = { path = "crates/language_models" }
language_selector = { path = "crates/language_selector" }
language_tools = { path = "crates/language_tools" }
languages = { path = "crates/languages" }
livekit_client = { path = "crates/livekit_client" }
livekit_client_macos = { path = "crates/livekit_client_macos" }
livekit_server = { path = "crates/livekit_server" }
live_kit_client = { path = "crates/live_kit_client" }
live_kit_server = { path = "crates/live_kit_server" }
lsp = { path = "crates/lsp" }
markdown = { path = "crates/markdown" }
markdown_preview = { path = "crates/markdown_preview" }
@@ -310,7 +292,6 @@ terminal = { path = "crates/terminal" }
terminal_view = { path = "crates/terminal_view" }
text = { path = "crates/text" }
theme = { path = "crates/theme" }
theme_extension = { path = "crates/theme_extension" }
theme_importer = { path = "crates/theme_importer" }
theme_selector = { path = "crates/theme_selector" }
time_format = { path = "crates/time_format" }
@@ -328,7 +309,6 @@ workspace = { path = "crates/workspace" }
worktree = { path = "crates/worktree" }
zed = { path = "crates/zed" }
zed_actions = { path = "crates/zed_actions" }
zeta = { path = "crates/zeta" }
#
# External crates
@@ -339,7 +319,7 @@ alacritty_terminal = { git = "https://github.com/alacritty/alacritty", rev = "91
any_vec = "0.14"
anyhow = "1.0.86"
arrayvec = { version = "0.7.4", features = ["serde"] }
ashpd = { version = "0.10", default-features = false, features = ["async-std"]}
ashpd = "0.9.1"
async-compat = "0.2.1"
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
async-dispatcher = "0.1"
@@ -362,6 +342,7 @@ cargo_metadata = "0.19"
cargo_toml = "0.20"
chrono = { version = "0.4", features = ["serde"] }
clap = { version = "4.4", features = ["derive"] }
clickhouse = "0.11.6"
cocoa = "0.26"
cocoa-foundation = "0.2.0"
convert_case = "0.6.0"
@@ -387,23 +368,20 @@ heed = { version = "0.20.1", features = ["read-txn-no-tls"] }
hex = "0.4.3"
html5ever = "0.27.0"
hyper = "0.14"
http = "1.1"
ignore = "0.4.22"
image = "0.25.1"
indexmap = { version = "1.6.2", features = ["serde"] }
indoc = "2"
itertools = "0.13.0"
jsonwebtoken = "9.3"
jupyter-protocol = { version = "0.5.0" }
jupyter-websocket-client = { version = "0.8.0" }
jupyter-protocol = { version = "0.2.0" }
jupyter-websocket-client = { version = "0.4.1" }
libc = "0.2"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
linkify = "0.10.0"
livekit = { git = "https://github.com/zed-industries/rust-sdks", rev="799f10133d93ba2a88642cd480d01ec4da53408c", features = ["dispatcher", "services-dispatcher", "rustls-tls-native-roots"], default-features = false }
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
markup5ever_rcdom = "0.3.0"
nanoid = "0.4"
nbformat = { version = "0.9.0" }
nbformat = "0.6.0"
nix = "0.29"
num-format = "0.4.4"
once_cell = "1.19.0"
@@ -413,10 +391,10 @@ parking_lot = "0.12.1"
pathdiff = "0.2"
pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" }
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
profiling = "1"
@@ -437,7 +415,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "fd110f
"stream",
] }
rsa = "0.9.6"
runtimelib = { version = "0.24.0", default-features = false, features = [
runtimelib = { version = "0.21.0", default-features = false, features = [
"async-dispatcher-runtime",
] }
rustc-demangle = "0.1.23"
@@ -509,7 +487,7 @@ unindent = "0.1.7"
unicode-segmentation = "1.10"
unicode-script = "0.5.7"
url = "2.2"
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
wasmparser = "0.215"
wasm-encoder = "0.215"
wasmtime = { version = "24", default-features = false, features = [
@@ -578,10 +556,6 @@ features = [
"Win32_UI_WindowsAndMessaging",
]
# TODO livekit https://github.com/RustAudio/cpal/pull/891
[patch.crates-io]
cpal = { git = "https://github.com/zed-industries/cpal", rev = "fd8bc2fd39f1f5fdee5a0690656caff9a26d9d50" }
[profile.dev]
split-debuginfo = "unpacked"
debug = "limited"
@@ -684,7 +658,6 @@ new_ret_no_self = { level = "allow" }
# We have a few `next` functions that differ in lifetimes
# compared to Iterator::next. Yet, clippy complains about those.
should_implement_trait = { level = "allow" }
let_underscore_future = "allow"
[workspace.metadata.cargo-machete]
ignored = ["bindgen", "cbindgen", "prost_build", "serde"]

View File

@@ -1,5 +1 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 20H16C14.9391 20 13.9217 19.6629 13.1716 19.0627C12.4214 18.4626 12 17.6487 12 16.8V7.2C12 6.35131 12.4214 5.53737 13.1716 4.93726C13.9217 4.33714 14.9391 4 16 4H17" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 20H8C9.06087 20 10.0783 19.5786 10.8284 18.8284C11.5786 18.0783 12 17.0609 12 16V15" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 4H8C9.06087 4 10.0783 4.42143 10.8284 5.17157C11.5786 5.92172 12 6.93913 12 8V9" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-text-cursor"><path d="M17 22h-1a4 4 0 0 1-4-4V6a4 4 0 0 1 4-4h1"/><path d="M7 22h1a4 4 0 0 0 4-4v-1"/><path d="M7 2h1a4 4 0 0 1 4 4v1"/></svg>

Before

Width:  |  Height:  |  Size: 715 B

After

Width:  |  Height:  |  Size: 345 B

View File

@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eraser">
<path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/>
<path d="M22 21H7"/><path d="m5 11 9 9"/>
</svg>

Before

Width:  |  Height:  |  Size: 365 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-diff"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M9 10h6"/><path d="M12 13V7"/><path d="M9 17h6"/></svg>

Before

Width:  |  Height:  |  Size: 348 B

View File

@@ -1,8 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.5 6.66666V8.66666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.5 5V11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.5 3V13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.5 5.33334V10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.5 4V12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5 6.66666V8.66666" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.20721 10.8551C7.67694 10.8551 6.33401 11.0424 5.52243 11.1878C5.15088 11.2543 4.808 10.9114 4.87454 10.5399C5.01987 9.72825 5.20721 8.38532 5.20721 6.85505C5.20721 5.69375 5.09932 4.64034 4.98377 3.84516C4.9431 3.56522 5.40267 3.3722 5.5684 3.60145C6.30333 4.61809 7.44022 6.08806 8.70721 7.35505C9.9742 8.62204 11.4442 9.75893 12.4608 10.4939C12.69 10.6596 12.497 11.1192 12.2171 11.0785C11.4219 10.963 10.3685 10.8551 9.20721 10.8551Z" fill="black" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.11129 10.6816L5.28324 8.85355C5.08798 8.65829 4.7714 8.65829 4.57613 8.85355L3.35355 10.0761C3.15829 10.2714 3.15829 10.588 3.35355 10.7832L5.1816 12.6113C5.37686 12.8066 5.69345 12.8066 5.88871 12.6113L7.11129 11.3887C7.30655 11.1934 7.30655 10.8769 7.11129 10.6816Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 751 B

After

Width:  |  Height:  |  Size: 946 B

View File

@@ -1,5 +0,0 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 3L8.5 10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 6.5H12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 13H12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 415 B

View File

@@ -34,7 +34,6 @@
"dat": "storage",
"db": "storage",
"dbf": "storage",
"diff": "diff",
"dll": "storage",
"doc": "document",
"docx": "document",
@@ -59,11 +58,6 @@
"gitignore": "vcs",
"gitkeep": "vcs",
"gitmodules": "vcs",
"TAG_EDITMSG": "vcs",
"MERGE_MSG": "vcs",
"COMMIT_EDITMSG": "vcs",
"NOTES_EDITMSG": "vcs",
"EDIT_DESCRIPTION": "vcs",
"gleam": "gleam",
"go": "go",
"gql": "graphql",
@@ -113,13 +107,11 @@
"mdf": "storage",
"mdx": "document",
"metadata": "code",
"metal": "metal",
"mjs": "javascript",
"mka": "audio",
"mkv": "video",
"ml": "ocaml",
"mli": "ocaml",
"mod": "go",
"mov": "video",
"mp3": "audio",
"mp4": "video",
@@ -135,7 +127,6 @@
"ogg": "audio",
"opus": "audio",
"otf": "font",
"pcss": "css",
"pdb": "storage",
"pdf": "document",
"php": "php",
@@ -182,9 +173,6 @@
"tsx": "react",
"ttf": "font",
"txt": "document",
"v": "v",
"vsh": "v",
"vv": "v",
"vue": "vue",
"wav": "audio",
"webm": "video",
@@ -193,7 +181,6 @@
"wmv": "video",
"woff": "font",
"woff2": "font",
"work": "go",
"wv": "audio",
"xls": "document",
"xlsx": "document",
@@ -248,9 +235,6 @@
"default": {
"icon": "icons/file_icons/file.svg"
},
"diff": {
"icon": "icons/file_icons/diff.svg"
},
"docker": {
"icon": "icons/file_icons/docker.svg"
},
@@ -323,9 +307,6 @@
"lua": {
"icon": "icons/file_icons/lua.svg"
},
"metal": {
"icon": "icons/file_icons/metal.svg"
},
"nim": {
"icon": "icons/file_icons/nim.svg"
},
@@ -398,9 +379,6 @@
"typescript": {
"icon": "icons/file_icons/typescript.svg"
},
"v": {
"icon": "icons/file_icons/v.svg"
},
"vcs": {
"icon": "icons/file_icons/git.svg"
},

View File

@@ -1 +0,0 @@
<svg width="16" height="16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.56 4.502 3.25 3.027V11.5h1.5V6.973l2.69 3.025 1.31 1.475V7.918l3.306 3.582h2.042L8.55 5.491 7.25 4.081V7.528L4.56 4.502Z" fill="#000"/></svg>

Before

Width:  |  Height:  |  Size: 269 B

View File

@@ -1,4 +0,0 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.5" d="M10.0469 12.8661L13.3884 3.31889C13.4386 3.1754 13.3167 3.03055 13.1667 3.05554L10.7292 3.46179C10.5875 3.48542 10.4693 3.58324 10.4197 3.71807L7.24789 12.3271C7.12763 12.6536 7.36919 13 7.71706 13H9.8581C9.94309 13 10.0188 12.9463 10.0469 12.8661Z" fill="black"/>
<path d="M6.90625 12.7321L3.61161 3.31889C3.56139 3.1754 3.6833 3.03055 3.83326 3.05554L6.27076 3.46179C6.4125 3.48542 6.53067 3.58324 6.58034 3.71807L9.90084 12.7309C9.94895 12.8614 9.85232 13 9.71317 13H7.28379C7.11381 13 6.9624 12.8926 6.90625 12.7321Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 663 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-git-branch"><line x1="6" x2="6" y1="3" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>

Before

Width:  |  Height:  |  Size: 348 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-globe"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>

Before

Width:  |  Height:  |  Size: 327 B

View File

@@ -1,12 +0,0 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2131_1193)">
<circle cx="7" cy="7" r="6" stroke="black" stroke-width="1.5"/>
<path d="M6 10H7M8 10H7M7 10V7.1C7 7.04477 6.95523 7 6.9 7H6" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="7" cy="4.5" r="1" fill="black"/>
</g>
<defs>
<clipPath id="clip0_2131_1193">
<rect width="14" height="14" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 479 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-left"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M9 3v18"/></svg>

Before

Width:  |  Height:  |  Size: 289 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-panel-right"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M15 3v18"/></svg>

Before

Width:  |  Height:  |  Size: 291 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-dot"><rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="12" cy="12" r="1"/></svg>

Before

Width:  |  Height:  |  Size: 301 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-minus"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/></svg>

Before

Width:  |  Height:  |  Size: 291 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-plus"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M8 12h8"/><path d="M12 8v8"/></svg>

Before

Width:  |  Height:  |  Size: 309 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-down"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"/></svg>

Before

Width:  |  Height:  |  Size: 405 B

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-thumbs-up"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>

Before

Width:  |  Height:  |  Size: 404 B

View File

@@ -1,3 +1,3 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.5 3L3 12H14L8.5 3Z" fill="black"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 4L4 12H12L8 4Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 150 B

After

Width:  |  Height:  |  Size: 155 B

View File

@@ -1,3 +1,3 @@
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 4.5L12 11.5M12 4.5L5 11.5" stroke="black" stroke-width="2" stroke-linecap="round"/>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 4L12 12M12 4L4 12" stroke="currentColor" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 199 B

After

Width:  |  Height:  |  Size: 177 B

View File

@@ -1,4 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 8.9V11C5.93097 11 5.06903 11 3 11V10.4L8 5.6V5H3V7.1" stroke="black" stroke-width="1.5"/>
<path d="M11 5L13 8L11 11" stroke="black" stroke-width="1.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 268 B

View File

@@ -108,9 +108,7 @@
"ctrl-'": "editor::ToggleHunkDiff",
"ctrl-\"": "editor::ExpandAllHunkDiffs",
"ctrl-i": "editor::ShowSignatureHelp",
"alt-g b": "editor::ToggleGitBlame",
"menu": "editor::OpenContextMenu",
"shift-f10": "editor::OpenContextMenu"
"alt-g b": "editor::ToggleGitBlame"
}
},
{
@@ -407,7 +405,7 @@
"ctrl-shift-p": "command_palette::Toggle",
"f1": "command_palette::Toggle",
"ctrl-shift-m": "diagnostics::Deploy",
"ctrl-shift-e": "pane::RevealInProjectPanel",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-b": "outline_panel::ToggleFocus",
"ctrl-?": "assistant::ToggleFocus",
"ctrl-alt-s": "workspace::SaveAll",
@@ -468,21 +466,13 @@
},
{
"context": "Editor && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && !inline_completion && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && inline_completion",
"use_key_equivalents": true,
"context": "Editor && inline_completion && !showing_completions",
"bindings": {
"tab": "editor::AcceptInlineCompletion"
}
@@ -604,7 +594,6 @@
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-ctrl-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrev",
@@ -660,16 +649,11 @@
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "FileFinder",
"bindings": {
"ctrl": "file_finder::ToggleMenu"
}
},
{
"context": "FileFinder && !menu_open",
"bindings": {
"ctrl-shift-p": "file_finder::SelectPrev",
"ctrl": "file_finder::OpenMenu",
"ctrl-j": "pane::SplitDown",
"ctrl-k": "pane::SplitUp",
"ctrl-h": "pane::SplitLeft",

View File

@@ -1,7 +1,6 @@
[
// Standard macOS bindings
{
"use_key_equivalents": true,
"bindings": {
"up": "menu::SelectPrev",
"shift-tab": "menu::SelectPrev",
@@ -41,7 +40,6 @@
},
{
"context": "Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "editor::Cancel",
"backspace": "editor::Backspace",
@@ -66,7 +64,6 @@
"cmd-v": "editor::Paste",
"cmd-z": "editor::Undo",
"cmd-shift-z": "editor::Redo",
"ctrl-shift-z": "zeta::RateCompletions",
"up": "editor::MoveUp",
"ctrl-up": "editor::MoveToStartOfParagraph",
"pageup": "editor::MovePageUp",
@@ -96,6 +93,8 @@
"ctrl-e": "editor::MoveToEndOfLine",
"cmd-up": "editor::MoveToBeginning",
"cmd-down": "editor::MoveToEnd",
"ctrl-home": "editor::MoveToBeginning",
"ctrl-end": "editor::MoveToEnd",
"shift-up": "editor::SelectUp",
"ctrl-shift-p": "editor::SelectUp",
"shift-down": "editor::SelectDown",
@@ -134,7 +133,6 @@
},
{
"context": "Editor && mode == full",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::Newline",
"shift-enter": "editor::Newline",
@@ -152,23 +150,20 @@
},
{
"context": "Editor && mode == full && inline_completion",
"use_key_equivalents": true,
"bindings": {
"alt-tab": "editor::NextInlineCompletion",
"alt-shift-tab": "editor::PreviousInlineCompletion",
"alt-]": "editor::NextInlineCompletion",
"alt-[": "editor::PreviousInlineCompletion",
"ctrl-right": "editor::AcceptPartialInlineCompletion"
}
},
{
"context": "Editor && !inline_completion",
"use_key_equivalents": true,
"bindings": {
"alt-tab": "editor::ShowInlineCompletion"
"alt-\\": "editor::ShowInlineCompletion"
}
},
{
"context": "Editor && mode == auto_height",
"use_key_equivalents": true,
"bindings": {
"ctrl-enter": "editor::Newline",
"shift-enter": "editor::Newline",
@@ -177,14 +172,12 @@
},
{
"context": "Markdown",
"use_key_equivalents": true,
"bindings": {
"cmd-c": "markdown::Copy"
}
},
{
"context": "Editor && jupyter && !ContextEditor",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-enter": "repl::Run",
"ctrl-alt-enter": "repl::RunInPlace"
@@ -192,7 +185,6 @@
},
{
"context": "AssistantPanel",
"use_key_equivalents": true,
"bindings": {
"cmd-k c": "assistant::CopyCode",
"cmd-g": "search::SelectNextMatch",
@@ -205,7 +197,6 @@
},
{
"context": "ContextEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "assistant::Assist",
"cmd-shift-enter": "assistant::Edit",
@@ -218,24 +209,8 @@
"alt-enter": "editor::Newline"
}
},
{
"context": "AssistantPanel2",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "assistant2::NewThread",
"cmd-shift-h": "assistant2::OpenHistory"
}
},
{
"context": "MessageEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "assistant2::Chat"
}
},
{
"context": "PromptLibrary",
"use_key_equivalents": true,
"bindings": {
"cmd-n": "prompt_library::NewPrompt",
"cmd-shift-s": "prompt_library::ToggleDefaultPrompt",
@@ -244,7 +219,6 @@
},
{
"context": "BufferSearchBar",
"use_key_equivalents": true,
"bindings": {
"escape": "buffer_search::Dismiss",
"tab": "buffer_search::FocusEditor",
@@ -258,7 +232,6 @@
},
{
"context": "BufferSearchBar && in_replace > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "search::ReplaceNext",
"cmd-enter": "search::ReplaceAll"
@@ -266,7 +239,6 @@
},
{
"context": "BufferSearchBar && !in_replace > Editor",
"use_key_equivalents": true,
"bindings": {
"up": "search::PreviousHistoryQuery",
"down": "search::NextHistoryQuery"
@@ -274,7 +246,6 @@
},
{
"context": "ProjectSearchBar",
"use_key_equivalents": true,
"bindings": {
"escape": "project_search::ToggleFocus",
"cmd-shift-j": "project_search::ToggleFilters",
@@ -286,7 +257,6 @@
},
{
"context": "ProjectSearchBar > Editor",
"use_key_equivalents": true,
"bindings": {
"up": "search::PreviousHistoryQuery",
"down": "search::NextHistoryQuery"
@@ -294,7 +264,6 @@
},
{
"context": "ProjectSearchBar && in_replace > Editor",
"use_key_equivalents": true,
"bindings": {
"enter": "search::ReplaceNext",
"cmd-enter": "search::ReplaceAll"
@@ -302,7 +271,6 @@
},
{
"context": "ProjectSearchView",
"use_key_equivalents": true,
"bindings": {
"escape": "project_search::ToggleFocus",
"cmd-shift-j": "project_search::ToggleFilters",
@@ -313,7 +281,6 @@
},
{
"context": "Pane",
"use_key_equivalents": true,
"bindings": {
"cmd-{": "pane::ActivatePrevItem",
"cmd-}": "pane::ActivateNextItem",
@@ -342,7 +309,6 @@
// Bindings from VS Code
{
"context": "Editor",
"use_key_equivalents": true,
"bindings": {
"cmd-[": "editor::Outdent",
"cmd-]": "editor::Indent",
@@ -377,7 +343,7 @@
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
"alt-shift-f12": "editor::FindAllReferences",
"ctrl-m": "editor::MoveToEnclosingBracket",
"cmd-|": "editor::MoveToEnclosingBracket",
"cmd-shift-\\": "editor::MoveToEnclosingBracket",
"alt-cmd-[": "editor::Fold",
"alt-cmd-]": "editor::UnfoldLines",
"cmd-k cmd-l": "editor::ToggleFold",
@@ -406,7 +372,6 @@
},
{
"context": "Editor && mode == full",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-o": "outline::Toggle",
"ctrl-g": "go_to_line::Toggle"
@@ -414,7 +379,6 @@
},
{
"context": "Pane",
"use_key_equivalents": true,
"bindings": {
"ctrl-1": ["pane::ActivateItem", 0],
"ctrl-2": ["pane::ActivateItem", 1],
@@ -434,7 +398,6 @@
},
{
"context": "Workspace",
"use_key_equivalents": true,
"bindings": {
// Change the default action on `menu::Confirm` by setting the parameter
// "alt-cmd-o": ["projects::OpenRecent", {"create_new_window": true }],
@@ -471,7 +434,7 @@
"ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }],
"cmd-shift-p": "command_palette::Toggle",
"cmd-shift-m": "diagnostics::Deploy",
"cmd-shift-e": "pane::RevealInProjectPanel",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-shift-b": "outline_panel::ToggleFocus",
"cmd-?": "assistant::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll",
@@ -490,7 +453,6 @@
},
{
"context": "Workspace && !Terminal",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-r": "task::Spawn",
"cmd-alt-r": "task::Rerun",
@@ -501,7 +463,6 @@
// Bindings from Sublime Text
{
"context": "Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-j": "editor::JoinLines",
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
@@ -521,7 +482,6 @@
// Bindings from Atom
{
"context": "Pane",
"use_key_equivalents": true,
"bindings": {
"cmd-k up": "pane::SplitUp",
"cmd-k down": "pane::SplitDown",
@@ -532,42 +492,31 @@
// Bindings that should be unified with bindings for more general actions
{
"context": "Editor && renaming",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmRename"
}
},
{
"context": "Editor && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion"
}
},
{
"context": "Editor && !inline_completion && showing_completions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCompletion",
"tab": "editor::ComposeCompletion"
}
},
{
"context": "Editor && inline_completion",
"use_key_equivalents": true,
"context": "Editor && inline_completion && !showing_completions",
"bindings": {
"tab": "editor::AcceptInlineCompletion"
}
},
{
"context": "Editor && showing_code_actions",
"use_key_equivalents": true,
"bindings": {
"enter": "editor::ConfirmCodeAction"
}
},
{
"context": "Editor && (showing_code_actions || showing_completions)",
"use_key_equivalents": true,
"bindings": {
"up": "editor::ContextMenuPrev",
"ctrl-p": "editor::ContextMenuPrev",
@@ -579,7 +528,6 @@
},
// Custom bindings
{
"use_key_equivalents": true,
"bindings": {
"ctrl-alt-cmd-f": "workspace::FollowNextCollaborator",
// TODO: Move this to a dock open action
@@ -590,7 +538,6 @@
},
{
"context": "Editor && mode == full",
"use_key_equivalents": true,
"bindings": {
"alt-enter": "editor::OpenExcerpts",
"shift-enter": "editor::ExpandExcerpts",
@@ -602,7 +549,6 @@
},
{
"context": "ProposedChangesEditor",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-y": "editor::ApplyDiffHunk",
"cmd-shift-a": "editor::ApplyAllDiffHunks"
@@ -610,7 +556,6 @@
},
{
"context": "PromptEditor",
"use_key_equivalents": true,
"bindings": {
"ctrl-[": "assistant::CyclePreviousInlineAssist",
"ctrl-]": "assistant::CycleNextInlineAssist"
@@ -618,14 +563,12 @@
},
{
"context": "ProjectSearchBar && !in_replace",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "project_search::SearchInNew"
}
},
{
"context": "OutlinePanel && not_editing",
"use_key_equivalents": true,
"bindings": {
"escape": "menu::Cancel",
"left": "outline_panel::CollapseSelectedEntry",
@@ -642,7 +585,6 @@
},
{
"context": "ProjectPanel",
"use_key_equivalents": true,
"bindings": {
"left": "project_panel::CollapseSelectedEntry",
"right": "project_panel::ExpandSelectedEntry",
@@ -662,7 +604,6 @@
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
"alt-cmd-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
"cmd-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
@@ -672,14 +613,12 @@
},
{
"context": "ProjectPanel && not_editing",
"use_key_equivalents": true,
"bindings": {
"space": "project_panel::Open"
}
},
{
"context": "CollabPanel && not_editing",
"use_key_equivalents": true,
"bindings": {
"ctrl-backspace": "collab_panel::Remove",
"space": "menu::Confirm"
@@ -687,21 +626,18 @@
},
{
"context": "(CollabPanel && editing) > Editor",
"use_key_equivalents": true,
"bindings": {
"space": "collab_panel::InsertSpace"
}
},
{
"context": "ChannelModal",
"use_key_equivalents": true,
"bindings": {
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "Picker > Editor",
"use_key_equivalents": true,
"bindings": {
"tab": "picker::ConfirmCompletion",
"alt-enter": ["picker::ConfirmInput", { "secondary": false }],
@@ -710,23 +646,15 @@
},
{
"context": "ChannelModal > Picker > Editor",
"use_key_equivalents": true,
"bindings": {
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "FileFinder",
"use_key_equivalents": true,
"bindings": {
"cmd": "file_finder::ToggleMenu"
}
},
{
"context": "FileFinder && !menu_open",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-p": "file_finder::SelectPrev",
"cmd": "file_finder::OpenMenu",
"cmd-j": "pane::SplitDown",
"cmd-k": "pane::SplitUp",
"cmd-h": "pane::SplitLeft",
@@ -735,7 +663,6 @@
},
{
"context": "FileFinder && menu_open",
"use_key_equivalents": true,
"bindings": {
"j": "pane::SplitDown",
"k": "pane::SplitUp",
@@ -745,7 +672,6 @@
},
{
"context": "TabSwitcher",
"use_key_equivalents": true,
"bindings": {
"ctrl-up": "menu::SelectPrev",
"ctrl-down": "menu::SelectNext",
@@ -755,7 +681,6 @@
},
{
"context": "Terminal",
"use_key_equivalents": true,
"bindings": {
"ctrl-cmd-space": "terminal::ShowCharacterPalette",
"cmd-c": "terminal::Copy",
@@ -789,30 +714,7 @@
"cmd-end": "terminal::ScrollToBottom",
"shift-home": "terminal::ScrollToTop",
"shift-end": "terminal::ScrollToBottom",
"ctrl-shift-space": "terminal::ToggleViMode",
"ctrl-k up": "pane::SplitUp",
"ctrl-k down": "pane::SplitDown",
"ctrl-k left": "pane::SplitLeft",
"ctrl-k right": "pane::SplitRight"
}
},
{
"context": "RateCompletionModal",
"use_key_equivalents": true,
"bindings": {
"cmd-enter": "zeta::ThumbsUp",
"shift-down": "zeta::NextEdit",
"shift-up": "zeta::PreviousEdit",
"right": "zeta::PreviewCompletion"
}
},
{
"context": "RateCompletionModal > Editor",
"use_key_equivalents": true,
"bindings": {
"escape": "zeta::FocusCompletions",
"cmd-shift-enter": "zeta::ThumbsUpActiveCompletion",
"cmd-shift-backspace": "zeta::ThumbsDownActiveCompletion"
"ctrl-shift-space": "terminal::ToggleViMode"
}
}
]

View File

@@ -15,10 +15,8 @@
"ctrl-b": "editor::MoveLeft",
"ctrl-n": "editor::MoveDown",
"ctrl-p": "editor::MoveUp",
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
"ctrl-a": "editor::MoveToBeginningOfLine",
"ctrl-e": "editor::MoveToEndOfLine",
"alt-f": "editor::MoveToNextSubwordEnd",
"alt-b": "editor::MoveToPreviousSubwordStart",
"ctrl-d": "editor::Delete",
@@ -55,14 +53,6 @@
"shift shift": "file_finder::Toggle"
}
},
{
"context": "BufferSearchBar > Editor",
"bindings": {
"ctrl-s": "search::SelectNextMatch",
"ctrl-r": "search::SelectPrevMatch",
"ctrl-g": "buffer_search::Dismiss"
}
},
{
"context": "Pane",
"bindings": {

View File

@@ -12,7 +12,7 @@
"ctrl->": "zed::IncreaseBufferFontSize",
"ctrl-<": "zed::DecreaseBufferFontSize",
"ctrl-shift-j": "editor::JoinLines",
"ctrl-d": "editor::DuplicateSelection",
"ctrl-d": "editor::DuplicateLineDown",
"ctrl-y": "editor::DeleteLine",
"ctrl-m": "editor::ScrollCursorCenter",
"ctrl-pagedown": "editor::MovePageDown",

View File

@@ -15,7 +15,7 @@
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
"ctrl-shift-l": "editor::SplitSelectionIntoLines",
"ctrl-shift-a": "editor::SelectLargerSyntaxNode",
"ctrl-shift-d": "editor::DuplicateSelection",
"ctrl-shift-d": "editor::DuplicateLineDown",
"alt-f3": "editor::SelectAllMatches", // find_all_under
"f12": "editor::GoToDefinition",
"ctrl-f12": "editor::GoToDefinitionSplit",

View File

@@ -15,10 +15,8 @@
"ctrl-b": "editor::MoveLeft",
"ctrl-n": "editor::MoveDown",
"ctrl-p": "editor::MoveUp",
"home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
"end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
"ctrl-a": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": false }],
"ctrl-e": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": false }],
"ctrl-a": "editor::MoveToBeginningOfLine",
"ctrl-e": "editor::MoveToEndOfLine",
"alt-f": "editor::MoveToNextSubwordEnd",
"alt-b": "editor::MoveToPreviousSubwordStart",
"ctrl-d": "editor::Delete",
@@ -55,14 +53,6 @@
"shift shift": "file_finder::Toggle"
}
},
{
"context": "BufferSearchBar > Editor",
"bindings": {
"ctrl-s": "search::SelectNextMatch",
"ctrl-r": "search::SelectPrevMatch",
"ctrl-g": "buffer_search::Dismiss"
}
},
{
"context": "Pane",
"bindings": {

View File

@@ -11,7 +11,7 @@
"ctrl->": "zed::IncreaseBufferFontSize",
"ctrl-<": "zed::DecreaseBufferFontSize",
"ctrl-shift-j": "editor::JoinLines",
"cmd-d": "editor::DuplicateSelection",
"cmd-d": "editor::DuplicateLineDown",
"cmd-backspace": "editor::DeleteLine",
"cmd-pagedown": "editor::MovePageDown",
"cmd-pageup": "editor::MovePageUp",

View File

@@ -18,7 +18,7 @@
"ctrl-shift-m": "editor::SelectLargerSyntaxNode",
"cmd-shift-l": "editor::SplitSelectionIntoLines",
"cmd-shift-a": "editor::SelectLargerSyntaxNode",
"cmd-shift-d": "editor::DuplicateSelection",
"cmd-shift-d": "editor::DuplicateLineDown",
"ctrl-cmd-g": "editor::SelectAllMatches", // find_all_under
"shift-f12": "editor::FindAllReferences",
"alt-cmd-down": "editor::GoToDefinition",

View File

@@ -1,6 +1,7 @@
[
{
"context": "VimControl && !menu",
"use_layout_keys": true,
"bindings": {
"i": ["vim::PushOperator", { "Object": { "around": false } }],
"a": ["vim::PushOperator", { "Object": { "around": true } }],
@@ -32,18 +33,6 @@
"(": "vim::SentenceBackward",
")": "vim::SentenceForward",
"|": "vim::GoToColumn",
"] ]": "vim::NextSectionStart",
"] [": "vim::NextSectionEnd",
"[ [": "vim::PreviousSectionStart",
"[ ]": "vim::PreviousSectionEnd",
"] m": "vim::NextMethodStart",
"] M": "vim::NextMethodEnd",
"[ m": "vim::PreviousMethodStart",
"[ M": "vim::PreviousMethodEnd",
"[ *": "vim::PreviousComment",
"[ /": "vim::PreviousComment",
"] *": "vim::NextComment",
"] /": "vim::NextComment",
// Word motions
"w": "vim::NextWordStart",
"e": "vim::NextWordEnd",
@@ -66,10 +55,6 @@
"n": "vim::MoveToNextMatch",
"shift-n": "vim::MoveToPrevMatch",
"%": "vim::Matching",
"] }": ["vim::UnmatchedForward", { "char": "}" }],
"[ {": ["vim::UnmatchedBackward", { "char": "{" }],
"] )": ["vim::UnmatchedForward", { "char": ")" }],
"[ (": ["vim::UnmatchedBackward", { "char": "(" }],
"f": ["vim::PushOperator", { "FindForward": { "before": false } }],
"t": ["vim::PushOperator", { "FindForward": { "before": true } }],
"shift-f": ["vim::PushOperator", { "FindBackward": { "after": false } }],
@@ -187,6 +172,7 @@
},
{
"context": "vim_mode == normal",
"use_layout_keys": true,
"bindings": {
"escape": "editor::Cancel",
"ctrl-[": "editor::Cancel",
@@ -219,7 +205,6 @@
"shift-s": "vim::SubstituteLine",
">": ["vim::PushOperator", "Indent"],
"<": ["vim::PushOperator", "Outdent"],
"=": ["vim::PushOperator", "AutoIndent"],
"g u": ["vim::PushOperator", "Lowercase"],
"g shift-u": ["vim::PushOperator", "Uppercase"],
"g ~": ["vim::PushOperator", "OppositeCase"],
@@ -241,6 +226,7 @@
},
{
"context": "VimControl && VimCount",
"use_layout_keys": true,
"bindings": {
"0": ["vim::Number", 0],
":": "vim::CountCommand"
@@ -248,6 +234,7 @@
},
{
"context": "vim_mode == visual",
"use_layout_keys": true,
"bindings": {
":": "vim::VisualCommand",
"u": "vim::ConvertToLowerCase",
@@ -284,7 +271,6 @@
"ctrl-[": ["vim::SwitchMode", "Normal"],
">": "vim::Indent",
"<": "vim::Outdent",
"=": "vim::AutoIndent",
"i": ["vim::PushOperator", { "Object": { "around": false } }],
"a": ["vim::PushOperator", { "Object": { "around": true } }],
"g c": "vim::ToggleComments",
@@ -297,6 +283,7 @@
},
{
"context": "vim_mode == insert",
"use_layout_keys": true,
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
@@ -321,25 +308,9 @@
"ctrl-o": "vim::TemporaryNormal"
}
},
{
"context": "vim_mode == helix_normal",
"bindings": {
"i": "vim::InsertBefore",
"a": "vim::InsertAfter",
"d": "vim::HelixDelete",
"w": "vim::NextWordStart",
"e": "vim::NextWordEnd",
"b": "vim::PreviousWordStart",
"h": "vim::Left",
"j": "vim::Down",
"k": "vim::Up",
"l": "vim::Right"
}
},
{
"context": "vim_mode == insert && !(showing_code_actions || showing_completions)",
"use_layout_keys": true,
"bindings": {
"ctrl-p": "editor::ShowCompletions",
"ctrl-n": "editor::ShowCompletions"
@@ -347,6 +318,7 @@
},
{
"context": "vim_mode == replace",
"use_layout_keys": true,
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
@@ -364,6 +336,7 @@
},
{
"context": "vim_mode == waiting",
"use_layout_keys": true,
"bindings": {
"tab": "vim::Tab",
"enter": "vim::Enter",
@@ -377,15 +350,16 @@
},
{
"context": "vim_mode == operator",
"use_layout_keys": true,
"bindings": {
"escape": "vim::ClearOperators",
"ctrl-c": "vim::ClearOperators",
"ctrl-[": "vim::ClearOperators",
"g c": "vim::Comment"
"ctrl-[": "vim::ClearOperators"
}
},
{
"context": "vim_operator == a || vim_operator == i || vim_operator == cs",
"use_layout_keys": true,
"bindings": {
"w": "vim::Word",
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
@@ -407,15 +381,12 @@
"shift-b": "vim::CurlyBrackets",
"<": "vim::AngleBrackets",
">": "vim::AngleBrackets",
"a": "vim::Argument",
"i": "vim::IndentObj",
"shift-i": ["vim::IndentObj", { "includeBelow": true }],
"f": "vim::Method",
"c": "vim::Class"
"a": "vim::Argument"
}
},
{
"context": "vim_operator == c",
"use_layout_keys": true,
"bindings": {
"c": "vim::CurrentLine",
"d": "editor::Rename", // zed specific
@@ -424,6 +395,7 @@
},
{
"context": "vim_operator == d",
"use_layout_keys": true,
"bindings": {
"d": "vim::CurrentLine",
"s": ["vim::PushOperator", "DeleteSurrounds"],
@@ -433,6 +405,7 @@
},
{
"context": "vim_operator == gu",
"use_layout_keys": true,
"bindings": {
"g u": "vim::CurrentLine",
"u": "vim::CurrentLine"
@@ -440,6 +413,7 @@
},
{
"context": "vim_operator == gU",
"use_layout_keys": true,
"bindings": {
"g shift-u": "vim::CurrentLine",
"shift-u": "vim::CurrentLine"
@@ -447,6 +421,7 @@
},
{
"context": "vim_operator == g~",
"use_layout_keys": true,
"bindings": {
"g ~": "vim::CurrentLine",
"~": "vim::CurrentLine"
@@ -454,6 +429,7 @@
},
{
"context": "vim_operator == gq",
"use_layout_keys": true,
"bindings": {
"g q": "vim::CurrentLine",
"q": "vim::CurrentLine",
@@ -463,6 +439,7 @@
},
{
"context": "vim_operator == y",
"use_layout_keys": true,
"bindings": {
"y": "vim::CurrentLine",
"s": ["vim::PushOperator", { "AddSurrounds": {} }]
@@ -470,36 +447,35 @@
},
{
"context": "vim_operator == ys",
"use_layout_keys": true,
"bindings": {
"s": "vim::CurrentLine"
}
},
{
"context": "vim_operator == >",
"use_layout_keys": true,
"bindings": {
">": "vim::CurrentLine"
}
},
{
"context": "vim_operator == <",
"use_layout_keys": true,
"bindings": {
"<": "vim::CurrentLine"
}
},
{
"context": "vim_operator == eq",
"bindings": {
"=": "vim::CurrentLine"
}
},
{
"context": "vim_operator == gc",
"use_layout_keys": true,
"bindings": {
"c": "vim::CurrentLine"
}
},
{
"context": "vim_mode == literal",
"use_layout_keys": true,
"bindings": {
"ctrl-@": ["vim::Literal", ["ctrl-@", "\u0000"]],
"ctrl-a": ["vim::Literal", ["ctrl-a", "\u0001"]],
@@ -543,6 +519,7 @@
},
{
"context": "BufferSearchBar && !in_replace",
"use_layout_keys": true,
"bindings": {
"enter": "vim::SearchSubmit",
"escape": "buffer_search::Dismiss"
@@ -550,6 +527,7 @@
},
{
"context": "ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView",
"use_layout_keys": true,
"bindings": {
// window related commands (ctrl-w X)
"ctrl-w": null,
@@ -573,12 +551,6 @@
"ctrl-w shift-l": ["workspace::SwapPaneInDirection", "Right"],
"ctrl-w shift-k": ["workspace::SwapPaneInDirection", "Up"],
"ctrl-w shift-j": ["workspace::SwapPaneInDirection", "Down"],
"ctrl-w >": ["vim::ResizePane", "Widen"],
"ctrl-w <": ["vim::ResizePane", "Narrow"],
"ctrl-w -": ["vim::ResizePane", "Shorten"],
"ctrl-w +": ["vim::ResizePane", "Lengthen"],
"ctrl-w _": "vim::MaximizePane",
"ctrl-w =": "vim::ResetPaneSizes",
"ctrl-w g t": "pane::ActivateNextItem",
"ctrl-w ctrl-g t": "pane::ActivateNextItem",
"ctrl-w g shift-t": "pane::ActivatePrevItem",
@@ -606,6 +578,7 @@
},
{
"context": "EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome",
"use_layout_keys": true,
"bindings": {
":": "command_palette::Toggle",
"g /": "pane::DeploySearch"
@@ -614,6 +587,7 @@
{
// netrw compatibility
"context": "ProjectPanel && not_editing",
"use_layout_keys": true,
"bindings": {
":": "command_palette::Toggle",
"%": "project_panel::NewFile",
@@ -633,12 +607,6 @@
"p": "project_panel::Open",
"x": "project_panel::RevealInFileManager",
"s": "project_panel::OpenWithSystem",
"] c": "project_panel::SelectNextGitEntry",
"[ c": "project_panel::SelectPrevGitEntry",
"] d": "project_panel::SelectNextDiagnostic",
"[ d": "project_panel::SelectPrevDiagnostic",
"}": "project_panel::SelectNextDirectory",
"{": "project_panel::SelectPrevDirectory",
"shift-g": "menu::SelectLast",
"g g": "menu::SelectFirst",
"-": "project_panel::SelectParent",
@@ -647,6 +615,7 @@
},
{
"context": "OutlinePanel && not_editing",
"use_layout_keys": true,
"bindings": {
"j": "menu::SelectNext",
"k": "menu::SelectPrev",

View File

@@ -144,15 +144,15 @@
// 4. Highlight the full line (default):
// "all"
"current_line_highlight": "all",
// The debounce delay before querying highlights from the language
// server based on the current cursor location.
"lsp_highlight_debounce": 75,
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Whether to display inline and alongside documentation for items in the
// completions menu
"show_completion_documentation": true,
// The debounce delay before re-querying the language server for completion
// documentation when not included in original completion list.
"completion_documentation_secondary_query_debounce": 300,
// Show method signatures in the editor, when inside parentheses.
"auto_signature_help": false,
/// Whether to show the signature help after completion or a bracket pair inserted.
@@ -300,8 +300,6 @@
"scroll_beyond_last_line": "one_page",
// The number of lines to keep above/below the cursor when scrolling.
"vertical_scroll_margin": 3,
// Whether to scroll when clicking near the edge of the visible text area.
"autoscroll_on_clicks": false,
// Scroll sensitivity multiplier. This multiplier is applied
// to both the horizontal and vertical delta values while scrolling.
"scroll_sensitivity": 1.0,
@@ -474,14 +472,6 @@
// Default width of the chat panel.
"default_width": 240
},
"git_panel": {
// Whether to show the git panel button in the status bar.
"button": true,
// Where to the git panel. Can be 'left' or 'right'.
"dock": "left",
// Default width of the git panel.
"default_width": 360
},
"message_editor": {
// Whether to automatically replace emoji shortcodes with emoji characters.
// For example: typing `:wave:` gets replaced with `👋`.
@@ -500,6 +490,9 @@
"version": "2",
// Whether the assistant is enabled.
"enabled": true,
// Whether to show inline hints showing the keybindings to use the inline assistant and the
// assistant panel.
"show_hints": true,
// Whether to show the assistant panel button in the status bar.
"button": true,
// Where to dock the assistant panel. Can be 'left', 'right' or 'bottom'.
@@ -567,28 +560,13 @@
"close_position": "right",
// Whether to show the file icon for a tab.
"file_icons": false,
// Whether to always show the close button on tabs.
"always_show_close_button": false,
// What to do after closing the current tab.
//
// 1. Activate the tab that was open previously (default)
// "history"
// 2. Activate the right neighbour tab if present
// "neighbour"
// 3. Activate the left neighbour tab if present
// "left_neighbour"
"activate_on_close": "history",
/// Which files containing diagnostic errors/warnings to mark in the tabs.
/// Diagnostics are only shown when file icons are also active.
/// This setting only works when can take the following three values:
///
/// 1. Do not mark any files:
/// "off"
/// 2. Only mark files with errors:
/// "errors"
/// 3. Mark files with errors and warnings:
/// "all"
"show_diagnostics": "off"
// "History"
// 2. Activate the neighbour tab (prefers the right one, if present)
// "Neighbour"
"activate_on_close": "history"
},
// Settings related to preview tabs.
"preview_tabs": {
@@ -695,7 +673,6 @@
"**/.git",
"**/.svn",
"**/.hg",
"**/.jj",
"**/CVS",
"**/.DS_Store",
"**/Thumbs.db",
@@ -706,7 +683,10 @@
// ignored by git. This is useful for files that are not tracked by git,
// but are still important to your project. Note that globs that are
// overly broad can slow down Zed's file scanning. Overridden by `file_scan_exclusions`.
"file_scan_inclusions": [".env*"],
"file_scan_inclusions": [
".env*",
"docker-compose.*.yml"
],
// Git gutter behavior configuration.
"git": {
// Control whether the git gutter is shown. May take 2 values:
@@ -908,7 +888,7 @@
//
"file_types": {
"Plain Text": ["txt"],
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json", "**/.vscode/**/*.json"],
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json", "**/Zed/**/*.json"],
"Shell Script": [".env.*"]
},
/// By default use a recent system version of node, or install our own.
@@ -1151,7 +1131,6 @@
"use_system_clipboard": "always",
"use_multiline_find": false,
"use_smartcase_find": false,
"highlight_on_yank_duration": 200,
"custom_digraphs": {}
},
// The server to connect to. If the environment variable
@@ -1209,8 +1188,6 @@
// "W": "workspace::Save"
// }
"command_aliases": {},
// Whether to show user picture in titlebar.
"show_user_picture": true,
// ssh_connections is an array of ssh connections.
// You can configure these from `project: Open Remote` in the command palette.
// Zed's ssh support will pull configuration from your ~/.ssh too.

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"name": "Andromeda",
"author": "Zed Industries",
"themes": [
@@ -46,7 +46,7 @@
"tab.active_background": "#1e2025ff",
"search.match_background": "#11a79366",
"panel.background": "#21242bff",
"panel.focused_border": "#10a793ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar.thumb.background": "#f7f7f84c",
"scrollbar.thumb.hover_background": "#252931ff",

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"name": "Atelier",
"author": "Zed Industries",
"themes": [
@@ -46,7 +46,7 @@
"tab.active_background": "#19171cff",
"search.match_background": "#576dda66",
"panel.background": "#221f26ff",
"panel.focused_border": "#566ddaff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar.thumb.background": "#efecf44c",
"scrollbar.thumb.hover_background": "#332f38ff",
@@ -431,7 +431,7 @@
"tab.active_background": "#efecf4ff",
"search.match_background": "#586dda66",
"panel.background": "#e6e3ebff",
"panel.focused_border": "#586cdaff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar.thumb.background": "#19171c4c",
"scrollbar.thumb.hover_background": "#cbc8d1ff",

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"name": "Ayu",
"author": "Zed Industries",
"themes": [
@@ -46,7 +46,7 @@
"tab.active_background": "#0d1016ff",
"search.match_background": "#5ac2fe66",
"panel.background": "#1f2127ff",
"panel.focused_border": "#5ac1feff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar.thumb.background": "#bfbdb64c",
"scrollbar.thumb.hover_background": "#2d2f34ff",
@@ -416,7 +416,7 @@
"tab.active_background": "#fcfcfcff",
"search.match_background": "#3b9ee566",
"panel.background": "#ececedff",
"panel.focused_border": "#3b9ee5ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar.thumb.background": "#5c61664c",
"scrollbar.thumb.hover_background": "#dfe0e1ff",

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"name": "Gruvbox",
"author": "Zed Industries",
"themes": [
@@ -55,7 +55,7 @@
"tab.active_background": "#282828ff",
"search.match_background": "#83a59866",
"panel.background": "#3a3735ff",
"panel.focused_border": "#83a598ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar.thumb.background": "#fbf1c74c",
"scrollbar.thumb.hover_background": "#494340ff",
@@ -439,7 +439,7 @@
"tab.active_background": "#1d2021ff",
"search.match_background": "#83a59866",
"panel.background": "#393634ff",
"panel.focused_border": "#83a598ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar.thumb.background": "#fbf1c74c",
"scrollbar.thumb.hover_background": "#494340ff",

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"name": "One",
"author": "Zed Industries",
"themes": [

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"name": "Rosé Pine",
"author": "Zed Industries",
"themes": [
@@ -46,7 +46,7 @@
"tab.active_background": "#191724ff",
"search.match_background": "#57949f66",
"panel.background": "#1c1b2aff",
"panel.focused_border": "#9bced6ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar.thumb.background": "#e0def44c",
"scrollbar.thumb.hover_background": "#232132ff",
@@ -426,7 +426,7 @@
"tab.active_background": "#faf4edff",
"search.match_background": "#9cced766",
"panel.background": "#fef9f2ff",
"panel.focused_border": "#57949fff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar.thumb.background": "#5752794c",
"scrollbar.thumb.hover_background": "#e5e0dfff",
@@ -806,7 +806,7 @@
"tab.active_background": "#232136ff",
"search.match_background": "#9cced766",
"panel.background": "#28253cff",
"panel.focused_border": "#9bced6ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar.thumb.background": "#e0def44c",
"scrollbar.thumb.hover_background": "#322f48ff",

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"name": "Sandcastle",
"author": "Zed Industries",
"themes": [
@@ -46,7 +46,7 @@
"tab.active_background": "#282c33ff",
"search.match_background": "#528b8b66",
"panel.background": "#2b3038ff",
"panel.focused_border": "#518b8bff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar.thumb.background": "#fdf4c14c",
"scrollbar.thumb.hover_background": "#313741ff",

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"name": "Solarized",
"author": "Zed Industries",
"themes": [
@@ -46,7 +46,7 @@
"tab.active_background": "#002a35ff",
"search.match_background": "#288bd166",
"panel.background": "#04313bff",
"panel.focused_border": "#278ad1ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar.thumb.background": "#fdf6e34c",
"scrollbar.thumb.hover_background": "#053541ff",
@@ -416,7 +416,7 @@
"tab.active_background": "#fdf6e3ff",
"search.match_background": "#298bd166",
"panel.background": "#f3eddaff",
"panel.focused_border": "#288bd1ff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar.thumb.background": "#002a354c",
"scrollbar.thumb.hover_background": "#dcdacbff",

View File

@@ -1,5 +1,5 @@
{
"$schema": "https://zed.dev/schema/themes/v0.2.0.json",
"$schema": "https://zed.dev/schema/themes/v0.1.0.json",
"name": "Summercamp",
"author": "Zed Industries",
"themes": [
@@ -46,7 +46,7 @@
"tab.active_background": "#1b1810ff",
"search.match_background": "#499bef66",
"panel.background": "#231f16ff",
"panel.focused_border": "#499befff",
"panel.focused_border": null,
"pane.focused_border": null,
"scrollbar.thumb.background": "#f8f5de4c",
"scrollbar.thumb.hover_background": "#29251bff",

View File

@@ -33,7 +33,7 @@ client.workspace = true
clock.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
context_server.workspace = true
context_servers.workspace = true
db.workspace = true
editor.workspace = true
feature_flags.workspace = true
@@ -50,7 +50,6 @@ indexed_docs.workspace = true
indoc.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
language_models.workspace = true
log.workspace = true
lsp.workspace = true

View File

@@ -5,6 +5,7 @@ pub mod assistant_settings;
mod context;
pub mod context_store;
mod inline_assistant;
mod model_selector;
mod patch;
mod prompt_library;
mod prompts;
@@ -14,12 +15,16 @@ pub mod slash_command_settings;
mod slash_command_working_set;
mod streaming_diff;
mod terminal_inline_assistant;
mod tool_working_set;
mod tools;
use crate::slash_command::project_command::ProjectSlashCommandFeatureFlag;
pub use crate::slash_command_working_set::{SlashCommandId, SlashCommandWorkingSet};
pub use crate::tool_working_set::{ToolId, ToolWorkingSet};
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
use assistant_settings::AssistantSettings;
use assistant_slash_command::SlashCommandRegistry;
use assistant_tool::ToolRegistry;
use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter;
pub use context::*;
@@ -28,10 +33,12 @@ use feature_flags::FeatureFlagAppExt;
use fs::Fs;
use gpui::impl_actions;
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
use indexed_docs::IndexedDocsRegistry;
pub(crate) use inline_assistant::*;
use language_model::{
LanguageModelId, LanguageModelProviderId, LanguageModelRegistry, LanguageModelResponseMessage,
};
pub(crate) use model_selector::*;
pub use patch::*;
pub use prompts::PromptBuilder;
use prompts::PromptLoadingParams;
@@ -242,7 +249,7 @@ pub fn init(
assistant_slash_command::init(cx);
assistant_tool::init(cx);
assistant_panel::init(cx);
context_server::init(cx);
context_servers::init(cx);
let prompt_builder = prompts::PromptBuilder::new(Some(PromptLoadingParams {
fs: fs.clone(),
@@ -255,6 +262,7 @@ pub fn init(
.map(Arc::new)
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
register_slash_commands(Some(prompt_builder.clone()), cx);
register_tools(cx);
inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
@@ -267,7 +275,7 @@ pub fn init(
client.telemetry().clone(),
cx,
);
indexed_docs::init(cx);
IndexedDocsRegistry::init_global(cx);
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(Assistant::NAMESPACE);
@@ -342,7 +350,8 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
slash_command_registry.register_command(terminal_command::TerminalSlashCommand, true);
slash_command_registry.register_command(now_command::NowSlashCommand, false);
slash_command_registry.register_command(diagnostics_command::DiagnosticsSlashCommand, true);
slash_command_registry.register_command(fetch_command::FetchSlashCommand, true);
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
if let Some(prompt_builder) = prompt_builder {
cx.observe_flag::<project_command::ProjectSlashCommandFeatureFlag, _>({
@@ -417,6 +426,11 @@ fn update_slash_commands_from_settings(cx: &mut AppContext) {
}
}
fn register_tools(cx: &mut AppContext) {
let tool_registry = ToolRegistry::global(cx);
tool_registry.register_tool(tools::now_tool::NowTool);
}
pub fn humanize_token_count(count: usize) -> String {
match count {
0..=999 => count.to_string(),

View File

@@ -1,5 +1,6 @@
use crate::slash_command::file_command::codeblock_fence_for_path;
use crate::slash_command_working_set::SlashCommandWorkingSet;
use crate::ToolWorkingSet;
use crate::{
assistant_settings::{AssistantDockPosition, AssistantSettings},
humanize_token_count,
@@ -16,13 +17,12 @@ use crate::{
ContextEvent, ContextId, ContextStore, ContextStoreEvent, CopyCode, CycleMessageRole,
DeployHistory, DeployPromptLibrary, Edit, InlineAssistant, InsertDraggedFiles,
InsertIntoEditor, InvokedSlashCommandId, InvokedSlashCommandStatus, Message, MessageId,
MessageMetadata, MessageStatus, NewContext, ParsedSlashCommand, PendingSlashCommandStatus,
QuoteSelection, RemoteContextMetadata, RequestType, SavedContextMetadata, Split, ToggleFocus,
ToggleModelSelector,
MessageMetadata, MessageStatus, ModelPickerDelegate, ModelSelector, NewContext,
ParsedSlashCommand, PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata,
RequestType, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
};
use anyhow::Result;
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
use assistant_tool::ToolWorkingSet;
use client::{proto, zed_urls, Client, Status};
use collections::{hash_map, BTreeSet, HashMap, HashSet};
use editor::{
@@ -55,7 +55,6 @@ use language_model::{
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role,
ZED_CLOUD_PROVIDER_ID,
};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use multi_buffer::MultiBufferRow;
use picker::{Picker, PickerDelegate};
use project::lsp_store::LocalLspAdapterDelegate;
@@ -143,7 +142,7 @@ pub struct AssistantPanel {
languages: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
subscriptions: Vec<Subscription>,
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
model_summary_editor: View<Editor>,
authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
configuration_subscription: Option<Subscription>,
@@ -305,7 +304,7 @@ impl PickerDelegate for SavedContextPickerDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.selected(selected)
.child(item),
)
}
@@ -341,12 +340,11 @@ impl AssistantPanel {
) -> Self {
let model_selector_menu_handle = PopoverMenuHandle::default();
let model_summary_editor = cx.new_view(Editor::single_line);
let context_editor_toolbar = cx.new_view(|cx| {
let context_editor_toolbar = cx.new_view(|_| {
ContextEditorToolbarItem::new(
workspace,
model_selector_menu_handle.clone(),
model_summary_editor.clone(),
cx,
)
});
@@ -417,6 +415,7 @@ impl AssistantPanel {
ControlFlow::Break(())
});
pane.set_can_split(false, cx);
pane.set_can_navigate(true, cx);
pane.display_nav_history_buttons(None);
pane.set_should_display_tab_bar(|_| true);
@@ -442,7 +441,7 @@ impl AssistantPanel {
)
}
})
.toggle_state(
.selected(
pane.active_item()
.map_or(false, |item| item.downcast::<ContextHistory>().is_some()),
);
@@ -451,7 +450,6 @@ impl AssistantPanel {
.gap(DynamicSpacing::Base02.rems(cx))
.child(
IconButton::new("new-chat", IconName::Plus)
.icon_size(IconSize::Small)
.on_click(
cx.listener(|_, _, cx| {
cx.dispatch_action(NewContext.boxed_clone())
@@ -1317,7 +1315,7 @@ impl AssistantPanel {
fn restart_context_servers(
workspace: &mut Workspace,
_action: &context_server::Restart,
_action: &context_servers::Restart,
cx: &mut ViewContext<Workspace>,
) {
let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
@@ -1926,7 +1924,7 @@ impl ContextEditor {
Content::ToolUse {
range: tool_use.source_range.clone(),
tool_use: LanguageModelToolUse {
id: tool_use.id.clone(),
id: tool_use.id.to_string(),
name: tool_use.name.clone(),
input: tool_use.input.clone(),
},
@@ -4456,36 +4454,23 @@ impl FollowableItem for ContextEditor {
}
pub struct ContextEditorToolbarItem {
fs: Arc<dyn Fs>,
active_context_editor: Option<WeakView<ContextEditor>>,
model_summary_editor: View<Editor>,
language_model_selector: View<LanguageModelSelector>,
language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
}
impl ContextEditorToolbarItem {
pub fn new(
workspace: &Workspace,
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
model_summary_editor: View<Editor>,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
fs: workspace.app_state().fs.clone(),
active_context_editor: None,
model_summary_editor,
language_model_selector: cx.new_view(|cx| {
let fs = workspace.app_state().fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
},
cx,
)
}),
language_model_selector_menu_handle: model_selector_menu_handle,
model_selector_menu_handle,
}
}
@@ -4574,8 +4559,8 @@ impl Render for ContextEditorToolbarItem {
// .map(|remaining_items| format!("Files to scan: {}", remaining_items))
// })
.child(
LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
ModelSelector::new(
self.fs.clone(),
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
@@ -4621,7 +4606,7 @@ impl Render for ContextEditorToolbarItem {
Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
}),
)
.with_handle(self.language_model_selector_menu_handle.clone()),
.with_handle(self.model_selector_menu_handle.clone()),
)
.children(self.render_remaining_tokens(cx));
@@ -4956,7 +4941,7 @@ fn render_slash_command_output_toggle(
("slash-command-output-fold-indicator", row.0 as u64),
!is_folded,
)
.toggle_state(is_folded)
.selected(is_folded)
.on_click(move |_e, cx| fold(!is_folded, cx))
.into_any_element()
}
@@ -4971,7 +4956,7 @@ fn fold_toggle(
) -> AnyElement {
move |row, is_folded, fold, _cx| {
Disclosure::new((name, row.0 as u64), !is_folded)
.toggle_state(is_folded)
.selected(is_folded)
.on_click(move |_e, cx| fold(!is_folded, cx))
.into_any_element()
}
@@ -5013,7 +4998,7 @@ fn render_quote_selection_output_toggle(
_cx: &mut WindowContext,
) -> AnyElement {
Disclosure::new(("quote-selection-indicator", row.0 as u64), !is_folded)
.toggle_state(is_folded)
.selected(is_folded)
.on_click(move |_e, cx| fold(!is_folded, cx))
.into_any_element()
}
@@ -5036,7 +5021,7 @@ fn render_pending_slash_command_gutter_decoration(
icon = icon.icon_color(Color::Muted);
}
PendingSlashCommandStatus::Running { .. } => {
icon = icon.toggle_state(true);
icon = icon.selected(true);
}
PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
}
@@ -5118,11 +5103,9 @@ fn make_lsp_adapter_delegate(
return Ok(None::<Arc<dyn LspAdapterDelegate>>);
};
let http_client = project.client().http_client().clone();
project.lsp_store().update(cx, |_, cx| {
project.lsp_store().update(cx, |lsp_store, cx| {
Ok(Some(LocalLspAdapterDelegate::new(
project.languages().clone(),
project.environment(),
cx.weak_model(),
lsp_store,
&worktree,
http_client,
project.fs().clone(),

View File

@@ -59,6 +59,7 @@ pub struct AssistantSettings {
pub inline_alternatives: Vec<LanguageModelSelection>,
pub using_outdated_settings_version: bool,
pub enable_experimental_live_diffs: bool,
pub show_hints: bool,
}
impl AssistantSettings {
@@ -201,6 +202,7 @@ impl AssistantSettingsContent {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
enabled: settings.enabled,
show_hints: None,
button: settings.button,
dock: settings.dock,
default_width: settings.default_width,
@@ -241,6 +243,7 @@ impl AssistantSettingsContent {
},
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
enabled: None,
show_hints: None,
button: settings.button,
dock: settings.dock,
default_width: settings.default_width,
@@ -353,6 +356,7 @@ impl Default for VersionedAssistantSettingsContent {
fn default() -> Self {
Self::V2(AssistantSettingsContentV2 {
enabled: None,
show_hints: None,
button: None,
dock: None,
default_width: None,
@@ -370,6 +374,11 @@ pub struct AssistantSettingsContentV2 {
///
/// Default: true
enabled: Option<bool>,
/// Whether to show inline hints that show keybindings for inline assistant
/// and assistant panel.
///
/// Default: true
show_hints: Option<bool>,
/// Whether to show the assistant panel button in the status bar.
///
/// Default: true
@@ -504,6 +513,7 @@ impl Settings for AssistantSettings {
let value = value.upgrade();
merge(&mut settings.enabled, value.enabled);
merge(&mut settings.show_hints, value.show_hints);
merge(&mut settings.button, value.button);
merge(&mut settings.dock, value.dock);
merge(
@@ -574,6 +584,7 @@ mod tests {
}),
inline_alternatives: None,
enabled: None,
show_hints: None,
button: None,
dock: None,
default_width: None,

View File

@@ -2,6 +2,7 @@
mod context_tests;
use crate::slash_command_working_set::SlashCommandWorkingSet;
use crate::ToolWorkingSet;
use crate::{
prompts::PromptBuilder,
slash_command::{file_command::FileCommandMetadata, SlashCommandLine},
@@ -11,11 +12,10 @@ use anyhow::{anyhow, Context as _, Result};
use assistant_slash_command::{
SlashCommandContent, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult,
};
use assistant_tool::ToolWorkingSet;
use client::{self, proto, telemetry::Telemetry};
use clock::ReplicaId;
use collections::{HashMap, HashSet};
use feature_flags::{FeatureFlagAppExt, ToolUseFeatureFlag};
use feature_flags::{FeatureFlag, FeatureFlagAppExt};
use fs::{Fs, RemoveOptions};
use futures::{future::Shared, FutureExt, StreamExt};
use gpui::{
@@ -27,8 +27,8 @@ use language::{AnchorRangeExt, Bias, Buffer, LanguageRegistry, OffsetRangeExt, P
use language_model::{
LanguageModel, LanguageModelCacheConfiguration, LanguageModelCompletionEvent,
LanguageModelImage, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse,
LanguageModelToolUseId, MessageContent, Role, StopReason,
LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolUse, MessageContent, Role,
StopReason,
};
use language_models::{
provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError},
@@ -385,7 +385,7 @@ pub enum ContextEvent {
},
UsePendingTools,
ToolFinished {
tool_use_id: LanguageModelToolUseId,
tool_use_id: Arc<str>,
output_range: Range<language::Anchor>,
},
Operation(ContextOperation),
@@ -479,7 +479,7 @@ pub enum Content {
},
ToolResult {
range: Range<language::Anchor>,
tool_use_id: LanguageModelToolUseId,
tool_use_id: Arc<str>,
},
}
@@ -546,7 +546,7 @@ pub struct Context {
pub(crate) slash_commands: Arc<SlashCommandWorkingSet>,
pub(crate) tools: Arc<ToolWorkingSet>,
slash_command_output_sections: Vec<SlashCommandOutputSection<language::Anchor>>,
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
pending_tool_uses_by_id: HashMap<Arc<str>, PendingToolUse>,
message_anchors: Vec<MessageAnchor>,
contents: Vec<Content>,
messages_metadata: HashMap<MessageId, MessageMetadata>,
@@ -1126,7 +1126,7 @@ impl Context {
self.pending_tool_uses_by_id.values().collect()
}
pub fn get_tool_use_by_id(&self, id: &LanguageModelToolUseId) -> Option<&PendingToolUse> {
pub fn get_tool_use_by_id(&self, id: &Arc<str>) -> Option<&PendingToolUse> {
self.pending_tool_uses_by_id.get(id)
}
@@ -2153,7 +2153,7 @@ impl Context {
pub fn insert_tool_output(
&mut self,
tool_use_id: LanguageModelToolUseId,
tool_use_id: Arc<str>,
output: Task<Result<String>>,
cx: &mut ModelContext<Self>,
) {
@@ -2340,10 +2340,11 @@ impl Context {
let source_range = buffer.anchor_after(start_ix)
..buffer.anchor_after(end_ix);
let tool_use_id: Arc<str> = tool_use.id.into();
this.pending_tool_uses_by_id.insert(
tool_use.id.clone(),
tool_use_id.clone(),
PendingToolUse {
id: tool_use.id,
id: tool_use_id,
name: tool_use.name,
input: tool_use.input,
status: PendingToolUseStatus::Idle,
@@ -3200,9 +3201,19 @@ pub enum PendingSlashCommandStatus {
Error(String),
}
pub(crate) struct ToolUseFeatureFlag;
impl FeatureFlag for ToolUseFeatureFlag {
const NAME: &'static str = "assistant-tool-use";
fn enabled_for_staff() -> bool {
false
}
}
#[derive(Debug, Clone)]
pub struct PendingToolUse {
pub id: LanguageModelToolUseId,
pub id: Arc<str>,
pub name: String,
pub input: serde_json::Value,
pub status: PendingToolUseStatus,

View File

@@ -1,5 +1,6 @@
use super::{AssistantEdit, MessageCacheMetadata};
use crate::slash_command_working_set::SlashCommandWorkingSet;
use crate::ToolWorkingSet;
use crate::{
assistant_panel, prompt_library, slash_command::file_command, AssistantEditKind, CacheStatus,
Context, ContextEvent, ContextId, ContextOperation, InvokedSlashCommandId, MessageId,
@@ -10,14 +11,13 @@ use assistant_slash_command::{
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, SlashCommandOutput,
SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult,
};
use assistant_tool::ToolWorkingSet;
use collections::{HashMap, HashSet};
use fs::FakeFs;
use futures::{
channel::mpsc,
stream::{self, StreamExt},
};
use gpui::{prelude::*, AppContext, Model, SharedString, Task, TestAppContext, WeakView};
use gpui::{AppContext, Model, SharedString, Task, TestAppContext, WeakView};
use language::{Buffer, BufferSnapshot, LanguageRegistry, LspAdapterDelegate};
use language_model::{LanguageModelCacheConfiguration, LanguageModelRegistry, Role};
use parking_lot::Mutex;
@@ -35,7 +35,7 @@ use std::{
sync::{atomic::AtomicBool, Arc},
};
use text::{network::Network, OffsetRangeExt as _, ReplicaId, ToOffset};
use ui::{IconName, WindowContext};
use ui::{Context as _, IconName, WindowContext};
use unindent::Unindent;
use util::{
test::{generate_marked_text, marked_text_ranges},

View File

@@ -1,16 +1,15 @@
use crate::slash_command::context_server_command;
use crate::SlashCommandId;
use crate::{
prompts::PromptBuilder, slash_command_working_set::SlashCommandWorkingSet, Context,
ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext, SavedContextMetadata,
};
use crate::{tools, SlashCommandId, ToolId, ToolWorkingSet};
use anyhow::{anyhow, Context as _, Result};
use assistant_tool::{ToolId, ToolWorkingSet};
use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
use clock::ReplicaId;
use collections::HashMap;
use context_server::manager::ContextServerManager;
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
use context_servers::manager::ContextServerManager;
use context_servers::ContextServerFactoryRegistry;
use fs::Fs;
use futures::StreamExt;
use fuzzy::StringMatchCandidate;
@@ -809,13 +808,13 @@ impl ContextStore {
fn handle_context_server_event(
&mut self,
context_server_manager: Model<ContextServerManager>,
event: &context_server::manager::Event,
event: &context_servers::manager::Event,
cx: &mut ModelContext<Self>,
) {
let slash_command_working_set = self.slash_commands.clone();
let tool_working_set = self.tools.clone();
match event {
context_server::manager::Event::ServerStarted { server_id } => {
context_servers::manager::Event::ServerStarted { server_id } => {
if let Some(server) = context_server_manager.read(cx).get_server(server_id) {
let context_server_manager = context_server_manager.clone();
cx.spawn({
@@ -826,7 +825,7 @@ impl ContextStore {
return;
};
if protocol.capable(context_server::protocol::ServerCapability::Prompts) {
if protocol.capable(context_servers::protocol::ServerCapability::Prompts) {
if let Some(prompts) = protocol.list_prompts().await.log_err() {
let slash_command_ids = prompts
.into_iter()
@@ -854,12 +853,12 @@ impl ContextStore {
}
}
if protocol.capable(context_server::protocol::ServerCapability::Tools) {
if protocol.capable(context_servers::protocol::ServerCapability::Tools) {
if let Some(tools) = protocol.list_tools().await.log_err() {
let tool_ids = tools.tools.into_iter().map(|tool| {
log::info!("registering context server tool: {:?}", tool.name);
tool_working_set.insert(
Arc::new(ContextServerTool::new(
Arc::new(tools::context_server_tool::ContextServerTool::new(
context_server_manager.clone(),
server.id(),
tool,
@@ -881,7 +880,7 @@ impl ContextStore {
.detach();
}
}
context_server::manager::Event::ServerStopped { server_id } => {
context_servers::manager::Event::ServerStopped { server_id } => {
if let Some(slash_command_ids) =
self.context_server_slash_command_ids.remove(server_id)
{

View File

@@ -1,7 +1,7 @@
use crate::{
assistant_settings::AssistantSettings, humanize_token_count, prompts::PromptBuilder,
AssistantPanel, AssistantPanelEvent, CharOperation, CycleNextInlineAssist,
CyclePreviousInlineAssist, LineDiff, LineOperation, RequestType, StreamingDiff,
CyclePreviousInlineAssist, LineDiff, LineOperation, ModelSelector, RequestType, StreamingDiff,
};
use anyhow::{anyhow, Context as _, Result};
use client::{telemetry::Telemetry, ErrorExt};
@@ -33,13 +33,12 @@ use language_model::{
LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelTextStream, Role,
};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_models::report_assistant_event;
use multi_buffer::MultiBufferRow;
use parking_lot::Mutex;
use project::{CodeAction, ProjectTransaction};
use rope::Rope;
use settings::{update_settings_file, Settings, SettingsStore};
use settings::{Settings, SettingsStore};
use smol::future::FutureExt;
use std::{
cmp,
@@ -1358,8 +1357,8 @@ enum PromptEditorEvent {
struct PromptEditor {
id: InlineAssistId,
fs: Arc<dyn Fs>,
editor: View<Editor>,
language_model_selector: View<LanguageModelSelector>,
edited_since_done: bool,
gutter_dimensions: Arc<Mutex<GutterDimensions>>,
prompt_history: VecDeque<String>,
@@ -1500,27 +1499,34 @@ impl Render for PromptEditor {
.w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))
.justify_center()
.gap_2()
.child(LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(move |cx| {
Tooltip::with_meta(
format!(
"Using {}",
LanguageModelRegistry::read_global(cx)
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
),
None,
"Change Model",
cx,
)
}),
))
.child(
ModelSelector::new(
self.fs.clone(),
IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.icon_color(Color::Muted)
.tooltip(move |cx| {
Tooltip::with_meta(
format!(
"Using {}",
LanguageModelRegistry::read_global(cx)
.active_model()
.map(|model| model.name().0)
.unwrap_or_else(|| "No model selected".into()),
),
None,
"Change Model",
cx,
)
}),
)
.with_info_text(
"Inline edits use context\n\
from the currently selected\n\
assistant panel tab.",
),
)
.map(|el| {
let CodegenStatus::Error(error) = self.codegen.read(cx).status(cx) else {
return el;
@@ -1534,7 +1540,7 @@ impl Render for PromptEditor {
v_flex()
.child(
IconButton::new("rate-limit-error", IconName::XCircle)
.toggle_state(self.show_rate_limit_notice)
.selected(self.show_rate_limit_notice)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.on_click(cx.listener(Self::toggle_rate_limit_notice)),
@@ -1626,19 +1632,6 @@ impl PromptEditor {
let mut this = Self {
id,
editor: prompt_editor,
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
},
cx,
)
}),
edited_since_done: false,
gutter_dimensions,
prompt_history,
@@ -1647,6 +1640,7 @@ impl PromptEditor {
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
editor_subscriptions: Vec::new(),
codegen,
fs,
pending_token_count: Task::ready(Ok(())),
token_counts: None,
_token_count_subscriptions: token_count_subscriptions,
@@ -2133,15 +2127,15 @@ impl PromptEditor {
"dont-show-again",
Label::new("Don't show again"),
if dismissed_rate_limit_notice() {
ui::ToggleState::Selected
ui::Selection::Selected
} else {
ui::ToggleState::Unselected
ui::Selection::Unselected
},
|selection, cx| {
let is_dismissed = match selection {
ui::ToggleState::Unselected => false,
ui::ToggleState::Indeterminate => return,
ui::ToggleState::Selected => true,
ui::Selection::Unselected => false,
ui::Selection::Indeterminate => return,
ui::Selection::Selected => true,
};
set_rate_limit_notice_dismissed(is_dismissed, cx)

View File

@@ -1,115 +1,33 @@
use feature_flags::ZedPro;
use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
use proto::Plan;
use workspace::ShowConfiguration;
use std::sync::Arc;
use feature_flags::ZedPro;
use gpui::{
Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Task,
View, WeakView,
};
use language_model::{LanguageModel, LanguageModelAvailability, LanguageModelRegistry};
use crate::assistant_settings::AssistantSettings;
use fs::Fs;
use gpui::{Action, AnyElement, DismissEvent, SharedString, Task};
use picker::{Picker, PickerDelegate};
use proto::Plan;
use settings::update_settings_file;
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, PopoverTrigger};
use workspace::ShowConfiguration;
const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro";
type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &AppContext) + 'static>;
pub struct LanguageModelSelector {
picker: View<Picker<LanguageModelPickerDelegate>>,
}
impl LanguageModelSelector {
pub fn new(
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &AppContext) + 'static,
cx: &mut ViewContext<Self>,
) -> Self {
let on_model_changed = Arc::new(on_model_changed);
let all_models = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
.flat_map(|provider| {
let icon = provider.icon();
provider.provided_models(cx).into_iter().map(move |model| {
let model = model.clone();
let icon = model.icon().unwrap_or(icon);
ModelInfo {
model: model.clone(),
icon,
availability: model.availability(),
}
})
})
.collect::<Vec<_>>();
let delegate = LanguageModelPickerDelegate {
language_model_selector: cx.view().downgrade(),
on_model_changed: on_model_changed.clone(),
all_models: all_models.clone(),
filtered_models: all_models,
selected_index: 0,
};
let picker =
cx.new_view(|cx| Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into())));
LanguageModelSelector { picker }
}
}
impl EventEmitter<DismissEvent> for LanguageModelSelector {}
impl FocusableView for LanguageModelSelector {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for LanguageModelSelector {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
self.picker.clone()
}
}
#[derive(IntoElement)]
pub struct LanguageModelSelectorPopoverMenu<T>
where
T: PopoverTrigger,
{
language_model_selector: View<LanguageModelSelector>,
pub struct ModelSelector<T: PopoverTrigger> {
handle: Option<PopoverMenuHandle<Picker<ModelPickerDelegate>>>,
fs: Arc<dyn Fs>,
trigger: T,
handle: Option<PopoverMenuHandle<LanguageModelSelector>>,
info_text: Option<SharedString>,
}
impl<T: PopoverTrigger> LanguageModelSelectorPopoverMenu<T> {
pub fn new(language_model_selector: View<LanguageModelSelector>, trigger: T) -> Self {
Self {
language_model_selector,
trigger,
handle: None,
}
}
pub fn with_handle(mut self, handle: PopoverMenuHandle<LanguageModelSelector>) -> Self {
self.handle = Some(handle);
self
}
}
impl<T: PopoverTrigger> RenderOnce for LanguageModelSelectorPopoverMenu<T> {
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
let language_model_selector = self.language_model_selector.clone();
PopoverMenu::new("model-switcher")
.menu(move |_cx| Some(language_model_selector.clone()))
.trigger(self.trigger)
.attach(gpui::AnchorCorner::BottomLeft)
.when_some(self.handle.clone(), |menu, handle| menu.with_handle(handle))
}
pub struct ModelPickerDelegate {
fs: Arc<dyn Fs>,
all_models: Vec<ModelInfo>,
filtered_models: Vec<ModelInfo>,
selected_index: usize,
}
#[derive(Clone)]
@@ -117,17 +35,31 @@ struct ModelInfo {
model: Arc<dyn LanguageModel>,
icon: IconName,
availability: LanguageModelAvailability,
is_selected: bool,
}
pub struct LanguageModelPickerDelegate {
language_model_selector: WeakView<LanguageModelSelector>,
on_model_changed: OnModelChanged,
all_models: Vec<ModelInfo>,
filtered_models: Vec<ModelInfo>,
selected_index: usize,
impl<T: PopoverTrigger> ModelSelector<T> {
pub fn new(fs: Arc<dyn Fs>, trigger: T) -> Self {
ModelSelector {
handle: None,
fs,
trigger,
info_text: None,
}
}
pub fn with_handle(mut self, handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>) -> Self {
self.handle = Some(handle);
self
}
pub fn with_info_text(mut self, text: impl Into<SharedString>) -> Self {
self.info_text = Some(text.into());
self
}
}
impl PickerDelegate for LanguageModelPickerDelegate {
impl PickerDelegate for ModelPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
@@ -205,17 +137,27 @@ impl PickerDelegate for LanguageModelPickerDelegate {
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(model_info) = self.filtered_models.get(self.selected_index) {
let model = model_info.model.clone();
(self.on_model_changed)(model.clone(), cx);
update_settings_file::<AssistantSettings>(self.fs.clone(), cx, move |settings, _| {
settings.set_model(model.clone())
});
// Update the selection status
let selected_model_id = model_info.model.id();
let selected_provider_id = model_info.model.provider_id();
for model in &mut self.all_models {
model.is_selected = model.model.id() == selected_model_id
&& model.model.provider_id() == selected_provider_id;
}
for model in &mut self.filtered_models {
model.is_selected = model.model.id() == selected_model_id
&& model.model.provider_id() == selected_provider_id;
}
cx.emit(DismissEvent);
}
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.language_model_selector
.update(cx, |_this, cx| cx.emit(DismissEvent))
.ok();
}
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
fn render_header(&self, cx: &mut ViewContext<Picker<Self>>) -> Option<AnyElement> {
let configured_models_count = LanguageModelRegistry::global(cx)
@@ -252,22 +194,11 @@ impl PickerDelegate for LanguageModelPickerDelegate {
let model_info = self.filtered_models.get(ix)?;
let provider_name: String = model_info.model.provider_name().0.clone().into();
let active_provider_id = LanguageModelRegistry::read_global(cx)
.active_provider()
.map(|m| m.id());
let active_model_id = LanguageModelRegistry::read_global(cx)
.active_model()
.map(|m| m.id());
let is_selected = Some(model_info.model.provider_id()) == active_provider_id
&& Some(model_info.model.id()) == active_model_id;
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.selected(selected)
.start_slot(
div().pr_0p5().child(
Icon::new(model_info.icon)
@@ -303,7 +234,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
}),
),
)
.end_slot(div().when(is_selected, |this| {
.end_slot(div().when(model_info.is_selected, |this| {
this.child(
Icon::new(IconName::Check)
.color(Color::Accent)
@@ -364,3 +295,58 @@ impl PickerDelegate for LanguageModelPickerDelegate {
)
}
}
impl<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let selected_provider = LanguageModelRegistry::read_global(cx)
.active_provider()
.map(|m| m.id());
let selected_model = LanguageModelRegistry::read_global(cx)
.active_model()
.map(|m| m.id());
let all_models = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.iter()
.flat_map(|provider| {
let provider_id = provider.id();
let icon = provider.icon();
let selected_model = selected_model.clone();
let selected_provider = selected_provider.clone();
provider.provided_models(cx).into_iter().map(move |model| {
let model = model.clone();
let icon = model.icon().unwrap_or(icon);
ModelInfo {
model: model.clone(),
icon,
availability: model.availability(),
is_selected: selected_model.as_ref() == Some(&model.id())
&& selected_provider.as_ref() == Some(&provider_id),
}
})
})
.collect::<Vec<_>>();
let delegate = ModelPickerDelegate {
fs: self.fs.clone(),
all_models: all_models.clone(),
filtered_models: all_models,
selected_index: 0,
};
let picker_view = cx.new_view(|cx| {
let picker = Picker::uniform_list(delegate, cx).max_height(Some(rems(20.).into()));
picker
});
PopoverMenu::new("model-switcher")
.menu(move |_cx| Some(picker_view.clone()))
.trigger(self.trigger)
.attach(gpui::AnchorCorner::BottomLeft)
.when_some(self.handle, |menu, handle| menu.with_handle(handle))
}
}

View File

@@ -11,8 +11,8 @@ use futures::{
use fuzzy::StringMatchCandidate;
use gpui::{
actions, point, size, transparent_black, Action, AppContext, BackgroundExecutor, Bounds,
EventEmitter, Global, PromptLevel, ReadGlobal, Subscription, Task, TextStyle, TitlebarOptions,
UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
EventEmitter, Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
TitlebarOptions, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
};
use heed::{
types::{SerdeBincode, SerdeJson, Str},
@@ -232,13 +232,13 @@ impl PickerDelegate for PromptPickerDelegate {
let element = ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.selected(selected)
.child(h_flex().h_5().line_height(relative(1.)).child(Label::new(
prompt.title.clone().unwrap_or("Untitled".into()),
)))
.end_slot::<IconButton>(default.then(|| {
IconButton::new("toggle-default-prompt", IconName::SparkleFilled)
.toggle_state(true)
.selected(true)
.icon_color(Color::Accent)
.shape(IconButtonShape::Square)
.tooltip(move |cx| Tooltip::text("Remove from Default Prompt", cx))
@@ -274,7 +274,7 @@ impl PickerDelegate for PromptPickerDelegate {
})
.child(
IconButton::new("toggle-default-prompt", IconName::Sparkle)
.toggle_state(default)
.selected(default)
.selected_icon(IconName::SparkleFilled)
.icon_color(if default { Color::Accent } else { Color::Muted })
.shape(IconButtonShape::Square)
@@ -928,8 +928,10 @@ impl PromptLibrary {
status: cx.theme().status().clone(),
inlay_hints_style:
editor::make_inlay_hints_style(cx),
inline_completion_styles:
editor::make_suggestion_styles(cx),
suggestions_style: HighlightStyle {
color: Some(cx.theme().status().predictive),
..HighlightStyle::default()
},
..EditorStyle::default()
},
)),
@@ -1053,7 +1055,7 @@ impl PromptLibrary {
IconName::Sparkle,
)
.style(ButtonStyle::Transparent)
.toggle_state(prompt_metadata.default)
.selected(prompt_metadata.default)
.selected_icon(IconName::SparkleFilled)
.icon_color(if prompt_metadata.default {
Color::Accent

View File

@@ -4,7 +4,7 @@ use assistant_slash_command::{
SlashCommandOutputSection, SlashCommandResult,
};
use collections::HashMap;
use context_server::{
use context_servers::{
manager::{ContextServer, ContextServerManager},
types::Prompt,
};
@@ -95,9 +95,9 @@ impl SlashCommand for ContextServerSlashCommand {
let completion_result = protocol
.completion(
context_server::types::CompletionReference::Prompt(
context_server::types::PromptReference {
r#type: context_server::types::PromptReferenceType::Prompt,
context_servers::types::CompletionReference::Prompt(
context_servers::types::PromptReference {
r#type: context_servers::types::PromptReferenceType::Prompt,
name: prompt_name,
},
),
@@ -152,7 +152,7 @@ impl SlashCommand for ContextServerSlashCommand {
if result
.messages
.iter()
.any(|msg| !matches!(msg.role, context_server::types::Role::User))
.any(|msg| !matches!(msg.role, context_servers::types::Role::User))
{
return Err(anyhow!(
"Prompt contains non-user roles, which is not supported"
@@ -164,7 +164,7 @@ impl SlashCommand for ContextServerSlashCommand {
.messages
.into_iter()
.filter_map(|msg| match msg.content {
context_server::types::MessageContent::Text { text, .. } => Some(text),
context_servers::types::MessageContent::Text { text } => Some(text),
_ => None,
})
.collect::<Vec<String>>()

View File

@@ -108,10 +108,6 @@ impl SlashCommand for FetchSlashCommand {
"Insert fetched URL contents".into()
}
fn icon(&self) -> IconName {
IconName::Globe
}
fn menu_text(&self) -> String {
self.description()
}
@@ -166,7 +162,7 @@ impl SlashCommand for FetchSlashCommand {
text,
sections: vec![SlashCommandOutputSection {
range,
icon: IconName::Globe,
icon: IconName::AtSign,
label: format!("fetch {}", url).into(),
metadata: None,
}],

View File

@@ -2,7 +2,7 @@ use std::sync::Arc;
use gpui::{AnyElement, DismissEvent, SharedString, Task, WeakView};
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger, Tooltip};
use ui::{prelude::*, ListItem, ListItemSpacing, PopoverMenu, PopoverTrigger};
use crate::assistant_panel::ContextEditor;
use crate::SlashCommandWorkingSet;
@@ -176,18 +176,12 @@ impl PickerDelegate for SlashCommandDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Dense)
.toggle_state(selected)
.tooltip({
let description = info.description.clone();
move |cx| cx.new_view(|_| Tooltip::new(description.clone())).into()
})
.selected(selected)
.child(
v_flex()
.group(format!("command-entry-label-{ix}"))
.w_full()
.py_0p5()
.min_w(px(250.))
.max_w(px(400.))
.child(
h_flex()
.gap_1p5()
@@ -198,7 +192,7 @@ impl PickerDelegate for SlashCommandDelegate {
{
label.push_str(&args);
}
Label::new(label).single_line().size(LabelSize::Small)
Label::new(label).size(LabelSize::Small)
}))
.children(info.args.clone().filter(|_| !selected).map(
|args| {
@@ -206,7 +200,6 @@ impl PickerDelegate for SlashCommandDelegate {
.font_buffer(cx)
.child(
Label::new(args)
.single_line()
.size(LabelSize::Small)
.color(Color::Muted),
)
@@ -217,11 +210,9 @@ impl PickerDelegate for SlashCommandDelegate {
)),
)
.child(
div().overflow_hidden().text_ellipsis().child(
Label::new(info.description.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
Label::new(info.description.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
),
),
@@ -229,7 +220,7 @@ impl PickerDelegate for SlashCommandDelegate {
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Dense)
.toggle_state(selected)
.selected(selected)
.child(renderer(cx)),
),
}

View File

@@ -1,7 +1,6 @@
use crate::assistant_settings::AssistantSettings;
use crate::{
humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent, RequestType,
DEFAULT_CONTEXT_LINES,
humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent,
ModelSelector, RequestType, DEFAULT_CONTEXT_LINES,
};
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
@@ -20,9 +19,8 @@ use language::Buffer;
use language_model::{
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use language_models::report_assistant_event;
use settings::{update_settings_file, Settings};
use settings::Settings;
use std::{
cmp,
sync::Arc,
@@ -32,7 +30,7 @@ use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
use terminal::Terminal;
use terminal_view::TerminalView;
use theme::ThemeSettings;
use ui::{prelude::*, text_for_action, IconButtonShape, Tooltip};
use ui::{prelude::*, IconButtonShape, Tooltip};
use util::ResultExt;
use workspace::{notifications::NotificationId, Toast, Workspace};
@@ -476,9 +474,9 @@ enum PromptEditorEvent {
struct PromptEditor {
id: TerminalInlineAssistId,
fs: Arc<dyn Fs>,
height_in_lines: u8,
editor: View<Editor>,
language_model_selector: View<LanguageModelSelector>,
edited_since_done: bool,
prompt_history: VecDeque<String>,
prompt_history_ix: Option<usize>,
@@ -614,8 +612,8 @@ impl Render for PromptEditor {
.w_12()
.justify_center()
.gap_2()
.child(LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
.child(ModelSelector::new(
self.fs.clone(),
IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
@@ -695,7 +693,7 @@ impl PromptEditor {
cx,
);
editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
editor.set_placeholder_text(Self::placeholder_text(cx), cx);
editor.set_placeholder_text("Add a prompt…", cx);
editor
});
@@ -709,19 +707,6 @@ impl PromptEditor {
id,
height_in_lines: 1,
editor: prompt_editor,
language_model_selector: cx.new_view(|cx| {
let fs = fs.clone();
LanguageModelSelector::new(
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
},
cx,
)
}),
edited_since_done: false,
prompt_history,
prompt_history_ix: None,
@@ -729,6 +714,7 @@ impl PromptEditor {
_codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
editor_subscriptions: Vec::new(),
codegen,
fs,
pending_token_count: Task::ready(Ok(())),
token_count: None,
_token_count_subscriptions: token_count_subscriptions,
@@ -740,14 +726,6 @@ impl PromptEditor {
this
}
fn placeholder_text(cx: &WindowContext) -> String {
let context_keybinding = text_for_action(&crate::ToggleFocus, cx)
.map(|keybinding| format!("{keybinding} for context"))
.unwrap_or_default();
format!("Generate…{context_keybinding} • ↓↑ for history")
}
fn subscribe_to_editor(&mut self, cx: &mut ViewContext<Self>) {
self.editor_subscriptions.clear();
self.editor_subscriptions

View File

@@ -1,10 +1,8 @@
use std::sync::Arc;
use assistant_tool::{Tool, ToolRegistry};
use collections::HashMap;
use gpui::AppContext;
use parking_lot::Mutex;
use crate::{Tool, ToolRegistry};
use std::sync::Arc;
#[derive(Copy, Clone, PartialEq, Eq, Hash, Default)]
pub struct ToolId(usize);

View File

@@ -0,0 +1,2 @@
pub mod context_server_tool;
pub mod now_tool;

View File

@@ -2,11 +2,10 @@ use std::sync::Arc;
use anyhow::{anyhow, bail};
use assistant_tool::Tool;
use context_servers::manager::ContextServerManager;
use context_servers::types;
use gpui::{Model, Task};
use crate::manager::ContextServerManager;
use crate::types;
pub struct ContextServerTool {
server_manager: Model<ContextServerManager>,
server_id: Arc<str>,

View File

@@ -30,7 +30,7 @@ impl Tool for NowTool {
}
fn description(&self) -> String {
"Returns the current datetime in RFC 3339 format. Only use this tool when the user specifically asks for it or the current task would benefit from knowing the current datetime.".into()
"Returns the current datetime in RFC 3339 format.".into()
}
fn input_schema(&self) -> serde_json::Value {

View File

@@ -1,77 +0,0 @@
[package]
name = "assistant2"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/assistant.rs"
doctest = false
[dependencies]
anthropic = { workspace = true, features = ["schemars"] }
anyhow.workspace = true
assets.workspace = true
assistant_tool.workspace = true
async-watch.workspace = true
client.workspace = true
chrono.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
context_server.workspace = true
db.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
handlebars.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
language_models.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true
menu.workspace = true
multi_buffer.workspace = true
ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
ordered-float.workspace = true
paths.workspace = true
parking_lot.workspace = true
picker.workspace = true
project.workspace = true
proto.workspace = true
rope.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
settings.workspace = true
similar.workspace = true
smol.workspace = true
telemetry_events.workspace = true
terminal_view.workspace = true
text.workspace = true
terminal.workspace = true
theme.workspace = true
time.workspace = true
time_format.workspace = true
ui.workspace = true
unindent.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
rand.workspace = true
indoc.workspace = true

View File

@@ -1,254 +0,0 @@
use std::sync::Arc;
use assistant_tool::ToolWorkingSet;
use collections::HashMap;
use gpui::{
list, AnyElement, AppContext, Empty, ListAlignment, ListState, Model, StyleRefinement,
Subscription, TextStyleRefinement, View, WeakView,
};
use language::LanguageRegistry;
use language_model::Role;
use markdown::{Markdown, MarkdownStyle};
use settings::Settings as _;
use theme::ThemeSettings;
use ui::prelude::*;
use workspace::Workspace;
use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent};
use crate::ui::ContextPill;
pub struct ActiveThread {
workspace: WeakView<Workspace>,
language_registry: Arc<LanguageRegistry>,
tools: Arc<ToolWorkingSet>,
thread: Model<Thread>,
messages: Vec<MessageId>,
list_state: ListState,
rendered_messages_by_id: HashMap<MessageId, View<Markdown>>,
last_error: Option<ThreadError>,
_subscriptions: Vec<Subscription>,
}
impl ActiveThread {
pub fn new(
thread: Model<Thread>,
workspace: WeakView<Workspace>,
language_registry: Arc<LanguageRegistry>,
tools: Arc<ToolWorkingSet>,
cx: &mut ViewContext<Self>,
) -> Self {
let subscriptions = vec![
cx.observe(&thread, |_, _, cx| cx.notify()),
cx.subscribe(&thread, Self::handle_thread_event),
];
let mut this = Self {
workspace,
language_registry,
tools,
thread: thread.clone(),
messages: Vec::new(),
rendered_messages_by_id: HashMap::default(),
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
let this = cx.view().downgrade();
move |ix, cx: &mut WindowContext| {
this.update(cx, |this, cx| this.render_message(ix, cx))
.unwrap()
}
}),
last_error: None,
_subscriptions: subscriptions,
};
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
this.push_message(&message.id, message.text.clone(), cx);
}
this
}
pub fn is_empty(&self) -> bool {
self.messages.is_empty()
}
pub fn summary(&self, cx: &AppContext) -> Option<SharedString> {
self.thread.read(cx).summary()
}
pub fn last_error(&self) -> Option<ThreadError> {
self.last_error.clone()
}
pub fn clear_last_error(&mut self) {
self.last_error.take();
}
fn push_message(&mut self, id: &MessageId, text: String, cx: &mut ViewContext<Self>) {
let old_len = self.messages.len();
self.messages.push(*id);
self.list_state.splice(old_len..old_len, 1);
let theme_settings = ThemeSettings::get_global(cx);
let ui_font_size = TextSize::Default.rems(cx);
let buffer_font_size = theme_settings.buffer_font_size;
let mut text_style = cx.text_style();
text_style.refine(&TextStyleRefinement {
font_family: Some(theme_settings.ui_font.family.clone()),
font_size: Some(ui_font_size.into()),
color: Some(cx.theme().colors().text),
..Default::default()
});
let markdown_style = MarkdownStyle {
base_text_style: text_style,
syntax: cx.theme().syntax().clone(),
selection_background_color: cx.theme().players().local().selection,
code_block: StyleRefinement {
text: Some(TextStyleRefinement {
font_family: Some(theme_settings.buffer_font.family.clone()),
font_size: Some(buffer_font_size.into()),
..Default::default()
}),
..Default::default()
},
inline_code: TextStyleRefinement {
font_family: Some(theme_settings.buffer_font.family.clone()),
font_size: Some(ui_font_size.into()),
background_color: Some(cx.theme().colors().editor_background),
..Default::default()
},
..Default::default()
};
let markdown = cx.new_view(|cx| {
Markdown::new(
text,
markdown_style,
Some(self.language_registry.clone()),
None,
cx,
)
});
self.rendered_messages_by_id.insert(*id, markdown);
}
fn handle_thread_event(
&mut self,
_: Model<Thread>,
event: &ThreadEvent,
cx: &mut ViewContext<Self>,
) {
match event {
ThreadEvent::ShowError(error) => {
self.last_error = Some(error.clone());
}
ThreadEvent::StreamedCompletion => {}
ThreadEvent::SummaryChanged => {}
ThreadEvent::StreamedAssistantText(message_id, text) => {
if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
markdown.update(cx, |markdown, cx| {
markdown.append(text, cx);
});
}
}
ThreadEvent::MessageAdded(message_id) => {
if let Some(message_text) = self
.thread
.read(cx)
.message(*message_id)
.map(|message| message.text.clone())
{
self.push_message(message_id, message_text, cx);
}
cx.notify();
}
ThreadEvent::UsePendingTools => {
let pending_tool_uses = self
.thread
.read(cx)
.pending_tool_uses()
.into_iter()
.filter(|tool_use| tool_use.status.is_idle())
.cloned()
.collect::<Vec<_>>();
for tool_use in pending_tool_uses {
if let Some(tool) = self.tools.tool(&tool_use.name, cx) {
let task = tool.run(tool_use.input, self.workspace.clone(), cx);
self.thread.update(cx, |thread, cx| {
thread.insert_tool_output(
tool_use.assistant_message_id,
tool_use.id.clone(),
task,
cx,
);
});
}
}
}
ThreadEvent::ToolFinished { .. } => {}
}
}
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
let message_id = self.messages[ix];
let Some(message) = self.thread.read(cx).message(message_id) else {
return Empty.into_any();
};
let Some(markdown) = self.rendered_messages_by_id.get(&message_id) else {
return Empty.into_any();
};
let context = self.thread.read(cx).context_for_message(message_id);
let (role_icon, role_name) = match message.role {
Role::User => (IconName::Person, "You"),
Role::Assistant => (IconName::ZedAssistant, "Assistant"),
Role::System => (IconName::Settings, "System"),
};
div()
.id(("message-container", ix))
.p_2()
.child(
v_flex()
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_md()
.child(
h_flex()
.justify_between()
.p_1p5()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(
h_flex()
.gap_2()
.child(Icon::new(role_icon).size(IconSize::Small))
.child(Label::new(role_name).size(LabelSize::Small)),
),
)
.child(v_flex().p_1p5().text_ui(cx).child(markdown.clone()))
.when_some(context, |parent, context| {
parent.child(
h_flex().flex_wrap().gap_2().p_1p5().children(
context
.iter()
.map(|context| ContextPill::new(context.clone())),
),
)
}),
)
.into_any()
}
}
impl Render for ActiveThread {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
list(self.list_state.clone()).flex_1()
}
}

View File

@@ -1,98 +0,0 @@
mod active_thread;
mod assistant_panel;
mod assistant_settings;
mod context;
mod context_picker;
mod inline_assistant;
mod message_editor;
mod prompts;
mod streaming_diff;
mod terminal_inline_assistant;
mod thread;
mod thread_history;
mod thread_store;
mod ui;
use std::sync::Arc;
use assistant_settings::AssistantSettings;
use client::Client;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
use fs::Fs;
use gpui::{actions, AppContext};
use prompts::PromptLoadingParams;
use settings::Settings as _;
use util::ResultExt;
pub use crate::assistant_panel::AssistantPanel;
actions!(
assistant2,
[
ToggleFocus,
NewThread,
ToggleModelSelector,
OpenHistory,
Chat,
ToggleInlineAssist,
CycleNextInlineAssist,
CyclePreviousInlineAssist
]
);
const NAMESPACE: &str = "assistant2";
/// Initializes the `assistant2` crate.
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, stdout_is_a_pty: bool, cx: &mut AppContext) {
AssistantSettings::register(cx);
assistant_panel::init(cx);
let prompt_builder = prompts::PromptBuilder::new(Some(PromptLoadingParams {
fs: fs.clone(),
repo_path: stdout_is_a_pty
.then(|| std::env::current_dir().log_err())
.flatten(),
cx,
}))
.log_err()
.map(Arc::new)
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
client.telemetry().clone(),
cx,
);
terminal_inline_assistant::init(
fs.clone(),
prompt_builder.clone(),
client.telemetry().clone(),
cx,
);
feature_gate_assistant2_actions(cx);
}
fn feature_gate_assistant2_actions(cx: &mut AppContext) {
const ASSISTANT1_NAMESPACE: &str = "assistant";
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(NAMESPACE);
});
cx.observe_flag::<Assistant2FeatureFlag, _>(move |is_enabled, cx| {
if is_enabled {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.show_namespace(NAMESPACE);
filter.hide_namespace(ASSISTANT1_NAMESPACE);
});
} else {
CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_namespace(NAMESPACE);
filter.show_namespace(ASSISTANT1_NAMESPACE);
});
}
})
.detach();
}

View File

@@ -1,532 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
use assistant_tool::ToolWorkingSet;
use client::zed_urls;
use gpui::{
prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, EventEmitter,
FocusHandle, FocusableView, FontWeight, Model, Pixels, Task, View, ViewContext, WeakView,
WindowContext,
};
use language::LanguageRegistry;
use time::UtcOffset;
use ui::{prelude::*, Divider, IconButtonShape, KeyBinding, Tab, Tooltip};
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::Workspace;
use crate::active_thread::ActiveThread;
use crate::message_editor::MessageEditor;
use crate::thread::{ThreadError, ThreadId};
use crate::thread_history::{PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::{NewThread, OpenHistory, ToggleFocus};
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
workspace.toggle_panel_focus::<AssistantPanel>(cx);
});
},
)
.detach();
}
enum ActiveView {
Thread,
History,
}
pub struct AssistantPanel {
workspace: WeakView<Workspace>,
language_registry: Arc<LanguageRegistry>,
thread_store: Model<ThreadStore>,
thread: View<ActiveThread>,
message_editor: View<MessageEditor>,
tools: Arc<ToolWorkingSet>,
local_timezone: UtcOffset,
active_view: ActiveView,
history: View<ThreadHistory>,
}
impl AssistantPanel {
pub fn load(
workspace: WeakView<Workspace>,
cx: AsyncWindowContext,
) -> Task<Result<View<Self>>> {
cx.spawn(|mut cx| async move {
let tools = Arc::new(ToolWorkingSet::default());
let thread_store = workspace
.update(&mut cx, |workspace, cx| {
let project = workspace.project().clone();
ThreadStore::new(project, tools.clone(), cx)
})?
.await?;
workspace.update(&mut cx, |workspace, cx| {
cx.new_view(|cx| Self::new(workspace, thread_store, tools, cx))
})
})
}
fn new(
workspace: &Workspace,
thread_store: Model<ThreadStore>,
tools: Arc<ToolWorkingSet>,
cx: &mut ViewContext<Self>,
) -> Self {
let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
let language_registry = workspace.project().read(cx).languages().clone();
let workspace = workspace.weak_handle();
let weak_self = cx.view().downgrade();
Self {
active_view: ActiveView::Thread,
workspace: workspace.clone(),
language_registry: language_registry.clone(),
thread_store: thread_store.clone(),
thread: cx.new_view(|cx| {
ActiveThread::new(
thread.clone(),
workspace.clone(),
language_registry,
tools.clone(),
cx,
)
}),
message_editor: cx.new_view(|cx| MessageEditor::new(workspace, thread.clone(), cx)),
tools,
local_timezone: UtcOffset::from_whole_seconds(
chrono::Local::now().offset().local_minus_utc(),
)
.unwrap(),
history: cx.new_view(|cx| ThreadHistory::new(weak_self, thread_store, cx)),
}
}
pub(crate) fn local_timezone(&self) -> UtcOffset {
self.local_timezone
}
fn new_thread(&mut self, cx: &mut ViewContext<Self>) {
let thread = self
.thread_store
.update(cx, |this, cx| this.create_thread(cx));
self.active_view = ActiveView::Thread;
self.thread = cx.new_view(|cx| {
ActiveThread::new(
thread.clone(),
self.workspace.clone(),
self.language_registry.clone(),
self.tools.clone(),
cx,
)
});
self.message_editor =
cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
self.message_editor.focus_handle(cx).focus(cx);
}
pub(crate) fn open_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
let Some(thread) = self
.thread_store
.update(cx, |this, cx| this.open_thread(thread_id, cx))
else {
return;
};
self.active_view = ActiveView::Thread;
self.thread = cx.new_view(|cx| {
ActiveThread::new(
thread.clone(),
self.workspace.clone(),
self.language_registry.clone(),
self.tools.clone(),
cx,
)
});
self.message_editor =
cx.new_view(|cx| MessageEditor::new(self.workspace.clone(), thread, cx));
self.message_editor.focus_handle(cx).focus(cx);
}
pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut ViewContext<Self>) {
self.thread_store
.update(cx, |this, cx| this.delete_thread(thread_id, cx));
}
}
impl FocusableView for AssistantPanel {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
match self.active_view {
ActiveView::Thread => self.message_editor.focus_handle(cx),
ActiveView::History => self.history.focus_handle(cx),
}
}
}
impl EventEmitter<PanelEvent> for AssistantPanel {}
impl Panel for AssistantPanel {
fn persistent_name() -> &'static str {
"AssistantPanel2"
}
fn position(&self, _cx: &WindowContext) -> DockPosition {
DockPosition::Right
}
fn position_is_valid(&self, _: DockPosition) -> bool {
true
}
fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
fn size(&self, _cx: &WindowContext) -> Pixels {
px(640.)
}
fn set_size(&mut self, _size: Option<Pixels>, _cx: &mut ViewContext<Self>) {}
fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
fn remote_id() -> Option<proto::PanelId> {
Some(proto::PanelId::AssistantPanel)
}
fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
Some(IconName::ZedAssistant)
}
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
Some("Assistant Panel")
}
fn toggle_action(&self) -> Box<dyn Action> {
Box::new(ToggleFocus)
}
}
impl AssistantPanel {
fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
h_flex()
.id("assistant-toolbar")
.justify_between()
.gap(DynamicSpacing::Base08.rems(cx))
.h(Tab::container_height(cx))
.px(DynamicSpacing::Base08.rems(cx))
.bg(cx.theme().colors().tab_bar_background)
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(h_flex().children(self.thread.read(cx).summary(cx).map(Label::new)))
.child(
h_flex()
.gap(DynamicSpacing::Base08.rems(cx))
.child(Divider::vertical())
.child(
IconButton::new("new-thread", IconName::Plus)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip({
let focus_handle = focus_handle.clone();
move |cx| {
Tooltip::for_action_in(
"New Thread",
&NewThread,
&focus_handle,
cx,
)
}
})
.on_click(move |_event, cx| {
cx.dispatch_action(NewThread.boxed_clone());
}),
)
.child(
IconButton::new("open-history", IconName::HistoryRerun)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip({
let focus_handle = focus_handle.clone();
move |cx| {
Tooltip::for_action_in(
"Open History",
&OpenHistory,
&focus_handle,
cx,
)
}
})
.on_click(move |_event, cx| {
cx.dispatch_action(OpenHistory.boxed_clone());
}),
)
.child(
IconButton::new("configure-assistant", IconName::Settings)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
.on_click(move |_event, _cx| {
println!("Configure Assistant");
}),
),
)
}
fn render_active_thread_or_empty_state(&self, cx: &mut ViewContext<Self>) -> AnyElement {
if self.thread.read(cx).is_empty() {
return self.render_thread_empty_state(cx).into_any_element();
}
self.thread.clone().into_any()
}
fn render_thread_empty_state(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let recent_threads = self
.thread_store
.update(cx, |this, cx| this.recent_threads(3, cx));
v_flex()
.gap_2()
.mx_auto()
.child(
v_flex().w_full().child(
svg()
.path("icons/logo_96.svg")
.text_color(cx.theme().colors().text)
.w(px(40.))
.h(px(40.))
.mx_auto()
.mb_4(),
),
)
.when(!recent_threads.is_empty(), |parent| {
parent
.child(
h_flex()
.w_full()
.justify_center()
.child(Label::new("Recent Threads:").size(LabelSize::Small)),
)
.child(
v_flex().gap_2().children(
recent_threads
.into_iter()
.map(|thread| PastThread::new(thread, cx.view().downgrade())),
),
)
.child(
h_flex().w_full().justify_center().child(
Button::new("view-all-past-threads", "View All Past Threads")
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.key_binding(KeyBinding::for_action_in(
&OpenHistory,
&self.focus_handle(cx),
cx,
))
.on_click(move |_event, cx| {
cx.dispatch_action(OpenHistory.boxed_clone());
}),
),
)
})
}
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
let last_error = self.thread.read(cx).last_error()?;
Some(
div()
.absolute()
.right_3()
.bottom_12()
.max_w_96()
.py_2()
.px_3()
.elevation_2(cx)
.occlude()
.child(match last_error {
ThreadError::PaymentRequired => self.render_payment_required_error(cx),
ThreadError::MaxMonthlySpendReached => {
self.render_max_monthly_spend_reached_error(cx)
}
ThreadError::Message(error_message) => {
self.render_error_message(&error_message, cx)
}
})
.into_any(),
)
}
fn render_payment_required_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(ERROR_MESSAGE)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
|this, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
},
)))
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.notify();
},
))),
)
.into_any()
}
fn render_max_monthly_spend_reached_error(&self, cx: &mut ViewContext<Self>) -> AnyElement {
const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
)
.child(
div()
.id("error-message")
.max_h_24()
.overflow_y_scroll()
.child(Label::new(ERROR_MESSAGE)),
)
.child(
h_flex()
.justify_end()
.mt_1()
.child(
Button::new("subscribe", "Update Monthly Spend Limit").on_click(
cx.listener(|this, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
}),
),
)
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.notify();
},
))),
)
.into_any()
}
fn render_error_message(
&self,
error_message: &SharedString,
cx: &mut ViewContext<Self>,
) -> AnyElement {
v_flex()
.gap_0p5()
.child(
h_flex()
.gap_1p5()
.items_center()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(
Label::new("Error interacting with language model")
.weight(FontWeight::MEDIUM),
),
)
.child(
div()
.id("error-message")
.max_h_32()
.overflow_y_scroll()
.child(Label::new(error_message.clone())),
)
.child(
h_flex()
.justify_end()
.mt_1()
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.thread.update(cx, |this, _cx| {
this.clear_last_error();
});
cx.notify();
},
))),
)
.into_any()
}
}
impl Render for AssistantPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.key_context("AssistantPanel2")
.justify_between()
.size_full()
.on_action(cx.listener(|this, _: &NewThread, cx| {
this.new_thread(cx);
}))
.on_action(cx.listener(|this, _: &OpenHistory, cx| {
this.active_view = ActiveView::History;
this.history.focus_handle(cx).focus(cx);
cx.notify();
}))
.child(self.render_toolbar(cx))
.map(|parent| match self.active_view {
ActiveView::Thread => parent
.child(self.render_active_thread_or_empty_state(cx))
.child(
h_flex()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(self.message_editor.clone()),
)
.children(self.render_last_error(cx)),
ActiveView::History => parent.child(self.history.clone()),
})
}
}

View File

@@ -1,485 +0,0 @@
use std::sync::Arc;
use ::open_ai::Model as OpenAiModel;
use anthropic::Model as AnthropicModel;
use gpui::Pixels;
use language_model::{CloudModel, LanguageModel};
use ollama::Model as OllamaModel;
use schemars::{schema::Schema, JsonSchema};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum AssistantDockPosition {
Left,
#[default]
Right,
Bottom,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(tag = "name", rename_all = "snake_case")]
pub enum AssistantProviderContentV1 {
#[serde(rename = "zed.dev")]
ZedDotDev { default_model: Option<CloudModel> },
#[serde(rename = "openai")]
OpenAi {
default_model: Option<OpenAiModel>,
api_url: Option<String>,
available_models: Option<Vec<OpenAiModel>>,
},
#[serde(rename = "anthropic")]
Anthropic {
default_model: Option<AnthropicModel>,
api_url: Option<String>,
},
#[serde(rename = "ollama")]
Ollama {
default_model: Option<OllamaModel>,
api_url: Option<String>,
},
}
#[derive(Debug, Default)]
pub struct AssistantSettings {
pub enabled: bool,
pub button: bool,
pub dock: AssistantDockPosition,
pub default_width: Pixels,
pub default_height: Pixels,
pub default_model: LanguageModelSelection,
pub inline_alternatives: Vec<LanguageModelSelection>,
pub using_outdated_settings_version: bool,
pub enable_experimental_live_diffs: bool,
}
/// Assistant panel settings
#[derive(Clone, Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum AssistantSettingsContent {
Versioned(VersionedAssistantSettingsContent),
Legacy(LegacyAssistantSettingsContent),
}
impl JsonSchema for AssistantSettingsContent {
fn schema_name() -> String {
VersionedAssistantSettingsContent::schema_name()
}
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> Schema {
VersionedAssistantSettingsContent::json_schema(gen)
}
fn is_referenceable() -> bool {
VersionedAssistantSettingsContent::is_referenceable()
}
}
impl Default for AssistantSettingsContent {
fn default() -> Self {
Self::Versioned(VersionedAssistantSettingsContent::default())
}
}
impl AssistantSettingsContent {
pub fn is_version_outdated(&self) -> bool {
match self {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(_) => true,
VersionedAssistantSettingsContent::V2(_) => false,
},
AssistantSettingsContent::Legacy(_) => true,
}
}
fn upgrade(&self) -> AssistantSettingsContentV2 {
match self {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
enabled: settings.enabled,
button: settings.button,
dock: settings.dock,
default_width: settings.default_width,
default_height: settings.default_width,
default_model: settings
.provider
.clone()
.and_then(|provider| match provider {
AssistantProviderContentV1::ZedDotDev { default_model } => {
default_model.map(|model| LanguageModelSelection {
provider: "zed.dev".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::OpenAi { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "openai".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::Anthropic { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "anthropic".to_string(),
model: model.id().to_string(),
})
}
AssistantProviderContentV1::Ollama { default_model, .. } => {
default_model.map(|model| LanguageModelSelection {
provider: "ollama".to_string(),
model: model.id().to_string(),
})
}
}),
inline_alternatives: None,
enable_experimental_live_diffs: None,
},
VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
},
AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
enabled: None,
button: settings.button,
dock: settings.dock,
default_width: settings.default_width,
default_height: settings.default_height,
default_model: Some(LanguageModelSelection {
provider: "openai".to_string(),
model: settings
.default_open_ai_model
.clone()
.unwrap_or_default()
.id()
.to_string(),
}),
inline_alternatives: None,
enable_experimental_live_diffs: None,
},
}
}
pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
let model = language_model.id().0.to_string();
let provider = language_model.provider_id().0.to_string();
match self {
AssistantSettingsContent::Versioned(settings) => match settings {
VersionedAssistantSettingsContent::V1(settings) => match provider.as_ref() {
"zed.dev" => {
log::warn!("attempted to set zed.dev model on outdated settings");
}
"anthropic" => {
let api_url = match &settings.provider {
Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
api_url.clone()
}
_ => None,
};
settings.provider = Some(AssistantProviderContentV1::Anthropic {
default_model: AnthropicModel::from_id(&model).ok(),
api_url,
});
}
"ollama" => {
let api_url = match &settings.provider {
Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
api_url.clone()
}
_ => None,
};
settings.provider = Some(AssistantProviderContentV1::Ollama {
default_model: Some(ollama::Model::new(&model, None, None)),
api_url,
});
}
"openai" => {
let (api_url, available_models) = match &settings.provider {
Some(AssistantProviderContentV1::OpenAi {
api_url,
available_models,
..
}) => (api_url.clone(), available_models.clone()),
_ => (None, None),
};
settings.provider = Some(AssistantProviderContentV1::OpenAi {
default_model: OpenAiModel::from_id(&model).ok(),
api_url,
available_models,
});
}
_ => {}
},
VersionedAssistantSettingsContent::V2(settings) => {
settings.default_model = Some(LanguageModelSelection { provider, model });
}
},
AssistantSettingsContent::Legacy(settings) => {
if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
settings.default_open_ai_model = Some(model);
}
}
}
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
#[serde(tag = "version")]
pub enum VersionedAssistantSettingsContent {
#[serde(rename = "1")]
V1(AssistantSettingsContentV1),
#[serde(rename = "2")]
V2(AssistantSettingsContentV2),
}
impl Default for VersionedAssistantSettingsContent {
fn default() -> Self {
Self::V2(AssistantSettingsContentV2 {
enabled: None,
button: None,
dock: None,
default_width: None,
default_height: None,
default_model: None,
inline_alternatives: None,
enable_experimental_live_diffs: None,
})
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AssistantSettingsContentV2 {
/// Whether the Assistant is enabled.
///
/// Default: true
enabled: Option<bool>,
/// Whether to show the assistant panel button in the status bar.
///
/// Default: true
button: Option<bool>,
/// Where to dock the assistant.
///
/// Default: right
dock: Option<AssistantDockPosition>,
/// Default width in pixels when the assistant is docked to the left or right.
///
/// Default: 640
default_width: Option<f32>,
/// Default height in pixels when the assistant is docked to the bottom.
///
/// Default: 320
default_height: Option<f32>,
/// The default model to use when creating new chats.
default_model: Option<LanguageModelSelection>,
/// Additional models with which to generate alternatives when performing inline assists.
inline_alternatives: Option<Vec<LanguageModelSelection>>,
/// Enable experimental live diffs in the assistant panel.
///
/// Default: false
enable_experimental_live_diffs: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct LanguageModelSelection {
#[schemars(schema_with = "providers_schema")]
pub provider: String,
pub model: String,
}
fn providers_schema(_: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
enum_values: Some(vec![
"anthropic".into(),
"google".into(),
"ollama".into(),
"openai".into(),
"zed.dev".into(),
"copilot_chat".into(),
]),
..Default::default()
}
.into()
}
impl Default for LanguageModelSelection {
fn default() -> Self {
Self {
provider: "openai".to_string(),
model: "gpt-4".to_string(),
}
}
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AssistantSettingsContentV1 {
/// Whether the Assistant is enabled.
///
/// Default: true
enabled: Option<bool>,
/// Whether to show the assistant panel button in the status bar.
///
/// Default: true
button: Option<bool>,
/// Where to dock the assistant.
///
/// Default: right
dock: Option<AssistantDockPosition>,
/// Default width in pixels when the assistant is docked to the left or right.
///
/// Default: 640
default_width: Option<f32>,
/// Default height in pixels when the assistant is docked to the bottom.
///
/// Default: 320
default_height: Option<f32>,
/// The provider of the assistant service.
///
/// This can be "openai", "anthropic", "ollama", "zed.dev"
/// each with their respective default models and configurations.
provider: Option<AssistantProviderContentV1>,
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
pub struct LegacyAssistantSettingsContent {
/// Whether to show the assistant panel button in the status bar.
///
/// Default: true
pub button: Option<bool>,
/// Where to dock the assistant.
///
/// Default: right
pub dock: Option<AssistantDockPosition>,
/// Default width in pixels when the assistant is docked to the left or right.
///
/// Default: 640
pub default_width: Option<f32>,
/// Default height in pixels when the assistant is docked to the bottom.
///
/// Default: 320
pub default_height: Option<f32>,
/// The default OpenAI model to use when creating new chats.
///
/// Default: gpt-4-1106-preview
pub default_open_ai_model: Option<OpenAiModel>,
/// OpenAI API base URL to use when creating new chats.
///
/// Default: https://api.openai.com/v1
pub openai_api_url: Option<String>,
}
impl Settings for AssistantSettings {
const KEY: Option<&'static str> = Some("assistant");
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
type FileContent = AssistantSettingsContent;
fn load(
sources: SettingsSources<Self::FileContent>,
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
let mut settings = AssistantSettings::default();
for value in sources.defaults_and_customizations() {
if value.is_version_outdated() {
settings.using_outdated_settings_version = true;
}
let value = value.upgrade();
merge(&mut settings.enabled, value.enabled);
merge(&mut settings.button, value.button);
merge(&mut settings.dock, value.dock);
merge(
&mut settings.default_width,
value.default_width.map(Into::into),
);
merge(
&mut settings.default_height,
value.default_height.map(Into::into),
);
merge(&mut settings.default_model, value.default_model);
merge(&mut settings.inline_alternatives, value.inline_alternatives);
merge(
&mut settings.enable_experimental_live_diffs,
value.enable_experimental_live_diffs,
);
}
Ok(settings)
}
}
fn merge<T>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;
}
}
#[cfg(test)]
mod tests {
use fs::Fs;
use gpui::{ReadGlobal, TestAppContext};
use super::*;
#[gpui::test]
async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
let fs = fs::FakeFs::new(cx.executor().clone());
fs.create_dir(paths::settings_file().parent().unwrap())
.await
.unwrap();
cx.update(|cx| {
let test_settings = settings::SettingsStore::test(cx);
cx.set_global(test_settings);
AssistantSettings::register(cx);
});
cx.update(|cx| {
assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
assert_eq!(
AssistantSettings::get_global(cx).default_model,
LanguageModelSelection {
provider: "zed.dev".into(),
model: "claude-3-5-sonnet".into(),
}
);
});
cx.update(|cx| {
settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
fs.clone(),
|settings, _| {
*settings = AssistantSettingsContent::Versioned(
VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
default_model: Some(LanguageModelSelection {
provider: "test-provider".into(),
model: "gpt-99".into(),
}),
inline_alternatives: None,
enabled: None,
button: None,
dock: None,
default_width: None,
default_height: None,
enable_experimental_live_diffs: None,
}),
)
},
);
});
cx.run_until_parked();
let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
assert!(raw_settings_value.contains(r#""version": "2""#));
#[derive(Debug, Deserialize)]
struct AssistantSettingsTest {
assistant: AssistantSettingsContent,
}
let assistant_settings: AssistantSettingsTest =
serde_json_lenient::from_str(&raw_settings_value).unwrap();
assert!(!assistant_settings.assistant.is_version_outdated());
}
}

View File

@@ -1,27 +0,0 @@
use gpui::SharedString;
use serde::{Deserialize, Serialize};
use util::post_inc;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct ContextId(pub(crate) usize);
impl ContextId {
pub fn post_inc(&mut self) -> Self {
Self(post_inc(&mut self.0))
}
}
/// Some context attached to a message in a thread.
#[derive(Debug, Clone)]
pub struct Context {
pub id: ContextId,
pub name: SharedString,
pub kind: ContextKind,
pub text: SharedString,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum ContextKind {
File,
FetchedUrl,
}

View File

@@ -1,227 +0,0 @@
mod fetch_context_picker;
mod file_context_picker;
use std::sync::Arc;
use gpui::{
AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, SharedString, Task, View,
WeakView,
};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, ListItemSpacing, Tooltip};
use util::ResultExt;
use workspace::Workspace;
use crate::context_picker::fetch_context_picker::FetchContextPicker;
use crate::context_picker::file_context_picker::FileContextPicker;
use crate::message_editor::MessageEditor;
#[derive(Debug, Clone)]
enum ContextPickerMode {
Default,
File(View<FileContextPicker>),
Fetch(View<FetchContextPicker>),
}
pub(super) struct ContextPicker {
mode: ContextPickerMode,
picker: View<Picker<ContextPickerDelegate>>,
}
impl ContextPicker {
pub fn new(
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = ContextPickerDelegate {
context_picker: cx.view().downgrade(),
workspace: workspace.clone(),
message_editor: message_editor.clone(),
entries: vec![
ContextPickerEntry {
name: "directory".into(),
description: "Insert any directory".into(),
icon: IconName::Folder,
},
ContextPickerEntry {
name: "file".into(),
description: "Insert any file".into(),
icon: IconName::File,
},
ContextPickerEntry {
name: "fetch".into(),
description: "Fetch content from URL".into(),
icon: IconName::Globe,
},
],
selected_ix: 0,
};
let picker = cx.new_view(|cx| {
Picker::nonsearchable_uniform_list(delegate, cx).max_height(Some(rems(20.).into()))
});
ContextPicker {
mode: ContextPickerMode::Default,
picker,
}
}
pub fn reset_mode(&mut self) {
self.mode = ContextPickerMode::Default;
}
}
impl EventEmitter<DismissEvent> for ContextPicker {}
impl FocusableView for ContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
match &self.mode {
ContextPickerMode::Default => self.picker.focus_handle(cx),
ContextPickerMode::File(file_picker) => file_picker.focus_handle(cx),
ContextPickerMode::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
}
}
}
impl Render for ContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
v_flex()
.w(px(400.))
.min_w(px(400.))
.map(|parent| match &self.mode {
ContextPickerMode::Default => parent.child(self.picker.clone()),
ContextPickerMode::File(file_picker) => parent.child(file_picker.clone()),
ContextPickerMode::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
})
}
}
#[derive(Clone)]
struct ContextPickerEntry {
name: SharedString,
description: SharedString,
icon: IconName,
}
pub(crate) struct ContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
entries: Vec<ContextPickerEntry>,
selected_ix: usize,
}
impl PickerDelegate for ContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.entries.len()
}
fn selected_index(&self) -> usize {
self.selected_ix
}
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
self.selected_ix = ix.min(self.entries.len().saturating_sub(1));
cx.notify();
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Select a context source…".into()
}
fn update_matches(&mut self, _query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
Task::ready(())
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(entry) = self.entries.get(self.selected_ix) {
self.context_picker
.update(cx, |this, cx| {
match entry.name.to_string().as_str() {
"file" => {
this.mode = ContextPickerMode::File(cx.new_view(|cx| {
FileContextPicker::new(
self.context_picker.clone(),
self.workspace.clone(),
self.message_editor.clone(),
cx,
)
}));
}
"fetch" => {
this.mode = ContextPickerMode::Fetch(cx.new_view(|cx| {
FetchContextPicker::new(
self.context_picker.clone(),
self.workspace.clone(),
self.message_editor.clone(),
cx,
)
}));
}
_ => {}
}
cx.focus_self();
})
.log_err();
}
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| match this.mode {
ContextPickerMode::Default => cx.emit(DismissEvent),
ContextPickerMode::File(_) | ContextPickerMode::Fetch(_) => {}
})
.log_err();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let entry = &self.entries[ix];
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Dense)
.toggle_state(selected)
.tooltip({
let description = entry.description.clone();
move |cx| cx.new_view(|_cx| Tooltip::new(description.clone())).into()
})
.child(
v_flex()
.group(format!("context-entry-label-{ix}"))
.w_full()
.py_0p5()
.min_w(px(250.))
.max_w(px(400.))
.child(
h_flex()
.gap_1p5()
.child(Icon::new(entry.icon).size(IconSize::XSmall))
.child(
Label::new(entry.name.clone())
.single_line()
.size(LabelSize::Small),
),
)
.child(
div().overflow_hidden().text_ellipsis().child(
Label::new(entry.description.clone())
.size(LabelSize::Small)
.color(Color::Muted),
),
),
),
)
}
}

View File

@@ -1,218 +0,0 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use anyhow::{bail, Context as _, Result};
use futures::AsyncReadExt as _;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakView};
use html_to_markdown::{convert_html_to_markdown, markdown, TagHandler};
use http_client::{AsyncBody, HttpClientWithUrl};
use picker::{Picker, PickerDelegate};
use ui::{prelude::*, ListItem, ListItemSpacing, ViewContext};
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::ContextPicker;
use crate::message_editor::MessageEditor;
pub struct FetchContextPicker {
picker: View<Picker<FetchContextPickerDelegate>>,
}
impl FetchContextPicker {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = FetchContextPickerDelegate::new(context_picker, workspace, message_editor);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker }
}
}
impl FocusableView for FetchContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for FetchContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
self.picker.clone()
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
enum ContentType {
Html,
Plaintext,
Json,
}
pub struct FetchContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
url: String,
}
impl FetchContextPickerDelegate {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
) -> Self {
FetchContextPickerDelegate {
context_picker,
workspace,
message_editor,
url: String::new(),
}
}
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
let mut url = url.to_owned();
if !url.starts_with("https://") && !url.starts_with("http://") {
url = format!("https://{url}");
}
let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading response body")?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let Some(content_type) = response.headers().get("content-type") else {
bail!("missing Content-Type header");
};
let content_type = content_type
.to_str()
.context("invalid Content-Type header")?;
let content_type = match content_type {
"text/html" => ContentType::Html,
"text/plain" => ContentType::Plaintext,
"application/json" => ContentType::Json,
_ => ContentType::Html,
};
match content_type {
ContentType::Html => {
let mut handlers: Vec<TagHandler> = vec![
Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
Rc::new(RefCell::new(markdown::ParagraphHandler)),
Rc::new(RefCell::new(markdown::HeadingHandler)),
Rc::new(RefCell::new(markdown::ListHandler)),
Rc::new(RefCell::new(markdown::TableHandler::new())),
Rc::new(RefCell::new(markdown::StyledTextHandler)),
];
if url.contains("wikipedia.org") {
use html_to_markdown::structure::wikipedia;
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
handlers.push(Rc::new(
RefCell::new(wikipedia::WikipediaCodeHandler::new()),
));
} else {
handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
}
convert_html_to_markdown(&body[..], &mut handlers)
}
ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
ContentType::Json => {
let json: serde_json::Value = serde_json::from_slice(&body)?;
Ok(format!(
"```json\n{}\n```",
serde_json::to_string_pretty(&json)?
))
}
}
}
}
impl PickerDelegate for FetchContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
1
}
fn selected_index(&self) -> usize {
0
}
fn set_selected_index(&mut self, _ix: usize, _cx: &mut ViewContext<Picker<Self>>) {}
fn placeholder_text(&self, _cx: &mut ui::WindowContext) -> Arc<str> {
"Enter a URL…".into()
}
fn update_matches(&mut self, query: String, _cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
self.url = query;
Task::ready(())
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let http_client = workspace.read(cx).client().http_client().clone();
let url = self.url.clone();
cx.spawn(|this, mut cx| async move {
let text = Self::build_message(http_client, &url).await?;
this.update(&mut cx, |this, cx| {
this.delegate
.message_editor
.update(cx, |message_editor, _cx| {
message_editor.insert_context(ContextKind::FetchedUrl, url, text);
})
})??;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(self.url.clone()),
)
}
}

View File

@@ -1,289 +0,0 @@
use std::fmt::Write as _;
use std::ops::RangeInclusive;
use std::path::{Path, PathBuf};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use fuzzy::PathMatch;
use gpui::{AppContext, DismissEvent, FocusHandle, FocusableView, Task, View, WeakView};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, WorktreeId};
use ui::{prelude::*, ListItem, ListItemSpacing};
use util::ResultExt as _;
use workspace::Workspace;
use crate::context::ContextKind;
use crate::context_picker::ContextPicker;
use crate::message_editor::MessageEditor;
pub struct FileContextPicker {
picker: View<Picker<FileContextPickerDelegate>>,
}
impl FileContextPicker {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
cx: &mut ViewContext<Self>,
) -> Self {
let delegate = FileContextPickerDelegate::new(context_picker, workspace, message_editor);
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self { picker }
}
}
impl FocusableView for FileContextPicker {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for FileContextPicker {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
self.picker.clone()
}
}
pub struct FileContextPickerDelegate {
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
matches: Vec<PathMatch>,
selected_index: usize,
}
impl FileContextPickerDelegate {
pub fn new(
context_picker: WeakView<ContextPicker>,
workspace: WeakView<Workspace>,
message_editor: WeakView<MessageEditor>,
) -> Self {
Self {
context_picker,
workspace,
message_editor,
matches: Vec::new(),
selected_index: 0,
}
}
fn search(
&mut self,
query: String,
cancellation_flag: Arc<AtomicBool>,
workspace: &View<Workspace>,
cx: &mut ViewContext<Picker<Self>>,
) -> Task<Vec<PathMatch>> {
if query.is_empty() {
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
let entries = workspace.recent_navigation_history(Some(10), cx);
let entries = entries
.into_iter()
.map(|entries| entries.0)
.chain(project.worktrees(cx).flat_map(|worktree| {
let worktree = worktree.read(cx);
let id = worktree.id();
worktree
.child_entries(Path::new(""))
.filter(|entry| entry.kind.is_file())
.map(move |entry| project::ProjectPath {
worktree_id: id,
path: entry.path.clone(),
})
}))
.collect::<Vec<_>>();
let path_prefix: Arc<str> = Arc::default();
Task::ready(
entries
.into_iter()
.filter_map(|entry| {
let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
let mut full_path = PathBuf::from(worktree.read(cx).root_name());
full_path.push(&entry.path);
Some(PathMatch {
score: 0.,
positions: Vec::new(),
worktree_id: entry.worktree_id.to_usize(),
path: full_path.into(),
path_prefix: path_prefix.clone(),
distance_to_relative_ancestor: 0,
is_dir: false,
})
})
.collect(),
)
} else {
let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
let candidate_sets = worktrees
.into_iter()
.map(|worktree| {
let worktree = worktree.read(cx);
PathMatchCandidateSet {
snapshot: worktree.snapshot(),
include_ignored: worktree
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name: true,
candidates: project::Candidates::Files,
}
})
.collect::<Vec<_>>();
let executor = cx.background_executor().clone();
cx.foreground_executor().spawn(async move {
fuzzy::match_path_sets(
candidate_sets.as_slice(),
query.as_str(),
None,
false,
100,
&cancellation_flag,
executor,
)
.await
})
}
}
}
impl PickerDelegate for FileContextPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Search files…".into()
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let Some(workspace) = self.workspace.upgrade() else {
return Task::ready(());
};
let search_task = self.search(query, Arc::<AtomicBool>::default(), &workspace, cx);
cx.spawn(|this, mut cx| async move {
// TODO: This should be probably be run in the background.
let paths = search_task.await;
this.update(&mut cx, |this, _cx| {
this.delegate.matches = paths;
})
.log_err();
})
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
let mat = &self.matches[self.selected_index];
let workspace = self.workspace.clone();
let Some(project) = workspace
.upgrade()
.map(|workspace| workspace.read(cx).project().clone())
else {
return;
};
let path = mat.path.clone();
let worktree_id = WorktreeId::from_usize(mat.worktree_id);
cx.spawn(|this, mut cx| async move {
let Some(open_buffer_task) = project
.update(&mut cx, |project, cx| {
project.open_buffer((worktree_id, path.clone()), cx)
})
.ok()
else {
return anyhow::Ok(());
};
let buffer = open_buffer_task.await?;
this.update(&mut cx, |this, cx| {
this.delegate
.message_editor
.update(cx, |message_editor, cx| {
let mut text = String::new();
text.push_str(&codeblock_fence_for_path(Some(&path), None));
text.push_str(&buffer.read(cx).text());
if !text.ends_with('\n') {
text.push('\n');
}
text.push_str("```\n");
message_editor.insert_context(
ContextKind::File,
path.to_string_lossy().to_string(),
text,
);
})
})??;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.context_picker
.update(cx, |this, cx| {
this.reset_mode();
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let mat = &self.matches[ix];
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(mat.path.to_string_lossy().to_string()),
)
}
}
fn codeblock_fence_for_path(path: Option<&Path>, row_range: Option<RangeInclusive<u32>>) -> String {
let mut text = String::new();
write!(text, "```").unwrap();
if let Some(path) = path {
if let Some(extension) = path.extension().and_then(|ext| ext.to_str()) {
write!(text, "{} ", extension).unwrap();
}
write!(text, "{}", path.display()).unwrap();
} else {
write!(text, "untitled").unwrap();
}
if let Some(row_range) = row_range {
write!(text, ":{}-{}", row_range.start() + 1, row_range.end() + 1).unwrap();
}
text.push('\n');
text
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,296 +0,0 @@
use std::rc::Rc;
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{AppContext, FocusableView, Model, TextStyle, View, WeakView};
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
use settings::Settings;
use theme::ThemeSettings;
use ui::{
prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, IconButtonShape, KeyBinding,
PopoverMenu, PopoverMenuHandle, Tooltip,
};
use workspace::Workspace;
use crate::context::{Context, ContextId, ContextKind};
use crate::context_picker::ContextPicker;
use crate::thread::{RequestKind, Thread};
use crate::ui::ContextPill;
use crate::{Chat, ToggleModelSelector};
pub struct MessageEditor {
thread: Model<Thread>,
editor: View<Editor>,
context: Vec<Context>,
next_context_id: ContextId,
context_picker: View<ContextPicker>,
pub(crate) context_picker_handle: PopoverMenuHandle<ContextPicker>,
language_model_selector: View<LanguageModelSelector>,
use_tools: bool,
}
impl MessageEditor {
pub fn new(
workspace: WeakView<Workspace>,
thread: Model<Thread>,
cx: &mut ViewContext<Self>,
) -> Self {
let weak_self = cx.view().downgrade();
Self {
thread,
editor: cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
editor.set_placeholder_text("Ask anything or type @ to add context", cx);
editor
}),
context: Vec::new(),
next_context_id: ContextId(0),
context_picker: cx.new_view(|cx| ContextPicker::new(workspace.clone(), weak_self, cx)),
context_picker_handle: PopoverMenuHandle::default(),
language_model_selector: cx.new_view(|cx| {
LanguageModelSelector::new(
|model, _cx| {
println!("Selected {:?}", model.name());
},
cx,
)
}),
use_tools: false,
}
}
pub fn insert_context(
&mut self,
kind: ContextKind,
name: impl Into<SharedString>,
text: impl Into<SharedString>,
) {
self.context.push(Context {
id: self.next_context_id.post_inc(),
name: name.into(),
kind,
text: text.into(),
});
}
fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
self.send_to_model(RequestKind::Chat, cx);
}
fn send_to_model(
&mut self,
request_kind: RequestKind,
cx: &mut ViewContext<Self>,
) -> Option<()> {
let provider = LanguageModelRegistry::read_global(cx).active_provider();
if provider
.as_ref()
.map_or(false, |provider| provider.must_accept_terms(cx))
{
cx.notify();
return None;
}
let model_registry = LanguageModelRegistry::read_global(cx);
let model = model_registry.active_model()?;
let user_message = self.editor.update(cx, |editor, cx| {
let text = editor.text(cx);
editor.clear(cx);
text
});
let context = self.context.drain(..).collect::<Vec<_>>();
self.thread.update(cx, |thread, cx| {
thread.insert_user_message(user_message, context, cx);
let mut request = thread.to_completion_request(request_kind, cx);
if self.use_tools {
request.tools = thread
.tools()
.tools(cx)
.into_iter()
.map(|tool| LanguageModelRequestTool {
name: tool.name(),
description: tool.description(),
input_schema: tool.input_schema(),
})
.collect();
}
thread.stream_completion(request, model, cx)
});
None
}
fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
let active_model = LanguageModelRegistry::read_global(cx).active_model();
LanguageModelSelectorPopoverMenu::new(
self.language_model_selector.clone(),
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(
h_flex()
.w_full()
.gap_0p5()
.child(
div()
.overflow_x_hidden()
.flex_grow()
.whitespace_nowrap()
.child(match (active_provider, active_model) {
(Some(provider), Some(model)) => h_flex()
.gap_1()
.child(
Icon::new(
model.icon().unwrap_or_else(|| provider.icon()),
)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(
Label::new(model.name().0)
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element(),
_ => Label::new("No model selected")
.size(LabelSize::Small)
.color(Color::Muted)
.into_any_element(),
}),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
)
.tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
)
}
}
impl FocusableView for MessageEditor {
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
self.editor.focus_handle(cx)
}
}
impl Render for MessageEditor {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let font_size = TextSize::Default.rems(cx);
let line_height = font_size.to_pixels(cx.rem_size()) * 1.3;
let focus_handle = self.editor.focus_handle(cx);
let context_picker = self.context_picker.clone();
v_flex()
.key_context("MessageEditor")
.on_action(cx.listener(Self::chat))
.size_full()
.gap_2()
.p_2()
.bg(cx.theme().colors().editor_background)
.child(
h_flex()
.flex_wrap()
.gap_2()
.child(
PopoverMenu::new("context-picker")
.menu(move |_cx| Some(context_picker.clone()))
.trigger(
IconButton::new("add-context", IconName::Plus)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small),
)
.attach(gpui::AnchorCorner::TopLeft)
.anchor(gpui::AnchorCorner::BottomLeft)
.offset(gpui::Point {
x: px(0.0),
y: px(-16.0),
})
.with_handle(self.context_picker_handle.clone()),
)
.children(self.context.iter().map(|context| {
ContextPill::new(context.clone()).on_remove({
let context = context.clone();
Rc::new(cx.listener(move |this, _event, cx| {
this.context.retain(|other| other.id != context.id);
cx.notify();
}))
})
}))
.when(!self.context.is_empty(), |parent| {
parent.child(
IconButton::new("remove-all-context", IconName::Eraser)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.tooltip(move |cx| Tooltip::text("Remove All Context", cx))
.on_click(cx.listener(|this, _event, cx| {
this.context.clear();
cx.notify();
})),
)
}),
)
.child({
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: cx.theme().colors().editor_foreground,
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features.clone(),
font_size: font_size.into(),
font_weight: settings.ui_font.weight,
line_height: line_height.into(),
..Default::default()
};
EditorElement::new(
&self.editor,
EditorStyle {
background: cx.theme().colors().editor_background,
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
)
})
.child(
h_flex()
.justify_between()
.child(h_flex().gap_2().child(CheckboxWithLabel::new(
"use-tools",
Label::new("Tools"),
self.use_tools.into(),
cx.listener(|this, selection, _cx| {
this.use_tools = match selection {
ToggleState::Selected => true,
ToggleState::Unselected | ToggleState::Indeterminate => false,
};
}),
)))
.child(
h_flex()
.gap_2()
.child(self.render_language_model_selector(cx))
.child(
ButtonLike::new("chat")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Submit"))
.children(
KeyBinding::for_action_in(&Chat, &focus_handle, cx)
.map(|binding| binding.into_any_element()),
)
.on_click(move |_event, cx| {
focus_handle.dispatch_action(&Chat, cx);
}),
),
),
)
}
}

View File

@@ -1,312 +0,0 @@
use anyhow::Result;
use assets::Assets;
use fs::Fs;
use futures::StreamExt;
use gpui::AssetSource;
use handlebars::{Handlebars, RenderError};
use language::{BufferSnapshot, LanguageName, Point};
use parking_lot::Mutex;
use serde::Serialize;
use std::{ops::Range, path::PathBuf, sync::Arc, time::Duration};
use text::LineEnding;
use util::ResultExt;
#[derive(Serialize)]
pub struct ContentPromptDiagnosticContext {
pub line_number: usize,
pub error_message: String,
pub code_content: String,
}
#[derive(Serialize)]
pub struct ContentPromptContext {
pub content_type: String,
pub language_name: Option<String>,
pub is_insert: bool,
pub is_truncated: bool,
pub document_content: String,
pub user_prompt: String,
pub rewrite_section: Option<String>,
pub diagnostic_errors: Vec<ContentPromptDiagnosticContext>,
}
#[derive(Serialize)]
pub struct TerminalAssistantPromptContext {
pub os: String,
pub arch: String,
pub shell: Option<String>,
pub working_directory: Option<String>,
pub latest_output: Vec<String>,
pub user_prompt: String,
}
#[derive(Serialize)]
pub struct ProjectSlashCommandPromptContext {
pub context_buffer: String,
}
pub struct PromptLoadingParams<'a> {
pub fs: Arc<dyn Fs>,
pub repo_path: Option<PathBuf>,
pub cx: &'a gpui::AppContext,
}
pub struct PromptBuilder {
handlebars: Arc<Mutex<Handlebars<'static>>>,
}
impl PromptBuilder {
pub fn new(loading_params: Option<PromptLoadingParams>) -> Result<Self> {
let mut handlebars = Handlebars::new();
Self::register_built_in_templates(&mut handlebars)?;
let handlebars = Arc::new(Mutex::new(handlebars));
if let Some(params) = loading_params {
Self::watch_fs_for_template_overrides(params, handlebars.clone());
}
Ok(Self { handlebars })
}
/// Watches the filesystem for changes to prompt template overrides.
///
/// This function sets up a file watcher on the prompt templates directory. It performs
/// an initial scan of the directory and registers any existing template overrides.
/// Then it continuously monitors for changes, reloading templates as they are
/// modified or added.
///
/// If the templates directory doesn't exist initially, it waits for it to be created.
/// If the directory is removed, it restores the built-in templates and waits for the
/// directory to be recreated.
///
/// # Arguments
///
/// * `params` - A `PromptLoadingParams` struct containing the filesystem, repository path,
/// and application context.
/// * `handlebars` - An `Arc<Mutex<Handlebars>>` for registering and updating templates.
fn watch_fs_for_template_overrides(
params: PromptLoadingParams,
handlebars: Arc<Mutex<Handlebars<'static>>>,
) {
let templates_dir = paths::prompt_overrides_dir(params.repo_path.as_deref());
params.cx.background_executor()
.spawn(async move {
let Some(parent_dir) = templates_dir.parent() else {
return;
};
let mut found_dir_once = false;
loop {
// Check if the templates directory exists and handle its status
// If it exists, log its presence and check if it's a symlink
// If it doesn't exist:
// - Log that we're using built-in prompts
// - Check if it's a broken symlink and log if so
// - Set up a watcher to detect when it's created
// After the first check, set the `found_dir_once` flag
// This allows us to avoid logging when looping back around after deleting the prompt overrides directory.
let dir_status = params.fs.is_dir(&templates_dir).await;
let symlink_status = params.fs.read_link(&templates_dir).await.ok();
if dir_status {
let mut log_message = format!("Prompt template overrides directory found at {}", templates_dir.display());
if let Some(target) = symlink_status {
log_message.push_str(" -> ");
log_message.push_str(&target.display().to_string());
}
log::info!("{}.", log_message);
} else {
if !found_dir_once {
log::info!("No prompt template overrides directory found at {}. Using built-in prompts.", templates_dir.display());
if let Some(target) = symlink_status {
log::info!("Symlink found pointing to {}, but target is invalid.", target.display());
}
}
if params.fs.is_dir(parent_dir).await {
let (mut changes, _watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
while let Some(changed_paths) = changes.next().await {
if changed_paths.iter().any(|p| &p.path == &templates_dir) {
let mut log_message = format!("Prompt template overrides directory detected at {}", templates_dir.display());
if let Ok(target) = params.fs.read_link(&templates_dir).await {
log_message.push_str(" -> ");
log_message.push_str(&target.display().to_string());
}
log::info!("{}.", log_message);
break;
}
}
} else {
return;
}
}
found_dir_once = true;
// Initial scan of the prompt overrides directory
if let Ok(mut entries) = params.fs.read_dir(&templates_dir).await {
while let Some(Ok(file_path)) = entries.next().await {
if file_path.to_string_lossy().ends_with(".hbs") {
if let Ok(content) = params.fs.load(&file_path).await {
let file_name = file_path.file_stem().unwrap().to_string_lossy();
log::debug!("Registering prompt template override: {}", file_name);
handlebars.lock().register_template_string(&file_name, content).log_err();
}
}
}
}
// Watch both the parent directory and the template overrides directory:
// - Monitor the parent directory to detect if the template overrides directory is deleted.
// - Monitor the template overrides directory to re-register templates when they change.
// Combine both watch streams into a single stream.
let (parent_changes, parent_watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
let (changes, watcher) = params.fs.watch(&templates_dir, Duration::from_secs(1)).await;
let mut combined_changes = futures::stream::select(changes, parent_changes);
while let Some(changed_paths) = combined_changes.next().await {
if changed_paths.iter().any(|p| &p.path == &templates_dir) {
if !params.fs.is_dir(&templates_dir).await {
log::info!("Prompt template overrides directory removed. Restoring built-in prompt templates.");
Self::register_built_in_templates(&mut handlebars.lock()).log_err();
break;
}
}
for event in changed_paths {
if event.path.starts_with(&templates_dir) && event.path.extension().map_or(false, |ext| ext == "hbs") {
log::info!("Reloading prompt template override: {}", event.path.display());
if let Some(content) = params.fs.load(&event.path).await.log_err() {
let file_name = event.path.file_stem().unwrap().to_string_lossy();
handlebars.lock().register_template_string(&file_name, content).log_err();
}
}
}
}
drop(watcher);
drop(parent_watcher);
}
})
.detach();
}
fn register_built_in_templates(handlebars: &mut Handlebars) -> Result<()> {
for path in Assets.list("prompts")? {
if let Some(id) = path.split('/').last().and_then(|s| s.strip_suffix(".hbs")) {
if let Some(prompt) = Assets.load(path.as_ref()).log_err().flatten() {
log::debug!("Registering built-in prompt template: {}", id);
let prompt = String::from_utf8_lossy(prompt.as_ref());
handlebars.register_template_string(id, LineEnding::normalize_cow(prompt))?
}
}
}
Ok(())
}
pub fn generate_inline_transformation_prompt(
&self,
user_prompt: String,
language_name: Option<&LanguageName>,
buffer: BufferSnapshot,
range: Range<usize>,
) -> Result<String, RenderError> {
let content_type = match language_name.as_ref().map(|l| l.0.as_ref()) {
None | Some("Markdown" | "Plain Text") => "text",
Some(_) => "code",
};
const MAX_CTX: usize = 50000;
let is_insert = range.is_empty();
let mut is_truncated = false;
let before_range = 0..range.start;
let truncated_before = if before_range.len() > MAX_CTX {
is_truncated = true;
let start = buffer.clip_offset(range.start - MAX_CTX, text::Bias::Right);
start..range.start
} else {
before_range
};
let after_range = range.end..buffer.len();
let truncated_after = if after_range.len() > MAX_CTX {
is_truncated = true;
let end = buffer.clip_offset(range.end + MAX_CTX, text::Bias::Left);
range.end..end
} else {
after_range
};
let mut document_content = String::new();
for chunk in buffer.text_for_range(truncated_before) {
document_content.push_str(chunk);
}
if is_insert {
document_content.push_str("<insert_here></insert_here>");
} else {
document_content.push_str("<rewrite_this>\n");
for chunk in buffer.text_for_range(range.clone()) {
document_content.push_str(chunk);
}
document_content.push_str("\n</rewrite_this>");
}
for chunk in buffer.text_for_range(truncated_after) {
document_content.push_str(chunk);
}
let rewrite_section = if !is_insert {
let mut section = String::new();
for chunk in buffer.text_for_range(range.clone()) {
section.push_str(chunk);
}
Some(section)
} else {
None
};
let diagnostics = buffer.diagnostics_in_range::<_, Point>(range, false);
let diagnostic_errors: Vec<ContentPromptDiagnosticContext> = diagnostics
.map(|entry| {
let start = entry.range.start;
ContentPromptDiagnosticContext {
line_number: (start.row + 1) as usize,
error_message: entry.diagnostic.message.clone(),
code_content: buffer.text_for_range(entry.range.clone()).collect(),
}
})
.collect();
let context = ContentPromptContext {
content_type: content_type.to_string(),
language_name: language_name.map(|s| s.to_string()),
is_insert,
is_truncated,
document_content,
user_prompt,
rewrite_section,
diagnostic_errors,
};
self.handlebars.lock().render("content_prompt", &context)
}
pub fn generate_terminal_assistant_prompt(
&self,
user_prompt: &str,
shell: Option<&str>,
working_directory: Option<&str>,
latest_output: &[String],
) -> Result<String, RenderError> {
let context = TerminalAssistantPromptContext {
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
shell: shell.map(|s| s.to_string()),
working_directory: working_directory.map(|s| s.to_string()),
latest_output: latest_output.to_vec(),
user_prompt: user_prompt.to_string(),
};
self.handlebars
.lock()
.render("terminal_assistant_prompt", &context)
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,531 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
use assistant_tool::ToolWorkingSet;
use chrono::{DateTime, Utc};
use collections::HashMap;
use futures::future::Shared;
use futures::{FutureExt as _, StreamExt as _};
use gpui::{AppContext, EventEmitter, ModelContext, SharedString, Task};
use language_model::{
LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
LanguageModelToolUseId, MessageContent, Role, StopReason,
};
use language_models::provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError};
use serde::{Deserialize, Serialize};
use util::{post_inc, TryFutureExt as _};
use uuid::Uuid;
use crate::context::{Context, ContextKind};
#[derive(Debug, Clone, Copy)]
pub enum RequestKind {
Chat,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
pub struct ThreadId(Arc<str>);
impl ThreadId {
pub fn new() -> Self {
Self(Uuid::new_v4().to_string().into())
}
}
impl std::fmt::Display for ThreadId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
pub struct MessageId(usize);
impl MessageId {
fn post_inc(&mut self) -> Self {
Self(post_inc(&mut self.0))
}
}
/// A message in a [`Thread`].
#[derive(Debug, Clone)]
pub struct Message {
pub id: MessageId,
pub role: Role,
pub text: String,
}
/// A thread of conversation with the LLM.
pub struct Thread {
id: ThreadId,
updated_at: DateTime<Utc>,
summary: Option<SharedString>,
pending_summary: Task<Option<()>>,
messages: Vec<Message>,
next_message_id: MessageId,
context_by_message: HashMap<MessageId, Vec<Context>>,
completion_count: usize,
pending_completions: Vec<PendingCompletion>,
tools: Arc<ToolWorkingSet>,
tool_uses_by_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
tool_results_by_message: HashMap<MessageId, Vec<LanguageModelToolResult>>,
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
}
impl Thread {
pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut ModelContext<Self>) -> Self {
Self {
id: ThreadId::new(),
updated_at: Utc::now(),
summary: None,
pending_summary: Task::ready(None),
messages: Vec::new(),
next_message_id: MessageId(0),
context_by_message: HashMap::default(),
completion_count: 0,
pending_completions: Vec::new(),
tools,
tool_uses_by_message: HashMap::default(),
tool_results_by_message: HashMap::default(),
pending_tool_uses_by_id: HashMap::default(),
}
}
pub fn id(&self) -> &ThreadId {
&self.id
}
pub fn is_empty(&self) -> bool {
self.messages.is_empty()
}
pub fn updated_at(&self) -> DateTime<Utc> {
self.updated_at
}
pub fn touch_updated_at(&mut self) {
self.updated_at = Utc::now();
}
pub fn summary(&self) -> Option<SharedString> {
self.summary.clone()
}
pub fn set_summary(&mut self, summary: impl Into<SharedString>, cx: &mut ModelContext<Self>) {
self.summary = Some(summary.into());
cx.emit(ThreadEvent::SummaryChanged);
}
pub fn message(&self, id: MessageId) -> Option<&Message> {
self.messages.iter().find(|message| message.id == id)
}
pub fn messages(&self) -> impl Iterator<Item = &Message> {
self.messages.iter()
}
pub fn tools(&self) -> &Arc<ToolWorkingSet> {
&self.tools
}
pub fn context_for_message(&self, id: MessageId) -> Option<&Vec<Context>> {
self.context_by_message.get(&id)
}
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
self.pending_tool_uses_by_id.values().collect()
}
pub fn insert_user_message(
&mut self,
text: impl Into<String>,
context: Vec<Context>,
cx: &mut ModelContext<Self>,
) {
let message_id = self.insert_message(Role::User, text, cx);
self.context_by_message.insert(message_id, context);
}
pub fn insert_message(
&mut self,
role: Role,
text: impl Into<String>,
cx: &mut ModelContext<Self>,
) -> MessageId {
let id = self.next_message_id.post_inc();
self.messages.push(Message {
id,
role,
text: text.into(),
});
self.touch_updated_at();
cx.emit(ThreadEvent::MessageAdded(id));
id
}
pub fn to_completion_request(
&self,
_request_kind: RequestKind,
_cx: &AppContext,
) -> LanguageModelRequest {
let mut request = LanguageModelRequest {
messages: vec![],
tools: Vec::new(),
stop: Vec::new(),
temperature: None,
};
for message in &self.messages {
let mut request_message = LanguageModelRequestMessage {
role: message.role,
content: Vec::new(),
cache: false,
};
if let Some(tool_results) = self.tool_results_by_message.get(&message.id) {
for tool_result in tool_results {
request_message
.content
.push(MessageContent::ToolResult(tool_result.clone()));
}
}
if let Some(context) = self.context_for_message(message.id) {
let mut file_context = String::new();
let mut fetch_context = String::new();
for context in context.iter() {
match context.kind {
ContextKind::File => {
file_context.push_str(&context.text);
file_context.push('\n');
}
ContextKind::FetchedUrl => {
fetch_context.push_str(&context.name);
fetch_context.push('\n');
fetch_context.push_str(&context.text);
fetch_context.push('\n');
}
}
}
let mut context_text = String::new();
if !file_context.is_empty() {
context_text.push_str("The following files are available:\n");
context_text.push_str(&file_context);
}
if !fetch_context.is_empty() {
context_text.push_str("The following fetched results are available\n");
context_text.push_str(&fetch_context);
}
request_message
.content
.push(MessageContent::Text(context_text))
}
if !message.text.is_empty() {
request_message
.content
.push(MessageContent::Text(message.text.clone()));
}
if let Some(tool_uses) = self.tool_uses_by_message.get(&message.id) {
for tool_use in tool_uses {
request_message
.content
.push(MessageContent::ToolUse(tool_use.clone()));
}
}
request.messages.push(request_message);
}
request
}
pub fn stream_completion(
&mut self,
request: LanguageModelRequest,
model: Arc<dyn LanguageModel>,
cx: &mut ModelContext<Self>,
) {
let pending_completion_id = post_inc(&mut self.completion_count);
let task = cx.spawn(|thread, mut cx| async move {
let stream = model.stream_completion(request, &cx);
let stream_completion = async {
let mut events = stream.await?;
let mut stop_reason = StopReason::EndTurn;
while let Some(event) = events.next().await {
let event = event?;
thread.update(&mut cx, |thread, cx| {
match event {
LanguageModelCompletionEvent::StartMessage { .. } => {
thread.insert_message(Role::Assistant, String::new(), cx);
}
LanguageModelCompletionEvent::Stop(reason) => {
stop_reason = reason;
}
LanguageModelCompletionEvent::Text(chunk) => {
if let Some(last_message) = thread.messages.last_mut() {
if last_message.role == Role::Assistant {
last_message.text.push_str(&chunk);
cx.emit(ThreadEvent::StreamedAssistantText(
last_message.id,
chunk,
));
}
}
}
LanguageModelCompletionEvent::ToolUse(tool_use) => {
if let Some(last_assistant_message) = thread
.messages
.iter()
.rfind(|message| message.role == Role::Assistant)
{
thread
.tool_uses_by_message
.entry(last_assistant_message.id)
.or_default()
.push(tool_use.clone());
thread.pending_tool_uses_by_id.insert(
tool_use.id.clone(),
PendingToolUse {
assistant_message_id: last_assistant_message.id,
id: tool_use.id,
name: tool_use.name,
input: tool_use.input,
status: PendingToolUseStatus::Idle,
},
);
}
}
}
thread.touch_updated_at();
cx.emit(ThreadEvent::StreamedCompletion);
cx.notify();
})?;
smol::future::yield_now().await;
}
thread.update(&mut cx, |thread, cx| {
thread
.pending_completions
.retain(|completion| completion.id != pending_completion_id);
if thread.summary.is_none() && thread.messages.len() >= 2 {
thread.summarize(cx);
}
})?;
anyhow::Ok(stop_reason)
};
let result = stream_completion.await;
thread
.update(&mut cx, |_thread, cx| match result.as_ref() {
Ok(stop_reason) => match stop_reason {
StopReason::ToolUse => {
cx.emit(ThreadEvent::UsePendingTools);
}
StopReason::EndTurn => {}
StopReason::MaxTokens => {}
},
Err(error) => {
if error.is::<PaymentRequiredError>() {
cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired));
} else if error.is::<MaxMonthlySpendReachedError>() {
cx.emit(ThreadEvent::ShowError(ThreadError::MaxMonthlySpendReached));
} else {
let error_message = error
.chain()
.map(|err| err.to_string())
.collect::<Vec<_>>()
.join("\n");
cx.emit(ThreadEvent::ShowError(ThreadError::Message(
SharedString::from(error_message.clone()),
)));
}
}
})
.ok();
});
self.pending_completions.push(PendingCompletion {
id: pending_completion_id,
_task: task,
});
}
pub fn summarize(&mut self, cx: &mut ModelContext<Self>) {
let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
return;
};
let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
return;
};
if !provider.is_authenticated(cx) {
return;
}
let mut request = self.to_completion_request(RequestKind::Chat, cx);
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![
"Generate a concise 3-7 word title for this conversation, omitting punctuation. Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`"
.into(),
],
cache: false,
});
self.pending_summary = cx.spawn(|this, mut cx| {
async move {
let stream = model.stream_completion_text(request, &cx);
let mut messages = stream.await?;
let mut new_summary = String::new();
while let Some(message) = messages.stream.next().await {
let text = message?;
let mut lines = text.lines();
new_summary.extend(lines.next());
// Stop if the LLM generated multiple lines.
if lines.next().is_some() {
break;
}
}
this.update(&mut cx, |this, cx| {
if !new_summary.is_empty() {
this.summary = Some(new_summary.into());
}
cx.emit(ThreadEvent::SummaryChanged);
})?;
anyhow::Ok(())
}
.log_err()
});
}
pub fn insert_tool_output(
&mut self,
assistant_message_id: MessageId,
tool_use_id: LanguageModelToolUseId,
output: Task<Result<String>>,
cx: &mut ModelContext<Self>,
) {
let insert_output_task = cx.spawn(|thread, mut cx| {
let tool_use_id = tool_use_id.clone();
async move {
let output = output.await;
thread
.update(&mut cx, |thread, cx| {
// The tool use was requested by an Assistant message,
// so we want to attach the tool results to the next
// user message.
let next_user_message = MessageId(assistant_message_id.0 + 1);
let tool_results = thread
.tool_results_by_message
.entry(next_user_message)
.or_default();
match output {
Ok(output) => {
tool_results.push(LanguageModelToolResult {
tool_use_id: tool_use_id.to_string(),
content: output,
is_error: false,
});
cx.emit(ThreadEvent::ToolFinished { tool_use_id });
}
Err(err) => {
tool_results.push(LanguageModelToolResult {
tool_use_id: tool_use_id.to_string(),
content: err.to_string(),
is_error: true,
});
if let Some(tool_use) =
thread.pending_tool_uses_by_id.get_mut(&tool_use_id)
{
tool_use.status = PendingToolUseStatus::Error(err.to_string());
}
}
}
})
.ok();
}
});
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
tool_use.status = PendingToolUseStatus::Running {
_task: insert_output_task.shared(),
};
}
}
}
#[derive(Debug, Clone)]
pub enum ThreadError {
PaymentRequired,
MaxMonthlySpendReached,
Message(SharedString),
}
#[derive(Debug, Clone)]
pub enum ThreadEvent {
ShowError(ThreadError),
StreamedCompletion,
StreamedAssistantText(MessageId, String),
MessageAdded(MessageId),
SummaryChanged,
UsePendingTools,
ToolFinished {
#[allow(unused)]
tool_use_id: LanguageModelToolUseId,
},
}
impl EventEmitter<ThreadEvent> for Thread {}
struct PendingCompletion {
id: usize,
_task: Task<()>,
}
#[derive(Debug, Clone)]
pub struct PendingToolUse {
pub id: LanguageModelToolUseId,
/// The ID of the Assistant message in which the tool use was requested.
pub assistant_message_id: MessageId,
pub name: String,
pub input: serde_json::Value,
pub status: PendingToolUseStatus,
}
#[derive(Debug, Clone)]
pub enum PendingToolUseStatus {
Idle,
Running { _task: Shared<Task<()>> },
Error(#[allow(unused)] String),
}
impl PendingToolUseStatus {
pub fn is_idle(&self) -> bool {
matches!(self, PendingToolUseStatus::Idle)
}
}

View File

@@ -1,156 +0,0 @@
use gpui::{
uniform_list, AppContext, FocusHandle, FocusableView, Model, UniformListScrollHandle, WeakView,
};
use time::{OffsetDateTime, UtcOffset};
use ui::{prelude::*, IconButtonShape, ListItem};
use crate::thread::Thread;
use crate::thread_store::ThreadStore;
use crate::AssistantPanel;
pub struct ThreadHistory {
focus_handle: FocusHandle,
assistant_panel: WeakView<AssistantPanel>,
thread_store: Model<ThreadStore>,
scroll_handle: UniformListScrollHandle,
}
impl ThreadHistory {
pub(crate) fn new(
assistant_panel: WeakView<AssistantPanel>,
thread_store: Model<ThreadStore>,
cx: &mut ViewContext<Self>,
) -> Self {
Self {
focus_handle: cx.focus_handle(),
assistant_panel,
thread_store,
scroll_handle: UniformListScrollHandle::default(),
}
}
}
impl FocusableView for ThreadHistory {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for ThreadHistory {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let threads = self.thread_store.update(cx, |this, cx| this.threads(cx));
v_flex()
.id("thread-history-container")
.track_focus(&self.focus_handle)
.overflow_y_scroll()
.size_full()
.p_1()
.map(|history| {
if threads.is_empty() {
history
.justify_center()
.child(
h_flex().w_full().justify_center().child(
Label::new("You don't have any past threads yet.")
.size(LabelSize::Small),
),
)
} else {
history.child(
uniform_list(
cx.view().clone(),
"thread-history",
threads.len(),
move |history, range, _cx| {
threads[range]
.iter()
.map(|thread| {
PastThread::new(
thread.clone(),
history.assistant_panel.clone(),
)
})
.collect()
},
)
.track_scroll(self.scroll_handle.clone())
.flex_grow(),
)
}
})
}
}
#[derive(IntoElement)]
pub struct PastThread {
thread: Model<Thread>,
assistant_panel: WeakView<AssistantPanel>,
}
impl PastThread {
pub fn new(thread: Model<Thread>, assistant_panel: WeakView<AssistantPanel>) -> Self {
Self {
thread,
assistant_panel,
}
}
}
impl RenderOnce for PastThread {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let (id, summary) = {
const DEFAULT_SUMMARY: SharedString = SharedString::new_static("New Thread");
let thread = self.thread.read(cx);
(
thread.id().clone(),
thread.summary().unwrap_or(DEFAULT_SUMMARY),
)
};
let thread_timestamp = time_format::format_localized_timestamp(
OffsetDateTime::from_unix_timestamp(self.thread.read(cx).updated_at().timestamp())
.unwrap(),
OffsetDateTime::now_utc(),
self.assistant_panel
.update(cx, |this, _cx| this.local_timezone())
.unwrap_or(UtcOffset::UTC),
time_format::TimestampFormat::EnhancedAbsolute,
);
ListItem::new(("past-thread", self.thread.entity_id()))
.start_slot(Icon::new(IconName::MessageBubbles))
.child(Label::new(summary))
.end_slot(
h_flex()
.gap_2()
.child(Label::new(thread_timestamp).color(Color::Disabled))
.child(
IconButton::new("delete", IconName::TrashAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
.on_click({
let assistant_panel = self.assistant_panel.clone();
let id = id.clone();
move |_event, cx| {
assistant_panel
.update(cx, |this, cx| {
this.delete_thread(&id, cx);
})
.ok();
}
}),
),
)
.on_click({
let assistant_panel = self.assistant_panel.clone();
let id = id.clone();
move |_event, cx| {
assistant_panel
.update(cx, |this, cx| {
this.open_thread(&id, cx);
})
.ok();
}
})
}
}

View File

@@ -1,242 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
use assistant_tool::{ToolId, ToolWorkingSet};
use collections::HashMap;
use context_server::manager::ContextServerManager;
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
use gpui::{prelude::*, AppContext, Model, ModelContext, Task};
use project::Project;
use unindent::Unindent;
use util::ResultExt as _;
use crate::thread::{Thread, ThreadId};
pub struct ThreadStore {
#[allow(unused)]
project: Model<Project>,
tools: Arc<ToolWorkingSet>,
context_server_manager: Model<ContextServerManager>,
context_server_tool_ids: HashMap<Arc<str>, Vec<ToolId>>,
threads: Vec<Model<Thread>>,
}
impl ThreadStore {
pub fn new(
project: Model<Project>,
tools: Arc<ToolWorkingSet>,
cx: &mut AppContext,
) -> Task<Result<Model<Self>>> {
cx.spawn(|mut cx| async move {
let this = cx.new_model(|cx: &mut ModelContext<Self>| {
let context_server_factory_registry =
ContextServerFactoryRegistry::default_global(cx);
let context_server_manager = cx.new_model(|cx| {
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
});
let mut this = Self {
project,
tools,
context_server_manager,
context_server_tool_ids: HashMap::default(),
threads: Vec::new(),
};
this.mock_recent_threads(cx);
this.register_context_server_handlers(cx);
this
})?;
Ok(this)
})
}
pub fn threads(&self, cx: &ModelContext<Self>) -> Vec<Model<Thread>> {
let mut threads = self
.threads
.iter()
.filter(|thread| !thread.read(cx).is_empty())
.cloned()
.collect::<Vec<_>>();
threads.sort_unstable_by_key(|thread| std::cmp::Reverse(thread.read(cx).updated_at()));
threads
}
pub fn recent_threads(&self, limit: usize, cx: &ModelContext<Self>) -> Vec<Model<Thread>> {
self.threads(cx).into_iter().take(limit).collect()
}
pub fn create_thread(&mut self, cx: &mut ModelContext<Self>) -> Model<Thread> {
let thread = cx.new_model(|cx| Thread::new(self.tools.clone(), cx));
self.threads.push(thread.clone());
thread
}
pub fn open_thread(&self, id: &ThreadId, cx: &mut ModelContext<Self>) -> Option<Model<Thread>> {
self.threads
.iter()
.find(|thread| thread.read(cx).id() == id)
.cloned()
}
pub fn delete_thread(&mut self, id: &ThreadId, cx: &mut ModelContext<Self>) {
self.threads.retain(|thread| thread.read(cx).id() != id);
}
fn register_context_server_handlers(&self, cx: &mut ModelContext<Self>) {
cx.subscribe(
&self.context_server_manager.clone(),
Self::handle_context_server_event,
)
.detach();
}
fn handle_context_server_event(
&mut self,
context_server_manager: Model<ContextServerManager>,
event: &context_server::manager::Event,
cx: &mut ModelContext<Self>,
) {
let tool_working_set = self.tools.clone();
match event {
context_server::manager::Event::ServerStarted { server_id } => {
if let Some(server) = context_server_manager.read(cx).get_server(server_id) {
let context_server_manager = context_server_manager.clone();
cx.spawn({
let server = server.clone();
let server_id = server_id.clone();
|this, mut cx| async move {
let Some(protocol) = server.client() else {
return;
};
if protocol.capable(context_server::protocol::ServerCapability::Tools) {
if let Some(tools) = protocol.list_tools().await.log_err() {
let tool_ids = tools
.tools
.into_iter()
.map(|tool| {
log::info!(
"registering context server tool: {:?}",
tool.name
);
tool_working_set.insert(Arc::new(
ContextServerTool::new(
context_server_manager.clone(),
server.id(),
tool,
),
))
})
.collect::<Vec<_>>();
this.update(&mut cx, |this, _cx| {
this.context_server_tool_ids.insert(server_id, tool_ids);
})
.log_err();
}
}
}
})
.detach();
}
}
context_server::manager::Event::ServerStopped { server_id } => {
if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) {
tool_working_set.remove(&tool_ids);
}
}
}
}
}
impl ThreadStore {
/// Creates some mocked recent threads for testing purposes.
fn mock_recent_threads(&mut self, cx: &mut ModelContext<Self>) {
use language_model::Role;
self.threads.push(cx.new_model(|cx| {
let mut thread = Thread::new(self.tools.clone(), cx);
thread.set_summary("Introduction to quantum computing", cx);
thread.insert_user_message("Hello! Can you help me understand quantum computing?", Vec::new(), cx);
thread.insert_message(Role::Assistant, "Of course! I'd be happy to help you understand quantum computing. Quantum computing is a fascinating field that uses the principles of quantum mechanics to process information. Unlike classical computers that use bits (0s and 1s), quantum computers use quantum bits or 'qubits'. These qubits can exist in multiple states simultaneously, a property called superposition. This allows quantum computers to perform certain calculations much faster than classical computers. What specific aspect of quantum computing would you like to know more about?", cx);
thread.insert_user_message("That's interesting! Can you explain how quantum entanglement is used in quantum computing?", Vec::new(), cx);
thread.insert_message(Role::Assistant, "Certainly! Quantum entanglement is a key principle used in quantum computing. When two qubits become entangled, the state of one qubit is directly related to the state of the other, regardless of the distance between them. This property is used in quantum computing to create complex quantum states and to perform operations on multiple qubits simultaneously. Entanglement allows quantum computers to process information in ways that classical computers cannot, potentially solving certain problems much more efficiently. For example, it's crucial in quantum error correction and in algorithms like quantum teleportation, which is important for quantum communication.", cx);
thread
}));
self.threads.push(cx.new_model(|cx| {
let mut thread = Thread::new(self.tools.clone(), cx);
thread.set_summary("Rust web development and async programming", cx);
thread.insert_user_message("Can you show me an example of Rust code for a simple web server?", Vec::new(), cx);
thread.insert_message(Role::Assistant, "Certainly! Here's an example of a simple web server in Rust using the `actix-web` framework:
```rust
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
async fn hello() -> impl Responder {
HttpResponse::Ok().body(\"Hello, World!\")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route(\"/\", web::get().to(hello))
})
.bind(\"127.0.0.1:8080\")?
.run()
.await
}
```
This code creates a basic web server that responds with 'Hello, World!' when you access the root URL. Here's a breakdown of what's happening:
1. We import necessary items from the `actix-web` crate.
2. We define an async `hello` function that returns a simple HTTP response.
3. In the `main` function, we set up the server to listen on `127.0.0.1:8080`.
4. We configure the app to respond to GET requests on the root path with our `hello` function.
To run this, you'd need to add `actix-web` to your `Cargo.toml` dependencies:
```toml
[dependencies]
actix-web = \"4.0\"
```
Then you can run the server with `cargo run` and access it at `http://localhost:8080`.".unindent(), cx);
thread.insert_user_message("That's great! Can you explain more about async functions in Rust?", Vec::new(), cx);
thread.insert_message(Role::Assistant, "Certainly! Async functions are a key feature in Rust for writing efficient, non-blocking code, especially for I/O-bound operations. Here's an overview:
1. **Syntax**: Async functions are declared using the `async` keyword:
```rust
async fn my_async_function() -> Result<(), Error> {
// Asynchronous code here
}
```
2. **Futures**: Async functions return a `Future`. A `Future` represents a value that may not be available yet but will be at some point.
3. **Await**: Inside an async function, you can use the `.await` syntax to wait for other async operations to complete:
```rust
async fn fetch_data() -> Result<String, Error> {
let response = make_http_request().await?;
let data = process_response(response).await?;
Ok(data)
}
```
4. **Non-blocking**: Async functions allow the runtime to work on other tasks while waiting for I/O or other operations to complete, making efficient use of system resources.
5. **Runtime**: To execute async code, you need a runtime like `tokio` or `async-std`. Actix-web, which we used in the previous example, includes its own runtime.
6. **Error Handling**: Async functions work well with Rust's `?` operator for error handling.
Async programming in Rust provides a powerful way to write concurrent code that's both safe and efficient. It's particularly useful for servers, network programming, and any application that deals with many concurrent operations.".unindent(), cx);
thread
}));
}
}

View File

@@ -1,3 +0,0 @@
mod context_pill;
pub use context_pill::*;

View File

@@ -1,49 +0,0 @@
use std::rc::Rc;
use gpui::ClickEvent;
use ui::{prelude::*, IconButtonShape};
use crate::context::Context;
#[derive(IntoElement)]
pub struct ContextPill {
context: Context,
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
}
impl ContextPill {
pub fn new(context: Context) -> Self {
Self {
context,
on_remove: None,
}
}
pub fn on_remove(mut self, on_remove: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>) -> Self {
self.on_remove = Some(on_remove);
self
}
}
impl RenderOnce for ContextPill {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
h_flex()
.gap_1()
.px_1()
.border_1()
.border_color(cx.theme().colors().border)
.rounded_md()
.child(Label::new(self.context.name.clone()).size(LabelSize::Small))
.when_some(self.on_remove, |parent, on_remove| {
parent.child(
IconButton::new("remove", IconName::Close)
.shape(IconButtonShape::Square)
.icon_size(IconSize::XSmall)
.on_click({
let on_remove = on_remove.clone();
move |event, cx| on_remove(event, cx)
}),
)
})
}
}

View File

@@ -18,7 +18,6 @@ use workspace::{ui::IconName, Workspace};
pub fn init(cx: &mut AppContext) {
SlashCommandRegistry::default_global(cx);
extension_slash_command::init(cx);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]

View File

@@ -3,39 +3,17 @@ use std::sync::{atomic::AtomicBool, Arc};
use anyhow::Result;
use async_trait::async_trait;
use extension::{Extension, ExtensionHostProxy, ExtensionSlashCommandProxy, WorktreeDelegate};
use gpui::{AppContext, Task, WeakView, WindowContext};
use extension::{Extension, WorktreeDelegate};
use gpui::{Task, WeakView, WindowContext};
use language::{BufferSnapshot, LspAdapterDelegate};
use ui::prelude::*;
use workspace::Workspace;
use crate::{
ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
SlashCommandRegistry, SlashCommandResult,
SlashCommandResult,
};
pub fn init(cx: &mut AppContext) {
let proxy = ExtensionHostProxy::default_global(cx);
proxy.register_slash_command_proxy(SlashCommandRegistryProxy {
slash_command_registry: SlashCommandRegistry::global(cx),
});
}
struct SlashCommandRegistryProxy {
slash_command_registry: Arc<SlashCommandRegistry>,
}
impl ExtensionSlashCommandProxy for SlashCommandRegistryProxy {
fn register_slash_command(
&self,
extension: Arc<dyn Extension>,
command: extension::SlashCommand,
) {
self.slash_command_registry
.register_command(ExtensionSlashCommand::new(extension, command), false)
}
}
/// An adapter that allows an [`LspAdapterDelegate`] to be used as a [`WorktreeDelegate`].
struct WorktreeDelegateAdapter(Arc<dyn LspAdapterDelegate>);

View File

@@ -1,5 +1,4 @@
mod tool_registry;
mod tool_working_set;
use std::sync::Arc;
@@ -7,8 +6,7 @@ use anyhow::Result;
use gpui::{AppContext, Task, WeakView, WindowContext};
use workspace::Workspace;
pub use crate::tool_registry::*;
pub use crate::tool_working_set::*;
pub use tool_registry::*;
pub fn init(cx: &mut AppContext) {
ToolRegistry::default_global(cx);

View File

@@ -1,22 +0,0 @@
[package]
name = "assistant_tools"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/assistant_tools.rs"
[dependencies]
anyhow.workspace = true
assistant_tool.workspace = true
chrono.workspace = true
gpui.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
workspace.workspace = true

View File

@@ -1,13 +0,0 @@
mod now_tool;
use assistant_tool::ToolRegistry;
use gpui::AppContext;
use crate::now_tool::NowTool;
pub fn init(cx: &mut AppContext) {
assistant_tool::init(cx);
let registry = ToolRegistry::global(cx);
registry.register_tool(NowTool);
}

View File

@@ -18,5 +18,5 @@ collections.workspace = true
derive_more.workspace = true
gpui.workspace = true
parking_lot.workspace = true
rodio = { version = "0.20.0", default-features = false, features = ["wav"] }
rodio = { version = "0.19.0", default-features = false, features = ["wav"] }
util.workspace = true

View File

@@ -17,8 +17,7 @@ test-support = [
"client/test-support",
"collections/test-support",
"gpui/test-support",
"livekit_client/test-support",
"livekit_client_macos/test-support",
"live_kit_client/test-support",
"project/test-support",
"util/test-support"
]
@@ -28,11 +27,11 @@ anyhow.workspace = true
audio.workspace = true
client.workspace = true
collections.workspace = true
feature_flags.workspace = true
fs.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
live_kit_client.workspace = true
log.workspace = true
postage.workspace = true
project.workspace = true
@@ -42,24 +41,13 @@ serde_derive.workspace = true
settings.workspace = true
util.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
livekit_client_macos = { workspace = true }
[target.'cfg(not(target_os = "macos"))'.dependencies]
livekit_client = { workspace = true }
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }
collections = { workspace = true, features = ["test-support"] }
fs = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
live_kit_client = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
util = { workspace = true, features = ["test-support"] }
http_client = { workspace = true, features = ["test-support"] }
[target.'cfg(target_os = "macos")'.dev-dependencies]
livekit_client_macos = { workspace = true, features = ["test-support"] }
[target.'cfg(not(target_os = "macos"))'.dev-dependencies]
livekit_client = { workspace = true, features = ["test-support"] }

View File

@@ -1,13 +1,546 @@
pub mod call_settings;
pub mod participant;
pub mod room;
#[cfg(target_os = "macos")]
mod macos;
use anyhow::{anyhow, Result};
use audio::Audio;
use call_settings::CallSettings;
use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Subscription,
Task, WeakModel,
};
use postage::watch;
use project::Project;
use room::Event;
use settings::Settings;
use std::sync::Arc;
#[cfg(target_os = "macos")]
pub use macos::*;
pub use participant::ParticipantLocation;
pub use room::Room;
#[cfg(not(target_os = "macos"))]
mod cross_platform;
struct GlobalActiveCall(Model<ActiveCall>);
#[cfg(not(target_os = "macos"))]
pub use cross_platform::*;
impl Global for GlobalActiveCall {}
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
CallSettings::register(cx);
let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx));
cx.set_global(GlobalActiveCall(active_call));
}
pub struct OneAtATime {
cancel: Option<oneshot::Sender<()>>,
}
impl OneAtATime {
/// spawn a task in the given context.
/// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None)
/// otherwise you'll see the result of the task.
fn spawn<F, Fut, R>(&mut self, cx: &mut AppContext, f: F) -> Task<Result<Option<R>>>
where
F: 'static + FnOnce(AsyncAppContext) -> Fut,
Fut: Future<Output = Result<R>>,
R: 'static,
{
let (tx, rx) = oneshot::channel();
self.cancel.replace(tx);
cx.spawn(|cx| async move {
futures::select_biased! {
_ = rx.fuse() => Ok(None),
result = f(cx).fuse() => result.map(Some),
}
})
}
fn running(&self) -> bool {
self.cancel
.as_ref()
.is_some_and(|cancel| !cancel.is_canceled())
}
}
#[derive(Clone)]
pub struct IncomingCall {
pub room_id: u64,
pub calling_user: Arc<User>,
pub participants: Vec<Arc<User>>,
pub initial_project: Option<proto::ParticipantProject>,
}
/// Singleton global maintaining the user's participation in a room across workspaces.
pub struct ActiveCall {
room: Option<(Model<Room>, Vec<Subscription>)>,
pending_room_creation: Option<Shared<Task<Result<Model<Room>, Arc<anyhow::Error>>>>>,
location: Option<WeakModel<Project>>,
_join_debouncer: OneAtATime,
pending_invites: HashSet<u64>,
incoming_call: (
watch::Sender<Option<IncomingCall>>,
watch::Receiver<Option<IncomingCall>>,
),
client: Arc<Client>,
user_store: Model<UserStore>,
_subscriptions: Vec<client::Subscription>,
}
impl EventEmitter<Event> for ActiveCall {}
impl ActiveCall {
fn new(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut ModelContext<Self>) -> Self {
Self {
room: None,
pending_room_creation: None,
location: None,
pending_invites: Default::default(),
incoming_call: watch::channel(),
_join_debouncer: OneAtATime { cancel: None },
_subscriptions: vec![
client.add_request_handler(cx.weak_model(), Self::handle_incoming_call),
client.add_message_handler(cx.weak_model(), Self::handle_call_canceled),
],
client,
user_store,
}
}
pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
self.room()?.read(cx).channel_id()
}
async fn handle_incoming_call(
this: Model<Self>,
envelope: TypedEnvelope<proto::IncomingCall>,
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let call = IncomingCall {
room_id: envelope.payload.room_id,
participants: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_users(envelope.payload.participant_user_ids, cx)
})?
.await?,
calling_user: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_user(envelope.payload.calling_user_id, cx)
})?
.await?,
initial_project: envelope.payload.initial_project,
};
this.update(&mut cx, |this, _| {
*this.incoming_call.0.borrow_mut() = Some(call);
})?;
Ok(proto::Ack {})
}
async fn handle_call_canceled(
this: Model<Self>,
envelope: TypedEnvelope<proto::CallCanceled>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _| {
let mut incoming_call = this.incoming_call.0.borrow_mut();
if incoming_call
.as_ref()
.map_or(false, |call| call.room_id == envelope.payload.room_id)
{
incoming_call.take();
}
})?;
Ok(())
}
pub fn global(cx: &AppContext) -> Model<Self> {
cx.global::<GlobalActiveCall>().0.clone()
}
pub fn try_global(cx: &AppContext) -> Option<Model<Self>> {
cx.try_global::<GlobalActiveCall>()
.map(|call| call.0.clone())
}
pub fn invite(
&mut self,
called_user_id: u64,
initial_project: Option<Model<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if !self.pending_invites.insert(called_user_id) {
return Task::ready(Err(anyhow!("user was already invited")));
}
cx.notify();
if self._join_debouncer.running() {
return Task::ready(Ok(()));
}
let room = if let Some(room) = self.room().cloned() {
Some(Task::ready(Ok(room)).shared())
} else {
self.pending_room_creation.clone()
};
let invite = if let Some(room) = room {
cx.spawn(move |_, mut cx| async move {
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))?
.await?,
)
} else {
None
};
room.update(&mut cx, move |room, cx| {
room.call(called_user_id, initial_project_id, cx)
})?
.await?;
anyhow::Ok(())
})
} else {
let client = self.client.clone();
let user_store = self.user_store.clone();
let room = cx
.spawn(move |this, mut cx| async move {
let create_room = async {
let room = cx
.update(|cx| {
Room::create(
called_user_id,
initial_project,
client,
user_store,
cx,
)
})?
.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))?
.await?;
anyhow::Ok(room)
};
let room = create_room.await;
this.update(&mut cx, |this, _| this.pending_room_creation = None)?;
room.map_err(Arc::new)
})
.shared();
self.pending_room_creation = Some(room.clone());
cx.background_executor().spawn(async move {
room.await.map_err(|err| anyhow!("{:?}", err))?;
anyhow::Ok(())
})
};
cx.spawn(move |this, mut cx| async move {
let result = invite.await;
if result.is_ok() {
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
} else {
//TODO: report collaboration error
log::error!("invite failed: {:?}", result);
}
this.update(&mut cx, |this, cx| {
this.pending_invites.remove(&called_user_id);
cx.notify();
})?;
result
})
}
pub fn cancel_invite(
&mut self,
called_user_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let room_id = if let Some(room) = self.room() {
room.read(cx).id()
} else {
return Task::ready(Err(anyhow!("no active call")));
};
let client = self.client.clone();
cx.background_executor().spawn(async move {
client
.request(proto::CancelCall {
room_id,
called_user_id,
})
.await?;
anyhow::Ok(())
})
}
pub fn incoming(&self) -> watch::Receiver<Option<IncomingCall>> {
self.incoming_call.1.clone()
}
pub fn accept_incoming(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.room.is_some() {
return Task::ready(Err(anyhow!("cannot join while on another call")));
}
let call = if let Some(call) = self.incoming_call.0.borrow_mut().take() {
call
} else {
return Task::ready(Err(anyhow!("no incoming call")));
};
if self.pending_room_creation.is_some() {
return Task::ready(Ok(()));
}
let room_id = call.room_id;
let client = self.client.clone();
let user_store = self.user_store.clone();
let join = self
._join_debouncer
.spawn(cx, move |cx| Room::join(room_id, client, user_store, cx));
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("accept incoming", cx)
})?;
Ok(())
})
}
pub fn decline_incoming(&mut self, _: &mut ModelContext<Self>) -> Result<()> {
let call = self
.incoming_call
.0
.borrow_mut()
.take()
.ok_or_else(|| anyhow!("no incoming call"))?;
report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
self.client.send(proto::DeclineCall {
room_id: call.room_id,
})?;
Ok(())
}
pub fn join_channel(
&mut self,
channel_id: ChannelId,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Model<Room>>>> {
if let Some(room) = self.room().cloned() {
if room.read(cx).channel_id() == Some(channel_id) {
return Task::ready(Ok(Some(room)));
} else {
room.update(cx, |room, cx| room.clear_state(cx));
}
}
if self.pending_room_creation.is_some() {
return Task::ready(Ok(None));
}
let client = self.client.clone();
let user_store = self.user_store.clone();
let join = self._join_debouncer.spawn(cx, move |cx| async move {
Room::join_channel(channel_id, client, user_store, cx).await
});
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("join channel", cx)
})?;
Ok(room)
})
}
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
self.report_call_event("hang up", cx);
Audio::end_call(cx);
let channel_id = self.channel_id(cx);
if let Some((room, _)) = self.room.take() {
cx.emit(Event::RoomLeft { channel_id });
room.update(cx, |room, cx| room.leave(cx))
} else {
Task::ready(Ok(()))
}
}
pub fn share_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("share project", cx);
room.update(cx, |room, cx| room.share_project(project, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
}
}
pub fn unshare_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("unshare project", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
} else {
Err(anyhow!("no active call"))
}
}
pub fn location(&self) -> Option<&WeakModel<Project>> {
self.location.as_ref()
}
pub fn set_location(
&mut self,
project: Option<&Model<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
self.location = project.map(|project| project.downgrade());
if let Some((room, _)) = self.room.as_ref() {
return room.update(cx, |room, cx| room.set_location(project, cx));
}
}
Task::ready(Ok(()))
}
fn set_room(
&mut self,
room: Option<Model<Room>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if room.as_ref() == self.room.as_ref().map(|room| &room.0) {
Task::ready(Ok(()))
} else {
cx.notify();
if let Some(room) = room {
if room.read(cx).status().is_offline() {
self.room = None;
Task::ready(Ok(()))
} else {
let subscriptions = vec![
cx.observe(&room, |this, room, cx| {
if room.read(cx).status().is_offline() {
this.set_room(None, cx).detach_and_log_err(cx);
}
cx.notify();
}),
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
];
self.room = Some((room.clone(), subscriptions));
let location = self
.location
.as_ref()
.and_then(|location| location.upgrade());
let channel_id = room.read(cx).channel_id();
cx.emit(Event::RoomJoined { channel_id });
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
}
} else {
self.room = None;
Task::ready(Ok(()))
}
}
}
pub fn room(&self) -> Option<&Model<Room>> {
self.room.as_ref().map(|(room, _)| room)
}
pub fn client(&self) -> Arc<Client> {
self.client.clone()
}
pub fn pending_invites(&self) -> &HashSet<u64> {
&self.pending_invites
}
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
if let Some(room) = self.room() {
let room = room.read(cx);
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client);
}
}
}
pub fn report_call_event_for_room(
operation: &'static str,
room_id: u64,
channel_id: Option<ChannelId>,
client: &Arc<Client>,
) {
let telemetry = client.telemetry();
telemetry.report_call_event(operation, Some(room_id), channel_id)
}
pub fn report_call_event_for_channel(
operation: &'static str,
channel_id: ChannelId,
client: &Arc<Client>,
cx: &AppContext,
) {
let room = ActiveCall::global(cx).read(cx).room();
let telemetry = client.telemetry();
telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id))
}
#[cfg(test)]
mod test {
use gpui::TestAppContext;
use crate::OneAtATime;
#[gpui::test]
async fn test_one_at_a_time(cx: &mut TestAppContext) {
let mut one_at_a_time = OneAtATime { cancel: None };
assert_eq!(
cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(1) }))
.await
.unwrap(),
Some(1)
);
let (a, b) = cx.update(|cx| {
(
one_at_a_time.spawn(cx, |_| async {
panic!("");
}),
one_at_a_time.spawn(cx, |_| async { Ok(3) }),
)
});
assert_eq!(a.await.unwrap(), None::<u32>);
assert_eq!(b.await.unwrap(), Some(3));
let promise = cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(4) }));
drop(one_at_a_time);
assert_eq!(promise.await.unwrap(), None);
}
}

View File

@@ -20,7 +20,7 @@ pub struct CallSettingsContent {
/// Whether your current project should be shared when joining an empty channel.
///
/// Default: false
/// Default: true
pub share_on_join: Option<bool>,
}

View File

@@ -1,552 +0,0 @@
pub mod participant;
pub mod room;
use crate::call_settings::CallSettings;
use anyhow::{anyhow, Result};
use audio::Audio;
use client::{proto, ChannelId, Client, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE};
use collections::HashSet;
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Global, Model, ModelContext, Subscription,
Task, WeakModel,
};
use postage::watch;
use project::Project;
use room::Event;
use settings::Settings;
use std::sync::Arc;
pub use livekit_client::{
track::RemoteVideoTrack, RemoteVideoTrackView, RemoteVideoTrackViewEvent,
};
pub use participant::ParticipantLocation;
pub use room::Room;
struct GlobalActiveCall(Model<ActiveCall>);
impl Global for GlobalActiveCall {}
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut AppContext) {
livekit_client::init(
cx.background_executor().dispatcher.clone(),
cx.http_client(),
);
CallSettings::register(cx);
let active_call = cx.new_model(|cx| ActiveCall::new(client, user_store, cx));
cx.set_global(GlobalActiveCall(active_call));
}
pub struct OneAtATime {
cancel: Option<oneshot::Sender<()>>,
}
impl OneAtATime {
/// spawn a task in the given context.
/// if another task is spawned before that resolves, or if the OneAtATime itself is dropped, the first task will be cancelled and return Ok(None)
/// otherwise you'll see the result of the task.
fn spawn<F, Fut, R>(&mut self, cx: &mut AppContext, f: F) -> Task<Result<Option<R>>>
where
F: 'static + FnOnce(AsyncAppContext) -> Fut,
Fut: Future<Output = Result<R>>,
R: 'static,
{
let (tx, rx) = oneshot::channel();
self.cancel.replace(tx);
cx.spawn(|cx| async move {
futures::select_biased! {
_ = rx.fuse() => Ok(None),
result = f(cx).fuse() => result.map(Some),
}
})
}
fn running(&self) -> bool {
self.cancel
.as_ref()
.is_some_and(|cancel| !cancel.is_canceled())
}
}
#[derive(Clone)]
pub struct IncomingCall {
pub room_id: u64,
pub calling_user: Arc<User>,
pub participants: Vec<Arc<User>>,
pub initial_project: Option<proto::ParticipantProject>,
}
/// Singleton global maintaining the user's participation in a room across workspaces.
pub struct ActiveCall {
room: Option<(Model<Room>, Vec<Subscription>)>,
pending_room_creation: Option<Shared<Task<Result<Model<Room>, Arc<anyhow::Error>>>>>,
location: Option<WeakModel<Project>>,
_join_debouncer: OneAtATime,
pending_invites: HashSet<u64>,
incoming_call: (
watch::Sender<Option<IncomingCall>>,
watch::Receiver<Option<IncomingCall>>,
),
client: Arc<Client>,
user_store: Model<UserStore>,
_subscriptions: Vec<client::Subscription>,
}
impl EventEmitter<Event> for ActiveCall {}
impl ActiveCall {
fn new(client: Arc<Client>, user_store: Model<UserStore>, cx: &mut ModelContext<Self>) -> Self {
Self {
room: None,
pending_room_creation: None,
location: None,
pending_invites: Default::default(),
incoming_call: watch::channel(),
_join_debouncer: OneAtATime { cancel: None },
_subscriptions: vec![
client.add_request_handler(cx.weak_model(), Self::handle_incoming_call),
client.add_message_handler(cx.weak_model(), Self::handle_call_canceled),
],
client,
user_store,
}
}
pub fn channel_id(&self, cx: &AppContext) -> Option<ChannelId> {
self.room()?.read(cx).channel_id()
}
async fn handle_incoming_call(
this: Model<Self>,
envelope: TypedEnvelope<proto::IncomingCall>,
mut cx: AsyncAppContext,
) -> Result<proto::Ack> {
let user_store = this.update(&mut cx, |this, _| this.user_store.clone())?;
let call = IncomingCall {
room_id: envelope.payload.room_id,
participants: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_users(envelope.payload.participant_user_ids, cx)
})?
.await?,
calling_user: user_store
.update(&mut cx, |user_store, cx| {
user_store.get_user(envelope.payload.calling_user_id, cx)
})?
.await?,
initial_project: envelope.payload.initial_project,
};
this.update(&mut cx, |this, _| {
*this.incoming_call.0.borrow_mut() = Some(call);
})?;
Ok(proto::Ack {})
}
async fn handle_call_canceled(
this: Model<Self>,
envelope: TypedEnvelope<proto::CallCanceled>,
mut cx: AsyncAppContext,
) -> Result<()> {
this.update(&mut cx, |this, _| {
let mut incoming_call = this.incoming_call.0.borrow_mut();
if incoming_call
.as_ref()
.map_or(false, |call| call.room_id == envelope.payload.room_id)
{
incoming_call.take();
}
})?;
Ok(())
}
pub fn global(cx: &AppContext) -> Model<Self> {
cx.global::<GlobalActiveCall>().0.clone()
}
pub fn try_global(cx: &AppContext) -> Option<Model<Self>> {
cx.try_global::<GlobalActiveCall>()
.map(|call| call.0.clone())
}
pub fn invite(
&mut self,
called_user_id: u64,
initial_project: Option<Model<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if !self.pending_invites.insert(called_user_id) {
return Task::ready(Err(anyhow!("user was already invited")));
}
cx.notify();
if self._join_debouncer.running() {
return Task::ready(Ok(()));
}
let room = if let Some(room) = self.room().cloned() {
Some(Task::ready(Ok(room)).shared())
} else {
self.pending_room_creation.clone()
};
let invite = if let Some(room) = room {
cx.spawn(move |_, mut cx| async move {
let room = room.await.map_err(|err| anyhow!("{:?}", err))?;
let initial_project_id = if let Some(initial_project) = initial_project {
Some(
room.update(&mut cx, |room, cx| room.share_project(initial_project, cx))?
.await?,
)
} else {
None
};
room.update(&mut cx, move |room, cx| {
room.call(called_user_id, initial_project_id, cx)
})?
.await?;
anyhow::Ok(())
})
} else {
let client = self.client.clone();
let user_store = self.user_store.clone();
let room = cx
.spawn(move |this, mut cx| async move {
let create_room = async {
let room = cx
.update(|cx| {
Room::create(
called_user_id,
initial_project,
client,
user_store,
cx,
)
})?
.await?;
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))?
.await?;
anyhow::Ok(room)
};
let room = create_room.await;
this.update(&mut cx, |this, _| this.pending_room_creation = None)?;
room.map_err(Arc::new)
})
.shared();
self.pending_room_creation = Some(room.clone());
cx.background_executor().spawn(async move {
room.await.map_err(|err| anyhow!("{:?}", err))?;
anyhow::Ok(())
})
};
cx.spawn(move |this, mut cx| async move {
let result = invite.await;
if result.is_ok() {
this.update(&mut cx, |this, cx| this.report_call_event("invite", cx))?;
} else {
//TODO: report collaboration error
log::error!("invite failed: {:?}", result);
}
this.update(&mut cx, |this, cx| {
this.pending_invites.remove(&called_user_id);
cx.notify();
})?;
result
})
}
pub fn cancel_invite(
&mut self,
called_user_id: u64,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
let room_id = if let Some(room) = self.room() {
room.read(cx).id()
} else {
return Task::ready(Err(anyhow!("no active call")));
};
let client = self.client.clone();
cx.background_executor().spawn(async move {
client
.request(proto::CancelCall {
room_id,
called_user_id,
})
.await?;
anyhow::Ok(())
})
}
pub fn incoming(&self) -> watch::Receiver<Option<IncomingCall>> {
self.incoming_call.1.clone()
}
pub fn accept_incoming(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.room.is_some() {
return Task::ready(Err(anyhow!("cannot join while on another call")));
}
let call = if let Some(call) = self.incoming_call.0.borrow_mut().take() {
call
} else {
return Task::ready(Err(anyhow!("no incoming call")));
};
if self.pending_room_creation.is_some() {
return Task::ready(Ok(()));
}
let room_id = call.room_id;
let client = self.client.clone();
let user_store = self.user_store.clone();
let join = self
._join_debouncer
.spawn(cx, move |cx| Room::join(room_id, client, user_store, cx));
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("accept incoming", cx)
})?;
Ok(())
})
}
pub fn decline_incoming(&mut self, _: &mut ModelContext<Self>) -> Result<()> {
let call = self
.incoming_call
.0
.borrow_mut()
.take()
.ok_or_else(|| anyhow!("no incoming call"))?;
report_call_event_for_room("decline incoming", call.room_id, None, &self.client);
self.client.send(proto::DeclineCall {
room_id: call.room_id,
})?;
Ok(())
}
pub fn join_channel(
&mut self,
channel_id: ChannelId,
cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Model<Room>>>> {
if let Some(room) = self.room().cloned() {
if room.read(cx).channel_id() == Some(channel_id) {
return Task::ready(Ok(Some(room)));
} else {
room.update(cx, |room, cx| room.clear_state(cx));
}
}
if self.pending_room_creation.is_some() {
return Task::ready(Ok(None));
}
let client = self.client.clone();
let user_store = self.user_store.clone();
let join = self._join_debouncer.spawn(cx, move |cx| async move {
Room::join_channel(channel_id, client, user_store, cx).await
});
cx.spawn(|this, mut cx| async move {
let room = join.await?;
this.update(&mut cx, |this, cx| this.set_room(room.clone(), cx))?
.await?;
this.update(&mut cx, |this, cx| {
this.report_call_event("join channel", cx)
})?;
Ok(room)
})
}
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
cx.notify();
self.report_call_event("hang up", cx);
Audio::end_call(cx);
let channel_id = self.channel_id(cx);
if let Some((room, _)) = self.room.take() {
cx.emit(Event::RoomLeft { channel_id });
room.update(cx, |room, cx| room.leave(cx))
} else {
Task::ready(Ok(()))
}
}
pub fn share_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Task<Result<u64>> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("share project", cx);
room.update(cx, |room, cx| room.share_project(project, cx))
} else {
Task::ready(Err(anyhow!("no active call")))
}
}
pub fn unshare_project(
&mut self,
project: Model<Project>,
cx: &mut ModelContext<Self>,
) -> Result<()> {
if let Some((room, _)) = self.room.as_ref() {
self.report_call_event("unshare project", cx);
room.update(cx, |room, cx| room.unshare_project(project, cx))
} else {
Err(anyhow!("no active call"))
}
}
pub fn location(&self) -> Option<&WeakModel<Project>> {
self.location.as_ref()
}
pub fn set_location(
&mut self,
project: Option<&Model<Project>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if project.is_some() || !*ZED_ALWAYS_ACTIVE {
self.location = project.map(|project| project.downgrade());
if let Some((room, _)) = self.room.as_ref() {
return room.update(cx, |room, cx| room.set_location(project, cx));
}
}
Task::ready(Ok(()))
}
fn set_room(
&mut self,
room: Option<Model<Room>>,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
if room.as_ref() == self.room.as_ref().map(|room| &room.0) {
Task::ready(Ok(()))
} else {
cx.notify();
if let Some(room) = room {
if room.read(cx).status().is_offline() {
self.room = None;
Task::ready(Ok(()))
} else {
let subscriptions = vec![
cx.observe(&room, |this, room, cx| {
if room.read(cx).status().is_offline() {
this.set_room(None, cx).detach_and_log_err(cx);
}
cx.notify();
}),
cx.subscribe(&room, |_, _, event, cx| cx.emit(event.clone())),
];
self.room = Some((room.clone(), subscriptions));
let location = self
.location
.as_ref()
.and_then(|location| location.upgrade());
let channel_id = room.read(cx).channel_id();
cx.emit(Event::RoomJoined { channel_id });
room.update(cx, |room, cx| room.set_location(location.as_ref(), cx))
}
} else {
self.room = None;
Task::ready(Ok(()))
}
}
}
pub fn room(&self) -> Option<&Model<Room>> {
self.room.as_ref().map(|(room, _)| room)
}
pub fn client(&self) -> Arc<Client> {
self.client.clone()
}
pub fn pending_invites(&self) -> &HashSet<u64> {
&self.pending_invites
}
pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) {
if let Some(room) = self.room() {
let room = room.read(cx);
report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client);
}
}
}
pub fn report_call_event_for_room(
operation: &'static str,
room_id: u64,
channel_id: Option<ChannelId>,
client: &Arc<Client>,
) {
let telemetry = client.telemetry();
telemetry.report_call_event(operation, Some(room_id), channel_id)
}
pub fn report_call_event_for_channel(
operation: &'static str,
channel_id: ChannelId,
client: &Arc<Client>,
cx: &AppContext,
) {
let room = ActiveCall::global(cx).read(cx).room();
let telemetry = client.telemetry();
telemetry.report_call_event(operation, room.map(|r| r.read(cx).id()), Some(channel_id))
}
#[cfg(test)]
mod test {
use gpui::TestAppContext;
use crate::OneAtATime;
#[gpui::test]
async fn test_one_at_a_time(cx: &mut TestAppContext) {
let mut one_at_a_time = OneAtATime { cancel: None };
assert_eq!(
cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(1) }))
.await
.unwrap(),
Some(1)
);
let (a, b) = cx.update(|cx| {
(
one_at_a_time.spawn(cx, |_| async {
panic!("");
}),
one_at_a_time.spawn(cx, |_| async { Ok(3) }),
)
});
assert_eq!(a.await.unwrap(), None::<u32>);
assert_eq!(b.await.unwrap(), Some(3));
let promise = cx.update(|cx| one_at_a_time.spawn(cx, |_| async { Ok(4) }));
drop(one_at_a_time);
assert_eq!(promise.await.unwrap(), None);
}
}

View File

@@ -1,68 +0,0 @@
#![cfg_attr(target_os = "windows", allow(unused))]
use anyhow::{anyhow, Result};
use client::{proto, ParticipantIndex, User};
use collections::HashMap;
use gpui::WeakModel;
use livekit_client::AudioStream;
use project::Project;
use std::sync::Arc;
#[cfg(not(target_os = "windows"))]
pub use livekit_client::id::TrackSid;
pub use livekit_client::track::{RemoteAudioTrack, RemoteVideoTrack};
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ParticipantLocation {
SharedProject { project_id: u64 },
UnsharedProject,
External,
}
impl ParticipantLocation {
pub fn from_proto(location: Option<proto::ParticipantLocation>) -> Result<Self> {
match location.and_then(|l| l.variant) {
Some(proto::participant_location::Variant::SharedProject(project)) => {
Ok(Self::SharedProject {
project_id: project.id,
})
}
Some(proto::participant_location::Variant::UnsharedProject(_)) => {
Ok(Self::UnsharedProject)
}
Some(proto::participant_location::Variant::External(_)) => Ok(Self::External),
None => Err(anyhow!("participant location was not provided")),
}
}
}
#[derive(Clone, Default)]
pub struct LocalParticipant {
pub projects: Vec<proto::ParticipantProject>,
pub active_project: Option<WeakModel<Project>>,
pub role: proto::ChannelRole,
}
pub struct RemoteParticipant {
pub user: Arc<User>,
pub peer_id: proto::PeerId,
pub role: proto::ChannelRole,
pub projects: Vec<proto::ParticipantProject>,
pub location: ParticipantLocation,
pub participant_index: ParticipantIndex,
pub muted: bool,
pub speaking: bool,
#[cfg(not(target_os = "windows"))]
pub video_tracks: HashMap<TrackSid, RemoteVideoTrack>,
#[cfg(not(target_os = "windows"))]
pub audio_tracks: HashMap<TrackSid, (RemoteAudioTrack, AudioStream)>,
}
impl RemoteParticipant {
pub fn has_video_tracks(&self) -> bool {
#[cfg(not(target_os = "windows"))]
return !self.video_tracks.is_empty();
#[cfg(target_os = "windows")]
return false;
}
}

Some files were not shown because too many files have changed in this diff Show More