Compare commits

..

8 Commits

Author SHA1 Message Date
Peter Tripp
9699ceac2f Magical semi-colon 2024-11-22 17:26:01 -05:00
Peter Tripp
6501c46bd6 Remove extra newline 2024-11-22 17:23:23 -05:00
Peter Tripp
bb484333db clippy 2024-11-22 17:19:55 -05:00
Peter Tripp
f5a60d9e06 clippy: replace filter/next with find. 2024-11-22 17:17:42 -05:00
Peter Tripp
c39368ff76 remove dbg 2024-11-22 17:06:15 -05:00
Peter Tripp
b46afa7d2b WIP
Fix for new_path_prompt not working on remote workspaces.

Steps to reproduce:
1. cargo run -- ssh://localhost/~/.zshrc
2. cmd-n, cmd-s enter filename.
2024-11-22 17:00:47 -05:00
Peter Tripp
81f3aa4e42 Fix for the fallback file picker throwing: File exists "os error 17".
This was reproducible when the first worktree is a non-folder worktree (e.g. setting.json) so we were trying to create the new file with a path under ~/.config/zed/settings.json/newfile.ext

Co-Authored-By: Conrad Irwin <conrad@zed.dev>
2024-11-22 16:37:18 -05:00
Peter Tripp
ebff818a18 wip 2024-11-21 13:26:15 -05:00
501 changed files with 6552 additions and 22860 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

@@ -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
@@ -278,12 +272,18 @@ jobs:
- name: Create macOS app bundle
run: script/bundle-mac
- name: Rename binaries
- name: Rename single-architecture binaries
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
run: |
mv target/aarch64-apple-darwin/release/Zed.dmg target/aarch64-apple-darwin/release/Zed-aarch64.dmg
mv target/x86_64-apple-darwin/release/Zed.dmg target/x86_64-apple-darwin/release/Zed-x86_64.dmg
- name: Upload app bundle (universal) to workflow run if main branch or specific label
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
with:
name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg
path: target/release/Zed.dmg
- name: Upload app bundle (aarch64) to workflow run if main branch or specific label
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4
if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
@@ -309,6 +309,7 @@ jobs:
target/zed-remote-server-macos-aarch64.gz
target/aarch64-apple-darwin/release/Zed-aarch64.dmg
target/x86_64-apple-darwin/release/Zed-x86_64.dmg
target/release/Zed.dmg
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -403,16 +404,3 @@ jobs:
target/release/zed-linux-aarch64.tar.gz
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
auto-release-preview:
name: Auto release preview
if: ${{ startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') }}
needs: [bundle-mac, bundle-linux, bundle-linux-aarch64]
runs-on:
- self-hosted
- bundle
steps:
- name: gh release
run: gh release edit $GITHUB_REF_NAME --draft=false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -12,7 +12,7 @@ jobs:
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

@@ -12,7 +12,7 @@ jobs:
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

1
.gitignore vendored
View File

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

1282
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,13 +5,10 @@ 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",
"crates/breadcrumbs",
"crates/call",
"crates/channel",
@@ -23,8 +20,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 +54,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",
@@ -87,6 +80,7 @@ members = [
"crates/project_panel",
"crates/project_symbols",
"crates/proto",
"crates/quick_action_bar",
"crates/recent_projects",
"crates/refineable",
"crates/refineable/derive_refineable",
@@ -122,7 +116,6 @@ members = [
"crates/terminal_view",
"crates/text",
"crates/theme",
"crates/theme_extension",
"crates/theme_importer",
"crates/theme_selector",
"crates/time_format",
@@ -135,7 +128,6 @@ members = [
"crates/util",
"crates/vcs_menu",
"crates/vim",
"crates/vim_mode_setting",
"crates/welcome",
"crates/workspace",
"crates/worktree",
@@ -191,13 +183,10 @@ 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" }
breadcrumbs = { path = "crates/breadcrumbs" }
call = { path = "crates/call" }
channel = { path = "crates/channel" }
@@ -209,8 +198,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" }
@@ -229,9 +217,7 @@ git = { path = "crates/git" }
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" }
@@ -242,16 +228,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" }
@@ -273,6 +256,7 @@ project = { path = "crates/project" }
project_panel = { path = "crates/project_panel" }
project_symbols = { path = "crates/project_symbols" }
proto = { path = "crates/proto" }
quick_action_bar = { path = "crates/quick_action_bar" }
recent_projects = { path = "crates/recent_projects" }
refineable = { path = "crates/refineable" }
release_channel = { path = "crates/release_channel" }
@@ -307,7 +291,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" }
@@ -319,7 +302,6 @@ ui_macros = { path = "crates/ui_macros" }
util = { path = "crates/util" }
vcs_menu = { path = "crates/vcs_menu" }
vim = { path = "crates/vim" }
vim_mode_setting = { path = "crates/vim_mode_setting" }
welcome = { path = "crates/welcome" }
workspace = { path = "crates/workspace" }
worktree = { path = "crates/worktree" }
@@ -335,7 +317,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"
@@ -384,23 +366,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"
@@ -410,10 +389,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"
@@ -434,14 +413,13 @@ 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"
rust-embed = { version = "8.4", features = ["include-exclude"] }
rustls = "0.21.12"
rustls-native-certs = "0.8.0"
scap = { version = "0.0.7" }
schemars = { version = "0.8", features = ["impl_json_schema"] }
semver = "1.0"
serde = { version = "1.0", features = ["derive", "rc"] }
@@ -576,10 +554,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"

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,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",
@@ -113,7 +112,6 @@
"mkv": "video",
"ml": "ocaml",
"mli": "ocaml",
"mod": "go",
"mov": "video",
"mp3": "audio",
"mp4": "video",
@@ -129,7 +127,6 @@
"ogg": "audio",
"opus": "audio",
"otf": "font",
"pcss": "css",
"pdb": "storage",
"pdf": "document",
"php": "php",
@@ -176,9 +173,6 @@
"tsx": "react",
"ttf": "font",
"txt": "document",
"v": "v",
"vsh": "v",
"vv": "v",
"vue": "vue",
"wav": "audio",
"webm": "video",
@@ -187,7 +181,6 @@
"wmv": "video",
"woff": "font",
"woff2": "font",
"work": "go",
"wv": "audio",
"xls": "document",
"xlsx": "document",
@@ -242,9 +235,6 @@
"default": {
"icon": "icons/file_icons/file.svg"
},
"diff": {
"icon": "icons/file_icons/diff.svg"
},
"docker": {
"icon": "icons/file_icons/docker.svg"
},
@@ -389,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,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-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

@@ -405,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",
@@ -594,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",
@@ -650,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

@@ -207,19 +207,6 @@
"alt-enter": "editor::Newline"
}
},
{
"context": "AssistantPanel2",
"bindings": {
"cmd-n": "assistant2::NewThread",
"cmd-shift-h": "assistant2::OpenHistory"
}
},
{
"context": "MessageEditor > Editor",
"bindings": {
"cmd-enter": "assistant2::Chat"
}
},
{
"context": "PromptLibrary",
"bindings": {
@@ -354,7 +341,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",
@@ -445,7 +432,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",
@@ -615,7 +602,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",
@@ -662,16 +648,11 @@
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "FileFinder",
"bindings": {
"cmd": "file_finder::ToggleMenu"
}
},
{
"context": "FileFinder && !menu_open",
"bindings": {
"cmd-shift-p": "file_finder::SelectPrev",
"cmd": "file_finder::OpenMenu",
"cmd-j": "pane::SplitDown",
"cmd-k": "pane::SplitUp",
"cmd-h": "pane::SplitLeft",
@@ -731,11 +712,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"
"ctrl-shift-space": "terminal::ToggleViMode"
}
}
]

View File

@@ -33,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",
@@ -67,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 } }],
@@ -221,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"],
@@ -288,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",
@@ -326,22 +308,6 @@
"ctrl-o": "vim::TemporaryNormal"
}
},
{
"context": "vim_mode == helix_normal",
"bindings": {
"i": "vim::InsertBefore",
"a": "vim::InsertAfter",
"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,
@@ -388,8 +354,7 @@
"bindings": {
"escape": "vim::ClearOperators",
"ctrl-c": "vim::ClearOperators",
"ctrl-[": "vim::ClearOperators",
"g c": "vim::Comment"
"ctrl-[": "vim::ClearOperators"
}
},
{
@@ -416,11 +381,7 @@
"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"
}
},
{
@@ -505,13 +466,6 @@
"<": "vim::CurrentLine"
}
},
{
"context": "vim_operator == eq",
"use_layout_keys": true,
"bindings": {
"=": "vim::CurrentLine"
}
},
{
"context": "vim_operator == gc",
"use_layout_keys": true,
@@ -597,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",

View File

@@ -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,
@@ -492,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'.
@@ -559,8 +560,6 @@
"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)
@@ -684,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:
@@ -886,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.
@@ -1129,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
@@ -1187,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": [

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": [

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": [

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": [

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": [

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": [

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": [

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": [

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::{LanguageModelPickerDelegate, LanguageModelSelector};
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<Picker<LanguageModelPickerDelegate>>,
model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
model_summary_editor: View<Editor>,
authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
configuration_subscription: Option<Subscription>,
@@ -416,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);
@@ -450,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())
@@ -1316,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 {
@@ -1925,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(),
},
@@ -4458,13 +4457,13 @@ pub struct ContextEditorToolbarItem {
fs: Arc<dyn Fs>,
active_context_editor: Option<WeakView<ContextEditor>>,
model_summary_editor: View<Editor>,
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
}
impl ContextEditorToolbarItem {
pub fn new(
workspace: &Workspace,
model_selector_menu_handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
model_summary_editor: View<Editor>,
) -> Self {
Self {
@@ -4560,17 +4559,8 @@ impl Render for ContextEditorToolbarItem {
// .map(|remaining_items| format!("Files to scan: {}", remaining_items))
// })
.child(
LanguageModelSelector::new(
{
let fs = self.fs.clone();
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
}
},
ModelSelector::new(
self.fs.clone(),
ButtonLike::new("active-model")
.style(ButtonStyle::Subtle)
.child(

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,7 +11,6 @@ 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::{

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;
@@ -771,7 +770,7 @@ impl ContextStore {
contexts.push(SavedContextMetadata {
title: title.to_string(),
path,
mtime: metadata.mtime.timestamp_for_user().into(),
mtime: metadata.mtime.into(),
});
}
}
@@ -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;
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,
@@ -1501,17 +1500,8 @@ impl Render for PromptEditor {
.justify_center()
.gap_2()
.child(
LanguageModelSelector::new(
{
let fs = self.fs.clone();
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
}
},
ModelSelector::new(
self.fs.clone(),
IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
@@ -1531,7 +1521,7 @@ impl Render for PromptEditor {
)
}),
)
.info_text(
.with_info_text(
"Inline edits use context\n\
from the currently selected\n\
assistant panel tab.",

View File

@@ -1,27 +1,30 @@
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, SharedString, Task};
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>;
#[derive(IntoElement)]
pub struct LanguageModelSelector<T: PopoverTrigger> {
handle: Option<PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>>,
on_model_changed: OnModelChanged,
pub struct ModelSelector<T: PopoverTrigger> {
handle: Option<PopoverMenuHandle<Picker<ModelPickerDelegate>>>,
fs: Arc<dyn Fs>,
trigger: T,
info_text: Option<SharedString>,
}
pub struct LanguageModelPickerDelegate {
on_model_changed: OnModelChanged,
pub struct ModelPickerDelegate {
fs: Arc<dyn Fs>,
all_models: Vec<ModelInfo>,
filtered_models: Vec<ModelInfo>,
selected_index: usize,
@@ -35,34 +38,28 @@ struct ModelInfo {
is_selected: bool,
}
impl<T: PopoverTrigger> LanguageModelSelector<T> {
pub fn new(
on_model_changed: impl Fn(Arc<dyn LanguageModel>, &AppContext) + 'static,
trigger: T,
) -> Self {
LanguageModelSelector {
impl<T: PopoverTrigger> ModelSelector<T> {
pub fn new(fs: Arc<dyn Fs>, trigger: T) -> Self {
ModelSelector {
handle: None,
on_model_changed: Arc::new(on_model_changed),
fs,
trigger,
info_text: None,
}
}
pub fn with_handle(
mut self,
handle: PopoverMenuHandle<Picker<LanguageModelPickerDelegate>>,
) -> Self {
pub fn with_handle(mut self, handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>) -> Self {
self.handle = Some(handle);
self
}
pub fn info_text(mut self, text: impl Into<SharedString>) -> 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 {
@@ -140,7 +137,9 @@ 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();
@@ -297,7 +296,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
}
}
impl<T: PopoverTrigger> RenderOnce for LanguageModelSelector<T> {
impl<T: PopoverTrigger> RenderOnce for ModelSelector<T> {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
let selected_provider = LanguageModelRegistry::read_global(cx)
.active_provider()
@@ -332,8 +331,8 @@ impl<T: PopoverTrigger> RenderOnce for LanguageModelSelector<T> {
})
.collect::<Vec<_>>();
let delegate = LanguageModelPickerDelegate {
on_model_changed: self.on_model_changed.clone(),
let delegate = ModelPickerDelegate {
fs: self.fs.clone(),
all_models: all_models.clone(),
filtered_models: all_models,
selected_index: 0,

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;
@@ -177,17 +177,11 @@ impl PickerDelegate for SlashCommandDelegate {
.inset(true)
.spacing(ListItemSpacing::Dense)
.selected(selected)
.tooltip({
let description = info.description.clone();
move |cx| cx.new_view(|_| Tooltip::new(description.clone())).into()
})
.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),
),
),
),

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;
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};
@@ -614,17 +612,8 @@ impl Render for PromptEditor {
.w_12()
.justify_center()
.gap_2()
.child(LanguageModelSelector::new(
{
let fs = self.fs.clone();
move |model, cx| {
update_settings_file::<AssistantSettings>(
fs.clone(),
cx,
move |settings, _| settings.set_model(model.clone()),
);
}
},
.child(ModelSelector::new(
self.fs.clone(),
IconButton::new("context", IconName::SettingsAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small)
@@ -704,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
});
@@ -737,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,41 +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]
anyhow.workspace = true
assistant_tool.workspace = true
client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
context_server.workspace = true
editor.workspace = true
feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
language_models.workspace = true
log.workspace = true
markdown.workspace = true
project.workspace = true
proto.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true

View File

@@ -1,52 +0,0 @@
mod assistant_panel;
mod message_editor;
mod thread;
mod thread_store;
use command_palette_hooks::CommandPaletteFilter;
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
use gpui::{actions, AppContext};
pub use crate::assistant_panel::AssistantPanel;
actions!(
assistant2,
[
ToggleFocus,
NewThread,
ToggleModelSelector,
OpenHistory,
Chat
]
);
const NAMESPACE: &str = "assistant2";
/// Initializes the `assistant2` crate.
pub fn init(cx: &mut AppContext) {
assistant_panel::init(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,722 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
use assistant_tool::ToolWorkingSet;
use client::zed_urls;
use collections::HashMap;
use gpui::{
list, prelude::*, px, svg, Action, AnyElement, AppContext, AsyncWindowContext, Empty,
EventEmitter, FocusHandle, FocusableView, FontWeight, ListAlignment, ListState, Model, Pixels,
StyleRefinement, Subscription, Task, TextStyleRefinement, View, ViewContext, WeakView,
WindowContext,
};
use language::LanguageRegistry;
use language_model::{LanguageModelRegistry, Role};
use language_model_selector::LanguageModelSelector;
use markdown::{Markdown, MarkdownStyle};
use settings::Settings;
use theme::ThemeSettings;
use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, KeyBinding, ListItem, Tab, Tooltip};
use workspace::dock::{DockPosition, Panel, PanelEvent};
use workspace::Workspace;
use crate::message_editor::MessageEditor;
use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent};
use crate::thread_store::ThreadStore;
use crate::{NewThread, OpenHistory, ToggleFocus, ToggleModelSelector};
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();
}
pub struct AssistantPanel {
workspace: WeakView<Workspace>,
language_registry: Arc<LanguageRegistry>,
#[allow(unused)]
thread_store: Model<ThreadStore>,
thread: Model<Thread>,
thread_messages: Vec<MessageId>,
rendered_messages_by_id: HashMap<MessageId, View<Markdown>>,
thread_list_state: ListState,
message_editor: View<MessageEditor>,
tools: Arc<ToolWorkingSet>,
last_error: Option<ThreadError>,
_subscriptions: Vec<Subscription>,
}
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 = cx.new_model(|cx| Thread::new(tools.clone(), cx));
let subscriptions = vec![
cx.observe(&thread, |_, _, cx| cx.notify()),
cx.subscribe(&thread, Self::handle_thread_event),
];
Self {
workspace: workspace.weak_handle(),
language_registry: workspace.project().read(cx).languages().clone(),
thread_store,
thread: thread.clone(),
thread_messages: Vec::new(),
rendered_messages_by_id: HashMap::default(),
thread_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()
}
}),
message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)),
tools,
last_error: None,
_subscriptions: subscriptions,
}
}
fn new_thread(&mut self, cx: &mut ViewContext<Self>) {
let tools = self.thread.read(cx).tools().clone();
let thread = cx.new_model(|cx| Thread::new(tools, cx));
let subscriptions = vec![
cx.observe(&thread, |_, _, cx| cx.notify()),
cx.subscribe(&thread, Self::handle_thread_event),
];
self.message_editor = cx.new_view(|cx| MessageEditor::new(thread.clone(), cx));
self.thread = thread;
self.thread_messages.clear();
self.thread_list_state.reset(0);
self.rendered_messages_by_id.clear();
self._subscriptions = subscriptions;
self.message_editor.focus_handle(cx).focus(cx);
}
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::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) => {
let old_len = self.thread_messages.len();
self.thread_messages.push(*message_id);
self.thread_list_state.splice(old_len..old_len, 1);
if let Some(message_text) = self
.thread
.read(cx)
.message(*message_id)
.map(|message| message.text.clone())
{
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(
message_text,
markdown_style,
Some(self.language_registry.clone()),
None,
cx,
)
});
self.rendered_messages_by_id.insert(*message_id, markdown);
}
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 { .. } => {}
}
}
}
impl FocusableView for AssistantPanel {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.message_editor.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().child(Label::new("Thread Title Goes Here")))
.child(
h_flex()
.gap(DynamicSpacing::Base08.rems(cx))
.child(self.render_language_model_selector(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_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();
LanguageModelSelector::new(
|model, _cx| {
println!("Selected {:?}", model.name());
},
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)),
)
}
fn render_message_list(&self, cx: &mut ViewContext<Self>) -> AnyElement {
if self.thread_messages.is_empty() {
#[allow(clippy::useless_vec)]
let recent_threads = vec![1, 2, 3];
return 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(),
),
)
.child(v_flex())
.child(
h_flex()
.w_full()
.justify_center()
.child(Label::new("Context Examples:").size(LabelSize::Small)),
)
.child(
h_flex()
.gap_2()
.justify_center()
.child(
h_flex()
.gap_1()
.p_0p5()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border_variant)
.child(
Icon::new(IconName::Terminal)
.size(IconSize::Small)
.color(Color::Disabled),
)
.child(Label::new("Terminal").size(LabelSize::Small)),
)
.child(
h_flex()
.gap_1()
.p_0p5()
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border_variant)
.child(
Icon::new(IconName::Folder)
.size(IconSize::Small)
.color(Color::Disabled),
)
.child(Label::new("/src/components").size(LabelSize::Small)),
),
)
.child(
h_flex()
.w_full()
.justify_center()
.child(Label::new("Recent Threads:").size(LabelSize::Small)),
)
.child(
v_flex().gap_2().children(
recent_threads
.iter()
.map(|_thread| self.render_past_thread(cx)),
),
)
.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());
}),
),
)
.into_any();
}
list(self.thread_list_state.clone()).flex_1().into_any()
}
fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
let message_id = self.thread_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 (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())),
)
.into_any()
}
fn render_past_thread(&self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
ListItem::new("temp")
.start_slot(Icon::new(IconName::MessageBubbles))
.child(Label::new("Some Thread Title"))
.end_slot(
h_flex()
.gap_2()
.child(Label::new("1 hour ago").color(Color::Disabled))
.child(
IconButton::new("delete", IconName::TrashAlt)
.shape(IconButtonShape::Square)
.icon_size(IconSize::Small),
),
)
}
fn render_last_error(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
let last_error = self.last_error.as_ref()?;
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.last_error = None;
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
},
)))
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.last_error = None;
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.last_error = None;
cx.open_url(&zed_urls::account_url(cx));
cx.notify();
}),
),
)
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
|this, _, cx| {
this.last_error = None;
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.last_error = None;
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| {
println!("Open History");
}))
.child(self.render_toolbar(cx))
.child(self.render_message_list(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))
}
}

View File

@@ -1,167 +0,0 @@
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{AppContext, FocusableView, Model, TextStyle, View};
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
use settings::Settings;
use theme::ThemeSettings;
use ui::{prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding};
use crate::thread::{RequestKind, Thread};
use crate::Chat;
pub struct MessageEditor {
thread: Model<Thread>,
editor: View<Editor>,
use_tools: bool,
}
impl MessageEditor {
pub fn new(thread: Model<Thread>, cx: &mut ViewContext<Self>) -> Self {
Self {
thread,
editor: cx.new_view(|cx| {
let mut editor = Editor::auto_height(80, cx);
editor.set_placeholder_text("Ask anything…", cx);
editor
}),
use_tools: false,
}
}
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
});
self.thread.update(cx, |thread, cx| {
thread.insert_user_message(user_message, 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
}
}
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);
v_flex()
.key_context("MessageEditor")
.on_action(cx.listener(Self::chat))
.size_full()
.gap_2()
.p_2()
.bg(cx.theme().colors().editor_background)
.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()
.child(
Button::new("add-context", "Add Context")
.style(ButtonStyle::Filled)
.icon(IconName::Plus)
.icon_position(IconPosition::Start),
)
.child(CheckboxWithLabel::new(
"use-tools",
Label::new("Tools"),
self.use_tools.into(),
cx.listener(|this, selection, _cx| {
this.use_tools = match selection {
Selection::Selected => true,
Selection::Unselected | Selection::Indeterminate => false,
};
}),
)),
)
.child(
h_flex()
.gap_2()
.child(Button::new("codebase", "Codebase").style(ButtonStyle::Filled))
.child(Label::new("or"))
.child(
ButtonLike::new("chat")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.child(Label::new("Chat"))
.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,364 +0,0 @@
use std::sync::Arc;
use anyhow::Result;
use assistant_tool::ToolWorkingSet;
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, LanguageModelRequest, LanguageModelRequestMessage,
LanguageModelToolResult, LanguageModelToolUse, LanguageModelToolUseId, MessageContent, Role,
StopReason,
};
use language_models::provider::cloud::{MaxMonthlySpendReachedError, PaymentRequiredError};
use serde::{Deserialize, Serialize};
use util::post_inc;
#[derive(Debug, Clone, Copy)]
pub enum RequestKind {
Chat,
}
#[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 {
messages: Vec<Message>,
next_message_id: MessageId,
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 {
messages: Vec::new(),
next_message_id: MessageId(0),
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 message(&self, id: MessageId) -> Option<&Message> {
self.messages.iter().find(|message| message.id == id)
}
pub fn tools(&self) -> &Arc<ToolWorkingSet> {
&self.tools
}
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>, cx: &mut ModelContext<Self>) {
let id = self.next_message_id.post_inc();
self.messages.push(Message {
id,
role: Role::User,
text: text.into(),
});
cx.emit(ThreadEvent::MessageAdded(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 !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 { .. } => {
let id = thread.next_message_id.post_inc();
thread.messages.push(Message {
id,
role: Role::Assistant,
text: String::new(),
});
cx.emit(ThreadEvent::MessageAdded(id));
}
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,
},
);
}
}
}
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);
})?;
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 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),
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,114 +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 util::ResultExt as _;
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>>,
}
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 this = Self {
project,
tools,
context_server_manager,
context_server_tool_ids: HashMap::default(),
};
this.register_context_server_handlers(cx);
this
})?;
Ok(this)
})
}
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);
}
}
}
}
}

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

@@ -16,16 +16,21 @@ doctest = false
anyhow.workspace = true
client.workspace = true
db.workspace = true
editor.workspace = true
gpui.workspace = true
http_client.workspace = true
log.workspace = true
markdown_preview.workspace = true
menu.workspace = true
paths.workspace = true
release_channel.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
tempfile.workspace = true
util.workspace = true
which.workspace = true
workspace.workspace = true

View File

@@ -1,19 +1,27 @@
mod update_notification;
use anyhow::{anyhow, Context, Result};
use client::{Client, TelemetrySettings};
use db::kvp::KEY_VALUE_STORE;
use db::RELEASE_CHANNEL;
use editor::{Editor, MultiBuffer};
use gpui::{
actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
SemanticVersion, Task, WindowContext,
SemanticVersion, SharedString, Task, View, ViewContext, VisualContext, WindowContext,
};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
use paths::remote_servers_dir;
use release_channel::{AppCommitSha, ReleaseChannel};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, SettingsStore};
use serde::Deserialize;
use serde_derive::Serialize;
use smol::{fs, io::AsyncReadExt};
use settings::{Settings, SettingsSources, SettingsStore};
use smol::{fs::File, process::Command};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
use std::{
env::{
self,
@@ -24,13 +32,24 @@ use std::{
sync::Arc,
time::Duration,
};
use update_notification::UpdateNotification;
use util::ResultExt;
use which::which;
use workspace::notifications::NotificationId;
use workspace::Workspace;
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
actions!(auto_update, [Check, DismissErrorMessage, ViewReleaseNotes,]);
actions!(
auto_update,
[
Check,
DismissErrorMessage,
ViewReleaseNotes,
ViewReleaseNotesLocally
]
);
#[derive(Serialize)]
struct UpdateRequestBody {
@@ -127,6 +146,12 @@ struct GlobalAutoUpdate(Option<Model<AutoUpdater>>);
impl Global for GlobalAutoUpdate {}
#[derive(Deserialize)]
struct ReleaseNotesBody {
title: String,
release_notes: String,
}
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
AutoUpdateSetting::register(cx);
@@ -136,6 +161,10 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
workspace.register_action(|_, action, cx| {
view_release_notes(action, cx);
});
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
view_release_notes_locally(workspace, cx);
});
})
.detach();
@@ -235,6 +264,121 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<(
None
}
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
let release_channel = ReleaseChannel::global(cx);
let url = match release_channel {
ReleaseChannel::Nightly => Some("https://github.com/zed-industries/zed/commits/nightly/"),
ReleaseChannel::Dev => Some("https://github.com/zed-industries/zed/commits/main/"),
_ => None,
};
if let Some(url) = url {
cx.open_url(url);
return;
}
let version = AppVersion::global(cx).to_string();
let client = client::Client::global(cx).http_client();
let url = client.build_url(&format!(
"/api/release_notes/v2/{}/{}",
release_channel.dev_name(),
version
));
let markdown = workspace
.app_state()
.languages
.language_for_name("Markdown");
workspace
.with_local_workspace(cx, move |_, cx| {
cx.spawn(|workspace, mut cx| async move {
let markdown = markdown.await.log_err();
let response = client.get(&url, Default::default(), true).await;
let Some(mut response) = response.log_err() else {
return;
};
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await.ok();
let body: serde_json::Result<ReleaseNotesBody> =
serde_json::from_slice(body.as_slice());
if let Ok(body) = body {
workspace
.update(&mut cx, |workspace, cx| {
let project = workspace.project().clone();
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer("", markdown, cx)
});
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, body.release_notes)], None, cx)
});
let language_registry = project.read(cx).languages().clone();
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let tab_description = SharedString::from(body.title.to_string());
let editor = cx.new_view(|cx| {
Editor::for_multibuffer(buffer, Some(project), true, cx)
});
let workspace_handle = workspace.weak_handle();
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
MarkdownPreviewMode::Default,
editor,
workspace_handle,
language_registry,
Some(tab_description),
cx,
);
workspace.add_item_to_active_pane(
Box::new(view.clone()),
None,
true,
cx,
);
cx.notify();
})
.log_err();
}
})
.detach();
})
.detach();
}
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
let updater = AutoUpdater::get(cx)?;
let version = updater.read(cx).current_version;
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
cx.spawn(|workspace, mut cx| async move {
let should_show_notification = should_show_notification.await?;
if should_show_notification {
workspace.update(&mut cx, |workspace, cx| {
let workspace_handle = workspace.weak_handle();
workspace.show_notification(
NotificationId::unique::<UpdateNotification>(),
cx,
|cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)),
);
updater.update(cx, |updater, cx| {
updater
.set_should_show_update_notification(false, cx)
.detach_and_log_err(cx);
});
})?;
}
anyhow::Ok(())
})
.detach();
None
}
impl AutoUpdater {
pub fn get(cx: &mut AppContext) -> Option<Model<Self>> {
cx.default_global::<GlobalAutoUpdate>().0.clone()
@@ -279,10 +423,6 @@ impl AutoUpdater {
}));
}
pub fn current_version(&self) -> SemanticVersion {
self.current_version
}
pub fn status(&self) -> AutoUpdateStatus {
self.status.clone()
}
@@ -506,7 +646,7 @@ impl AutoUpdater {
Ok(())
}
pub fn set_should_show_update_notification(
fn set_should_show_update_notification(
&self,
should_show: bool,
cx: &AppContext,
@@ -528,7 +668,7 @@ impl AutoUpdater {
})
}
pub fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
cx.background_executor().spawn(async move {
Ok(KEY_VALUE_STORE
.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?

View File

@@ -1,28 +0,0 @@
[package]
name = "auto_update_ui"
version = "0.1.0"
edition = "2021"
publish = false
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/auto_update_ui.rs"
[dependencies]
anyhow.workspace = true
auto_update.workspace = true
client.workspace = true
editor.workspace = true
gpui.workspace = true
http_client.workspace = true
markdown_preview.workspace = true
menu.workspace = true
release_channel.workspace = true
serde.workspace = true
serde_json.workspace = true
smol.workspace = true
util.workspace = true
workspace.workspace = true

View File

@@ -1,147 +0,0 @@
mod update_notification;
use auto_update::AutoUpdater;
use editor::{Editor, MultiBuffer};
use gpui::{actions, prelude::*, AppContext, SharedString, View, ViewContext};
use http_client::HttpClient;
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
use release_channel::{AppVersion, ReleaseChannel};
use serde::Deserialize;
use smol::io::AsyncReadExt;
use util::ResultExt as _;
use workspace::notifications::NotificationId;
use workspace::Workspace;
use crate::update_notification::UpdateNotification;
actions!(auto_update, [ViewReleaseNotesLocally]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
view_release_notes_locally(workspace, cx);
});
})
.detach();
}
#[derive(Deserialize)]
struct ReleaseNotesBody {
title: String,
release_notes: String,
}
fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
let release_channel = ReleaseChannel::global(cx);
let url = match release_channel {
ReleaseChannel::Nightly => Some("https://github.com/zed-industries/zed/commits/nightly/"),
ReleaseChannel::Dev => Some("https://github.com/zed-industries/zed/commits/main/"),
_ => None,
};
if let Some(url) = url {
cx.open_url(url);
return;
}
let version = AppVersion::global(cx).to_string();
let client = client::Client::global(cx).http_client();
let url = client.build_url(&format!(
"/api/release_notes/v2/{}/{}",
release_channel.dev_name(),
version
));
let markdown = workspace
.app_state()
.languages
.language_for_name("Markdown");
workspace
.with_local_workspace(cx, move |_, cx| {
cx.spawn(|workspace, mut cx| async move {
let markdown = markdown.await.log_err();
let response = client.get(&url, Default::default(), true).await;
let Some(mut response) = response.log_err() else {
return;
};
let mut body = Vec::new();
response.body_mut().read_to_end(&mut body).await.ok();
let body: serde_json::Result<ReleaseNotesBody> =
serde_json::from_slice(body.as_slice());
if let Ok(body) = body {
workspace
.update(&mut cx, |workspace, cx| {
let project = workspace.project().clone();
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer("", markdown, cx)
});
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, body.release_notes)], None, cx)
});
let language_registry = project.read(cx).languages().clone();
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let tab_description = SharedString::from(body.title.to_string());
let editor = cx.new_view(|cx| {
Editor::for_multibuffer(buffer, Some(project), true, cx)
});
let workspace_handle = workspace.weak_handle();
let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
MarkdownPreviewMode::Default,
editor,
workspace_handle,
language_registry,
Some(tab_description),
cx,
);
workspace.add_item_to_active_pane(
Box::new(view.clone()),
None,
true,
cx,
);
cx.notify();
})
.log_err();
}
})
.detach();
})
.detach();
}
pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
let updater = AutoUpdater::get(cx)?;
let version = updater.read(cx).current_version();
let should_show_notification = updater.read(cx).should_show_update_notification(cx);
cx.spawn(|workspace, mut cx| async move {
let should_show_notification = should_show_notification.await?;
if should_show_notification {
workspace.update(&mut cx, |workspace, cx| {
let workspace_handle = workspace.weak_handle();
workspace.show_notification(
NotificationId::unique::<UpdateNotification>(),
cx,
|cx| cx.new_view(|_| UpdateNotification::new(version, workspace_handle)),
);
updater.update(cx, |updater, cx| {
updater
.set_should_show_update_notification(false, cx)
.detach_and_log_err(cx);
});
})?;
}
anyhow::Ok(())
})
.detach();
None
}

View File

@@ -17,7 +17,7 @@ test-support = [
"client/test-support",
"collections/test-support",
"gpui/test-support",
"livekit_client/test-support",
"live_kit_client/test-support",
"project/test-support",
"util/test-support"
]
@@ -27,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
@@ -41,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

@@ -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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,545 +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 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) {
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

@@ -3,8 +3,8 @@ use client::ParticipantIndex;
use client::{proto, User};
use collections::HashMap;
use gpui::WeakModel;
pub use livekit_client_macos::Frame;
pub use livekit_client_macos::{RemoteAudioTrack, RemoteVideoTrack};
pub use live_kit_client::Frame;
pub use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack};
use project::Project;
use std::sync::Arc;
@@ -49,12 +49,6 @@ pub struct RemoteParticipant {
pub participant_index: ParticipantIndex,
pub muted: bool,
pub speaking: bool,
pub video_tracks: HashMap<livekit_client_macos::Sid, Arc<RemoteVideoTrack>>,
pub audio_tracks: HashMap<livekit_client_macos::Sid, Arc<RemoteAudioTrack>>,
}
impl RemoteParticipant {
pub fn has_video_tracks(&self) -> bool {
!self.video_tracks.is_empty()
}
pub video_tracks: HashMap<live_kit_client::Sid, Arc<RemoteVideoTrack>>,
pub audio_tracks: HashMap<live_kit_client::Sid, Arc<RemoteAudioTrack>>,
}

View File

@@ -15,7 +15,7 @@ use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Task, WeakModel,
};
use language::LanguageRegistry;
use livekit_client_macos::{LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RoomUpdate};
use live_kit_client::{LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RoomUpdate};
use postage::{sink::Sink, stream::Stream, watch};
use project::Project;
use settings::Settings as _;
@@ -97,7 +97,7 @@ impl Room {
if let Some(live_kit) = self.live_kit.as_ref() {
matches!(
*live_kit.room.status().borrow(),
livekit_client_macos::ConnectionState::Connected { .. }
live_kit_client::ConnectionState::Connected { .. }
)
} else {
false
@@ -113,7 +113,7 @@ impl Room {
cx: &mut ModelContext<Self>,
) -> Self {
let live_kit_room = if let Some(connection_info) = live_kit_connection_info {
let room = livekit_client_macos::Room::new();
let room = live_kit_client::Room::new();
let mut status = room.status();
// Consume the initial status of the room.
let _ = status.try_recv();
@@ -125,7 +125,7 @@ impl Room {
break;
};
if status == livekit_client_macos::ConnectionState::Disconnected {
if status == live_kit_client::ConnectionState::Disconnected {
this.update(&mut cx, |this, cx| this.leave(cx).log_err())
.ok();
break;
@@ -156,7 +156,7 @@ impl Room {
cx.spawn(|this, mut cx| async move {
connect.await?;
this.update(&mut cx, |this, cx| {
if this.can_use_microphone(cx) {
if this.can_use_microphone() {
if let Some(live_kit) = &this.live_kit {
if !live_kit.muted_by_user && !live_kit.deafened {
return this.share_microphone(cx);
@@ -1317,7 +1317,7 @@ impl Room {
self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
}
pub fn can_use_microphone(&self, _cx: &AppContext) -> bool {
pub fn can_use_microphone(&self) -> bool {
use proto::ChannelRole::*;
match self.local_participant.role {
Admin | Member | Talker => true,
@@ -1631,7 +1631,7 @@ impl Room {
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_display_sources(&self, sources: Vec<livekit_client_macos::MacOSDisplay>) {
pub fn set_display_sources(&self, sources: Vec<live_kit_client::MacOSDisplay>) {
self.live_kit
.as_ref()
.unwrap()
@@ -1641,7 +1641,7 @@ impl Room {
}
struct LiveKitRoom {
room: Arc<livekit_client_macos::Room>,
room: Arc<live_kit_client::Room>,
screen_track: LocalTrack,
microphone_track: LocalTrack,
/// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user.

View File

@@ -16,15 +16,11 @@ doctest = false
name = "cli"
path = "src/main.rs"
[features]
no-bundled-uninstall = []
default = []
[dependencies]
anyhow.workspace = true
clap.workspace = true
collections.workspace = true
ipc-channel = "0.19"
ipc-channel = "0.18"
once_cell.workspace = true
parking_lot.workspace = true
paths.workspace = true

View File

@@ -1,5 +0,0 @@
fn main() {
if std::env::var("ZED_UPDATE_EXPLANATION").is_ok() {
println!(r#"cargo:rustc-cfg=feature="no-bundled-uninstall""#);
}
}

View File

@@ -59,13 +59,6 @@ struct Args {
/// Run zed in dev-server mode
#[arg(long)]
dev_server_token: Option<String>,
/// Uninstall Zed from user system
#[cfg(all(
any(target_os = "linux", target_os = "macos"),
not(feature = "no-bundled-uninstall")
))]
#[arg(long)]
uninstall: bool,
}
fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
@@ -126,29 +119,6 @@ fn main() -> Result<()> {
return Ok(());
}
#[cfg(all(
any(target_os = "linux", target_os = "macos"),
not(feature = "no-bundled-uninstall")
))]
if args.uninstall {
static UNINSTALL_SCRIPT: &[u8] = include_bytes!("../../../script/uninstall.sh");
let tmp_dir = tempfile::tempdir()?;
let script_path = tmp_dir.path().join("uninstall.sh");
fs::write(&script_path, UNINSTALL_SCRIPT)?;
use std::os::unix::fs::PermissionsExt as _;
fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755))?;
let status = std::process::Command::new("sh")
.arg(&script_path)
.env("ZED_CHANNEL", &*release_channel::RELEASE_CHANNEL_NAME)
.status()
.context("Failed to execute uninstall script")?;
std::process::exit(status.code().unwrap_or(1));
}
let (server, server_name) =
IpcOneShotServer::<IpcHandshake>::new().context("Handshake before Zed spawn")?;
let url = format!("zed-cli://{server_name}");
@@ -548,7 +518,7 @@ mod mac_os {
},
LocalPath {
executable: PathBuf,
short_version_string: String,
plist: InfoPlist,
},
}
@@ -587,9 +557,17 @@ mod mac_os {
}
_ => {
println!("Bundle path {bundle_path:?} has no *.app extension, attempting to locate a dev build");
let plist_path = bundle_path
.parent()
.with_context(|| format!("Bundle path {bundle_path:?} has no parent"))?
.join("WebRTC.framework/Resources/Info.plist");
let plist =
plist::from_file::<_, InfoPlist>(&plist_path).with_context(|| {
format!("Reading dev bundle plist file at {plist_path:?}")
})?;
Ok(Bundle::LocalPath {
executable: bundle_path,
short_version_string: "test-dev-version".to_string(),
plist,
})
}
}
@@ -599,16 +577,9 @@ mod mac_os {
impl InstalledApp for Bundle {
fn zed_version_string(&self) -> String {
let is_dev = matches!(self, Self::LocalPath { .. });
let version = match self {
Bundle::App { plist, .. } => &plist.bundle_short_version_string,
Bundle::LocalPath {
short_version_string,
..
} => &short_version_string,
};
format!(
"Zed {}{} {}",
version,
self.plist().bundle_short_version_string,
if is_dev { " (dev)" } else { "" },
self.path().display(),
)
@@ -690,6 +661,13 @@ mod mac_os {
}
impl Bundle {
fn plist(&self) -> &InfoPlist {
match self {
Self::App { plist, .. } => plist,
Self::LocalPath { plist, .. } => plist,
}
}
fn path(&self) -> &Path {
match self {
Self::App { app_bundle, .. } => app_bundle,

View File

@@ -1067,8 +1067,6 @@ impl Client {
let proxy = http.proxy().cloned();
let credentials = credentials.clone();
let rpc_url = self.rpc_url(http, release_channel);
let system_id = self.telemetry.system_id();
let metrics_id = self.telemetry.metrics_id();
cx.background_executor().spawn(async move {
use HttpOrHttps::*;
@@ -1120,12 +1118,6 @@ impl Client {
"x-zed-release-channel",
HeaderValue::from_str(release_channel.map(|r| r.dev_name()).unwrap_or("unknown"))?,
);
if let Some(system_id) = system_id {
request_headers.insert("x-zed-system-id", HeaderValue::from_str(&system_id)?);
}
if let Some(metrics_id) = metrics_id {
request_headers.insert("x-zed-metrics-id", HeaderValue::from_str(&metrics_id)?);
}
match url_scheme {
Https => {

View File

@@ -533,10 +533,6 @@ impl Telemetry {
self.state.lock().metrics_id.clone()
}
pub fn system_id(self: &Arc<Self>) -> Option<Arc<str>> {
self.state.lock().system_id.clone()
}
pub fn installation_id(self: &Arc<Self>) -> Option<Arc<str>> {
self.state.lock().installation_id.clone()
}

View File

@@ -5,9 +5,9 @@ HTTP_PORT = 8080
API_TOKEN = "secret"
INVITE_LINK_PREFIX = "http://localhost:3000/invites/"
ZED_ENVIRONMENT = "development"
LIVEKIT_SERVER = "http://localhost:7880"
LIVEKIT_KEY = "devkey"
LIVEKIT_SECRET = "secret"
LIVE_KIT_SERVER = "http://localhost:7880"
LIVE_KIT_KEY = "devkey"
LIVE_KIT_SECRET = "secret"
BLOB_STORE_ACCESS_KEY = "the-blob-store-access-key"
BLOB_STORE_SECRET_KEY = "the-blob-store-secret-key"
BLOB_STORE_BUCKET = "the-extensions-bucket"

View File

@@ -40,7 +40,7 @@ google_ai.workspace = true
hex.workspace = true
http_client.workspace = true
jsonwebtoken.workspace = true
livekit_server.workspace = true
live_kit_server.workspace = true
log.workspace = true
nanoid.workspace = true
open_ai.workspace = true
@@ -77,16 +77,9 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "re
util.workspace = true
uuid.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
livekit_client_macos = { workspace = true, features = ["test-support"] }
[target.'cfg(not(target_os = "macos"))'.dependencies]
livekit_client = { workspace = true, features = ["test-support"] }
[dev-dependencies]
assistant = { workspace = true, features = ["test-support"] }
assistant_tool.workspace = true
context_server.workspace = true
context_servers.workspace = true
async-trait.workspace = true
audio.workspace = true
call = { workspace = true, features = ["test-support"] }
@@ -97,7 +90,6 @@ collections = { workspace = true, features = ["test-support"] }
ctor.workspace = true
editor = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
extension.workspace = true
file_finder.workspace = true
fs = { workspace = true, features = ["test-support"] }
git = { workspace = true, features = ["test-support"] }
@@ -107,6 +99,7 @@ hyper.workspace = true
indoc.workspace = true
language = { workspace = true, features = ["test-support"] }
language_model = { workspace = true, features = ["test-support"] }
live_kit_client = { workspace = true, features = ["test-support"] }
lsp = { workspace = true, features = ["test-support"] }
menu.workspace = true
multi_buffer = { workspace = true, features = ["test-support"] }
@@ -130,11 +123,5 @@ util.workspace = true
workspace = { workspace = true, features = ["test-support"] }
worktree = { 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"] }
[package.metadata.cargo-machete]
ignored = ["async-stripe"]

View File

@@ -109,17 +109,17 @@ spec:
secretKeyRef:
name: zed-client
key: checksum-seed
- name: LIVEKIT_SERVER
- name: LIVE_KIT_SERVER
valueFrom:
secretKeyRef:
name: livekit
key: server
- name: LIVEKIT_KEY
- name: LIVE_KIT_KEY
valueFrom:
secretKeyRef:
name: livekit
key: key
- name: LIVEKIT_SECRET
- name: LIVE_KIT_SECRET
valueFrom:
secretKeyRef:
name: livekit

View File

@@ -61,39 +61,6 @@ impl std::fmt::Display for CloudflareIpCountryHeader {
}
}
pub struct SystemIdHeader(String);
impl Header for SystemIdHeader {
fn name() -> &'static HeaderName {
static SYSTEM_ID_HEADER: OnceLock<HeaderName> = OnceLock::new();
SYSTEM_ID_HEADER.get_or_init(|| HeaderName::from_static("x-zed-system-id"))
}
fn decode<'i, I>(values: &mut I) -> Result<Self, axum::headers::Error>
where
Self: Sized,
I: Iterator<Item = &'i axum::http::HeaderValue>,
{
let system_id = values
.next()
.ok_or_else(axum::headers::Error::invalid)?
.to_str()
.map_err(|_| axum::headers::Error::invalid())?;
Ok(Self(system_id.to_string()))
}
fn encode<E: Extend<axum::http::HeaderValue>>(&self, _values: &mut E) {
unimplemented!()
}
}
impl std::fmt::Display for SystemIdHeader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
Router::new()
.route("/user", get(get_authenticated_user))

View File

@@ -1420,6 +1420,8 @@ fn for_snowflake(
"enable screen share" => "Screen Share Enabled".to_string(),
"disable screen share" => "Screen Share Disabled".to_string(),
"decline incoming" => "Incoming Call Declined".to_string(),
"enable camera" => "Camera Enabled".to_string(),
"disable camera" => "Camera Disabled".to_string(),
_ => format!("Unknown Call Event: {}", e.operation),
};
@@ -1442,64 +1444,65 @@ fn for_snowflake(
Event::App(e) => {
let mut properties = json!({});
let event_type = match e.operation.trim() {
// App
"extensions: install extension" => "Extension Installed".to_string(),
"open" => "App Opened".to_string(),
"first open" => "App First Opened".to_string(),
"first open for release channel" => {
"App First Opened For Release Channel".to_string()
"project search: open" => "Project Search Opened".to_string(),
"first open" => {
properties["is_first_open"] = json!(true);
"App First Opened".to_string()
}
"close" => "App Closed".to_string(),
// Project
"open project" => "Project Opened".to_string(),
"open node project" => {
properties["project_type"] = json!("node");
"Project Opened".to_string()
}
"open pnpm project" => {
properties["project_type"] = json!("pnpm");
"Project Opened".to_string()
}
"open yarn project" => {
properties["project_type"] = json!("yarn");
"Project Opened".to_string()
}
// SSH
"create ssh server" => "SSH Server Created".to_string(),
"create ssh project" => "SSH Project Created".to_string(),
"open ssh project" => "SSH Project Opened".to_string(),
// Welcome Page
"welcome page: change keymap" => "Welcome Keymap Changed".to_string(),
"welcome page: change theme" => "Welcome Theme Changed".to_string(),
"extensions: uninstall extension" => "Extension Uninstalled".to_string(),
"welcome page: close" => "Welcome Page Closed".to_string(),
"welcome page: edit settings" => "Welcome Settings Edited".to_string(),
"welcome page: install cli" => "Welcome CLI Installed".to_string(),
"welcome page: open" => "Welcome Page Opened".to_string(),
"welcome page: open extensions" => "Welcome Extensions Page Opened".to_string(),
"open project" => {
properties["is_first_time"] = json!(false);
"Project Opened".to_string()
}
"welcome page: install cli" => "CLI Installed".to_string(),
"project diagnostics: open" => "Project Diagnostics Opened".to_string(),
"extensions page: open" => "Extensions Page Opened".to_string(),
"welcome page: change theme" => "Welcome Theme Changed".to_string(),
"welcome page: toggle metric telemetry" => {
properties["enabled"] = json!(false);
"Welcome Telemetry Toggled".to_string()
}
"welcome page: change keymap" => "Keymap Changed".to_string(),
"welcome page: toggle vim" => {
properties["enabled"] = json!(false);
"Welcome Vim Mode Toggled".to_string()
}
"welcome page: sign in to copilot" => "Welcome Copilot Signed In".to_string(),
"welcome page: toggle diagnostic telemetry" => {
"Welcome Diagnostic Telemetry Toggled".to_string()
"Welcome Telemetry Toggled".to_string()
}
"welcome page: toggle metric telemetry" => {
"Welcome Metric Telemetry Toggled".to_string()
}
"welcome page: toggle vim" => "Welcome Vim Mode Toggled".to_string(),
"welcome page: view docs" => "Welcome Documentation Viewed".to_string(),
// Extensions
"extensions page: open" => "Extensions Page Opened".to_string(),
"extensions: install extension" => "Extension Installed".to_string(),
"extensions: uninstall extension" => "Extension Uninstalled".to_string(),
// Misc
"welcome page: open" => "Welcome Page Opened".to_string(),
"close" => "App Closed".to_string(),
"markdown preview: open" => "Markdown Preview Opened".to_string(),
"project diagnostics: open" => "Project Diagnostics Opened".to_string(),
"project search: open" => "Project Search Opened".to_string(),
"welcome page: open extensions" => "Extensions Page Opened".to_string(),
"open node project" | "open pnpm project" | "open yarn project" => {
properties["project_type"] = json!("node");
properties["is_first_time"] = json!(false);
"Project Opened".to_string()
}
"repl sessions: open" => "REPL Session Started".to_string(),
// Feature Upsell
"welcome page: toggle helix" => {
properties["enabled"] = json!(false);
"Helix Mode Toggled".to_string()
}
"welcome page: edit settings" => {
properties["changed_settings"] = json!([]);
"Settings Edited".to_string()
}
"welcome page: view docs" => "Documentation Viewed".to_string(),
"open ssh project" => {
properties["is_first_time"] = json!(false);
"SSH Project Opened".to_string()
}
"create ssh server" => "SSH Server Created".to_string(),
"create ssh project" => "SSH Project Created".to_string(),
"first open for release channel" => {
properties["is_first_for_channel"] = json!(true);
"App First Opened For Release Channel".to_string()
}
"feature upsell: toggle vim" => {
properties["source"] = json!("Feature Upsell");
"Vim Mode Toggled".to_string()
@@ -1575,8 +1578,8 @@ fn for_snowflake(
})
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SnowflakeRow {
#[derive(Serialize, Deserialize)]
struct SnowflakeRow {
pub time: chrono::DateTime<chrono::Utc>,
pub user_id: Option<String>,
pub device_id: Option<String>,
@@ -1586,41 +1589,47 @@ pub struct SnowflakeRow {
pub insert_id: Option<String>,
}
impl SnowflakeRow {
pub fn new(
event_type: impl Into<String>,
metrics_id: Option<Uuid>,
is_staff: bool,
system_id: Option<String>,
event_properties: serde_json::Value,
) -> Self {
Self {
time: chrono::Utc::now(),
event_type: event_type.into(),
device_id: system_id,
user_id: metrics_id.map(|id| id.to_string()),
insert_id: Some(uuid::Uuid::new_v4().to_string()),
event_properties,
user_properties: Some(json!({"is_staff": is_staff})),
}
}
#[derive(Serialize, Deserialize)]
struct SnowflakeData {
/// Identifier unique to each Zed installation (differs for stable, preview, dev)
pub installation_id: Option<String>,
/// Identifier unique to each logged in Zed user (randomly generated on first sign in)
/// Identifier unique to each Zed session (differs for each time you open Zed)
pub session_id: Option<String>,
pub metrics_id: Option<String>,
/// True for Zed staff, otherwise false
pub is_staff: Option<bool>,
/// Zed version number
pub app_version: String,
pub os_name: String,
pub os_version: Option<String>,
pub architecture: String,
/// Zed release channel (stable, preview, dev)
pub release_channel: Option<String>,
pub signed_in: bool,
pub async fn write(
self,
client: &Option<aws_sdk_kinesis::Client>,
stream: &Option<String>,
) -> anyhow::Result<()> {
let Some((client, stream)) = client.as_ref().zip(stream.as_ref()) else {
return Ok(());
};
let row = serde_json::to_vec(&self)?;
client
.put_record()
.stream_name(stream)
.partition_key(&self.user_id.unwrap_or_default())
.data(row.into())
.send()
.await?;
Ok(())
}
#[serde(flatten)]
pub editor_event: Option<EditorEvent>,
#[serde(flatten)]
pub inline_completion_event: Option<InlineCompletionEvent>,
#[serde(flatten)]
pub call_event: Option<CallEvent>,
#[serde(flatten)]
pub assistant_event: Option<AssistantEvent>,
#[serde(flatten)]
pub cpu_event: Option<CpuEvent>,
#[serde(flatten)]
pub memory_event: Option<MemoryEvent>,
#[serde(flatten)]
pub app_event: Option<AppEvent>,
#[serde(flatten)]
pub setting_event: Option<SettingEvent>,
#[serde(flatten)]
pub extension_event: Option<ExtensionEvent>,
#[serde(flatten)]
pub edit_event: Option<EditEvent>,
#[serde(flatten)]
pub repl_event: Option<ReplEvent>,
#[serde(flatten)]
pub action_event: Option<ActionEvent>,
}

View File

@@ -1,5 +1,3 @@
use serde::Serialize;
/// A number of cents.
#[derive(
Debug,
@@ -14,7 +12,6 @@ use serde::Serialize;
derive_more::AddAssign,
derive_more::Sub,
derive_more::SubAssign,
Serialize,
)]
pub struct Cents(pub u32);

View File

@@ -154,9 +154,9 @@ impl Database {
}
let role = role.unwrap();
let livekit_room = format!("channel-{}", nanoid::nanoid!(30));
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
let room_id = self
.get_or_create_channel_room(channel_id, &livekit_room, &tx)
.get_or_create_channel_room(channel_id, &live_kit_room, &tx)
.await?;
self.join_channel_room_internal(room_id, user_id, connection, role, &tx)
@@ -896,7 +896,7 @@ impl Database {
pub(crate) async fn get_or_create_channel_room(
&self,
channel_id: ChannelId,
livekit_room: &str,
live_kit_room: &str,
tx: &DatabaseTransaction,
) -> Result<RoomId> {
let room = room::Entity::find()
@@ -909,7 +909,7 @@ impl Database {
} else {
let result = room::Entity::insert(room::ActiveModel {
channel_id: ActiveValue::Set(Some(channel_id)),
live_kit_room: ActiveValue::Set(livekit_room.to_string()),
live_kit_room: ActiveValue::Set(live_kit_room.to_string()),
..Default::default()
})
.exec(tx)

View File

@@ -103,11 +103,11 @@ impl Database {
&self,
user_id: UserId,
connection: ConnectionId,
livekit_room: &str,
live_kit_room: &str,
) -> Result<proto::Room> {
self.transaction(|tx| async move {
let room = room::ActiveModel {
live_kit_room: ActiveValue::set(livekit_room.into()),
live_kit_room: ActiveValue::set(live_kit_room.into()),
..Default::default()
}
.insert(&*tx)
@@ -1316,7 +1316,7 @@ impl Database {
channel,
proto::Room {
id: db_room.id.to_proto(),
livekit_room: db_room.live_kit_room,
live_kit_room: db_room.live_kit_room,
participants: participants.into_values().collect(),
pending_participants,
followers,

View File

@@ -156,9 +156,9 @@ pub struct Config {
pub clickhouse_password: Option<String>,
pub clickhouse_database: Option<String>,
pub invite_link_prefix: String,
pub livekit_server: Option<String>,
pub livekit_key: Option<String>,
pub livekit_secret: Option<String>,
pub live_kit_server: Option<String>,
pub live_kit_key: Option<String>,
pub live_kit_secret: Option<String>,
pub llm_database_url: Option<String>,
pub llm_database_max_connections: Option<u32>,
pub llm_database_migrations_path: Option<PathBuf>,
@@ -210,9 +210,9 @@ impl Config {
database_max_connections: 0,
api_token: "".into(),
invite_link_prefix: "".into(),
livekit_server: None,
livekit_key: None,
livekit_secret: None,
live_kit_server: None,
live_kit_key: None,
live_kit_secret: None,
llm_database_url: None,
llm_database_max_connections: None,
llm_database_migrations_path: None,
@@ -277,7 +277,7 @@ impl ServiceMode {
pub struct AppState {
pub db: Arc<Database>,
pub llm_db: Option<Arc<LlmDatabase>>,
pub livekit_client: Option<Arc<dyn livekit_server::api::Client>>,
pub live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
pub blob_store_client: Option<aws_sdk_s3::Client>,
pub stripe_client: Option<Arc<stripe::Client>>,
pub stripe_billing: Option<Arc<StripeBilling>>,
@@ -309,17 +309,17 @@ impl AppState {
None
};
let livekit_client = if let Some(((server, key), secret)) = config
.livekit_server
let live_kit_client = if let Some(((server, key), secret)) = config
.live_kit_server
.as_ref()
.zip(config.livekit_key.as_ref())
.zip(config.livekit_secret.as_ref())
.zip(config.live_kit_key.as_ref())
.zip(config.live_kit_secret.as_ref())
{
Some(Arc::new(livekit_server::api::LiveKitClient::new(
Some(Arc::new(live_kit_server::api::LiveKitClient::new(
server.clone(),
key.clone(),
secret.clone(),
)) as Arc<dyn livekit_server::api::Client>)
)) as Arc<dyn live_kit_server::api::Client>)
} else {
None
};
@@ -329,7 +329,7 @@ impl AppState {
let this = Self {
db: db.clone(),
llm_db,
livekit_client,
live_kit_client,
blob_store_client: build_blob_store_client(&config).await.log_err(),
stripe_billing: stripe_client
.clone()

View File

@@ -3,11 +3,9 @@ pub mod db;
mod telemetry;
mod token;
use crate::api::events::SnowflakeRow;
use crate::api::CloudflareIpCountryHeader;
use crate::build_kinesis_client;
use crate::{
build_clickhouse_client, db::UserId, executor::Executor, Cents, Config, Error, Result,
api::CloudflareIpCountryHeader, build_clickhouse_client, db::UserId, executor::Executor, Cents,
Config, Error, Result,
};
use anyhow::{anyhow, Context as _};
use authorization::authorize_access_to_language_model;
@@ -30,7 +28,6 @@ use rpc::{
proto::Plan, LanguageModelProvider, PerformCompletionParams, EXPIRED_LLM_TOKEN_HEADER_NAME,
};
use rpc::{ListModelsResponse, MAX_LLM_MONTHLY_SPEND_REACHED_HEADER_NAME};
use serde_json::json;
use std::{
pin::Pin,
sync::Arc,
@@ -48,7 +45,6 @@ pub struct LlmState {
pub executor: Executor,
pub db: Arc<LlmDatabase>,
pub http_client: ReqwestClient,
pub kinesis_client: Option<aws_sdk_kinesis::Client>,
pub clickhouse_client: Option<clickhouse::Client>,
active_user_count_by_model:
RwLock<HashMap<(LanguageModelProvider, String), (DateTime<Utc>, ActiveUserCount)>>,
@@ -81,11 +77,6 @@ impl LlmState {
executor,
db,
http_client,
kinesis_client: if config.kinesis_access_key.is_some() {
build_kinesis_client(&config).await.log_err()
} else {
None
},
clickhouse_client: config
.clickhouse_url
.as_ref()
@@ -530,50 +521,25 @@ async fn check_usage_limit(
UsageMeasure::TokensPerDay => "tokens_per_day",
};
tracing::info!(
target: "user rate limit",
user_id = claims.user_id,
login = claims.github_user_login,
authn.jti = claims.jti,
is_staff = claims.is_staff,
provider = provider.to_string(),
model = model.name,
requests_this_minute = usage.requests_this_minute,
tokens_this_minute = usage.tokens_this_minute,
tokens_this_day = usage.tokens_this_day,
users_in_recent_minutes = users_in_recent_minutes,
users_in_recent_days = users_in_recent_days,
max_requests_per_minute = per_user_max_requests_per_minute,
max_tokens_per_minute = per_user_max_tokens_per_minute,
max_tokens_per_day = per_user_max_tokens_per_day,
);
SnowflakeRow::new(
"Language Model Rate Limited",
claims.metrics_id,
claims.is_staff,
claims.system_id.clone(),
json!({
"usage": usage,
"users_in_recent_minutes": users_in_recent_minutes,
"users_in_recent_days": users_in_recent_days,
"max_requests_per_minute": per_user_max_requests_per_minute,
"max_tokens_per_minute": per_user_max_tokens_per_minute,
"max_tokens_per_day": per_user_max_tokens_per_day,
"plan": match claims.plan {
Plan::Free => "free".to_string(),
Plan::ZedPro => "zed_pro".to_string(),
},
"model": model.name.clone(),
"provider": provider.to_string(),
"usage_measure": resource.to_string(),
}),
)
.write(&state.kinesis_client, &state.config.kinesis_stream)
.await
.log_err();
if let Some(client) = state.clickhouse_client.as_ref() {
tracing::info!(
target: "user rate limit",
user_id = claims.user_id,
login = claims.github_user_login,
authn.jti = claims.jti,
is_staff = claims.is_staff,
provider = provider.to_string(),
model = model.name,
requests_this_minute = usage.requests_this_minute,
tokens_this_minute = usage.tokens_this_minute,
tokens_this_day = usage.tokens_this_day,
users_in_recent_minutes = users_in_recent_minutes,
users_in_recent_days = users_in_recent_days,
max_requests_per_minute = per_user_max_requests_per_minute,
max_tokens_per_minute = per_user_max_tokens_per_minute,
max_tokens_per_day = per_user_max_tokens_per_day,
);
report_llm_rate_limit(
client,
LlmRateLimitEventRow {
@@ -686,27 +652,6 @@ impl<S> Drop for TokenCountingStream<S> {
tokens_this_minute = usage.tokens_this_minute,
);
let properties = json!({
"plan": match claims.plan {
Plan::Free => "free".to_string(),
Plan::ZedPro => "zed_pro".to_string(),
},
"model": model,
"provider": provider,
"usage": usage,
"tokens": tokens
});
SnowflakeRow::new(
"Language Model Used",
claims.metrics_id,
claims.is_staff,
claims.system_id.clone(),
properties,
)
.write(&state.kinesis_client, &state.config.kinesis_stream)
.await
.log_err();
if let Some(clickhouse_client) = state.clickhouse_client.as_ref() {
report_llm_usage(
clickhouse_client,

View File

@@ -9,7 +9,7 @@ use strum::IntoEnumIterator as _;
use super::*;
#[derive(Debug, PartialEq, Clone, Copy, Default, serde::Serialize)]
#[derive(Debug, PartialEq, Clone, Copy, Default)]
pub struct TokenUsage {
pub input: usize,
pub input_cache_creation: usize,
@@ -23,7 +23,7 @@ impl TokenUsage {
}
}
#[derive(Debug, PartialEq, Clone, Copy, serde::Serialize)]
#[derive(Debug, PartialEq, Clone, Copy)]
pub struct Usage {
pub requests_this_minute: usize,
pub tokens_this_minute: usize,

View File

@@ -8,7 +8,6 @@ use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use thiserror::Error;
use uuid::Uuid;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
@@ -17,10 +16,6 @@ pub struct LlmTokenClaims {
pub exp: u64,
pub jti: String,
pub user_id: u64,
#[serde(default)]
pub system_id: Option<String>,
#[serde(default)]
pub metrics_id: Option<Uuid>,
pub github_user_login: String,
pub is_staff: bool,
pub has_llm_closed_beta_feature_flag: bool,
@@ -41,7 +36,6 @@ impl LlmTokenClaims {
has_llm_closed_beta_feature_flag: bool,
has_llm_subscription: bool,
plan: rpc::proto::Plan,
system_id: Option<String>,
config: &Config,
) -> Result<String> {
let secret = config
@@ -55,8 +49,6 @@ impl LlmTokenClaims {
exp: (now + LLM_TOKEN_LIFETIME).timestamp() as u64,
jti: uuid::Uuid::new_v4().to_string(),
user_id: user.id.to_proto(),
system_id,
metrics_id: Some(user.metrics_id),
github_user_login: user.github_login.clone(),
is_staff,
has_llm_closed_beta_feature_flag,

View File

@@ -1,6 +1,6 @@
mod connection_pool;
use crate::api::{CloudflareIpCountryHeader, SystemIdHeader};
use crate::api::CloudflareIpCountryHeader;
use crate::llm::LlmTokenClaims;
use crate::{
auth,
@@ -137,7 +137,6 @@ struct Session {
/// The GeoIP country code for the user.
#[allow(unused)]
geoip_country_code: Option<String>,
system_id: Option<String>,
_executor: Executor,
}
@@ -418,7 +417,7 @@ impl Server {
let peer = self.peer.clone();
let timeout = self.app_state.executor.sleep(CLEANUP_TIMEOUT);
let pool = self.connection_pool.clone();
let livekit_client = self.app_state.livekit_client.clone();
let live_kit_client = self.app_state.live_kit_client.clone();
let span = info_span!("start server");
self.app_state.executor.spawn_detached(
@@ -463,8 +462,8 @@ impl Server {
for room_id in room_ids {
let mut contacts_to_update = HashSet::default();
let mut canceled_calls_to_user_ids = Vec::new();
let mut livekit_room = String::new();
let mut delete_livekit_room = false;
let mut live_kit_room = String::new();
let mut delete_live_kit_room = false;
if let Some(mut refreshed_room) = app_state
.db
@@ -487,8 +486,8 @@ impl Server {
.extend(refreshed_room.canceled_calls_to_user_ids.iter().copied());
canceled_calls_to_user_ids =
mem::take(&mut refreshed_room.canceled_calls_to_user_ids);
livekit_room = mem::take(&mut refreshed_room.room.livekit_room);
delete_livekit_room = refreshed_room.room.participants.is_empty();
live_kit_room = mem::take(&mut refreshed_room.room.live_kit_room);
delete_live_kit_room = refreshed_room.room.participants.is_empty();
}
{
@@ -539,9 +538,9 @@ impl Server {
}
}
if let Some(live_kit) = livekit_client.as_ref() {
if delete_livekit_room {
live_kit.delete_room(livekit_room).await.trace_err();
if let Some(live_kit) = live_kit_client.as_ref() {
if delete_live_kit_room {
live_kit.delete_room(live_kit_room).await.trace_err();
}
}
}
@@ -683,7 +682,6 @@ impl Server {
principal: Principal,
zed_version: ZedVersion,
geoip_country_code: Option<String>,
system_id: Option<String>,
send_connection_id: Option<oneshot::Sender<ConnectionId>>,
executor: Executor,
) -> impl Future<Output = ()> {
@@ -739,7 +737,6 @@ impl Server {
app_state: this.app_state.clone(),
http_client,
geoip_country_code,
system_id,
_executor: executor.clone(),
supermaven_client,
};
@@ -1059,7 +1056,6 @@ pub fn routes(server: Arc<Server>) -> Router<(), Body> {
.layer(Extension(server))
}
#[allow(clippy::too_many_arguments)]
pub async fn handle_websocket_request(
TypedHeader(ProtocolVersion(protocol_version)): TypedHeader<ProtocolVersion>,
app_version_header: Option<TypedHeader<AppVersionHeader>>,
@@ -1067,7 +1063,6 @@ pub async fn handle_websocket_request(
Extension(server): Extension<Arc<Server>>,
Extension(principal): Extension<Principal>,
country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
system_id_header: Option<TypedHeader<SystemIdHeader>>,
ws: WebSocketUpgrade,
) -> axum::response::Response {
if protocol_version != rpc::PROTOCOL_VERSION {
@@ -1109,7 +1104,6 @@ pub async fn handle_websocket_request(
principal,
version,
country_code_header.map(|header| header.to_string()),
system_id_header.map(|header| header.to_string()),
None,
Executor::Production,
)
@@ -1210,15 +1204,15 @@ async fn create_room(
response: Response<proto::CreateRoom>,
session: Session,
) -> Result<()> {
let livekit_room = nanoid::nanoid!(30);
let live_kit_room = nanoid::nanoid!(30);
let live_kit_connection_info = util::maybe!(async {
let live_kit = session.app_state.livekit_client.as_ref();
let live_kit = session.app_state.live_kit_client.as_ref();
let live_kit = live_kit?;
let user_id = session.user_id().to_string();
let token = live_kit
.room_token(&livekit_room, &user_id.to_string())
.room_token(&live_kit_room, &user_id.to_string())
.trace_err()?;
Some(proto::LiveKitConnectionInfo {
@@ -1232,7 +1226,7 @@ async fn create_room(
let room = session
.db()
.await
.create_room(session.user_id(), session.connection_id, &livekit_room)
.create_room(session.user_id(), session.connection_id, &live_kit_room)
.await?;
response.send(proto::CreateRoomResponse {
@@ -1284,22 +1278,22 @@ async fn join_room(
.trace_err();
}
let live_kit_connection_info = if let Some(live_kit) = session.app_state.livekit_client.as_ref()
{
live_kit
.room_token(
&joined_room.room.livekit_room,
&session.user_id().to_string(),
)
.trace_err()
.map(|token| proto::LiveKitConnectionInfo {
server_url: live_kit.url().into(),
token,
can_publish: true,
})
} else {
None
};
let live_kit_connection_info =
if let Some(live_kit) = session.app_state.live_kit_client.as_ref() {
live_kit
.room_token(
&joined_room.room.live_kit_room,
&session.user_id().to_string(),
)
.trace_err()
.map(|token| proto::LiveKitConnectionInfo {
server_url: live_kit.url().into(),
token,
can_publish: true,
})
} else {
None
};
response.send(proto::JoinRoomResponse {
room: Some(joined_room.room),
@@ -1506,7 +1500,7 @@ async fn set_room_participant_role(
let user_id = UserId::from_proto(request.user_id);
let role = ChannelRole::from(request.role());
let (livekit_room, can_publish) = {
let (live_kit_room, can_publish) = {
let room = session
.db()
.await
@@ -1518,18 +1512,18 @@ async fn set_room_participant_role(
)
.await?;
let livekit_room = room.livekit_room.clone();
let live_kit_room = room.live_kit_room.clone();
let can_publish = ChannelRole::from(request.role()).can_use_microphone();
room_updated(&room, &session.peer);
(livekit_room, can_publish)
(live_kit_room, can_publish)
};
if let Some(live_kit) = session.app_state.livekit_client.as_ref() {
if let Some(live_kit) = session.app_state.live_kit_client.as_ref() {
live_kit
.update_participant(
livekit_room.clone(),
live_kit_room.clone(),
request.user_id.to_string(),
livekit_server::proto::ParticipantPermission {
live_kit_server::proto::ParticipantPermission {
can_subscribe: true,
can_publish,
can_publish_data: can_publish,
@@ -3091,7 +3085,7 @@ async fn join_channel_internal(
let live_kit_connection_info =
session
.app_state
.livekit_client
.live_kit_client
.as_ref()
.and_then(|live_kit| {
let (can_publish, token) = if role == ChannelRole::Guest {
@@ -3099,7 +3093,7 @@ async fn join_channel_internal(
false,
live_kit
.guest_token(
&joined_room.room.livekit_room,
&joined_room.room.live_kit_room,
&session.user_id().to_string(),
)
.trace_err()?,
@@ -3109,7 +3103,7 @@ async fn join_channel_internal(
true,
live_kit
.room_token(
&joined_room.room.livekit_room,
&joined_room.room.live_kit_room,
&session.user_id().to_string(),
)
.trace_err()?,
@@ -4059,7 +4053,6 @@ async fn get_llm_api_token(
has_llm_closed_beta_feature_flag,
has_llm_subscription,
session.current_plan(&db).await?,
session.system_id.clone(),
&session.app_state.config,
)?;
response.send(proto::GetLlmTokenResponse { token })?;
@@ -4313,8 +4306,8 @@ async fn leave_room_for_session(session: &Session, connection_id: ConnectionId)
let room_id;
let canceled_calls_to_user_ids;
let livekit_room;
let delete_livekit_room;
let live_kit_room;
let delete_live_kit_room;
let room;
let channel;
@@ -4327,8 +4320,8 @@ async fn leave_room_for_session(session: &Session, connection_id: ConnectionId)
room_id = RoomId::from_proto(left_room.room.id);
canceled_calls_to_user_ids = mem::take(&mut left_room.canceled_calls_to_user_ids);
livekit_room = mem::take(&mut left_room.room.livekit_room);
delete_livekit_room = left_room.deleted;
live_kit_room = mem::take(&mut left_room.room.live_kit_room);
delete_live_kit_room = left_room.deleted;
room = mem::take(&mut left_room.room);
channel = mem::take(&mut left_room.channel);
@@ -4368,14 +4361,14 @@ async fn leave_room_for_session(session: &Session, connection_id: ConnectionId)
update_user_contacts(contact_user_id, session).await?;
}
if let Some(live_kit) = session.app_state.livekit_client.as_ref() {
if let Some(live_kit) = session.app_state.live_kit_client.as_ref() {
live_kit
.remove_participant(livekit_room.clone(), session.user_id().to_string())
.remove_participant(live_kit_room.clone(), session.user_id().to_string())
.await
.trace_err();
if delete_livekit_room {
live_kit.delete_room(livekit_room).await.trace_err();
if delete_live_kit_room {
live_kit.delete_room(live_kit_room).await.trace_err();
}
}

View File

@@ -1,6 +1,3 @@
// todo(windows): Actually run the tests
#![cfg(not(target_os = "windows"))]
use std::sync::Arc;
use call::Room;

View File

@@ -107,9 +107,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
});
assert!(project_b.read_with(cx_b, |project, cx| project.is_read_only(cx)));
assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx)));
cx_b.update(|cx_b| {
assert!(room_b.read_with(cx_b, |room, cx| !room.can_use_microphone(cx)));
});
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
assert!(room_b
.update(cx_b, |room, cx| room.share_microphone(cx))
.await
@@ -135,9 +133,7 @@ async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut Test
assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx)));
// B sees themselves as muted, and can unmute.
cx_b.update(|cx_b| {
assert!(room_b.read_with(cx_b, |room, cx| room.can_use_microphone(cx)));
});
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
room_b.read_with(cx_b, |room, _| assert!(room.is_muted()));
room_b.update(cx_b, |room, cx| room.toggle_mute(cx));
cx_a.run_until_parked();
@@ -230,9 +226,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
let room_b = cx_b
.read(ActiveCall::global)
.update(cx_b, |call, _| call.room().unwrap().clone());
cx_b.update(|cx_b| {
assert!(room_b.read_with(cx_b, |room, cx| !room.can_use_microphone(cx)));
});
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
// A tries to grant write access to B, but cannot because B has not
// yet signed the zed CLA.
@@ -250,9 +244,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
.unwrap_err();
cx_a.run_until_parked();
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
cx_b.update(|cx_b| {
assert!(room_b.read_with(cx_b, |room, cx| !room.can_use_microphone(cx)));
});
assert!(room_b.read_with(cx_b, |room, _| !room.can_use_microphone()));
// A tries to grant write access to B, but cannot because B has not
// yet signed the zed CLA.
@@ -270,9 +262,7 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
.unwrap();
cx_a.run_until_parked();
assert!(room_b.read_with(cx_b, |room, _| !room.can_share_projects()));
cx_b.update(|cx_b| {
assert!(room_b.read_with(cx_b, |room, cx| room.can_use_microphone(cx)));
});
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
// User B signs the zed CLA.
server
@@ -297,7 +287,5 @@ async fn test_channel_requires_zed_cla(cx_a: &mut TestAppContext, cx_b: &mut Tes
.unwrap();
cx_a.run_until_parked();
assert!(room_b.read_with(cx_b, |room, _| room.can_share_projects()));
cx_b.update(|cx_b| {
assert!(room_b.read_with(cx_b, |room, cx| room.can_use_microphone(cx)));
});
assert!(room_b.read_with(cx_b, |room, _| room.can_use_microphone()));
}

View File

@@ -9,9 +9,10 @@ use collab_ui::{
use editor::{Editor, ExcerptRange, MultiBuffer};
use gpui::{
point, BackgroundExecutor, BorrowAppContext, Context, Entity, SharedString, TestAppContext,
TestScreenCaptureSource, View, VisualContext, VisualTestContext,
View, VisualContext, VisualTestContext,
};
use language::Capability;
use live_kit_client::MacOSDisplay;
use project::WorktreeSettings;
use rpc::proto::PeerId;
use serde_json::json;
@@ -428,17 +429,17 @@ async fn test_basic_following(
);
// Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
let display = TestScreenCaptureSource::new();
let display = MacOSDisplay::new();
active_call_b
.update(cx_b, |call, cx| call.set_location(None, cx))
.await
.unwrap();
cx_b.set_screen_capture_sources(vec![display]);
active_call_b
.update(cx_b, |call, cx| {
call.room()
.unwrap()
.update(cx, |room, cx| room.share_screen(cx))
call.room().unwrap().update(cx, |room, cx| {
room.set_display_sources(vec![display.clone()]);
room.share_screen(cx)
})
})
.await
.unwrap();

View File

@@ -6,8 +6,7 @@ use crate::{
},
};
use anyhow::{anyhow, Result};
use assistant::{ContextStore, PromptBuilder, SlashCommandWorkingSet};
use assistant_tool::ToolWorkingSet;
use assistant::{ContextStore, PromptBuilder, SlashCommandWorkingSet, ToolWorkingSet};
use call::{room, ActiveCall, ParticipantLocation, Room};
use client::{User, RECEIVE_TIMEOUT};
use collections::{HashMap, HashSet};
@@ -16,7 +15,7 @@ use futures::{channel::mpsc, StreamExt as _};
use git::repository::GitFileStatus;
use gpui::{
px, size, AppContext, BackgroundExecutor, Model, Modifiers, MouseButton, MouseDownEvent,
TestAppContext, TestScreenCaptureSource, UpdateGlobal,
TestAppContext, UpdateGlobal,
};
use language::{
language_settings::{
@@ -25,6 +24,7 @@ use language::{
tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticEntry, FakeLspAdapter,
Language, LanguageConfig, LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
};
use live_kit_client::MacOSDisplay;
use lsp::LanguageServerId;
use parking_lot::Mutex;
use project::lsp_store::FormatTarget;
@@ -241,15 +241,15 @@ async fn test_basic_calls(
);
// User A shares their screen
let display = TestScreenCaptureSource::new();
let display = MacOSDisplay::new();
let events_b = active_call_events(cx_b);
let events_c = active_call_events(cx_c);
cx_a.set_screen_capture_sources(vec![display]);
active_call_a
.update(cx_a, |call, cx| {
call.room()
.unwrap()
.update(cx, |room, cx| room.share_screen(cx))
call.room().unwrap().update(cx, |room, cx| {
room.set_display_sources(vec![display.clone()]);
room.share_screen(cx)
})
})
.await
.unwrap();
@@ -328,7 +328,7 @@ async fn test_basic_calls(
// to automatically leave the room. User C leaves the room as well because
// nobody else is in there.
server
.test_livekit_server
.test_live_kit_server
.disconnect_client(client_b.user_id().unwrap().to_string())
.await;
executor.run_until_parked();
@@ -843,7 +843,7 @@ async fn test_client_disconnecting_from_room(
// User B gets disconnected from the LiveKit server, which causes it
// to automatically leave the room.
server
.test_livekit_server
.test_live_kit_server
.disconnect_client(client_b.user_id().unwrap().to_string())
.await;
executor.run_until_parked();
@@ -1942,7 +1942,7 @@ async fn test_mute_deafen(
room_a.read_with(cx_a, |room, _| assert!(!room.is_muted()));
room_b.read_with(cx_b, |room, _| assert!(!room.is_muted()));
// Users A and B are both unmuted.
// Users A and B are both muted.
assert_eq!(
participant_audio_state(&room_a, cx_a),
&[ParticipantAudioState {
@@ -2074,17 +2074,7 @@ async fn test_mute_deafen(
audio_tracks_playing: participant
.audio_tracks
.values()
.map({
#[cfg(target_os = "macos")]
{
|track| track.is_playing()
}
#[cfg(not(target_os = "macos"))]
{
|(track, _)| track.rtc_track().enabled()
}
})
.map(|track| track.is_playing())
.collect(),
})
.collect::<Vec<_>>()
@@ -6067,13 +6057,13 @@ async fn test_join_call_after_screen_was_shared(
assert_eq!(call_b.calling_user.github_login, "user_a");
// User A shares their screen
let display = TestScreenCaptureSource::new();
cx_a.set_screen_capture_sources(vec![display]);
let display = MacOSDisplay::new();
active_call_a
.update(cx_a, |call, cx| {
call.room()
.unwrap()
.update(cx, |room, cx| room.share_screen(cx))
call.room().unwrap().update(cx, |room, cx| {
room.set_display_sources(vec![display.clone()]);
room.share_screen(cx)
})
})
.await
.unwrap();
@@ -6496,8 +6486,8 @@ async fn test_context_collaboration_with_reconnect(
assert_eq!(project.collaborators().len(), 1);
});
cx_a.update(context_server::init);
cx_b.update(context_server::init);
cx_a.update(context_servers::init);
cx_b.update(context_servers::init);
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let context_store_a = cx_a
.update(|cx| {

View File

@@ -835,7 +835,7 @@ impl RandomizedTest for ProjectCollaborationTest {
.map_ok(|_| ())
.boxed(),
LspRequestKind::CodeAction => project
.code_actions(&buffer, offset..offset, None, cx)
.code_actions(&buffer, offset..offset, cx)
.map(|_| Ok(()))
.boxed(),
LspRequestKind::Definition => project

View File

@@ -1,7 +1,6 @@
use crate::tests::TestServer;
use call::ActiveCall;
use collections::HashSet;
use extension::ExtensionHostProxy;
use fs::{FakeFs, Fs as _};
use futures::StreamExt as _;
use gpui::{BackgroundExecutor, Context as _, SemanticVersion, TestAppContext, UpdateGlobal as _};
@@ -82,7 +81,6 @@ async fn test_sharing_an_ssh_remote_project(
http_client: remote_http_client,
node_runtime: node,
languages,
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
},
cx,
)
@@ -245,7 +243,6 @@ async fn test_ssh_collaboration_git_branches(
http_client: remote_http_client,
node_runtime: node,
languages,
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
},
cx,
)
@@ -403,7 +400,6 @@ async fn test_ssh_collaboration_formatting_with_prettier(
http_client: remote_http_client,
node_runtime: NodeRuntime::unavailable(),
languages,
extension_host_proxy: Arc::new(ExtensionHostProxy::new()),
},
cx,
)

View File

@@ -45,15 +45,9 @@ use std::{
};
use workspace::{Workspace, WorkspaceStore};
#[cfg(not(target_os = "macos"))]
use livekit_client::test::TestServer as LivekitTestServer;
#[cfg(target_os = "macos")]
use livekit_client_macos::TestServer as LivekitTestServer;
pub struct TestServer {
pub app_state: Arc<AppState>,
pub test_livekit_server: Arc<LivekitTestServer>,
pub test_live_kit_server: Arc<live_kit_client::TestServer>,
server: Arc<Server>,
next_github_user_id: i32,
connection_killers: Arc<Mutex<HashMap<PeerId, Arc<AtomicBool>>>>,
@@ -85,7 +79,7 @@ pub struct ContactsSummary {
impl TestServer {
pub async fn start(deterministic: BackgroundExecutor) -> Self {
static NEXT_LIVEKIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0);
static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0);
let use_postgres = env::var("USE_POSTGRES").ok();
let use_postgres = use_postgres.as_deref();
@@ -94,16 +88,16 @@ impl TestServer {
} else {
TestDb::sqlite(deterministic.clone())
};
let livekit_server_id = NEXT_LIVEKIT_SERVER_ID.fetch_add(1, SeqCst);
let livekit_server = LivekitTestServer::create(
format!("http://livekit.{}.test", livekit_server_id),
format!("devkey-{}", livekit_server_id),
format!("secret-{}", livekit_server_id),
let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst);
let live_kit_server = live_kit_client::TestServer::create(
format!("http://livekit.{}.test", live_kit_server_id),
format!("devkey-{}", live_kit_server_id),
format!("secret-{}", live_kit_server_id),
deterministic.clone(),
)
.unwrap();
let executor = Executor::Deterministic(deterministic.clone());
let app_state = Self::build_app_state(&test_db, &livekit_server, executor.clone()).await;
let app_state = Self::build_app_state(&test_db, &live_kit_server, executor.clone()).await;
let epoch = app_state
.db
.create_server(&app_state.config.zed_environment)
@@ -120,7 +114,7 @@ impl TestServer {
forbid_connections: Default::default(),
next_github_user_id: 0,
_test_db: test_db,
test_livekit_server: livekit_server,
test_live_kit_server: live_kit_server,
}
}
@@ -250,7 +244,6 @@ impl TestServer {
Principal::User(user),
ZedVersion(SemanticVersion::new(1, 0, 0)),
None,
None,
Some(connection_id_tx),
Executor::Deterministic(cx.background_executor().clone()),
))
@@ -506,13 +499,13 @@ impl TestServer {
pub async fn build_app_state(
test_db: &TestDb,
livekit_test_server: &LivekitTestServer,
live_kit_test_server: &live_kit_client::TestServer,
executor: Executor,
) -> Arc<AppState> {
Arc::new(AppState {
db: test_db.db().clone(),
llm_db: None,
livekit_client: Some(Arc::new(livekit_test_server.create_api_client())),
live_kit_client: Some(Arc::new(live_kit_test_server.create_api_client())),
blob_store_client: None,
stripe_client: None,
stripe_billing: None,
@@ -526,9 +519,9 @@ impl TestServer {
database_max_connections: 0,
api_token: "".into(),
invite_link_prefix: "".into(),
livekit_server: None,
livekit_key: None,
livekit_secret: None,
live_kit_server: None,
live_kit_key: None,
live_kit_secret: None,
llm_database_url: None,
llm_database_max_connections: None,
llm_database_migrations_path: None,
@@ -578,7 +571,7 @@ impl Deref for TestServer {
impl Drop for TestServer {
fn drop(&mut self) {
self.server.teardown();
self.test_livekit_server.teardown().unwrap();
self.test_live_kit_server.teardown().unwrap();
}
}

View File

@@ -58,11 +58,12 @@ settings.workspace = true
smallvec.workspace = true
story = { workspace = true, optional = true }
theme.workspace = true
time.workspace = true
time_format.workspace = true
time.workspace = true
title_bar.workspace = true
ui.workspace = true
util.workspace = true
vcs_menu.workspace = true
workspace.workspace = true
[dev-dependencies]

View File

@@ -474,10 +474,11 @@ impl CollabPanel {
project_id: project.id,
worktree_root_names: project.worktree_root_names.clone(),
host_user_id: participant.user.id,
is_last: projects.peek().is_none() && !participant.has_video_tracks(),
is_last: projects.peek().is_none()
&& participant.video_tracks.is_empty(),
});
}
if participant.has_video_tracks() {
if !participant.video_tracks.is_empty() {
self.entries.push(ListEntry::ParticipantScreen {
peer_id: Some(participant.peer_id),
is_last: true,

View File

@@ -33,6 +33,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
notification_panel::init(cx);
notifications::init(app_state, cx);
title_bar::init(cx);
vcs_menu::init(cx);
}
fn notification_window_options(

View File

@@ -11,7 +11,7 @@ use command_palette_hooks::{
};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global,
actions, Action, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Global,
ParentElement, Render, Styled, Task, UpdateGlobal, View, ViewContext, VisualContext, WeakView,
};
use picker::{Picker, PickerDelegate};
@@ -21,7 +21,9 @@ use settings::Settings;
use ui::{h_flex, prelude::*, v_flex, HighlightedLabel, KeyBinding, ListItem, ListItemSpacing};
use util::ResultExt;
use workspace::{ModalView, Workspace, WorkspaceSettings};
use zed_actions::{command_palette::Toggle, OpenZedUrl};
use zed_actions::OpenZedUrl;
actions!(command_palette, [Toggle]);
pub fn init(cx: &mut AppContext) {
client::init_settings(cx);

View File

@@ -1 +0,0 @@
../../LICENSE-GPL

View File

@@ -1,77 +0,0 @@
use std::sync::Arc;
use extension::{Extension, ExtensionContextServerProxy, ExtensionHostProxy, ProjectDelegate};
use gpui::{AppContext, Model};
use crate::{ContextServerFactoryRegistry, ServerCommand};
struct ExtensionProject {
worktree_ids: Vec<u64>,
}
impl ProjectDelegate for ExtensionProject {
fn worktree_ids(&self) -> Vec<u64> {
self.worktree_ids.clone()
}
}
pub fn init(cx: &mut AppContext) {
let proxy = ExtensionHostProxy::default_global(cx);
proxy.register_context_server_proxy(ContextServerFactoryRegistryProxy {
context_server_factory_registry: ContextServerFactoryRegistry::global(cx),
});
}
struct ContextServerFactoryRegistryProxy {
context_server_factory_registry: Model<ContextServerFactoryRegistry>,
}
impl ExtensionContextServerProxy for ContextServerFactoryRegistryProxy {
fn register_context_server(
&self,
extension: Arc<dyn Extension>,
id: Arc<str>,
cx: &mut AppContext,
) {
self.context_server_factory_registry
.update(cx, |registry, _| {
registry.register_server_factory(
id.clone(),
Arc::new({
move |project, cx| {
log::info!(
"loading command for context server {id} from extension {}",
extension.manifest().id
);
let id = id.clone();
let extension = extension.clone();
cx.spawn(|mut cx| async move {
let extension_project =
project.update(&mut cx, |project, cx| {
Arc::new(ExtensionProject {
worktree_ids: project
.visible_worktrees(cx)
.map(|worktree| worktree.read(cx).id().to_proto())
.collect(),
})
})?;
let command = extension
.context_server_command(id.clone(), extension_project)
.await?;
log::info!("loaded command for context server {id}: {command:?}");
Ok(ServerCommand {
path: command.command,
args: command.args,
env: Some(command.env.into_iter().collect()),
})
})
}
}),
)
});
}
}

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