Merge branch 'main' into push-qtxsrylymylz
8
.github/actions/build_docs/action.yml
vendored
|
@ -19,7 +19,7 @@ runs:
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
run: ./script/linux
|
run: ./script/linux
|
||||||
|
|
||||||
- name: Check for broken links
|
- name: Check for broken links (in MD)
|
||||||
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
|
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
|
||||||
with:
|
with:
|
||||||
args: --no-progress --exclude '^http' './docs/src/**/*'
|
args: --no-progress --exclude '^http' './docs/src/**/*'
|
||||||
|
@ -30,3 +30,9 @@ runs:
|
||||||
run: |
|
run: |
|
||||||
mkdir -p target/deploy
|
mkdir -p target/deploy
|
||||||
mdbook build ./docs --dest-dir=../target/deploy/docs/
|
mdbook build ./docs --dest-dir=../target/deploy/docs/
|
||||||
|
|
||||||
|
- name: Check for broken links (in HTML)
|
||||||
|
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
|
||||||
|
with:
|
||||||
|
args: --no-progress --exclude '^http' 'target/deploy/docs/'
|
||||||
|
fail: true
|
||||||
|
|
3
.github/workflows/ci.yml
vendored
|
@ -771,7 +771,8 @@ jobs:
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
name: Create a Windows installer
|
name: Create a Windows installer
|
||||||
runs-on: [self-hosted, Windows, X64]
|
runs-on: [self-hosted, Windows, X64]
|
||||||
if: false && (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))
|
if: contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||||
|
# if: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))
|
||||||
needs: [windows_tests]
|
needs: [windows_tests]
|
||||||
env:
|
env:
|
||||||
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
|
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
|
||||||
|
|
553
Cargo.lock
generated
31
Cargo.toml
|
@ -1,13 +1,13 @@
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"crates/activity_indicator",
|
|
||||||
"crates/acp_thread",
|
"crates/acp_thread",
|
||||||
"crates/agent_ui",
|
"crates/activity_indicator",
|
||||||
"crates/agent",
|
"crates/agent",
|
||||||
"crates/agent_settings",
|
|
||||||
"crates/ai_onboarding",
|
|
||||||
"crates/agent_servers",
|
"crates/agent_servers",
|
||||||
|
"crates/agent_settings",
|
||||||
|
"crates/agent_ui",
|
||||||
|
"crates/ai_onboarding",
|
||||||
"crates/anthropic",
|
"crates/anthropic",
|
||||||
"crates/askpass",
|
"crates/askpass",
|
||||||
"crates/assets",
|
"crates/assets",
|
||||||
|
@ -29,6 +29,9 @@ members = [
|
||||||
"crates/cli",
|
"crates/cli",
|
||||||
"crates/client",
|
"crates/client",
|
||||||
"crates/clock",
|
"crates/clock",
|
||||||
|
"crates/cloud_api_client",
|
||||||
|
"crates/cloud_api_types",
|
||||||
|
"crates/cloud_llm_client",
|
||||||
"crates/collab",
|
"crates/collab",
|
||||||
"crates/collab_ui",
|
"crates/collab_ui",
|
||||||
"crates/collections",
|
"crates/collections",
|
||||||
|
@ -48,8 +51,8 @@ members = [
|
||||||
"crates/diagnostics",
|
"crates/diagnostics",
|
||||||
"crates/docs_preprocessor",
|
"crates/docs_preprocessor",
|
||||||
"crates/editor",
|
"crates/editor",
|
||||||
"crates/explorer_command_injector",
|
|
||||||
"crates/eval",
|
"crates/eval",
|
||||||
|
"crates/explorer_command_injector",
|
||||||
"crates/extension",
|
"crates/extension",
|
||||||
"crates/extension_api",
|
"crates/extension_api",
|
||||||
"crates/extension_cli",
|
"crates/extension_cli",
|
||||||
|
@ -70,7 +73,6 @@ members = [
|
||||||
"crates/gpui",
|
"crates/gpui",
|
||||||
"crates/gpui_macros",
|
"crates/gpui_macros",
|
||||||
"crates/gpui_tokio",
|
"crates/gpui_tokio",
|
||||||
|
|
||||||
"crates/html_to_markdown",
|
"crates/html_to_markdown",
|
||||||
"crates/http_client",
|
"crates/http_client",
|
||||||
"crates/http_client_tls",
|
"crates/http_client_tls",
|
||||||
|
@ -99,7 +101,6 @@ members = [
|
||||||
"crates/markdown_preview",
|
"crates/markdown_preview",
|
||||||
"crates/media",
|
"crates/media",
|
||||||
"crates/menu",
|
"crates/menu",
|
||||||
"crates/svg_preview",
|
|
||||||
"crates/migrator",
|
"crates/migrator",
|
||||||
"crates/mistral",
|
"crates/mistral",
|
||||||
"crates/multi_buffer",
|
"crates/multi_buffer",
|
||||||
|
@ -140,6 +141,7 @@ members = [
|
||||||
"crates/semantic_version",
|
"crates/semantic_version",
|
||||||
"crates/session",
|
"crates/session",
|
||||||
"crates/settings",
|
"crates/settings",
|
||||||
|
"crates/settings_profile_selector",
|
||||||
"crates/settings_ui",
|
"crates/settings_ui",
|
||||||
"crates/snippet",
|
"crates/snippet",
|
||||||
"crates/snippet_provider",
|
"crates/snippet_provider",
|
||||||
|
@ -152,6 +154,7 @@ members = [
|
||||||
"crates/sum_tree",
|
"crates/sum_tree",
|
||||||
"crates/supermaven",
|
"crates/supermaven",
|
||||||
"crates/supermaven_api",
|
"crates/supermaven_api",
|
||||||
|
"crates/svg_preview",
|
||||||
"crates/tab_switcher",
|
"crates/tab_switcher",
|
||||||
"crates/task",
|
"crates/task",
|
||||||
"crates/tasks_ui",
|
"crates/tasks_ui",
|
||||||
|
@ -186,6 +189,7 @@ members = [
|
||||||
"crates/zed",
|
"crates/zed",
|
||||||
"crates/zed_actions",
|
"crates/zed_actions",
|
||||||
"crates/zeta",
|
"crates/zeta",
|
||||||
|
"crates/zeta_cli",
|
||||||
"crates/zlog",
|
"crates/zlog",
|
||||||
"crates/zlog_settings",
|
"crates/zlog_settings",
|
||||||
|
|
||||||
|
@ -251,6 +255,9 @@ channel = { path = "crates/channel" }
|
||||||
cli = { path = "crates/cli" }
|
cli = { path = "crates/cli" }
|
||||||
client = { path = "crates/client" }
|
client = { path = "crates/client" }
|
||||||
clock = { path = "crates/clock" }
|
clock = { path = "crates/clock" }
|
||||||
|
cloud_api_client = { path = "crates/cloud_api_client" }
|
||||||
|
cloud_api_types = { path = "crates/cloud_api_types" }
|
||||||
|
cloud_llm_client = { path = "crates/cloud_llm_client" }
|
||||||
collab = { path = "crates/collab" }
|
collab = { path = "crates/collab" }
|
||||||
collab_ui = { path = "crates/collab_ui" }
|
collab_ui = { path = "crates/collab_ui" }
|
||||||
collections = { path = "crates/collections" }
|
collections = { path = "crates/collections" }
|
||||||
|
@ -337,6 +344,7 @@ picker = { path = "crates/picker" }
|
||||||
plugin = { path = "crates/plugin" }
|
plugin = { path = "crates/plugin" }
|
||||||
plugin_macros = { path = "crates/plugin_macros" }
|
plugin_macros = { path = "crates/plugin_macros" }
|
||||||
prettier = { path = "crates/prettier" }
|
prettier = { path = "crates/prettier" }
|
||||||
|
settings_profile_selector = { path = "crates/settings_profile_selector" }
|
||||||
project = { path = "crates/project" }
|
project = { path = "crates/project" }
|
||||||
project_panel = { path = "crates/project_panel" }
|
project_panel = { path = "crates/project_panel" }
|
||||||
project_symbols = { path = "crates/project_symbols" }
|
project_symbols = { path = "crates/project_symbols" }
|
||||||
|
@ -645,7 +653,6 @@ which = "6.0.0"
|
||||||
windows-core = "0.61"
|
windows-core = "0.61"
|
||||||
wit-component = "0.221"
|
wit-component = "0.221"
|
||||||
workspace-hack = "0.1.0"
|
workspace-hack = "0.1.0"
|
||||||
zed_llm_client = "= 0.8.6"
|
|
||||||
zstd = "0.11"
|
zstd = "0.11"
|
||||||
|
|
||||||
[workspace.dependencies.async-stripe]
|
[workspace.dependencies.async-stripe]
|
||||||
|
@ -672,14 +679,16 @@ features = [
|
||||||
"UI_ViewManagement",
|
"UI_ViewManagement",
|
||||||
"Wdk_System_SystemServices",
|
"Wdk_System_SystemServices",
|
||||||
"Win32_Globalization",
|
"Win32_Globalization",
|
||||||
"Win32_Graphics_Direct2D",
|
"Win32_Graphics_Direct3D",
|
||||||
"Win32_Graphics_Direct2D_Common",
|
"Win32_Graphics_Direct3D11",
|
||||||
|
"Win32_Graphics_Direct3D_Fxc",
|
||||||
|
"Win32_Graphics_DirectComposition",
|
||||||
"Win32_Graphics_DirectWrite",
|
"Win32_Graphics_DirectWrite",
|
||||||
"Win32_Graphics_Dwm",
|
"Win32_Graphics_Dwm",
|
||||||
|
"Win32_Graphics_Dxgi",
|
||||||
"Win32_Graphics_Dxgi_Common",
|
"Win32_Graphics_Dxgi_Common",
|
||||||
"Win32_Graphics_Gdi",
|
"Win32_Graphics_Gdi",
|
||||||
"Win32_Graphics_Imaging",
|
"Win32_Graphics_Imaging",
|
||||||
"Win32_Graphics_Imaging_D2D",
|
|
||||||
"Win32_Networking_WinSock",
|
"Win32_Networking_WinSock",
|
||||||
"Win32_Security",
|
"Win32_Security",
|
||||||
"Win32_Security_Credentials",
|
"Win32_Security_Credentials",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# Zed
|
# Zed
|
||||||
|
|
||||||
|
[](https://zed.dev)
|
||||||
[](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
|
[](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
|
||||||
|
|
||||||
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
|
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
|
||||||
|
|
8
assets/badge/v0.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"label": "",
|
||||||
|
"message": "Zed",
|
||||||
|
"logoSvg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 96 96\"><rect width=\"96\" height=\"96\" fill=\"#000\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M9 6C7.34315 6 6 7.34315 6 9V75H0V9C0 4.02944 4.02944 0 9 0H89.3787C93.3878 0 95.3955 4.84715 92.5607 7.68198L43.0551 57.1875H57V51H63V58.6875C63 61.1728 60.9853 63.1875 58.5 63.1875H37.0551L26.7426 73.5H73.5V36H79.5V73.5C79.5 76.8137 76.8137 79.5 73.5 79.5H20.7426L10.2426 90H87C88.6569 90 90 88.6569 90 87V21H96V87C96 91.9706 91.9706 96 87 96H6.62132C2.61224 96 0.604504 91.1529 3.43934 88.318L52.7574 39H39V45H33V37.5C33 35.0147 35.0147 33 37.5 33H58.7574L69.2574 22.5H22.5V60H16.5V22.5C16.5 19.1863 19.1863 16.5 22.5 16.5H75.2574L85.7574 6H9Z\" fill=\"#fff\"/></svg>",
|
||||||
|
"logoWidth": 16,
|
||||||
|
"labelColor": "black",
|
||||||
|
"color": "white"
|
||||||
|
}
|
3
assets/icons/editor_atom.svg
Normal file
After Width: | Height: | Size: 6.3 KiB |
9
assets/icons/editor_cursor.svg
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path opacity="0.6" d="M3.5 11V5.5L8.5 8L3.5 11Z" fill="black"/>
|
||||||
|
<path opacity="0.4" d="M8.5 14L3.5 11L8.5 8V14Z" fill="black"/>
|
||||||
|
<path opacity="0.6" d="M8.5 5.5H3.5L8.5 2.5L8.5 5.5Z" fill="black"/>
|
||||||
|
<path opacity="0.8" d="M8.5 5.5V2.5L13.5 5.5H8.5Z" fill="black"/>
|
||||||
|
<path opacity="0.2" d="M13.5 11L8.5 14L11 9.5L13.5 11Z" fill="black"/>
|
||||||
|
<path opacity="0.5" d="M13.5 11L11 9.5L13.5 5V11Z" fill="black"/>
|
||||||
|
<path d="M3.5 11V5L8.5 2.11325L13.5 5V11L8.5 13.8868L3.5 11Z" stroke="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 583 B |
10
assets/icons/editor_emacs.svg
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_2716_663)">
|
||||||
|
<path d="M8.47552 2.45453C11.5167 2.45457 13.9814 4.94501 13.9814 8.01623C13.9814 11.0875 11.5167 13.578 8.47552 13.5781C5.43427 13.5781 2.96948 11.0875 2.96948 8.01623C2.9695 4.94498 5.43429 2.45453 8.47552 2.45453ZM10.8795 4.70348C10.7605 4.16887 10.1328 3.85468 9.53627 3.96342C8.97622 4.06552 7.62871 4.45681 7.62057 4.45916C9.29414 4.44469 9.57429 4.4726 9.69939 4.64751C9.77324 4.7508 9.66576 4.89248 9.21944 4.96538C8.73515 5.04447 7.73014 5.13958 7.72343 5.14022C6.75441 5.19776 6.07177 5.20168 5.86705 5.63512C5.73334 5.91827 6.00968 6.16857 6.13082 6.32527C6.64271 6.89455 7.38215 7.20158 7.85809 7.42767C8.03716 7.51274 8.56257 7.67345 8.56257 7.67345C7.01855 7.58853 5.90474 8.06267 5.2514 8.60855C4.51246 9.29204 4.83937 10.1067 6.35327 10.6084C7.24742 10.9047 7.69094 11.0439 9.02473 10.9238C9.81031 10.8815 9.9342 10.9068 9.94203 10.9712C9.95275 11.062 9.06932 11.2874 8.82812 11.357C8.21455 11.534 6.60645 11.8913 6.59758 11.8932C6.60115 11.8935 7.06249 11.9257 7.65531 11.8735C7.89632 11.8522 8.81142 11.7624 9.49557 11.6123C9.49557 11.6123 10.3297 11.4338 10.7759 11.2693C11.2429 11.0973 11.497 10.9512 11.6113 10.7443C11.6063 10.7019 11.6465 10.5516 11.4313 10.4613C10.8807 10.2304 10.2423 10.2721 8.9789 10.2453C7.57789 10.1972 7.11184 9.9626 6.86356 9.77373C6.62548 9.58212 6.74518 9.05204 7.76528 8.5851C8.27917 8.33646 10.2935 7.87759 10.2935 7.87759C9.61511 7.54227 8.35014 6.95284 8.09005 6.82552C7.86199 6.71388 7.49701 6.54572 7.4179 6.34233C7.32824 6.14709 7.6297 5.97888 7.79813 5.9307C8.34057 5.77424 9.10635 5.67701 9.8033 5.66609C10.1536 5.66061 10.2105 5.63806 10.2105 5.63806C10.6939 5.55787 11.0121 5.22722 10.8795 4.70348Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2716_663">
|
||||||
|
<rect width="12" height="12" fill="white" transform="translate(2.5 2)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
3
assets/icons/editor_jet_brains.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3.6725 13.9985C3.36161 13.9982 3.06354 13.8746 2.84371 13.6548C2.62388 13.435 2.50026 13.1369 2.5 12.826V7.494C2.5 6.8325 2.7675 6.185 3.2365 5.7165L6.219 2.736C6.45192 2.50247 6.72867 2.31724 7.03335 2.19094C7.33804 2.06464 7.66467 1.99975 7.9945 2H13.3275C13.6384 2.00027 13.9365 2.12388 14.1563 2.34371C14.3761 2.56354 14.4997 2.86162 14.5 3.1725V8.5045C14.4983 9.17074 14.2336 9.80936 13.7635 10.2815L10.781 13.264C10.5477 13.4976 10.2706 13.6829 9.96561 13.8092C9.66059 13.9355 9.33364 14.0003 9.0035 14V13.9985H3.6725ZM8.157 10.5715H5.243V11.257H8.157V10.5715ZM4.4815 5.257H11.243V12.0165L13.3715 9.888C13.7373 9.52036 13.9433 9.02316 13.9445 8.5045V3.1725C13.9445 2.8335 13.6685 2.5555 13.3275 2.5555H7.9945C7.73753 2.55499 7.483 2.6053 7.24556 2.70356C7.00813 2.80181 6.79246 2.94606 6.611 3.128L4.4815 5.257ZM4.3855 5.353L3.628 6.11C3.26258 6.47809 3.0569 6.97533 3.0555 7.494V12.826C3.0555 13.165 3.3315 13.443 3.6725 13.443H9.0055C9.26249 13.4434 9.51701 13.3929 9.75445 13.2946C9.99188 13.1963 10.2075 13.052 10.389 12.87L11.145 12.1145H4.3855V5.353Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
5
assets/icons/editor_sublime.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0945 8.01611C13.0945 7.87619 12.9911 7.79551 12.8642 7.8356L4.13456 10.6038C4.00742 10.6441 3.90427 10.7904 3.90427 10.9301V13.7593C3.90427 13.8992 4.00742 13.9801 4.13456 13.9398L12.8642 11.1719C12.9911 11.1315 13.0945 10.9852 13.0945 10.8453V8.01611Z" fill="black"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.90427 7.92597C3.90427 8.06588 4.00742 8.21218 4.13456 8.25252L12.8655 11.0209C12.9926 11.0613 13.0958 10.9803 13.0958 10.8407V8.01124C13.0958 7.87158 12.9926 7.72529 12.8655 7.68494L4.13456 4.91652C4.00742 4.87618 3.90427 4.95686 3.90427 5.09677V7.92597Z" fill="black"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0945 2.20248C13.0945 2.06256 12.9911 1.98163 12.8642 2.02197L4.13456 4.78988C4.00742 4.83022 3.90427 4.97652 3.90427 5.11644V7.94563C3.90427 8.08554 4.00742 8.16622 4.13456 8.12614L12.8642 5.35797C12.9911 5.31763 13.0945 5.17133 13.0945 5.03167V2.20248Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1 KiB |
3
assets/icons/editor_vs_code.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0094 13.9181C11.1984 13.9917 11.4139 13.987 11.6047 13.8952L14.0753 12.7064C14.3349 12.5814 14.5 12.3187 14.5 12.0305V3.9696C14.5 3.68136 14.3349 3.41862 14.0753 3.2937L11.6047 2.10485C11.3543 1.98438 11.0614 2.01389 10.8416 2.17363C10.8102 2.19645 10.7803 2.22193 10.7523 2.25001L6.02261 6.56498L3.96246 5.00115C3.77068 4.85558 3.50244 4.86751 3.32432 5.02953L2.66356 5.63059C2.44569 5.82877 2.44544 6.17152 2.66302 6.37004L4.44965 8.00001L2.66302 9.62998C2.44544 9.82849 2.44569 10.1713 2.66356 10.3694L3.32432 10.9705C3.50244 11.1325 3.77068 11.1444 3.96246 10.9989L6.02261 9.43504L10.7523 13.75C10.8271 13.8249 10.915 13.8812 11.0094 13.9181ZM11.5018 5.27587L7.91309 8.00001L11.5018 10.7241V5.27587Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 876 B |
4
assets/icons/shield_check.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M13.0001 8.62505C13.0001 11.75 10.8126 13.3125 8.21266 14.2187C8.07651 14.2648 7.92862 14.2626 7.79392 14.2125C5.18771 13.3125 3.00024 11.75 3.00024 8.62505V4.25012C3.00024 4.08436 3.06609 3.92539 3.1833 3.80818C3.30051 3.69098 3.45948 3.62513 3.62523 3.62513C4.87521 3.62513 6.43769 2.87514 7.52517 1.92516C7.65758 1.81203 7.82601 1.74988 8.00016 1.74988C8.17431 1.74988 8.34275 1.81203 8.47515 1.92516C9.56889 2.88139 11.1251 3.62513 12.3751 3.62513C12.5408 3.62513 12.6998 3.69098 12.817 3.80818C12.9342 3.92539 13.0001 4.08436 13.0001 4.25012V8.62505Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M6 8.00002L7.33333 9.33335L10 6.66669" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 883 B |
|
@ -232,7 +232,7 @@
|
||||||
"ctrl-n": "agent::NewThread",
|
"ctrl-n": "agent::NewThread",
|
||||||
"ctrl-alt-n": "agent::NewTextThread",
|
"ctrl-alt-n": "agent::NewTextThread",
|
||||||
"ctrl-shift-h": "agent::OpenHistory",
|
"ctrl-shift-h": "agent::OpenHistory",
|
||||||
"ctrl-alt-c": "agent::OpenConfiguration",
|
"ctrl-alt-c": "agent::OpenSettings",
|
||||||
"ctrl-alt-p": "agent::OpenRulesLibrary",
|
"ctrl-alt-p": "agent::OpenRulesLibrary",
|
||||||
"ctrl-i": "agent::ToggleProfileSelector",
|
"ctrl-i": "agent::ToggleProfileSelector",
|
||||||
"ctrl-alt-/": "agent::ToggleModelSelector",
|
"ctrl-alt-/": "agent::ToggleModelSelector",
|
||||||
|
@ -495,7 +495,7 @@
|
||||||
"shift-f12": "editor::GoToImplementation",
|
"shift-f12": "editor::GoToImplementation",
|
||||||
"alt-ctrl-f12": "editor::GoToTypeDefinitionSplit",
|
"alt-ctrl-f12": "editor::GoToTypeDefinitionSplit",
|
||||||
"alt-shift-f12": "editor::FindAllReferences",
|
"alt-shift-f12": "editor::FindAllReferences",
|
||||||
"ctrl-m": "editor::MoveToEnclosingBracket",
|
"ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains
|
||||||
"ctrl-|": "editor::MoveToEnclosingBracket",
|
"ctrl-|": "editor::MoveToEnclosingBracket",
|
||||||
"ctrl-{": "editor::Fold",
|
"ctrl-{": "editor::Fold",
|
||||||
"ctrl-}": "editor::UnfoldLines",
|
"ctrl-}": "editor::UnfoldLines",
|
||||||
|
@ -598,6 +598,7 @@
|
||||||
"ctrl-shift-t": "pane::ReopenClosedItem",
|
"ctrl-shift-t": "pane::ReopenClosedItem",
|
||||||
"ctrl-k ctrl-s": "zed::OpenKeymapEditor",
|
"ctrl-k ctrl-s": "zed::OpenKeymapEditor",
|
||||||
"ctrl-k ctrl-t": "theme_selector::Toggle",
|
"ctrl-k ctrl-t": "theme_selector::Toggle",
|
||||||
|
"ctrl-alt-super-p": "settings_profile_selector::Toggle",
|
||||||
"ctrl-t": "project_symbols::Toggle",
|
"ctrl-t": "project_symbols::Toggle",
|
||||||
"ctrl-p": "file_finder::Toggle",
|
"ctrl-p": "file_finder::Toggle",
|
||||||
"ctrl-tab": "tab_switcher::Toggle",
|
"ctrl-tab": "tab_switcher::Toggle",
|
||||||
|
@ -1167,5 +1168,14 @@
|
||||||
"up": "menu::SelectPrevious",
|
"up": "menu::SelectPrevious",
|
||||||
"down": "menu::SelectNext"
|
"down": "menu::SelectNext"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Onboarding",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"ctrl-1": "onboarding::ActivateBasicsPage",
|
||||||
|
"ctrl-2": "onboarding::ActivateEditingPage",
|
||||||
|
"ctrl-3": "onboarding::ActivateAISetupPage"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -272,7 +272,7 @@
|
||||||
"cmd-n": "agent::NewThread",
|
"cmd-n": "agent::NewThread",
|
||||||
"cmd-alt-n": "agent::NewTextThread",
|
"cmd-alt-n": "agent::NewTextThread",
|
||||||
"cmd-shift-h": "agent::OpenHistory",
|
"cmd-shift-h": "agent::OpenHistory",
|
||||||
"cmd-alt-c": "agent::OpenConfiguration",
|
"cmd-alt-c": "agent::OpenSettings",
|
||||||
"cmd-alt-p": "agent::OpenRulesLibrary",
|
"cmd-alt-p": "agent::OpenRulesLibrary",
|
||||||
"cmd-i": "agent::ToggleProfileSelector",
|
"cmd-i": "agent::ToggleProfileSelector",
|
||||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||||
|
@ -549,7 +549,7 @@
|
||||||
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
|
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
|
||||||
"alt-shift-f12": "editor::FindAllReferences",
|
"alt-shift-f12": "editor::FindAllReferences",
|
||||||
"cmd-|": "editor::MoveToEnclosingBracket",
|
"cmd-|": "editor::MoveToEnclosingBracket",
|
||||||
"ctrl-m": "editor::MoveToEnclosingBracket",
|
"ctrl-m": "editor::MoveToEnclosingBracket", // From Jetbrains
|
||||||
"alt-cmd-[": "editor::Fold",
|
"alt-cmd-[": "editor::Fold",
|
||||||
"alt-cmd-]": "editor::UnfoldLines",
|
"alt-cmd-]": "editor::UnfoldLines",
|
||||||
"cmd-k cmd-l": "editor::ToggleFold",
|
"cmd-k cmd-l": "editor::ToggleFold",
|
||||||
|
@ -665,6 +665,7 @@
|
||||||
"cmd-shift-t": "pane::ReopenClosedItem",
|
"cmd-shift-t": "pane::ReopenClosedItem",
|
||||||
"cmd-k cmd-s": "zed::OpenKeymapEditor",
|
"cmd-k cmd-s": "zed::OpenKeymapEditor",
|
||||||
"cmd-k cmd-t": "theme_selector::Toggle",
|
"cmd-k cmd-t": "theme_selector::Toggle",
|
||||||
|
"ctrl-alt-cmd-p": "settings_profile_selector::Toggle",
|
||||||
"cmd-t": "project_symbols::Toggle",
|
"cmd-t": "project_symbols::Toggle",
|
||||||
"cmd-p": "file_finder::Toggle",
|
"cmd-p": "file_finder::Toggle",
|
||||||
"ctrl-tab": "tab_switcher::Toggle",
|
"ctrl-tab": "tab_switcher::Toggle",
|
||||||
|
@ -1269,5 +1270,14 @@
|
||||||
"up": "menu::SelectPrevious",
|
"up": "menu::SelectPrevious",
|
||||||
"down": "menu::SelectNext"
|
"down": "menu::SelectNext"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Onboarding",
|
||||||
|
"use_key_equivalents": true,
|
||||||
|
"bindings": {
|
||||||
|
"cmd-1": "onboarding::ActivateBasicsPage",
|
||||||
|
"cmd-2": "onboarding::ActivateEditingPage",
|
||||||
|
"cmd-3": "onboarding::ActivateAISetupPage"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
"ctrl-shift-i": "agent::ToggleFocus",
|
"ctrl-shift-i": "agent::ToggleFocus",
|
||||||
"ctrl-l": "agent::ToggleFocus",
|
"ctrl-l": "agent::ToggleFocus",
|
||||||
"ctrl-shift-l": "agent::ToggleFocus",
|
"ctrl-shift-l": "agent::ToggleFocus",
|
||||||
"ctrl-shift-j": "agent::OpenConfiguration"
|
"ctrl-shift-j": "agent::OpenSettings"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -95,7 +95,7 @@
|
||||||
"ctrl-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
|
"ctrl-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||||
"alt-shift-f10": "task::Spawn",
|
"alt-shift-f10": "task::Spawn",
|
||||||
"ctrl-e": "file_finder::Toggle",
|
"ctrl-e": "file_finder::Toggle",
|
||||||
"ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
// "ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
||||||
"ctrl-shift-n": "file_finder::Toggle",
|
"ctrl-shift-n": "file_finder::Toggle",
|
||||||
"ctrl-shift-a": "command_palette::Toggle",
|
"ctrl-shift-a": "command_palette::Toggle",
|
||||||
"shift shift": "command_palette::Toggle",
|
"shift shift": "command_palette::Toggle",
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
"cmd-shift-i": "agent::ToggleFocus",
|
"cmd-shift-i": "agent::ToggleFocus",
|
||||||
"cmd-l": "agent::ToggleFocus",
|
"cmd-l": "agent::ToggleFocus",
|
||||||
"cmd-shift-l": "agent::ToggleFocus",
|
"cmd-shift-l": "agent::ToggleFocus",
|
||||||
"cmd-shift-j": "agent::OpenConfiguration"
|
"cmd-shift-j": "agent::OpenSettings"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -97,7 +97,7 @@
|
||||||
"cmd-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
|
"cmd-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||||
"ctrl-alt-r": "task::Spawn",
|
"ctrl-alt-r": "task::Spawn",
|
||||||
"cmd-e": "file_finder::Toggle",
|
"cmd-e": "file_finder::Toggle",
|
||||||
"cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
// "cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
||||||
"cmd-shift-o": "file_finder::Toggle",
|
"cmd-shift-o": "file_finder::Toggle",
|
||||||
"cmd-shift-a": "command_palette::Toggle",
|
"cmd-shift-a": "command_palette::Toggle",
|
||||||
"shift shift": "command_palette::Toggle",
|
"shift shift": "command_palette::Toggle",
|
||||||
|
|
|
@ -1877,5 +1877,25 @@
|
||||||
"save_breakpoints": true,
|
"save_breakpoints": true,
|
||||||
"dock": "bottom",
|
"dock": "bottom",
|
||||||
"button": true
|
"button": true
|
||||||
}
|
},
|
||||||
|
// Configures any number of settings profiles that are temporarily applied on
|
||||||
|
// top of your existing user settings when selected from
|
||||||
|
// `settings profile selector: toggle`.
|
||||||
|
// Examples:
|
||||||
|
// "profiles": {
|
||||||
|
// "Presenting": {
|
||||||
|
// "agent_font_size": 20.0,
|
||||||
|
// "buffer_font_size": 20.0,
|
||||||
|
// "theme": "One Light",
|
||||||
|
// "ui_font_size": 20.0
|
||||||
|
// },
|
||||||
|
// "Python (ty)": {
|
||||||
|
// "languages": {
|
||||||
|
// "Python": {
|
||||||
|
// "language_servers": ["ty"]
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
"profiles": []
|
||||||
}
|
}
|
||||||
|
|
|
@ -580,6 +580,9 @@ pub struct AcpThread {
|
||||||
pub enum AcpThreadEvent {
|
pub enum AcpThreadEvent {
|
||||||
NewEntry,
|
NewEntry,
|
||||||
EntryUpdated(usize),
|
EntryUpdated(usize),
|
||||||
|
ToolAuthorizationRequired,
|
||||||
|
Stopped,
|
||||||
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
||||||
|
@ -676,6 +679,18 @@ impl AcpThread {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn used_tools_since_last_user_message(&self) -> bool {
|
||||||
|
for entry in self.entries.iter().rev() {
|
||||||
|
match entry {
|
||||||
|
AgentThreadEntry::UserMessage(..) => return false,
|
||||||
|
AgentThreadEntry::AssistantMessage(..) => continue,
|
||||||
|
AgentThreadEntry::ToolCall(..) => return true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
pub fn handle_session_update(
|
pub fn handle_session_update(
|
||||||
&mut self,
|
&mut self,
|
||||||
update: acp::SessionUpdate,
|
update: acp::SessionUpdate,
|
||||||
|
@ -879,6 +894,7 @@ impl AcpThread {
|
||||||
};
|
};
|
||||||
|
|
||||||
self.upsert_tool_call_inner(tool_call, status, cx);
|
self.upsert_tool_call_inner(tool_call, status, cx);
|
||||||
|
cx.emit(AcpThreadEvent::ToolAuthorizationRequired);
|
||||||
rx
|
rx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1018,12 +1034,18 @@ impl AcpThread {
|
||||||
.log_err();
|
.log_err();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
async move {
|
cx.spawn(async move |this, cx| match rx.await {
|
||||||
match rx.await {
|
Ok(Err(e)) => {
|
||||||
Ok(Err(e)) => Err(e)?,
|
this.update(cx, |_, cx| cx.emit(AcpThreadEvent::Error))
|
||||||
_ => Ok(()),
|
.log_err();
|
||||||
|
Err(e)?
|
||||||
}
|
}
|
||||||
}
|
_ => {
|
||||||
|
this.update(cx, |_, cx| cx.emit(AcpThreadEvent::Stopped))
|
||||||
|
.log_err();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
})
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1597,6 +1619,7 @@ mod tests {
|
||||||
name: "test",
|
name: "test",
|
||||||
connection,
|
connection,
|
||||||
child_status: io_task,
|
child_status: io_task,
|
||||||
|
current_thread: thread_rc,
|
||||||
};
|
};
|
||||||
|
|
||||||
AcpThread::new(
|
AcpThread::new(
|
||||||
|
|
|
@ -7,6 +7,7 @@ use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use std::{cell::RefCell, error::Error, fmt, path::Path, rc::Rc};
|
use std::{cell::RefCell, error::Error, fmt, path::Path, rc::Rc};
|
||||||
use ui::App;
|
use ui::App;
|
||||||
|
use util::ResultExt as _;
|
||||||
|
|
||||||
use crate::{AcpThread, AgentConnection};
|
use crate::{AcpThread, AgentConnection};
|
||||||
|
|
||||||
|
@ -46,7 +47,7 @@ impl acp_old::Client for OldAcpClientDelegate {
|
||||||
thread.push_assistant_content_block(thought.into(), true, cx)
|
thread.push_assistant_content_block(thought.into(), true, cx)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.ok();
|
.log_err();
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -364,6 +365,7 @@ pub struct OldAcpAgentConnection {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub connection: acp_old::AgentConnection,
|
pub connection: acp_old::AgentConnection,
|
||||||
pub child_status: Task<Result<()>>,
|
pub child_status: Task<Result<()>>,
|
||||||
|
pub current_thread: Rc<RefCell<WeakEntity<AcpThread>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentConnection for OldAcpAgentConnection {
|
impl AgentConnection for OldAcpAgentConnection {
|
||||||
|
@ -383,6 +385,7 @@ impl AgentConnection for OldAcpAgentConnection {
|
||||||
}
|
}
|
||||||
.into_any(),
|
.into_any(),
|
||||||
);
|
);
|
||||||
|
let current_thread = self.current_thread.clone();
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
let result = task.await?;
|
let result = task.await?;
|
||||||
let result = acp_old::InitializeParams::response_from_any(result)?;
|
let result = acp_old::InitializeParams::response_from_any(result)?;
|
||||||
|
@ -396,6 +399,7 @@ impl AgentConnection for OldAcpAgentConnection {
|
||||||
let session_id = acp::SessionId("acp-old-no-id".into());
|
let session_id = acp::SessionId("acp-old-no-id".into());
|
||||||
AcpThread::new(self.clone(), project, session_id, cx)
|
AcpThread::new(self.clone(), project, session_id, cx)
|
||||||
});
|
});
|
||||||
|
current_thread.replace(thread.downgrade());
|
||||||
thread
|
thread
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -25,6 +25,7 @@ assistant_context.workspace = true
|
||||||
assistant_tool.workspace = true
|
assistant_tool.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
|
cloud_llm_client.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
component.workspace = true
|
component.workspace = true
|
||||||
context_server.workspace = true
|
context_server.workspace = true
|
||||||
|
@ -35,9 +36,9 @@ futures.workspace = true
|
||||||
git.workspace = true
|
git.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
heed.workspace = true
|
heed.workspace = true
|
||||||
|
http_client.workspace = true
|
||||||
icons.workspace = true
|
icons.workspace = true
|
||||||
indoc.workspace = true
|
indoc.workspace = true
|
||||||
http_client.workspace = true
|
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
language_model.workspace = true
|
language_model.workspace = true
|
||||||
|
@ -46,7 +47,6 @@ paths.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
prompt_store.workspace = true
|
prompt_store.workspace = true
|
||||||
proto.workspace = true
|
|
||||||
ref-cast.workspace = true
|
ref-cast.workspace = true
|
||||||
rope.workspace = true
|
rope.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
|
@ -63,7 +63,6 @@ time.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
zed_llm_client.workspace = true
|
|
||||||
zstd.workspace = true
|
zstd.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
@ -13,6 +13,7 @@ use anyhow::{Result, anyhow};
|
||||||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use client::{ModelRequestUsage, RequestUsage};
|
use client::{ModelRequestUsage, RequestUsage};
|
||||||
|
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use feature_flags::{self, FeatureFlagAppExt};
|
use feature_flags::{self, FeatureFlagAppExt};
|
||||||
use futures::{FutureExt, StreamExt as _, future::Shared};
|
use futures::{FutureExt, StreamExt as _, future::Shared};
|
||||||
|
@ -36,7 +37,6 @@ use project::{
|
||||||
git_store::{GitStore, GitStoreCheckpoint, RepositoryState},
|
git_store::{GitStore, GitStoreCheckpoint, RepositoryState},
|
||||||
};
|
};
|
||||||
use prompt_store::{ModelContext, PromptBuilder};
|
use prompt_store::{ModelContext, PromptBuilder};
|
||||||
use proto::Plan;
|
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
@ -49,7 +49,6 @@ use std::{
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use util::{ResultExt as _, post_inc};
|
use util::{ResultExt as _, post_inc};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
|
|
||||||
|
|
||||||
const MAX_RETRY_ATTEMPTS: u8 = 4;
|
const MAX_RETRY_ATTEMPTS: u8 = 4;
|
||||||
const BASE_RETRY_DELAY: Duration = Duration::from_secs(5);
|
const BASE_RETRY_DELAY: Duration = Duration::from_secs(5);
|
||||||
|
@ -1681,7 +1680,7 @@ impl Thread {
|
||||||
|
|
||||||
let completion_mode = request
|
let completion_mode = request
|
||||||
.mode
|
.mode
|
||||||
.unwrap_or(zed_llm_client::CompletionMode::Normal);
|
.unwrap_or(cloud_llm_client::CompletionMode::Normal);
|
||||||
|
|
||||||
self.last_received_chunk_at = Some(Instant::now());
|
self.last_received_chunk_at = Some(Instant::now());
|
||||||
|
|
||||||
|
@ -3255,8 +3254,10 @@ impl Thread {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_model_request_usage(&self, amount: u32, limit: UsageLimit, cx: &mut Context<Self>) {
|
fn update_model_request_usage(&self, amount: u32, limit: UsageLimit, cx: &mut Context<Self>) {
|
||||||
self.project.update(cx, |project, cx| {
|
self.project
|
||||||
project.user_store().update(cx, |user_store, cx| {
|
.read(cx)
|
||||||
|
.user_store()
|
||||||
|
.update(cx, |user_store, cx| {
|
||||||
user_store.update_model_request_usage(
|
user_store.update_model_request_usage(
|
||||||
ModelRequestUsage(RequestUsage {
|
ModelRequestUsage(RequestUsage {
|
||||||
amount: amount as i32,
|
amount: amount as i32,
|
||||||
|
@ -3264,8 +3265,7 @@ impl Thread {
|
||||||
}),
|
}),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deny_tool_use(
|
pub fn deny_tool_use(
|
||||||
|
|
|
@ -47,6 +47,7 @@ impl AgentServer for Codex {
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||||
let project = project.clone();
|
let project = project.clone();
|
||||||
|
let working_directory = project.read(cx).active_project_directory(cx);
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||||
|
@ -65,6 +66,7 @@ impl AgentServer for Codex {
|
||||||
args: command.args,
|
args: command.args,
|
||||||
env: command.env,
|
env: command.env,
|
||||||
},
|
},
|
||||||
|
working_directory,
|
||||||
)
|
)
|
||||||
.into();
|
.into();
|
||||||
ContextServer::start(client.clone(), cx).await?;
|
ContextServer::start(client.clone(), cx).await?;
|
||||||
|
@ -310,7 +312,7 @@ pub(crate) mod tests {
|
||||||
|
|
||||||
AgentServerCommand {
|
AgentServerCommand {
|
||||||
path: cli_path,
|
path: cli_path,
|
||||||
args: vec!["mcp".into()],
|
args: vec![],
|
||||||
env: None,
|
env: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@ use futures::{FutureExt, StreamExt, channel::mpsc, select};
|
||||||
use gpui::{Entity, TestAppContext};
|
use gpui::{Entity, TestAppContext};
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use project::{FakeFs, Project};
|
use project::{FakeFs, Project};
|
||||||
use serde_json::json;
|
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
use util::path;
|
use util::path;
|
||||||
|
|
||||||
|
@ -27,7 +26,11 @@ pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppCont
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
thread.read_with(cx, |thread, _| {
|
thread.read_with(cx, |thread, _| {
|
||||||
assert_eq!(thread.entries().len(), 2);
|
assert!(
|
||||||
|
thread.entries().len() >= 2,
|
||||||
|
"Expected at least 2 entries. Got: {:?}",
|
||||||
|
thread.entries()
|
||||||
|
);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
thread.entries()[0],
|
thread.entries()[0],
|
||||||
AgentThreadEntry::UserMessage(_)
|
AgentThreadEntry::UserMessage(_)
|
||||||
|
@ -108,19 +111,19 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
|
pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
|
||||||
let fs = init_test(cx).await;
|
let _fs = init_test(cx).await;
|
||||||
fs.insert_tree(
|
|
||||||
path!("/private/tmp"),
|
let tempdir = tempfile::tempdir().unwrap();
|
||||||
json!({"foo": "Lorem ipsum dolor", "bar": "bar", "baz": "baz"}),
|
let foo_path = tempdir.path().join("foo");
|
||||||
)
|
std::fs::write(&foo_path, "Lorem ipsum dolor").expect("failed to write file");
|
||||||
.await;
|
|
||||||
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
|
let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
|
||||||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||||
|
|
||||||
thread
|
thread
|
||||||
.update(cx, |thread, cx| {
|
.update(cx, |thread, cx| {
|
||||||
thread.send_raw(
|
thread.send_raw(
|
||||||
"Read the '/private/tmp/foo' file and tell me what you see.",
|
&format!("Read {} and tell me what you see.", foo_path.display()),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -143,6 +146,8 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp
|
||||||
.any(|entry| { matches!(entry, AgentThreadEntry::AssistantMessage(_)) })
|
.any(|entry| { matches!(entry, AgentThreadEntry::AssistantMessage(_)) })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
drop(tempdir);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn test_tool_call_with_confirmation(
|
pub async fn test_tool_call_with_confirmation(
|
||||||
|
@ -155,7 +160,7 @@ pub async fn test_tool_call_with_confirmation(
|
||||||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||||
let full_turn = thread.update(cx, |thread, cx| {
|
let full_turn = thread.update(cx, |thread, cx| {
|
||||||
thread.send_raw(
|
thread.send_raw(
|
||||||
r#"Run `touch hello.txt && echo "Hello, world!" | tee hello.txt`"#,
|
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -175,10 +180,10 @@ pub async fn test_tool_call_with_confirmation(
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let tool_call_id = thread.read_with(cx, |thread, _cx| {
|
let tool_call_id = thread.read_with(cx, |thread, cx| {
|
||||||
let AgentThreadEntry::ToolCall(ToolCall {
|
let AgentThreadEntry::ToolCall(ToolCall {
|
||||||
id,
|
id,
|
||||||
content,
|
label,
|
||||||
status: ToolCallStatus::WaitingForConfirmation { .. },
|
status: ToolCallStatus::WaitingForConfirmation { .. },
|
||||||
..
|
..
|
||||||
}) = &thread
|
}) = &thread
|
||||||
|
@ -190,7 +195,8 @@ pub async fn test_tool_call_with_confirmation(
|
||||||
panic!();
|
panic!();
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(content.iter().any(|c| c.to_markdown(_cx).contains("touch")));
|
let label = label.read(cx).source();
|
||||||
|
assert!(label.contains("touch"), "Got: {}", label);
|
||||||
|
|
||||||
id.clone()
|
id.clone()
|
||||||
});
|
});
|
||||||
|
@ -242,7 +248,7 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
|
||||||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||||
let full_turn = thread.update(cx, |thread, cx| {
|
let full_turn = thread.update(cx, |thread, cx| {
|
||||||
thread.send_raw(
|
thread.send_raw(
|
||||||
r#"Run `touch hello.txt && echo "Hello, world!" >> hello.txt`"#,
|
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
@ -262,10 +268,10 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
thread.read_with(cx, |thread, _cx| {
|
thread.read_with(cx, |thread, cx| {
|
||||||
let AgentThreadEntry::ToolCall(ToolCall {
|
let AgentThreadEntry::ToolCall(ToolCall {
|
||||||
id,
|
id,
|
||||||
content,
|
label,
|
||||||
status: ToolCallStatus::WaitingForConfirmation { .. },
|
status: ToolCallStatus::WaitingForConfirmation { .. },
|
||||||
..
|
..
|
||||||
}) = &thread.entries()[first_tool_call_ix]
|
}) = &thread.entries()[first_tool_call_ix]
|
||||||
|
@ -273,7 +279,8 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
|
||||||
panic!("{:?}", thread.entries()[1]);
|
panic!("{:?}", thread.entries()[1]);
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(content.iter().any(|c| c.to_markdown(_cx).contains("touch")));
|
let label = label.read(cx).source();
|
||||||
|
assert!(label.contains("touch"), "Got: {}", label);
|
||||||
|
|
||||||
id.clone()
|
id.clone()
|
||||||
});
|
});
|
||||||
|
|
|
@ -107,6 +107,7 @@ impl AgentServer for Gemini {
|
||||||
name,
|
name,
|
||||||
connection,
|
connection,
|
||||||
child_status,
|
child_status,
|
||||||
|
current_thread: thread_rc,
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(connection)
|
Ok(connection)
|
||||||
|
|
|
@ -13,6 +13,7 @@ path = "src/agent_settings.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
cloud_llm_client.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
language_model.workspace = true
|
language_model.workspace = true
|
||||||
|
@ -20,7 +21,6 @@ schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
zed_llm_client.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
fs.workspace = true
|
fs.workspace = true
|
||||||
|
|
|
@ -321,11 +321,11 @@ pub enum CompletionMode {
|
||||||
Burn,
|
Burn,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<CompletionMode> for zed_llm_client::CompletionMode {
|
impl From<CompletionMode> for cloud_llm_client::CompletionMode {
|
||||||
fn from(value: CompletionMode) -> Self {
|
fn from(value: CompletionMode) -> Self {
|
||||||
match value {
|
match value {
|
||||||
CompletionMode::Normal => zed_llm_client::CompletionMode::Normal,
|
CompletionMode::Normal => cloud_llm_client::CompletionMode::Normal,
|
||||||
CompletionMode::Burn => zed_llm_client::CompletionMode::Max,
|
CompletionMode::Burn => cloud_llm_client::CompletionMode::Max,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ audio.workspace = true
|
||||||
buffer_diff.workspace = true
|
buffer_diff.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
|
cloud_llm_client.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
command_palette_hooks.workspace = true
|
command_palette_hooks.workspace = true
|
||||||
component.workspace = true
|
component.workspace = true
|
||||||
|
@ -46,9 +47,9 @@ futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
html_to_markdown.workspace = true
|
html_to_markdown.workspace = true
|
||||||
indoc.workspace = true
|
|
||||||
http_client.workspace = true
|
http_client.workspace = true
|
||||||
indexed_docs.workspace = true
|
indexed_docs.workspace = true
|
||||||
|
indoc.workspace = true
|
||||||
inventory.workspace = true
|
inventory.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
jsonschema.workspace = true
|
jsonschema.workspace = true
|
||||||
|
@ -97,7 +98,6 @@ watch.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
zed_actions.workspace = true
|
zed_actions.workspace = true
|
||||||
zed_llm_client.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assistant_tools.workspace = true
|
assistant_tools.workspace = true
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use acp_thread::{AgentConnection, Plan};
|
use acp_thread::{AgentConnection, Plan};
|
||||||
use agent_servers::AgentServer;
|
use agent_servers::AgentServer;
|
||||||
|
use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
|
||||||
|
use audio::{Audio, Sound};
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
@ -18,10 +20,10 @@ use editor::{
|
||||||
use file_icons::FileIcons;
|
use file_icons::FileIcons;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
|
Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
|
||||||
FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement,
|
FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, PlatformDisplay, SharedString,
|
||||||
Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
|
StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation,
|
||||||
Window, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*,
|
UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop, linear_gradient,
|
||||||
pulsating_between,
|
list, percentage, point, prelude::*, pulsating_between,
|
||||||
};
|
};
|
||||||
use language::language_settings::SoftWrap;
|
use language::language_settings::SoftWrap;
|
||||||
use language::{Buffer, Language};
|
use language::{Buffer, Language};
|
||||||
|
@ -45,7 +47,10 @@ use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSe
|
||||||
use crate::acp::message_history::MessageHistory;
|
use crate::acp::message_history::MessageHistory;
|
||||||
use crate::agent_diff::AgentDiff;
|
use crate::agent_diff::AgentDiff;
|
||||||
use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
|
use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
|
||||||
use crate::{AgentDiffPane, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll};
|
use crate::ui::{AgentNotification, AgentNotificationEvent};
|
||||||
|
use crate::{
|
||||||
|
AgentDiffPane, AgentPanel, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll,
|
||||||
|
};
|
||||||
|
|
||||||
const RESPONSE_PADDING_X: Pixels = px(19.);
|
const RESPONSE_PADDING_X: Pixels = px(19.);
|
||||||
|
|
||||||
|
@ -59,6 +64,8 @@ pub struct AcpThreadView {
|
||||||
message_set_from_history: bool,
|
message_set_from_history: bool,
|
||||||
_message_editor_subscription: Subscription,
|
_message_editor_subscription: Subscription,
|
||||||
mention_set: Arc<Mutex<MentionSet>>,
|
mention_set: Arc<Mutex<MentionSet>>,
|
||||||
|
notifications: Vec<WindowHandle<AgentNotification>>,
|
||||||
|
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
|
||||||
last_error: Option<Entity<Markdown>>,
|
last_error: Option<Entity<Markdown>>,
|
||||||
list_state: ListState,
|
list_state: ListState,
|
||||||
auth_task: Option<Task<()>>,
|
auth_task: Option<Task<()>>,
|
||||||
|
@ -174,6 +181,8 @@ impl AcpThreadView {
|
||||||
message_set_from_history: false,
|
message_set_from_history: false,
|
||||||
_message_editor_subscription: message_editor_subscription,
|
_message_editor_subscription: message_editor_subscription,
|
||||||
mention_set,
|
mention_set,
|
||||||
|
notifications: Vec::new(),
|
||||||
|
notification_subscriptions: HashMap::default(),
|
||||||
diff_editors: Default::default(),
|
diff_editors: Default::default(),
|
||||||
list_state: list_state,
|
list_state: list_state,
|
||||||
last_error: None,
|
last_error: None,
|
||||||
|
@ -381,7 +390,9 @@ impl AcpThreadView {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(thread) = self.thread() else { return };
|
let Some(thread) = self.thread() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
|
let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
|
@ -564,6 +575,30 @@ impl AcpThreadView {
|
||||||
self.sync_thread_entry_view(index, window, cx);
|
self.sync_thread_entry_view(index, window, cx);
|
||||||
self.list_state.splice(index..index + 1, 1);
|
self.list_state.splice(index..index + 1, 1);
|
||||||
}
|
}
|
||||||
|
AcpThreadEvent::ToolAuthorizationRequired => {
|
||||||
|
self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx);
|
||||||
|
}
|
||||||
|
AcpThreadEvent::Stopped => {
|
||||||
|
let used_tools = thread.read(cx).used_tools_since_last_user_message();
|
||||||
|
self.notify_with_sound(
|
||||||
|
if used_tools {
|
||||||
|
"Finished running tools"
|
||||||
|
} else {
|
||||||
|
"New message"
|
||||||
|
},
|
||||||
|
IconName::ZedAssistant,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
AcpThreadEvent::Error => {
|
||||||
|
self.notify_with_sound(
|
||||||
|
"Agent stopped due to an error",
|
||||||
|
IconName::Warning,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
@ -2160,6 +2195,154 @@ impl AcpThreadView {
|
||||||
self.list_state.scroll_to(ListOffset::default());
|
self.list_state.scroll_to(ListOffset::default());
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn notify_with_sound(
|
||||||
|
&mut self,
|
||||||
|
caption: impl Into<SharedString>,
|
||||||
|
icon: IconName,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.play_notification_sound(window, cx);
|
||||||
|
self.show_notification(caption, icon, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn play_notification_sound(&self, window: &Window, cx: &mut App) {
|
||||||
|
let settings = AgentSettings::get_global(cx);
|
||||||
|
if settings.play_sound_when_agent_done && !window.is_window_active() {
|
||||||
|
Audio::play_sound(Sound::AgentDone, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_notification(
|
||||||
|
&mut self,
|
||||||
|
caption: impl Into<SharedString>,
|
||||||
|
icon: IconName,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
if window.is_window_active() || !self.notifications.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = self.title(cx);
|
||||||
|
|
||||||
|
match AgentSettings::get_global(cx).notify_when_agent_waiting {
|
||||||
|
NotifyWhenAgentWaiting::PrimaryScreen => {
|
||||||
|
if let Some(primary) = cx.primary_display() {
|
||||||
|
self.pop_up(icon, caption.into(), title, window, primary, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NotifyWhenAgentWaiting::AllScreens => {
|
||||||
|
let caption = caption.into();
|
||||||
|
for screen in cx.displays() {
|
||||||
|
self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NotifyWhenAgentWaiting::Never => {
|
||||||
|
// Don't show anything
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop_up(
|
||||||
|
&mut self,
|
||||||
|
icon: IconName,
|
||||||
|
caption: SharedString,
|
||||||
|
title: SharedString,
|
||||||
|
window: &mut Window,
|
||||||
|
screen: Rc<dyn PlatformDisplay>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let options = AgentNotification::window_options(screen, cx);
|
||||||
|
|
||||||
|
let project_name = self.workspace.upgrade().and_then(|workspace| {
|
||||||
|
workspace
|
||||||
|
.read(cx)
|
||||||
|
.project()
|
||||||
|
.read(cx)
|
||||||
|
.visible_worktrees(cx)
|
||||||
|
.next()
|
||||||
|
.map(|worktree| worktree.read(cx).root_name().to_string())
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(screen_window) = cx
|
||||||
|
.open_window(options, |_, cx| {
|
||||||
|
cx.new(|_| {
|
||||||
|
AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.log_err()
|
||||||
|
{
|
||||||
|
if let Some(pop_up) = screen_window.entity(cx).log_err() {
|
||||||
|
self.notification_subscriptions
|
||||||
|
.entry(screen_window)
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(cx.subscribe_in(&pop_up, window, {
|
||||||
|
|this, _, event, window, cx| match event {
|
||||||
|
AgentNotificationEvent::Accepted => {
|
||||||
|
let handle = window.window_handle();
|
||||||
|
cx.activate(true);
|
||||||
|
|
||||||
|
let workspace_handle = this.workspace.clone();
|
||||||
|
|
||||||
|
// If there are multiple Zed windows, activate the correct one.
|
||||||
|
cx.defer(move |cx| {
|
||||||
|
handle
|
||||||
|
.update(cx, |_view, window, _cx| {
|
||||||
|
window.activate_window();
|
||||||
|
|
||||||
|
if let Some(workspace) = workspace_handle.upgrade() {
|
||||||
|
workspace.update(_cx, |workspace, cx| {
|
||||||
|
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.dismiss_notifications(cx);
|
||||||
|
}
|
||||||
|
AgentNotificationEvent::Dismissed => {
|
||||||
|
this.dismiss_notifications(cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
self.notifications.push(screen_window);
|
||||||
|
|
||||||
|
// If the user manually refocuses the original window, dismiss the popup.
|
||||||
|
self.notification_subscriptions
|
||||||
|
.entry(screen_window)
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push({
|
||||||
|
let pop_up_weak = pop_up.downgrade();
|
||||||
|
|
||||||
|
cx.observe_window_activation(window, move |_, window, cx| {
|
||||||
|
if window.is_window_active() {
|
||||||
|
if let Some(pop_up) = pop_up_weak.upgrade() {
|
||||||
|
pop_up.update(cx, |_, cx| {
|
||||||
|
cx.emit(AgentNotificationEvent::Dismissed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismiss_notifications(&mut self, cx: &mut Context<Self>) {
|
||||||
|
for window in self.notifications.drain(..) {
|
||||||
|
window
|
||||||
|
.update(cx, |_, window, _| {
|
||||||
|
window.remove_window();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
self.notification_subscriptions.remove(&window);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Focusable for AcpThreadView {
|
impl Focusable for AcpThreadView {
|
||||||
|
@ -2441,3 +2624,331 @@ fn plan_label_markdown_style(
|
||||||
..default_md_style
|
..default_md_style
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use agent_client_protocol::SessionId;
|
||||||
|
use editor::EditorSettings;
|
||||||
|
use fs::FakeFs;
|
||||||
|
use futures::future::try_join_all;
|
||||||
|
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
|
||||||
|
use rand::Rng;
|
||||||
|
use settings::SettingsStore;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_notification_for_stop_event(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let (thread_view, cx) = setup_thread_view(StubAgentServer::default(), cx).await;
|
||||||
|
|
||||||
|
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
|
||||||
|
message_editor.update_in(cx, |editor, window, cx| {
|
||||||
|
editor.set_text("Hello", window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.deactivate_window();
|
||||||
|
|
||||||
|
thread_view.update_in(cx, |thread_view, window, cx| {
|
||||||
|
thread_view.chat(&Chat, window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
cx.windows()
|
||||||
|
.iter()
|
||||||
|
.any(|window| window.downcast::<AgentNotification>().is_some())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_notification_for_error(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let (thread_view, cx) =
|
||||||
|
setup_thread_view(StubAgentServer::new(SaboteurAgentConnection), cx).await;
|
||||||
|
|
||||||
|
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
|
||||||
|
message_editor.update_in(cx, |editor, window, cx| {
|
||||||
|
editor.set_text("Hello", window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.deactivate_window();
|
||||||
|
|
||||||
|
thread_view.update_in(cx, |thread_view, window, cx| {
|
||||||
|
thread_view.chat(&Chat, window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
cx.windows()
|
||||||
|
.iter()
|
||||||
|
.any(|window| window.downcast::<AgentNotification>().is_some())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let tool_call_id = acp::ToolCallId("1".into());
|
||||||
|
let tool_call = acp::ToolCall {
|
||||||
|
id: tool_call_id.clone(),
|
||||||
|
label: "Label".into(),
|
||||||
|
kind: acp::ToolKind::Edit,
|
||||||
|
status: acp::ToolCallStatus::Pending,
|
||||||
|
content: vec!["hi".into()],
|
||||||
|
locations: vec![],
|
||||||
|
raw_input: None,
|
||||||
|
};
|
||||||
|
let connection = StubAgentConnection::new(vec![acp::SessionUpdate::ToolCall(tool_call)])
|
||||||
|
.with_permission_requests(HashMap::from_iter([(
|
||||||
|
tool_call_id,
|
||||||
|
vec![acp::PermissionOption {
|
||||||
|
id: acp::PermissionOptionId("1".into()),
|
||||||
|
label: "Allow".into(),
|
||||||
|
kind: acp::PermissionOptionKind::AllowOnce,
|
||||||
|
}],
|
||||||
|
)]));
|
||||||
|
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
|
||||||
|
|
||||||
|
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
|
||||||
|
message_editor.update_in(cx, |editor, window, cx| {
|
||||||
|
editor.set_text("Hello", window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.deactivate_window();
|
||||||
|
|
||||||
|
thread_view.update_in(cx, |thread_view, window, cx| {
|
||||||
|
thread_view.chat(&Chat, window, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
cx.windows()
|
||||||
|
.iter()
|
||||||
|
.any(|window| window.downcast::<AgentNotification>().is_some())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn setup_thread_view(
|
||||||
|
agent: impl AgentServer + 'static,
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) -> (Entity<AcpThreadView>, &mut VisualTestContext) {
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
let project = Project::test(fs, [], cx).await;
|
||||||
|
let (workspace, cx) =
|
||||||
|
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||||
|
|
||||||
|
let thread_view = cx.update(|window, cx| {
|
||||||
|
cx.new(|cx| {
|
||||||
|
AcpThreadView::new(
|
||||||
|
Rc::new(agent),
|
||||||
|
workspace.downgrade(),
|
||||||
|
project,
|
||||||
|
Rc::new(RefCell::new(MessageHistory::default())),
|
||||||
|
1,
|
||||||
|
None,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
(thread_view, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StubAgentServer<C> {
|
||||||
|
connection: C,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> StubAgentServer<C> {
|
||||||
|
fn new(connection: C) -> Self {
|
||||||
|
Self { connection }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StubAgentServer<StubAgentConnection> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(StubAgentConnection::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C> AgentServer for StubAgentServer<C>
|
||||||
|
where
|
||||||
|
C: 'static + AgentConnection + Send + Clone,
|
||||||
|
{
|
||||||
|
fn logo(&self) -> ui::IconName {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn empty_state_headline(&self) -> &'static str {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn empty_state_message(&self) -> &'static str {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connect(
|
||||||
|
&self,
|
||||||
|
_root_dir: &Path,
|
||||||
|
_project: &Entity<Project>,
|
||||||
|
_cx: &mut App,
|
||||||
|
) -> Task<gpui::Result<Rc<dyn AgentConnection>>> {
|
||||||
|
Task::ready(Ok(Rc::new(self.connection.clone())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
struct StubAgentConnection {
|
||||||
|
sessions: Arc<Mutex<HashMap<acp::SessionId, WeakEntity<AcpThread>>>>,
|
||||||
|
permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
|
||||||
|
updates: Vec<acp::SessionUpdate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StubAgentConnection {
|
||||||
|
fn new(updates: Vec<acp::SessionUpdate>) -> Self {
|
||||||
|
Self {
|
||||||
|
updates,
|
||||||
|
permission_requests: HashMap::default(),
|
||||||
|
sessions: Arc::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_permission_requests(
|
||||||
|
mut self,
|
||||||
|
permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
|
||||||
|
) -> Self {
|
||||||
|
self.permission_requests = permission_requests;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AgentConnection for StubAgentConnection {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"StubAgentConnection"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_thread(
|
||||||
|
self: Rc<Self>,
|
||||||
|
project: Entity<Project>,
|
||||||
|
_cwd: &Path,
|
||||||
|
cx: &mut gpui::AsyncApp,
|
||||||
|
) -> Task<gpui::Result<Entity<AcpThread>>> {
|
||||||
|
let session_id = SessionId(
|
||||||
|
rand::thread_rng()
|
||||||
|
.sample_iter(&rand::distributions::Alphanumeric)
|
||||||
|
.take(7)
|
||||||
|
.map(char::from)
|
||||||
|
.collect::<String>()
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
let thread = cx
|
||||||
|
.new(|cx| AcpThread::new(self.clone(), project, session_id.clone(), cx))
|
||||||
|
.unwrap();
|
||||||
|
self.sessions.lock().insert(session_id, thread.downgrade());
|
||||||
|
Task::ready(Ok(thread))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authenticate(&self, _cx: &mut App) -> Task<gpui::Result<()>> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task<gpui::Result<()>> {
|
||||||
|
let sessions = self.sessions.lock();
|
||||||
|
let thread = sessions.get(¶ms.session_id).unwrap();
|
||||||
|
let mut tasks = vec![];
|
||||||
|
for update in &self.updates {
|
||||||
|
let thread = thread.clone();
|
||||||
|
let update = update.clone();
|
||||||
|
let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) = &update
|
||||||
|
&& let Some(options) = self.permission_requests.get(&tool_call.id)
|
||||||
|
{
|
||||||
|
Some((tool_call.clone(), options.clone()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let task = cx.spawn(async move |cx| {
|
||||||
|
if let Some((tool_call, options)) = permission_request {
|
||||||
|
let permission = thread.update(cx, |thread, cx| {
|
||||||
|
thread.request_tool_call_permission(
|
||||||
|
tool_call.clone(),
|
||||||
|
options.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
permission.await?;
|
||||||
|
}
|
||||||
|
thread.update(cx, |thread, cx| {
|
||||||
|
thread.handle_session_update(update.clone(), cx).unwrap();
|
||||||
|
})?;
|
||||||
|
anyhow::Ok(())
|
||||||
|
});
|
||||||
|
tasks.push(task);
|
||||||
|
}
|
||||||
|
cx.spawn(async move |_| {
|
||||||
|
try_join_all(tasks).await?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct SaboteurAgentConnection;
|
||||||
|
|
||||||
|
impl AgentConnection for SaboteurAgentConnection {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"SaboteurAgentConnection"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_thread(
|
||||||
|
self: Rc<Self>,
|
||||||
|
project: Entity<Project>,
|
||||||
|
_cwd: &Path,
|
||||||
|
cx: &mut gpui::AsyncApp,
|
||||||
|
) -> Task<gpui::Result<Entity<AcpThread>>> {
|
||||||
|
Task::ready(Ok(cx
|
||||||
|
.new(|cx| AcpThread::new(self, project, SessionId("test".into()), cx))
|
||||||
|
.unwrap()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authenticate(&self, _cx: &mut App) -> Task<gpui::Result<()>> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self, _params: acp::PromptArguments, _cx: &mut App) -> Task<gpui::Result<()>> {
|
||||||
|
Task::ready(Err(anyhow::anyhow!("Error prompting")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_test(cx: &mut TestAppContext) {
|
||||||
|
cx.update(|cx| {
|
||||||
|
let settings_store = SettingsStore::test(cx);
|
||||||
|
cx.set_global(settings_store);
|
||||||
|
language::init(cx);
|
||||||
|
Project::init_settings(cx);
|
||||||
|
AgentSettings::register(cx);
|
||||||
|
workspace::init_settings(cx);
|
||||||
|
ThemeSettings::register(cx);
|
||||||
|
release_channel::init(SemanticVersion::default(), cx);
|
||||||
|
EditorSettings::register(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use assistant_tool::ToolUseStatus;
|
use assistant_tool::ToolUseStatus;
|
||||||
use audio::{Audio, Sound};
|
use audio::{Audio, Sound};
|
||||||
|
use cloud_llm_client::CompletionIntent;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use editor::actions::{MoveUp, Paste};
|
use editor::actions::{MoveUp, Paste};
|
||||||
use editor::scroll::Autoscroll;
|
use editor::scroll::Autoscroll;
|
||||||
|
@ -52,7 +53,6 @@ use util::ResultExt as _;
|
||||||
use util::markdown::MarkdownCodeBlock;
|
use util::markdown::MarkdownCodeBlock;
|
||||||
use workspace::{CollaboratorId, Workspace};
|
use workspace::{CollaboratorId, Workspace};
|
||||||
use zed_actions::assistant::OpenRulesLibrary;
|
use zed_actions::assistant::OpenRulesLibrary;
|
||||||
use zed_llm_client::CompletionIntent;
|
|
||||||
|
|
||||||
const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
|
const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
|
||||||
const EDIT_PREVIOUS_MESSAGE_MIN_LINES: usize = 1;
|
const EDIT_PREVIOUS_MESSAGE_MIN_LINES: usize = 1;
|
||||||
|
|
|
@ -7,6 +7,7 @@ use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||||
|
use cloud_llm_client::Plan;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use context_server::ContextServerId;
|
use context_server::ContextServerId;
|
||||||
use extension::ExtensionManifest;
|
use extension::ExtensionManifest;
|
||||||
|
@ -25,7 +26,6 @@ use project::{
|
||||||
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
||||||
project_settings::{ContextServerSettings, ProjectSettings},
|
project_settings::{ContextServerSettings, ProjectSettings},
|
||||||
};
|
};
|
||||||
use proto::Plan;
|
|
||||||
use settings::{Settings, update_settings_file};
|
use settings::{Settings, update_settings_file};
|
||||||
use ui::{
|
use ui::{
|
||||||
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
|
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
|
||||||
|
@ -180,7 +180,7 @@ impl AgentConfiguration {
|
||||||
let current_plan = if is_zed_provider {
|
let current_plan = if is_zed_provider {
|
||||||
self.workspace
|
self.workspace
|
||||||
.upgrade()
|
.upgrade()
|
||||||
.and_then(|workspace| workspace.read(cx).user_store().read(cx).current_plan())
|
.and_then(|workspace| workspace.read(cx).user_store().read(cx).plan())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
@ -406,7 +406,9 @@ impl AgentConfiguration {
|
||||||
SwitchField::new(
|
SwitchField::new(
|
||||||
"always-allow-tool-actions-switch",
|
"always-allow-tool-actions-switch",
|
||||||
"Allow running commands without asking for confirmation",
|
"Allow running commands without asking for confirmation",
|
||||||
"The agent can perform potentially destructive actions without asking for your confirmation.",
|
Some(
|
||||||
|
"The agent can perform potentially destructive actions without asking for your confirmation.".into(),
|
||||||
|
),
|
||||||
always_allow_tool_actions,
|
always_allow_tool_actions,
|
||||||
move |state, _window, cx| {
|
move |state, _window, cx| {
|
||||||
let allow = state == &ToggleState::Selected;
|
let allow = state == &ToggleState::Selected;
|
||||||
|
@ -424,7 +426,7 @@ impl AgentConfiguration {
|
||||||
SwitchField::new(
|
SwitchField::new(
|
||||||
"single-file-review",
|
"single-file-review",
|
||||||
"Enable single-file agent reviews",
|
"Enable single-file agent reviews",
|
||||||
"Agent edits are also displayed in single-file editors for review.",
|
Some("Agent edits are also displayed in single-file editors for review.".into()),
|
||||||
single_file_review,
|
single_file_review,
|
||||||
move |state, _window, cx| {
|
move |state, _window, cx| {
|
||||||
let allow = state == &ToggleState::Selected;
|
let allow = state == &ToggleState::Selected;
|
||||||
|
@ -442,7 +444,9 @@ impl AgentConfiguration {
|
||||||
SwitchField::new(
|
SwitchField::new(
|
||||||
"sound-notification",
|
"sound-notification",
|
||||||
"Play sound when finished generating",
|
"Play sound when finished generating",
|
||||||
"Hear a notification sound when the agent is done generating changes or needs your input.",
|
Some(
|
||||||
|
"Hear a notification sound when the agent is done generating changes or needs your input.".into(),
|
||||||
|
),
|
||||||
play_sound_when_agent_done,
|
play_sound_when_agent_done,
|
||||||
move |state, _window, cx| {
|
move |state, _window, cx| {
|
||||||
let allow = state == &ToggleState::Selected;
|
let allow = state == &ToggleState::Selected;
|
||||||
|
@ -460,7 +464,9 @@ impl AgentConfiguration {
|
||||||
SwitchField::new(
|
SwitchField::new(
|
||||||
"modifier-send",
|
"modifier-send",
|
||||||
"Use modifier to submit a message",
|
"Use modifier to submit a message",
|
||||||
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.",
|
Some(
|
||||||
|
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.".into(),
|
||||||
|
),
|
||||||
use_modifier_to_send,
|
use_modifier_to_send,
|
||||||
move |state, _window, cx| {
|
move |state, _window, cx| {
|
||||||
let allow = state == &ToggleState::Selected;
|
let allow = state == &ToggleState::Selected;
|
||||||
|
@ -502,7 +508,7 @@ impl AgentConfiguration {
|
||||||
.blend(cx.theme().colors().text_accent.opacity(0.2));
|
.blend(cx.theme().colors().text_accent.opacity(0.2));
|
||||||
|
|
||||||
let (plan_name, label_color, bg_color) = match plan {
|
let (plan_name, label_color, bg_color) = match plan {
|
||||||
Plan::Free => ("Free", Color::Default, free_chip_bg),
|
Plan::ZedFree => ("Free", Color::Default, free_chip_bg),
|
||||||
Plan::ZedProTrial => ("Pro Trial", Color::Accent, pro_chip_bg),
|
Plan::ZedProTrial => ("Pro Trial", Color::Accent, pro_chip_bg),
|
||||||
Plan::ZedPro => ("Pro", Color::Accent, pro_chip_bg),
|
Plan::ZedPro => ("Pro", Color::Accent, pro_chip_bg),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1521,6 +1521,9 @@ impl AgentDiff {
|
||||||
self.update_reviewing_editors(workspace, window, cx);
|
self.update_reviewing_editors(workspace, window, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AcpThreadEvent::Stopped
|
||||||
|
| AcpThreadEvent::ToolAuthorizationRequired
|
||||||
|
| AcpThreadEvent::Error => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
|
||||||
use assistant_slash_command::SlashCommandWorkingSet;
|
use assistant_slash_command::SlashCommandWorkingSet;
|
||||||
use assistant_tool::ToolWorkingSet;
|
use assistant_tool::ToolWorkingSet;
|
||||||
use client::{DisableAiSettings, UserStore, zed_urls};
|
use client::{DisableAiSettings, UserStore, zed_urls};
|
||||||
|
use cloud_llm_client::{CompletionIntent, Plan, UsageLimit};
|
||||||
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||||
use feature_flags::{self, FeatureFlagAppExt};
|
use feature_flags::{self, FeatureFlagAppExt};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
|
@ -59,7 +60,6 @@ use language_model::{
|
||||||
};
|
};
|
||||||
use project::{Project, ProjectPath, Worktree};
|
use project::{Project, ProjectPath, Worktree};
|
||||||
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
|
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
|
||||||
use proto::Plan;
|
|
||||||
use rules_library::{RulesLibrary, open_rules_library};
|
use rules_library::{RulesLibrary, open_rules_library};
|
||||||
use search::{BufferSearchBar, buffer_search};
|
use search::{BufferSearchBar, buffer_search};
|
||||||
use settings::{Settings, update_settings_file};
|
use settings::{Settings, update_settings_file};
|
||||||
|
@ -77,10 +77,9 @@ use workspace::{
|
||||||
};
|
};
|
||||||
use zed_actions::{
|
use zed_actions::{
|
||||||
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
|
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
|
||||||
agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding, ToggleModelSelector},
|
agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
|
||||||
assistant::{OpenRulesLibrary, ToggleFocus},
|
assistant::{OpenRulesLibrary, ToggleFocus},
|
||||||
};
|
};
|
||||||
use zed_llm_client::{CompletionIntent, UsageLimit};
|
|
||||||
|
|
||||||
const AGENT_PANEL_KEY: &str = "agent_panel";
|
const AGENT_PANEL_KEY: &str = "agent_panel";
|
||||||
|
|
||||||
|
@ -105,7 +104,7 @@ pub fn init(cx: &mut App) {
|
||||||
panel.update(cx, |panel, cx| panel.open_history(window, cx));
|
panel.update(cx, |panel, cx| panel.open_history(window, cx));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.register_action(|workspace, _: &OpenConfiguration, window, cx| {
|
.register_action(|workspace, _: &OpenSettings, window, cx| {
|
||||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||||
panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
|
panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
|
||||||
|
@ -579,7 +578,6 @@ impl AgentPanel {
|
||||||
MessageEditor::new(
|
MessageEditor::new(
|
||||||
fs.clone(),
|
fs.clone(),
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
user_store.clone(),
|
|
||||||
message_editor_context_store.clone(),
|
message_editor_context_store.clone(),
|
||||||
prompt_store.clone(),
|
prompt_store.clone(),
|
||||||
thread_store.downgrade(),
|
thread_store.downgrade(),
|
||||||
|
@ -848,7 +846,6 @@ impl AgentPanel {
|
||||||
MessageEditor::new(
|
MessageEditor::new(
|
||||||
self.fs.clone(),
|
self.fs.clone(),
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
self.user_store.clone(),
|
|
||||||
context_store.clone(),
|
context_store.clone(),
|
||||||
self.prompt_store.clone(),
|
self.prompt_store.clone(),
|
||||||
self.thread_store.downgrade(),
|
self.thread_store.downgrade(),
|
||||||
|
@ -1122,7 +1119,6 @@ impl AgentPanel {
|
||||||
MessageEditor::new(
|
MessageEditor::new(
|
||||||
self.fs.clone(),
|
self.fs.clone(),
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
self.user_store.clone(),
|
|
||||||
context_store,
|
context_store,
|
||||||
self.prompt_store.clone(),
|
self.prompt_store.clone(),
|
||||||
self.thread_store.downgrade(),
|
self.thread_store.downgrade(),
|
||||||
|
@ -2088,7 +2084,7 @@ impl AgentPanel {
|
||||||
|
|
||||||
menu = menu
|
menu = menu
|
||||||
.action("Rules…", Box::new(OpenRulesLibrary::default()))
|
.action("Rules…", Box::new(OpenRulesLibrary::default()))
|
||||||
.action("Settings", Box::new(OpenConfiguration))
|
.action("Settings", Box::new(OpenSettings))
|
||||||
.action(zoom_in_label, Box::new(ToggleZoom));
|
.action(zoom_in_label, Box::new(ToggleZoom));
|
||||||
menu
|
menu
|
||||||
}))
|
}))
|
||||||
|
@ -2293,10 +2289,10 @@ impl AgentPanel {
|
||||||
| ActiveView::Configuration => return false,
|
| ActiveView::Configuration => return false,
|
||||||
}
|
}
|
||||||
|
|
||||||
let plan = self.user_store.read(cx).current_plan();
|
let plan = self.user_store.read(cx).plan();
|
||||||
let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
|
let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
|
||||||
|
|
||||||
matches!(plan, Some(Plan::Free)) && has_previous_trial
|
matches!(plan, Some(Plan::ZedFree)) && has_previous_trial
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
|
fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
|
||||||
|
@ -2482,14 +2478,14 @@ impl AgentPanel {
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.full_width()
|
.full_width()
|
||||||
.key_binding(KeyBinding::for_action_in(
|
.key_binding(KeyBinding::for_action_in(
|
||||||
&OpenConfiguration,
|
&OpenSettings,
|
||||||
&focus_handle,
|
&focus_handle,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
.on_click(|_event, window, cx| {
|
.on_click(|_event, window, cx| {
|
||||||
window.dispatch_action(
|
window.dispatch_action(
|
||||||
OpenConfiguration.boxed_clone(),
|
OpenSettings.boxed_clone(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
@ -2713,16 +2709,11 @@ impl AgentPanel {
|
||||||
.style(ButtonStyle::Tinted(ui::TintColor::Warning))
|
.style(ButtonStyle::Tinted(ui::TintColor::Warning))
|
||||||
.label_size(LabelSize::Small)
|
.label_size(LabelSize::Small)
|
||||||
.key_binding(
|
.key_binding(
|
||||||
KeyBinding::for_action_in(
|
KeyBinding::for_action_in(&OpenSettings, &focus_handle, window, cx)
|
||||||
&OpenConfiguration,
|
.map(|kb| kb.size(rems_from_px(12.))),
|
||||||
&focus_handle,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.map(|kb| kb.size(rems_from_px(12.))),
|
|
||||||
)
|
)
|
||||||
.on_click(|_event, window, cx| {
|
.on_click(|_event, window, cx| {
|
||||||
window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
|
window.dispatch_action(OpenSettings.boxed_clone(), cx)
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
ConfigurationError::ProviderPendingTermsAcceptance(provider) => {
|
ConfigurationError::ProviderPendingTermsAcceptance(provider) => {
|
||||||
|
@ -2916,7 +2907,7 @@ impl AgentPanel {
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let error_message = match plan {
|
let error_message = match plan {
|
||||||
Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
|
Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
|
||||||
Plan::ZedProTrial | Plan::Free => "Upgrade to Zed Pro for more prompts.",
|
Plan::ZedProTrial | Plan::ZedFree => "Upgrade to Zed Pro for more prompts.",
|
||||||
};
|
};
|
||||||
|
|
||||||
let icon = Icon::new(IconName::XCircle)
|
let icon = Icon::new(IconName::XCircle)
|
||||||
|
@ -3226,7 +3217,7 @@ impl Render for AgentPanel {
|
||||||
.on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
|
.on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
|
||||||
this.open_history(window, cx);
|
this.open_history(window, cx);
|
||||||
}))
|
}))
|
||||||
.on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
|
.on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
|
||||||
this.open_configuration(window, cx);
|
this.open_configuration(window, cx);
|
||||||
}))
|
}))
|
||||||
.on_action(cx.listener(Self::open_active_thread_as_markdown))
|
.on_action(cx.listener(Self::open_active_thread_as_markdown))
|
||||||
|
|
|
@ -265,8 +265,8 @@ fn update_command_palette_filter(cx: &mut App) {
|
||||||
filter.hide_namespace("agent");
|
filter.hide_namespace("agent");
|
||||||
filter.hide_namespace("assistant");
|
filter.hide_namespace("assistant");
|
||||||
filter.hide_namespace("copilot");
|
filter.hide_namespace("copilot");
|
||||||
|
filter.hide_namespace("supermaven");
|
||||||
filter.hide_namespace("zed_predict_onboarding");
|
filter.hide_namespace("zed_predict_onboarding");
|
||||||
|
|
||||||
filter.hide_namespace("edit_prediction");
|
filter.hide_namespace("edit_prediction");
|
||||||
|
|
||||||
use editor::actions::{
|
use editor::actions::{
|
||||||
|
|
|
@ -6,6 +6,7 @@ use agent::{
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use client::telemetry::Telemetry;
|
use client::telemetry::Telemetry;
|
||||||
|
use cloud_llm_client::CompletionIntent;
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
||||||
use futures::{
|
use futures::{
|
||||||
|
@ -35,7 +36,6 @@ use std::{
|
||||||
};
|
};
|
||||||
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
|
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
|
||||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||||
use zed_llm_client::CompletionIntent;
|
|
||||||
|
|
||||||
pub struct BufferCodegen {
|
pub struct BufferCodegen {
|
||||||
alternatives: Vec<Entity<CodegenAlternative>>,
|
alternatives: Vec<Entity<CodegenAlternative>>,
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
#![allow(unused, dead_code)]
|
#![allow(unused, dead_code)]
|
||||||
|
|
||||||
use client::{ModelRequestUsage, RequestUsage};
|
use client::{ModelRequestUsage, RequestUsage};
|
||||||
|
use cloud_llm_client::{Plan, UsageLimit};
|
||||||
use gpui::Global;
|
use gpui::Global;
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use zed_llm_client::{Plan, UsageLimit};
|
|
||||||
|
|
||||||
/// Debug only: Used for testing various account states
|
/// Debug only: Used for testing various account states
|
||||||
///
|
///
|
||||||
|
|
|
@ -48,7 +48,7 @@ use text::{OffsetRangeExt, ToPoint as _};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use util::{RangeExt, ResultExt, maybe};
|
use util::{RangeExt, ResultExt, maybe};
|
||||||
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
|
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
|
||||||
use zed_actions::agent::OpenConfiguration;
|
use zed_actions::agent::OpenSettings;
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
|
@ -345,7 +345,7 @@ impl InlineAssistant {
|
||||||
if let Some(answer) = answer {
|
if let Some(answer) = answer {
|
||||||
if answer == 0 {
|
if answer == 0 {
|
||||||
cx.update(|window, cx| {
|
cx.update(|window, cx| {
|
||||||
window.dispatch_action(Box::new(OpenConfiguration), cx)
|
window.dispatch_action(Box::new(OpenSettings), cx)
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
|
@ -576,7 +576,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
.on_click(|_, window, cx| {
|
.on_click(|_, window, cx| {
|
||||||
window.dispatch_action(
|
window.dispatch_action(
|
||||||
zed_actions::agent::OpenConfiguration.boxed_clone(),
|
zed_actions::agent::OpenSettings.boxed_clone(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -17,7 +17,7 @@ use agent::{
|
||||||
use agent_settings::{AgentSettings, CompletionMode};
|
use agent_settings::{AgentSettings, CompletionMode};
|
||||||
use ai_onboarding::ApiKeysWithProviders;
|
use ai_onboarding::ApiKeysWithProviders;
|
||||||
use buffer_diff::BufferDiff;
|
use buffer_diff::BufferDiff;
|
||||||
use client::UserStore;
|
use cloud_llm_client::CompletionIntent;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use editor::actions::{MoveUp, Paste};
|
use editor::actions::{MoveUp, Paste};
|
||||||
use editor::display_map::CreaseId;
|
use editor::display_map::CreaseId;
|
||||||
|
@ -42,7 +42,6 @@ use language_model::{
|
||||||
use multi_buffer;
|
use multi_buffer;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use prompt_store::PromptStore;
|
use prompt_store::PromptStore;
|
||||||
use proto::Plan;
|
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
|
@ -53,7 +52,6 @@ use util::ResultExt as _;
|
||||||
use workspace::{CollaboratorId, Workspace};
|
use workspace::{CollaboratorId, Workspace};
|
||||||
use zed_actions::agent::Chat;
|
use zed_actions::agent::Chat;
|
||||||
use zed_actions::agent::ToggleModelSelector;
|
use zed_actions::agent::ToggleModelSelector;
|
||||||
use zed_llm_client::CompletionIntent;
|
|
||||||
|
|
||||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
|
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
|
||||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||||
|
@ -79,7 +77,6 @@ pub struct MessageEditor {
|
||||||
editor: Entity<Editor>,
|
editor: Entity<Editor>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
user_store: Entity<UserStore>,
|
|
||||||
context_store: Entity<ContextStore>,
|
context_store: Entity<ContextStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
history_store: Option<WeakEntity<HistoryStore>>,
|
history_store: Option<WeakEntity<HistoryStore>>,
|
||||||
|
@ -159,7 +156,6 @@ impl MessageEditor {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
user_store: Entity<UserStore>,
|
|
||||||
context_store: Entity<ContextStore>,
|
context_store: Entity<ContextStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
prompt_store: Option<Entity<PromptStore>>,
|
||||||
thread_store: WeakEntity<ThreadStore>,
|
thread_store: WeakEntity<ThreadStore>,
|
||||||
|
@ -231,7 +227,6 @@ impl MessageEditor {
|
||||||
Self {
|
Self {
|
||||||
editor: editor.clone(),
|
editor: editor.clone(),
|
||||||
project: thread.read(cx).project().clone(),
|
project: thread.read(cx).project().clone(),
|
||||||
user_store,
|
|
||||||
thread,
|
thread,
|
||||||
incompatible_tools_state: incompatible_tools.clone(),
|
incompatible_tools_state: incompatible_tools.clone(),
|
||||||
workspace,
|
workspace,
|
||||||
|
@ -1287,24 +1282,12 @@ impl MessageEditor {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_store = self.user_store.read(cx);
|
let user_store = self.project.read(cx).user_store().read(cx);
|
||||||
|
if user_store.is_usage_based_billing_enabled() {
|
||||||
let ubb_enable = user_store
|
|
||||||
.usage_based_billing_enabled()
|
|
||||||
.map_or(false, |enabled| enabled);
|
|
||||||
|
|
||||||
if ubb_enable {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let plan = user_store
|
let plan = user_store.plan().unwrap_or(cloud_llm_client::Plan::ZedFree);
|
||||||
.current_plan()
|
|
||||||
.map(|plan| match plan {
|
|
||||||
Plan::Free => zed_llm_client::Plan::ZedFree,
|
|
||||||
Plan::ZedPro => zed_llm_client::Plan::ZedPro,
|
|
||||||
Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
|
||||||
})
|
|
||||||
.unwrap_or(zed_llm_client::Plan::ZedFree);
|
|
||||||
|
|
||||||
let usage = user_store.model_request_usage()?;
|
let usage = user_store.model_request_usage()?;
|
||||||
|
|
||||||
|
@ -1769,7 +1752,6 @@ impl AgentPreview for MessageEditor {
|
||||||
) -> Option<AnyElement> {
|
) -> Option<AnyElement> {
|
||||||
if let Some(workspace) = workspace.upgrade() {
|
if let Some(workspace) = workspace.upgrade() {
|
||||||
let fs = workspace.read(cx).app_state().fs.clone();
|
let fs = workspace.read(cx).app_state().fs.clone();
|
||||||
let user_store = workspace.read(cx).app_state().user_store.clone();
|
|
||||||
let project = workspace.read(cx).project().clone();
|
let project = workspace.read(cx).project().clone();
|
||||||
let weak_project = project.downgrade();
|
let weak_project = project.downgrade();
|
||||||
let context_store = cx.new(|_cx| ContextStore::new(weak_project, None));
|
let context_store = cx.new(|_cx| ContextStore::new(weak_project, None));
|
||||||
|
@ -1782,7 +1764,6 @@ impl AgentPreview for MessageEditor {
|
||||||
MessageEditor::new(
|
MessageEditor::new(
|
||||||
fs,
|
fs,
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
user_store,
|
|
||||||
context_store,
|
context_store,
|
||||||
None,
|
None,
|
||||||
thread_store.downgrade(),
|
thread_store.downgrade(),
|
||||||
|
|
|
@ -10,6 +10,7 @@ use agent::{
|
||||||
use agent_settings::AgentSettings;
|
use agent_settings::AgentSettings;
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use client::telemetry::Telemetry;
|
use client::telemetry::Telemetry;
|
||||||
|
use cloud_llm_client::CompletionIntent;
|
||||||
use collections::{HashMap, VecDeque};
|
use collections::{HashMap, VecDeque};
|
||||||
use editor::{MultiBuffer, actions::SelectAll};
|
use editor::{MultiBuffer, actions::SelectAll};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
|
@ -27,7 +28,6 @@ use terminal_view::TerminalView;
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{Toast, Workspace, notifications::NotificationId};
|
use workspace::{Toast, Workspace, notifications::NotificationId};
|
||||||
use zed_llm_client::CompletionIntent;
|
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use client::{ModelRequestUsage, RequestUsage, zed_urls};
|
use client::{ModelRequestUsage, RequestUsage, zed_urls};
|
||||||
|
use cloud_llm_client::{Plan, UsageLimit};
|
||||||
use component::{empty_example, example_group_with_title, single_example};
|
use component::{empty_example, example_group_with_title, single_example};
|
||||||
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
|
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
|
||||||
use ui::{Callout, prelude::*};
|
use ui::{Callout, prelude::*};
|
||||||
use zed_llm_client::{Plan, UsageLimit};
|
|
||||||
|
|
||||||
#[derive(IntoElement, RegisterComponent)]
|
#[derive(IntoElement, RegisterComponent)]
|
||||||
pub struct UsageCallout {
|
pub struct UsageCallout {
|
||||||
|
|
|
@ -16,10 +16,10 @@ default = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
|
cloud_llm_client.workspace = true
|
||||||
component.workspace = true
|
component.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
language_model.workspace = true
|
language_model.workspace = true
|
||||||
proto.workspace = true
|
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
telemetry.workspace = true
|
telemetry.workspace = true
|
||||||
|
|
|
@ -136,10 +136,7 @@ impl RenderOnce for ApiKeysWithoutProviders {
|
||||||
.full_width()
|
.full_width()
|
||||||
.style(ButtonStyle::Outlined)
|
.style(ButtonStyle::Outlined)
|
||||||
.on_click(move |_, window, cx| {
|
.on_click(move |_, window, cx| {
|
||||||
window.dispatch_action(
|
window.dispatch_action(zed_actions::agent::OpenSettings.boxed_clone(), cx);
|
||||||
zed_actions::agent::OpenConfiguration.boxed_clone(),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use client::{Client, UserStore};
|
use client::{Client, UserStore};
|
||||||
|
use cloud_llm_client::Plan;
|
||||||
use gpui::{Entity, IntoElement, ParentElement};
|
use gpui::{Entity, IntoElement, ParentElement};
|
||||||
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
|
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
|
@ -56,15 +57,8 @@ impl AgentPanelOnboarding {
|
||||||
|
|
||||||
impl Render for AgentPanelOnboarding {
|
impl Render for AgentPanelOnboarding {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let enrolled_in_trial = matches!(
|
let enrolled_in_trial = self.user_store.read(cx).plan() == Some(Plan::ZedProTrial);
|
||||||
self.user_store.read(cx).current_plan(),
|
let is_pro_user = self.user_store.read(cx).plan() == Some(Plan::ZedPro);
|
||||||
Some(proto::Plan::ZedProTrial)
|
|
||||||
);
|
|
||||||
|
|
||||||
let is_pro_user = matches!(
|
|
||||||
self.user_store.read(cx).current_plan(),
|
|
||||||
Some(proto::Plan::ZedPro)
|
|
||||||
);
|
|
||||||
|
|
||||||
AgentPanelOnboardingCard::new()
|
AgentPanelOnboardingCard::new()
|
||||||
.child(
|
.child(
|
||||||
|
|
|
@ -9,6 +9,7 @@ pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProvider
|
||||||
pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
|
pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
|
||||||
pub use agent_panel_onboarding_content::AgentPanelOnboarding;
|
pub use agent_panel_onboarding_content::AgentPanelOnboarding;
|
||||||
pub use ai_upsell_card::AiUpsellCard;
|
pub use ai_upsell_card::AiUpsellCard;
|
||||||
|
use cloud_llm_client::Plan;
|
||||||
pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
|
pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
|
||||||
pub use young_account_banner::YoungAccountBanner;
|
pub use young_account_banner::YoungAccountBanner;
|
||||||
|
|
||||||
|
@ -79,7 +80,7 @@ impl From<client::Status> for SignInStatus {
|
||||||
pub struct ZedAiOnboarding {
|
pub struct ZedAiOnboarding {
|
||||||
pub sign_in_status: SignInStatus,
|
pub sign_in_status: SignInStatus,
|
||||||
pub has_accepted_terms_of_service: bool,
|
pub has_accepted_terms_of_service: bool,
|
||||||
pub plan: Option<proto::Plan>,
|
pub plan: Option<Plan>,
|
||||||
pub account_too_young: bool,
|
pub account_too_young: bool,
|
||||||
pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
|
pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||||
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||||
|
@ -99,8 +100,8 @@ impl ZedAiOnboarding {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
sign_in_status: status.into(),
|
sign_in_status: status.into(),
|
||||||
has_accepted_terms_of_service: store.current_user_has_accepted_terms().unwrap_or(false),
|
has_accepted_terms_of_service: store.has_accepted_terms_of_service(),
|
||||||
plan: store.current_plan(),
|
plan: store.plan(),
|
||||||
account_too_young: store.account_too_young(),
|
account_too_young: store.account_too_young(),
|
||||||
continue_with_zed_ai,
|
continue_with_zed_ai,
|
||||||
accept_terms_of_service: Arc::new({
|
accept_terms_of_service: Arc::new({
|
||||||
|
@ -113,11 +114,9 @@ impl ZedAiOnboarding {
|
||||||
sign_in: Arc::new(move |_window, cx| {
|
sign_in: Arc::new(move |_window, cx| {
|
||||||
cx.spawn({
|
cx.spawn({
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
async move |cx| {
|
async move |cx| client.sign_in_with_optional_connect(true, cx).await
|
||||||
client.authenticate_and_connect(true, cx).await;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.detach();
|
.detach_and_log_err(cx);
|
||||||
}),
|
}),
|
||||||
dismiss_onboarding: None,
|
dismiss_onboarding: None,
|
||||||
}
|
}
|
||||||
|
@ -411,9 +410,9 @@ impl RenderOnce for ZedAiOnboarding {
|
||||||
if matches!(self.sign_in_status, SignInStatus::SignedIn) {
|
if matches!(self.sign_in_status, SignInStatus::SignedIn) {
|
||||||
if self.has_accepted_terms_of_service {
|
if self.has_accepted_terms_of_service {
|
||||||
match self.plan {
|
match self.plan {
|
||||||
None | Some(proto::Plan::Free) => self.render_free_plan_state(cx),
|
None | Some(Plan::ZedFree) => self.render_free_plan_state(cx),
|
||||||
Some(proto::Plan::ZedProTrial) => self.render_trial_state(cx),
|
Some(Plan::ZedProTrial) => self.render_trial_state(cx),
|
||||||
Some(proto::Plan::ZedPro) => self.render_pro_plan_state(cx),
|
Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.render_accept_terms_of_service()
|
self.render_accept_terms_of_service()
|
||||||
|
@ -433,7 +432,7 @@ impl Component for ZedAiOnboarding {
|
||||||
fn onboarding(
|
fn onboarding(
|
||||||
sign_in_status: SignInStatus,
|
sign_in_status: SignInStatus,
|
||||||
has_accepted_terms_of_service: bool,
|
has_accepted_terms_of_service: bool,
|
||||||
plan: Option<proto::Plan>,
|
plan: Option<Plan>,
|
||||||
account_too_young: bool,
|
account_too_young: bool,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
ZedAiOnboarding {
|
ZedAiOnboarding {
|
||||||
|
@ -468,25 +467,15 @@ impl Component for ZedAiOnboarding {
|
||||||
),
|
),
|
||||||
single_example(
|
single_example(
|
||||||
"Free Plan",
|
"Free Plan",
|
||||||
onboarding(SignInStatus::SignedIn, true, Some(proto::Plan::Free), false),
|
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedFree), false),
|
||||||
),
|
),
|
||||||
single_example(
|
single_example(
|
||||||
"Pro Trial",
|
"Pro Trial",
|
||||||
onboarding(
|
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedProTrial), false),
|
||||||
SignInStatus::SignedIn,
|
|
||||||
true,
|
|
||||||
Some(proto::Plan::ZedProTrial),
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
single_example(
|
single_example(
|
||||||
"Pro Plan",
|
"Pro Plan",
|
||||||
onboarding(
|
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedPro), false),
|
||||||
SignInStatus::SignedIn,
|
|
||||||
true,
|
|
||||||
Some(proto::Plan::ZedPro),
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use client::{Client, zed_urls};
|
use client::{Client, zed_urls};
|
||||||
|
use cloud_llm_client::Plan;
|
||||||
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
|
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
|
||||||
use ui::{Divider, List, Vector, VectorName, prelude::*};
|
use ui::{Divider, List, Vector, VectorName, prelude::*};
|
||||||
|
|
||||||
|
@ -10,22 +11,22 @@ use crate::{BulletItem, SignInStatus};
|
||||||
pub struct AiUpsellCard {
|
pub struct AiUpsellCard {
|
||||||
pub sign_in_status: SignInStatus,
|
pub sign_in_status: SignInStatus,
|
||||||
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
|
||||||
|
pub user_plan: Option<Plan>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AiUpsellCard {
|
impl AiUpsellCard {
|
||||||
pub fn new(client: Arc<Client>) -> Self {
|
pub fn new(client: Arc<Client>, user_plan: Option<Plan>) -> Self {
|
||||||
let status = *client.status().borrow();
|
let status = *client.status().borrow();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
user_plan,
|
||||||
sign_in_status: status.into(),
|
sign_in_status: status.into(),
|
||||||
sign_in: Arc::new(move |_window, cx| {
|
sign_in: Arc::new(move |_window, cx| {
|
||||||
cx.spawn({
|
cx.spawn({
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
async move |cx| {
|
async move |cx| client.sign_in_with_optional_connect(true, cx).await
|
||||||
client.authenticate_and_connect(true, cx).await;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.detach();
|
.detach_and_log_err(cx);
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +35,7 @@ impl AiUpsellCard {
|
||||||
impl RenderOnce for AiUpsellCard {
|
impl RenderOnce for AiUpsellCard {
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let pro_section = v_flex()
|
let pro_section = v_flex()
|
||||||
|
.flex_grow()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
|
@ -56,6 +58,7 @@ impl RenderOnce for AiUpsellCard {
|
||||||
);
|
);
|
||||||
|
|
||||||
let free_section = v_flex()
|
let free_section = v_flex()
|
||||||
|
.flex_grow()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
|
@ -71,7 +74,7 @@ impl RenderOnce for AiUpsellCard {
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
List::new()
|
List::new()
|
||||||
.child(BulletItem::new("50 prompts with the Claude models"))
|
.child(BulletItem::new("50 prompts with Claude models"))
|
||||||
.child(BulletItem::new("2,000 accepted edit predictions")),
|
.child(BulletItem::new("2,000 accepted edit predictions")),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -132,22 +135,28 @@ impl RenderOnce for AiUpsellCard {
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.relative()
|
.relative()
|
||||||
.p_6()
|
.p_4()
|
||||||
.pt_4()
|
.pt_3()
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().colors().border)
|
.border_color(cx.theme().colors().border)
|
||||||
.rounded_lg()
|
.rounded_lg()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(grid_bg)
|
.child(grid_bg)
|
||||||
.child(gradient_bg)
|
.child(gradient_bg)
|
||||||
.child(Headline::new("Try Zed AI"))
|
.child(Label::new("Try Zed AI").size(LabelSize::Large))
|
||||||
.child(Label::new(DESCRIPTION).color(Color::Muted).mb_2())
|
.child(
|
||||||
|
div()
|
||||||
|
.max_w_3_4()
|
||||||
|
.mb_2()
|
||||||
|
.child(Label::new(DESCRIPTION).color(Color::Muted)),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
.w_full()
|
||||||
.mt_1p5()
|
.mt_1p5()
|
||||||
.mb_2p5()
|
.mb_2p5()
|
||||||
.items_start()
|
.items_start()
|
||||||
.gap_12()
|
.gap_6()
|
||||||
.child(free_section)
|
.child(free_section)
|
||||||
.child(pro_section),
|
.child(pro_section),
|
||||||
)
|
)
|
||||||
|
@ -183,6 +192,7 @@ impl Component for AiUpsellCard {
|
||||||
AiUpsellCard {
|
AiUpsellCard {
|
||||||
sign_in_status: SignInStatus::SignedOut,
|
sign_in_status: SignInStatus::SignedOut,
|
||||||
sign_in: Arc::new(|_, _| {}),
|
sign_in: Arc::new(|_, _| {}),
|
||||||
|
user_plan: None,
|
||||||
}
|
}
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
),
|
),
|
||||||
|
@ -191,6 +201,7 @@ impl Component for AiUpsellCard {
|
||||||
AiUpsellCard {
|
AiUpsellCard {
|
||||||
sign_in_status: SignInStatus::SignedIn,
|
sign_in_status: SignInStatus::SignedIn,
|
||||||
sign_in: Arc::new(|_, _| {}),
|
sign_in: Arc::new(|_, _| {}),
|
||||||
|
user_plan: None,
|
||||||
}
|
}
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
),
|
),
|
||||||
|
|
|
@ -19,6 +19,7 @@ assistant_slash_commands.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
clock.workspace = true
|
clock.workspace = true
|
||||||
|
cloud_llm_client.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
context_server.workspace = true
|
context_server.workspace = true
|
||||||
fs.workspace = true
|
fs.workspace = true
|
||||||
|
@ -48,7 +49,6 @@ util.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
zed_llm_client.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
indoc.workspace = true
|
indoc.workspace = true
|
||||||
|
|
|
@ -11,6 +11,7 @@ use assistant_slash_command::{
|
||||||
use assistant_slash_commands::FileCommandMetadata;
|
use assistant_slash_commands::FileCommandMetadata;
|
||||||
use client::{self, Client, proto, telemetry::Telemetry};
|
use client::{self, Client, proto, telemetry::Telemetry};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
|
use cloud_llm_client::CompletionIntent;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use fs::{Fs, RenameOptions};
|
use fs::{Fs, RenameOptions};
|
||||||
use futures::{FutureExt, StreamExt, future::Shared};
|
use futures::{FutureExt, StreamExt, future::Shared};
|
||||||
|
@ -46,7 +47,6 @@ use text::{BufferSnapshot, ToPoint};
|
||||||
use ui::IconName;
|
use ui::IconName;
|
||||||
use util::{ResultExt, TryFutureExt, post_inc};
|
use util::{ResultExt, TryFutureExt, post_inc};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use zed_llm_client::CompletionIntent;
|
|
||||||
|
|
||||||
pub use crate::context_store::*;
|
pub use crate::context_store::*;
|
||||||
|
|
||||||
|
|
|
@ -21,9 +21,11 @@ assistant_tool.workspace = true
|
||||||
buffer_diff.workspace = true
|
buffer_diff.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
|
cloud_llm_client.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
component.workspace = true
|
component.workspace = true
|
||||||
derive_more.workspace = true
|
derive_more.workspace = true
|
||||||
|
diffy = "0.4.2"
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
feature_flags.workspace = true
|
feature_flags.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
|
@ -63,8 +65,6 @@ web_search.workspace = true
|
||||||
which.workspace = true
|
which.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
zed_llm_client.workspace = true
|
|
||||||
diffy = "0.4.2"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
lsp = { workspace = true, features = ["test-support"] }
|
lsp = { workspace = true, features = ["test-support"] }
|
||||||
|
|
|
@ -7,6 +7,7 @@ mod streaming_fuzzy_matcher;
|
||||||
use crate::{Template, Templates};
|
use crate::{Template, Templates};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use assistant_tool::ActionLog;
|
use assistant_tool::ActionLog;
|
||||||
|
use cloud_llm_client::CompletionIntent;
|
||||||
use create_file_parser::{CreateFileParser, CreateFileParserEvent};
|
use create_file_parser::{CreateFileParser, CreateFileParserEvent};
|
||||||
pub use edit_parser::EditFormat;
|
pub use edit_parser::EditFormat;
|
||||||
use edit_parser::{EditParser, EditParserEvent, EditParserMetrics};
|
use edit_parser::{EditParser, EditParserEvent, EditParserMetrics};
|
||||||
|
@ -29,7 +30,6 @@ use std::{cmp, iter, mem, ops::Range, path::PathBuf, pin::Pin, sync::Arc, task::
|
||||||
use streaming_diff::{CharOperation, StreamingDiff};
|
use streaming_diff::{CharOperation, StreamingDiff};
|
||||||
use streaming_fuzzy_matcher::StreamingFuzzyMatcher;
|
use streaming_fuzzy_matcher::StreamingFuzzyMatcher;
|
||||||
use util::debug_panic;
|
use util::debug_panic;
|
||||||
use zed_llm_client::CompletionIntent;
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct CreateFilePromptTemplate {
|
struct CreateFilePromptTemplate {
|
||||||
|
|
|
@ -6,6 +6,7 @@ use anyhow::{Context as _, Result, anyhow};
|
||||||
use assistant_tool::{
|
use assistant_tool::{
|
||||||
ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
|
ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
|
||||||
};
|
};
|
||||||
|
use cloud_llm_client::{WebSearchResponse, WebSearchResult};
|
||||||
use futures::{Future, FutureExt, TryFutureExt};
|
use futures::{Future, FutureExt, TryFutureExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
|
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
|
||||||
|
@ -17,7 +18,6 @@ use serde::{Deserialize, Serialize};
|
||||||
use ui::{IconName, Tooltip, prelude::*};
|
use ui::{IconName, Tooltip, prelude::*};
|
||||||
use web_search::WebSearchRegistry;
|
use web_search::WebSearchRegistry;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
use zed_llm_client::{WebSearchResponse, WebSearchResult};
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct WebSearchToolInput {
|
pub struct WebSearchToolInput {
|
||||||
|
|
|
@ -18,6 +18,6 @@ collections.workspace = true
|
||||||
derive_more.workspace = true
|
derive_more.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
rodio = { version = "0.20.0", default-features = false, features = ["wav"] }
|
rodio = { version = "0.21.1", default-features = false, features = ["wav", "playback", "tracing"] }
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
|
|
|
@ -3,12 +3,9 @@ use std::{io::Cursor, sync::Arc};
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use gpui::{App, AssetSource, Global};
|
use gpui::{App, AssetSource, Global};
|
||||||
use rodio::{
|
use rodio::{Decoder, Source, source::Buffered};
|
||||||
Decoder, Source,
|
|
||||||
source::{Buffered, SamplesConverter},
|
|
||||||
};
|
|
||||||
|
|
||||||
type Sound = Buffered<SamplesConverter<Decoder<Cursor<Vec<u8>>>, f32>>;
|
type Sound = Buffered<Decoder<Cursor<Vec<u8>>>>;
|
||||||
|
|
||||||
pub struct SoundRegistry {
|
pub struct SoundRegistry {
|
||||||
cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
|
cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
|
||||||
|
@ -48,7 +45,7 @@ impl SoundRegistry {
|
||||||
.with_context(|| format!("No asset available for path {path}"))??
|
.with_context(|| format!("No asset available for path {path}"))??
|
||||||
.into_owned();
|
.into_owned();
|
||||||
let cursor = Cursor::new(bytes);
|
let cursor = Cursor::new(bytes);
|
||||||
let source = Decoder::new(cursor)?.convert_samples::<f32>().buffered();
|
let source = Decoder::new(cursor)?.buffered();
|
||||||
|
|
||||||
self.cache.lock().insert(name.to_string(), source.clone());
|
self.cache.lock().insert(name.to_string(), source.clone());
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use assets::SoundRegistry;
|
use assets::SoundRegistry;
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
use gpui::{App, AssetSource, BorrowAppContext, Global};
|
use gpui::{App, AssetSource, BorrowAppContext, Global};
|
||||||
use rodio::{OutputStream, OutputStreamHandle};
|
use rodio::{OutputStream, OutputStreamBuilder};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
mod assets;
|
mod assets;
|
||||||
|
@ -37,8 +37,7 @@ impl Sound {
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Audio {
|
pub struct Audio {
|
||||||
_output_stream: Option<OutputStream>,
|
output_handle: Option<OutputStream>,
|
||||||
output_handle: Option<OutputStreamHandle>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deref, DerefMut)]
|
#[derive(Deref, DerefMut)]
|
||||||
|
@ -51,11 +50,9 @@ impl Audio {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_output_exists(&mut self) -> Option<&OutputStreamHandle> {
|
fn ensure_output_exists(&mut self) -> Option<&OutputStream> {
|
||||||
if self.output_handle.is_none() {
|
if self.output_handle.is_none() {
|
||||||
let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
|
self.output_handle = OutputStreamBuilder::open_default_stream().log_err();
|
||||||
self.output_handle = output_handle;
|
|
||||||
self._output_stream = _output_stream;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.output_handle.as_ref()
|
self.output_handle.as_ref()
|
||||||
|
@ -69,7 +66,7 @@ impl Audio {
|
||||||
cx.update_global::<GlobalAudio, _>(|this, cx| {
|
cx.update_global::<GlobalAudio, _>(|this, cx| {
|
||||||
let output_handle = this.ensure_output_exists()?;
|
let output_handle = this.ensure_output_exists()?;
|
||||||
let source = SoundRegistry::global(cx).get(sound.file()).log_err()?;
|
let source = SoundRegistry::global(cx).get(sound.file()).log_err()?;
|
||||||
output_handle.play_raw(source).log_err()?;
|
output_handle.mixer().add(source);
|
||||||
Some(())
|
Some(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -80,7 +77,6 @@ impl Audio {
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.update_global::<GlobalAudio, _>(|this, _| {
|
cx.update_global::<GlobalAudio, _>(|this, _| {
|
||||||
this._output_stream.take();
|
|
||||||
this.output_handle.take();
|
this.output_handle.take();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,7 +126,7 @@ impl ChannelMembership {
|
||||||
proto::channel_member::Kind::Member => 0,
|
proto::channel_member::Kind::Member => 0,
|
||||||
proto::channel_member::Kind::Invitee => 1,
|
proto::channel_member::Kind::Invitee => 1,
|
||||||
},
|
},
|
||||||
username_order: self.user.github_login.as_str(),
|
username_order: &self.user.github_login,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -259,20 +259,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||||
assert_channels(&channel_store, &[(0, "the-channel".to_string())], cx);
|
assert_channels(&channel_store, &[(0, "the-channel".to_string())], cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
|
||||||
assert_eq!(get_users.payload.user_ids, vec![5]);
|
|
||||||
server.respond(
|
|
||||||
get_users.receipt(),
|
|
||||||
proto::UsersResponse {
|
|
||||||
users: vec![proto::User {
|
|
||||||
id: 5,
|
|
||||||
github_login: "nathansobo".into(),
|
|
||||||
avatar_url: "http://avatar.com/nathansobo".into(),
|
|
||||||
name: None,
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Join a channel and populate its existing messages.
|
// Join a channel and populate its existing messages.
|
||||||
let channel = channel_store.update(cx, |store, cx| {
|
let channel = channel_store.update(cx, |store, cx| {
|
||||||
let channel_id = store.ordered_channels().next().unwrap().1.id;
|
let channel_id = store.ordered_channels().next().unwrap().1.id;
|
||||||
|
@ -334,7 +320,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||||
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
&[
|
&[
|
||||||
("nathansobo".into(), "a".into()),
|
("user-5".into(), "a".into()),
|
||||||
("maxbrunsfeld".into(), "b".into())
|
("maxbrunsfeld".into(), "b".into())
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -437,7 +423,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||||
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
&[
|
&[
|
||||||
("nathansobo".into(), "y".into()),
|
("user-5".into(), "y".into()),
|
||||||
("maxbrunsfeld".into(), "z".into())
|
("maxbrunsfeld".into(), "z".into())
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,11 +17,12 @@ test-support = ["clock/test-support", "collections/test-support", "gpui/test-sup
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
async-recursion = "0.3"
|
|
||||||
async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manual-roots"] }
|
async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manual-roots"] }
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
chrono = { workspace = true, features = ["serde"] }
|
chrono = { workspace = true, features = ["serde"] }
|
||||||
clock.workspace = true
|
clock.workspace = true
|
||||||
|
cloud_api_client.workspace = true
|
||||||
|
cloud_llm_client.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
credentials_provider.workspace = true
|
credentials_provider.workspace = true
|
||||||
derive_more.workspace = true
|
derive_more.workspace = true
|
||||||
|
@ -33,8 +34,8 @@ http_client.workspace = true
|
||||||
http_client_tls.workspace = true
|
http_client_tls.workspace = true
|
||||||
httparse = "1.10"
|
httparse = "1.10"
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
paths.workspace = true
|
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
|
paths.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
|
@ -46,19 +47,18 @@ serde_json.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
|
telemetry.workspace = true
|
||||||
telemetry_events.workspace = true
|
telemetry_events.workspace = true
|
||||||
text.workspace = true
|
text.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
time.workspace = true
|
time.workspace = true
|
||||||
tiny_http.workspace = true
|
tiny_http.workspace = true
|
||||||
tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io"] }
|
tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io"] }
|
||||||
|
tokio.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
worktree.workspace = true
|
|
||||||
telemetry.workspace = true
|
|
||||||
tokio.workspace = true
|
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
zed_llm_client.workspace = true
|
worktree.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
clock = { workspace = true, features = ["test-support"] }
|
clock = { workspace = true, features = ["test-support"] }
|
||||||
|
|
|
@ -6,22 +6,21 @@ pub mod telemetry;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod zed_urls;
|
pub mod zed_urls;
|
||||||
|
|
||||||
use anyhow::{Context as _, Result, anyhow, bail};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use async_recursion::async_recursion;
|
|
||||||
use async_tungstenite::tungstenite::{
|
use async_tungstenite::tungstenite::{
|
||||||
client::IntoClientRequest,
|
client::IntoClientRequest,
|
||||||
error::Error as WebsocketError,
|
error::Error as WebsocketError,
|
||||||
http::{HeaderValue, Request, StatusCode},
|
http::{HeaderValue, Request, StatusCode},
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use clock::SystemClock;
|
use clock::SystemClock;
|
||||||
|
use cloud_api_client::CloudApiClient;
|
||||||
use credentials_provider::CredentialsProvider;
|
use credentials_provider::CredentialsProvider;
|
||||||
use futures::{
|
use futures::{
|
||||||
AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt,
|
AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt,
|
||||||
channel::oneshot, future::BoxFuture,
|
channel::oneshot, future::BoxFuture,
|
||||||
};
|
};
|
||||||
use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions};
|
use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions};
|
||||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
use http_client::{HttpClient, HttpClientWithUrl, http};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use postage::watch;
|
use postage::watch;
|
||||||
use proxy::connect_proxy_stream;
|
use proxy::connect_proxy_stream;
|
||||||
|
@ -31,7 +30,6 @@ use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsSources};
|
use settings::{Settings, SettingsSources};
|
||||||
use std::pin::Pin;
|
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
convert::TryFrom,
|
convert::TryFrom,
|
||||||
|
@ -45,6 +43,7 @@ use std::{
|
||||||
},
|
},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
use std::{cmp, pin::Pin};
|
||||||
use telemetry::Telemetry;
|
use telemetry::Telemetry;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tokio::net::TcpStream;
|
use tokio::net::TcpStream;
|
||||||
|
@ -78,7 +77,7 @@ pub static ZED_ALWAYS_ACTIVE: LazyLock<bool> =
|
||||||
LazyLock::new(|| std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty()));
|
LazyLock::new(|| std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty()));
|
||||||
|
|
||||||
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(500);
|
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(500);
|
||||||
pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(10);
|
pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(30);
|
||||||
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(20);
|
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(20);
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
|
@ -162,20 +161,8 @@ pub fn init(client: &Arc<Client>, cx: &mut App) {
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
move |_: &SignIn, cx| {
|
move |_: &SignIn, cx| {
|
||||||
if let Some(client) = client.upgrade() {
|
if let Some(client) = client.upgrade() {
|
||||||
cx.spawn(
|
cx.spawn(async move |cx| client.sign_in_with_optional_connect(true, &cx).await)
|
||||||
async move |cx| match client.authenticate_and_connect(true, &cx).await {
|
.detach_and_log_err(cx);
|
||||||
ConnectionResult::Timeout => {
|
|
||||||
log::error!("Initial authentication timed out");
|
|
||||||
}
|
|
||||||
ConnectionResult::ConnectionReset => {
|
|
||||||
log::error!("Initial authentication connection reset");
|
|
||||||
}
|
|
||||||
ConnectionResult::Result(r) => {
|
|
||||||
r.log_err();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.detach();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -213,6 +200,7 @@ pub struct Client {
|
||||||
id: AtomicU64,
|
id: AtomicU64,
|
||||||
peer: Arc<Peer>,
|
peer: Arc<Peer>,
|
||||||
http: Arc<HttpClientWithUrl>,
|
http: Arc<HttpClientWithUrl>,
|
||||||
|
cloud_client: Arc<CloudApiClient>,
|
||||||
telemetry: Arc<Telemetry>,
|
telemetry: Arc<Telemetry>,
|
||||||
credentials_provider: ClientCredentialsProvider,
|
credentials_provider: ClientCredentialsProvider,
|
||||||
state: RwLock<ClientState>,
|
state: RwLock<ClientState>,
|
||||||
|
@ -283,6 +271,8 @@ pub enum Status {
|
||||||
SignedOut,
|
SignedOut,
|
||||||
UpgradeRequired,
|
UpgradeRequired,
|
||||||
Authenticating,
|
Authenticating,
|
||||||
|
Authenticated,
|
||||||
|
AuthenticationError,
|
||||||
Connecting,
|
Connecting,
|
||||||
ConnectionError,
|
ConnectionError,
|
||||||
Connected {
|
Connected {
|
||||||
|
@ -586,6 +576,7 @@ impl Client {
|
||||||
id: AtomicU64::new(0),
|
id: AtomicU64::new(0),
|
||||||
peer: Peer::new(0),
|
peer: Peer::new(0),
|
||||||
telemetry: Telemetry::new(clock, http.clone(), cx),
|
telemetry: Telemetry::new(clock, http.clone(), cx),
|
||||||
|
cloud_client: Arc::new(CloudApiClient::new(http.clone())),
|
||||||
http,
|
http,
|
||||||
credentials_provider: ClientCredentialsProvider::new(cx),
|
credentials_provider: ClientCredentialsProvider::new(cx),
|
||||||
state: Default::default(),
|
state: Default::default(),
|
||||||
|
@ -618,6 +609,10 @@ impl Client {
|
||||||
self.http.clone()
|
self.http.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn cloud_client(&self) -> Arc<CloudApiClient> {
|
||||||
|
self.cloud_client.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_id(&self, id: u64) -> &Self {
|
pub fn set_id(&self, id: u64) -> &Self {
|
||||||
self.id.store(id, Ordering::SeqCst);
|
self.id.store(id, Ordering::SeqCst);
|
||||||
self
|
self
|
||||||
|
@ -704,7 +699,7 @@ impl Client {
|
||||||
|
|
||||||
let mut delay = INITIAL_RECONNECTION_DELAY;
|
let mut delay = INITIAL_RECONNECTION_DELAY;
|
||||||
loop {
|
loop {
|
||||||
match client.authenticate_and_connect(true, &cx).await {
|
match client.connect(true, &cx).await {
|
||||||
ConnectionResult::Timeout => {
|
ConnectionResult::Timeout => {
|
||||||
log::error!("client connect attempt timed out")
|
log::error!("client connect attempt timed out")
|
||||||
}
|
}
|
||||||
|
@ -727,11 +722,10 @@ impl Client {
|
||||||
},
|
},
|
||||||
&cx,
|
&cx,
|
||||||
);
|
);
|
||||||
cx.background_executor().timer(delay).await;
|
let jitter =
|
||||||
delay = delay
|
Duration::from_millis(rng.gen_range(0..delay.as_millis() as u64));
|
||||||
.mul_f32(rng.gen_range(0.5..=2.5))
|
cx.background_executor().timer(delay + jitter).await;
|
||||||
.max(INITIAL_RECONNECTION_DELAY)
|
delay = cmp::min(delay * 2, MAX_RECONNECTION_DELAY);
|
||||||
.min(MAX_RECONNECTION_DELAY);
|
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -875,17 +869,122 @@ impl Client {
|
||||||
.is_some()
|
.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_recursion(?Send)]
|
pub async fn sign_in(
|
||||||
pub async fn authenticate_and_connect(
|
self: &Arc<Self>,
|
||||||
|
try_provider: bool,
|
||||||
|
cx: &AsyncApp,
|
||||||
|
) -> Result<Credentials> {
|
||||||
|
if self.status().borrow().is_signed_out() {
|
||||||
|
self.set_status(Status::Authenticating, cx);
|
||||||
|
} else {
|
||||||
|
self.set_status(Status::Reauthenticating, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut credentials = None;
|
||||||
|
|
||||||
|
let old_credentials = self.state.read().credentials.clone();
|
||||||
|
if let Some(old_credentials) = old_credentials {
|
||||||
|
self.cloud_client.set_credentials(
|
||||||
|
old_credentials.user_id as u32,
|
||||||
|
old_credentials.access_token.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch the authenticated user with the old credentials, to ensure they are still valid.
|
||||||
|
if self.cloud_client.get_authenticated_user().await.is_ok() {
|
||||||
|
credentials = Some(old_credentials);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if credentials.is_none() && try_provider {
|
||||||
|
if let Some(stored_credentials) = self.credentials_provider.read_credentials(cx).await {
|
||||||
|
self.cloud_client.set_credentials(
|
||||||
|
stored_credentials.user_id as u32,
|
||||||
|
stored_credentials.access_token.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch the authenticated user with the stored credentials, and
|
||||||
|
// clear them from the credentials provider if that fails.
|
||||||
|
if self.cloud_client.get_authenticated_user().await.is_ok() {
|
||||||
|
credentials = Some(stored_credentials);
|
||||||
|
} else {
|
||||||
|
self.credentials_provider
|
||||||
|
.delete_credentials(cx)
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if credentials.is_none() {
|
||||||
|
let mut status_rx = self.status();
|
||||||
|
let _ = status_rx.next().await;
|
||||||
|
futures::select_biased! {
|
||||||
|
authenticate = self.authenticate(cx).fuse() => {
|
||||||
|
match authenticate {
|
||||||
|
Ok(creds) => {
|
||||||
|
if IMPERSONATE_LOGIN.is_none() {
|
||||||
|
self.credentials_provider
|
||||||
|
.write_credentials(creds.user_id, creds.access_token.clone(), cx)
|
||||||
|
.await
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
credentials = Some(creds);
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
self.set_status(Status::AuthenticationError, cx);
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = status_rx.next().fuse() => {
|
||||||
|
return Err(anyhow!("authentication canceled"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let credentials = credentials.unwrap();
|
||||||
|
self.set_id(credentials.user_id);
|
||||||
|
self.cloud_client
|
||||||
|
.set_credentials(credentials.user_id as u32, credentials.access_token.clone());
|
||||||
|
self.state.write().credentials = Some(credentials.clone());
|
||||||
|
self.set_status(Status::Authenticated, cx);
|
||||||
|
|
||||||
|
Ok(credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs a sign-in and also connects to Collab.
|
||||||
|
///
|
||||||
|
/// This is called in places where we *don't* need to connect in the future. We will replace these calls with calls
|
||||||
|
/// to `sign_in` when we're ready to remove auto-connection to Collab.
|
||||||
|
pub async fn sign_in_with_optional_connect(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
try_provider: bool,
|
||||||
|
cx: &AsyncApp,
|
||||||
|
) -> Result<()> {
|
||||||
|
let credentials = self.sign_in(try_provider, cx).await?;
|
||||||
|
|
||||||
|
let connect_result = match self.connect_with_credentials(credentials, cx).await {
|
||||||
|
ConnectionResult::Timeout => Err(anyhow!("connection timed out")),
|
||||||
|
ConnectionResult::ConnectionReset => Err(anyhow!("connection reset")),
|
||||||
|
ConnectionResult::Result(result) => result.context("client auth and connect"),
|
||||||
|
};
|
||||||
|
connect_result.log_err();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn connect(
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
try_provider: bool,
|
try_provider: bool,
|
||||||
cx: &AsyncApp,
|
cx: &AsyncApp,
|
||||||
) -> ConnectionResult<()> {
|
) -> ConnectionResult<()> {
|
||||||
let was_disconnected = match *self.status().borrow() {
|
let was_disconnected = match *self.status().borrow() {
|
||||||
Status::SignedOut => true,
|
Status::SignedOut | Status::Authenticated => true,
|
||||||
Status::ConnectionError
|
Status::ConnectionError
|
||||||
| Status::ConnectionLost
|
| Status::ConnectionLost
|
||||||
| Status::Authenticating { .. }
|
| Status::Authenticating { .. }
|
||||||
|
| Status::AuthenticationError
|
||||||
| Status::Reauthenticating { .. }
|
| Status::Reauthenticating { .. }
|
||||||
| Status::ReconnectionError { .. } => false,
|
| Status::ReconnectionError { .. } => false,
|
||||||
Status::Connected { .. } | Status::Connecting { .. } | Status::Reconnecting { .. } => {
|
Status::Connected { .. } | Status::Connecting { .. } | Status::Reconnecting { .. } => {
|
||||||
|
@ -898,39 +997,10 @@ impl Client {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if was_disconnected {
|
let credentials = match self.sign_in(try_provider, cx).await {
|
||||||
self.set_status(Status::Authenticating, cx);
|
Ok(credentials) => credentials,
|
||||||
} else {
|
Err(err) => return ConnectionResult::Result(Err(err)),
|
||||||
self.set_status(Status::Reauthenticating, cx)
|
};
|
||||||
}
|
|
||||||
|
|
||||||
let mut read_from_provider = false;
|
|
||||||
let mut credentials = self.state.read().credentials.clone();
|
|
||||||
if credentials.is_none() && try_provider {
|
|
||||||
credentials = self.credentials_provider.read_credentials(cx).await;
|
|
||||||
read_from_provider = credentials.is_some();
|
|
||||||
}
|
|
||||||
|
|
||||||
if credentials.is_none() {
|
|
||||||
let mut status_rx = self.status();
|
|
||||||
let _ = status_rx.next().await;
|
|
||||||
futures::select_biased! {
|
|
||||||
authenticate = self.authenticate(cx).fuse() => {
|
|
||||||
match authenticate {
|
|
||||||
Ok(creds) => credentials = Some(creds),
|
|
||||||
Err(err) => {
|
|
||||||
self.set_status(Status::ConnectionError, cx);
|
|
||||||
return ConnectionResult::Result(Err(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ = status_rx.next().fuse() => {
|
|
||||||
return ConnectionResult::Result(Err(anyhow!("authentication canceled")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let credentials = credentials.unwrap();
|
|
||||||
self.set_id(credentials.user_id);
|
|
||||||
|
|
||||||
if was_disconnected {
|
if was_disconnected {
|
||||||
self.set_status(Status::Connecting, cx);
|
self.set_status(Status::Connecting, cx);
|
||||||
|
@ -938,17 +1008,20 @@ impl Client {
|
||||||
self.set_status(Status::Reconnecting, cx);
|
self.set_status(Status::Reconnecting, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.connect_with_credentials(credentials, cx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn connect_with_credentials(
|
||||||
|
self: &Arc<Self>,
|
||||||
|
credentials: Credentials,
|
||||||
|
cx: &AsyncApp,
|
||||||
|
) -> ConnectionResult<()> {
|
||||||
let mut timeout =
|
let mut timeout =
|
||||||
futures::FutureExt::fuse(cx.background_executor().timer(CONNECTION_TIMEOUT));
|
futures::FutureExt::fuse(cx.background_executor().timer(CONNECTION_TIMEOUT));
|
||||||
futures::select_biased! {
|
futures::select_biased! {
|
||||||
connection = self.establish_connection(&credentials, cx).fuse() => {
|
connection = self.establish_connection(&credentials, cx).fuse() => {
|
||||||
match connection {
|
match connection {
|
||||||
Ok(conn) => {
|
Ok(conn) => {
|
||||||
self.state.write().credentials = Some(credentials.clone());
|
|
||||||
if !read_from_provider && IMPERSONATE_LOGIN.is_none() {
|
|
||||||
self.credentials_provider.write_credentials(credentials.user_id, credentials.access_token, cx).await.log_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
futures::select_biased! {
|
futures::select_biased! {
|
||||||
result = self.set_connection(conn, cx).fuse() => {
|
result = self.set_connection(conn, cx).fuse() => {
|
||||||
match result.context("client auth and connect") {
|
match result.context("client auth and connect") {
|
||||||
|
@ -966,15 +1039,8 @@ impl Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(EstablishConnectionError::Unauthorized) => {
|
Err(EstablishConnectionError::Unauthorized) => {
|
||||||
self.state.write().credentials.take();
|
self.set_status(Status::ConnectionError, cx);
|
||||||
if read_from_provider {
|
ConnectionResult::Result(Err(EstablishConnectionError::Unauthorized).context("client auth and connect"))
|
||||||
self.credentials_provider.delete_credentials(cx).await.log_err();
|
|
||||||
self.set_status(Status::SignedOut, cx);
|
|
||||||
self.authenticate_and_connect(false, cx).await
|
|
||||||
} else {
|
|
||||||
self.set_status(Status::ConnectionError, cx);
|
|
||||||
ConnectionResult::Result(Err(EstablishConnectionError::Unauthorized).context("client auth and connect"))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(EstablishConnectionError::UpgradeRequired) => {
|
Err(EstablishConnectionError::UpgradeRequired) => {
|
||||||
self.set_status(Status::UpgradeRequired, cx);
|
self.set_status(Status::UpgradeRequired, cx);
|
||||||
|
@ -1138,7 +1204,7 @@ impl Client {
|
||||||
.to_str()
|
.to_str()
|
||||||
.map_err(EstablishConnectionError::other)?
|
.map_err(EstablishConnectionError::other)?
|
||||||
.to_string();
|
.to_string();
|
||||||
Url::parse(&collab_url).with_context(|| format!("parsing colab rpc url {collab_url}"))
|
Url::parse(&collab_url).with_context(|| format!("parsing collab rpc url {collab_url}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1158,6 +1224,7 @@ impl Client {
|
||||||
|
|
||||||
let http = self.http.clone();
|
let http = self.http.clone();
|
||||||
let proxy = http.proxy().cloned();
|
let proxy = http.proxy().cloned();
|
||||||
|
let user_agent = http.user_agent().cloned();
|
||||||
let credentials = credentials.clone();
|
let credentials = credentials.clone();
|
||||||
let rpc_url = self.rpc_url(http, release_channel);
|
let rpc_url = self.rpc_url(http, release_channel);
|
||||||
let system_id = self.telemetry.system_id();
|
let system_id = self.telemetry.system_id();
|
||||||
|
@ -1209,7 +1276,7 @@ impl Client {
|
||||||
// We then modify the request to add our desired headers.
|
// We then modify the request to add our desired headers.
|
||||||
let request_headers = request.headers_mut();
|
let request_headers = request.headers_mut();
|
||||||
request_headers.insert(
|
request_headers.insert(
|
||||||
"Authorization",
|
http::header::AUTHORIZATION,
|
||||||
HeaderValue::from_str(&credentials.authorization_header())?,
|
HeaderValue::from_str(&credentials.authorization_header())?,
|
||||||
);
|
);
|
||||||
request_headers.insert(
|
request_headers.insert(
|
||||||
|
@ -1221,6 +1288,9 @@ impl Client {
|
||||||
"x-zed-release-channel",
|
"x-zed-release-channel",
|
||||||
HeaderValue::from_str(release_channel.map(|r| r.dev_name()).unwrap_or("unknown"))?,
|
HeaderValue::from_str(release_channel.map(|r| r.dev_name()).unwrap_or("unknown"))?,
|
||||||
);
|
);
|
||||||
|
if let Some(user_agent) = user_agent {
|
||||||
|
request_headers.insert(http::header::USER_AGENT, user_agent);
|
||||||
|
}
|
||||||
if let Some(system_id) = system_id {
|
if let Some(system_id) = system_id {
|
||||||
request_headers.insert("x-zed-system-id", HeaderValue::from_str(&system_id)?);
|
request_headers.insert("x-zed-system-id", HeaderValue::from_str(&system_id)?);
|
||||||
}
|
}
|
||||||
|
@ -1365,96 +1435,31 @@ impl Client {
|
||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
http: Arc<HttpClientWithUrl>,
|
http: Arc<HttpClientWithUrl>,
|
||||||
login: String,
|
login: String,
|
||||||
mut api_token: String,
|
api_token: String,
|
||||||
) -> Result<Credentials> {
|
) -> Result<Credentials> {
|
||||||
#[derive(Deserialize)]
|
#[derive(Serialize)]
|
||||||
struct AuthenticatedUserResponse {
|
struct ImpersonateUserBody {
|
||||||
user: User,
|
github_login: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct User {
|
struct ImpersonateUserResponse {
|
||||||
id: u64,
|
user_id: u64,
|
||||||
|
access_token: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
let github_user = {
|
let url = self
|
||||||
#[derive(Deserialize)]
|
.http
|
||||||
struct GithubUser {
|
.build_zed_cloud_url("/internal/users/impersonate", &[])?;
|
||||||
id: i32,
|
let request = Request::post(url.as_str())
|
||||||
login: String,
|
.header("Content-Type", "application/json")
|
||||||
created_at: DateTime<Utc>,
|
.header("Authorization", format!("Bearer {api_token}"))
|
||||||
}
|
.body(
|
||||||
|
serde_json::to_string(&ImpersonateUserBody {
|
||||||
let request = {
|
github_login: login,
|
||||||
let mut request_builder =
|
})?
|
||||||
Request::get(&format!("https://api.github.com/users/{login}"));
|
.into(),
|
||||||
if let Ok(github_token) = std::env::var("GITHUB_TOKEN") {
|
)?;
|
||||||
request_builder =
|
|
||||||
request_builder.header("Authorization", format!("Bearer {}", github_token));
|
|
||||||
}
|
|
||||||
|
|
||||||
request_builder.body(AsyncBody::empty())?
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut response = http
|
|
||||||
.send(request)
|
|
||||||
.await
|
|
||||||
.context("error fetching GitHub user")?;
|
|
||||||
|
|
||||||
let mut body = Vec::new();
|
|
||||||
response
|
|
||||||
.body_mut()
|
|
||||||
.read_to_end(&mut body)
|
|
||||||
.await
|
|
||||||
.context("error reading GitHub user")?;
|
|
||||||
|
|
||||||
if !response.status().is_success() {
|
|
||||||
let text = String::from_utf8_lossy(body.as_slice());
|
|
||||||
bail!(
|
|
||||||
"status error {}, response: {text:?}",
|
|
||||||
response.status().as_u16()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
serde_json::from_slice::<GithubUser>(body.as_slice()).map_err(|err| {
|
|
||||||
log::error!("Error deserializing: {:?}", err);
|
|
||||||
log::error!(
|
|
||||||
"GitHub API response text: {:?}",
|
|
||||||
String::from_utf8_lossy(body.as_slice())
|
|
||||||
);
|
|
||||||
anyhow!("error deserializing GitHub user")
|
|
||||||
})?
|
|
||||||
};
|
|
||||||
|
|
||||||
let query_params = [
|
|
||||||
("github_login", &github_user.login),
|
|
||||||
("github_user_id", &github_user.id.to_string()),
|
|
||||||
(
|
|
||||||
"github_user_created_at",
|
|
||||||
&github_user.created_at.to_rfc3339(),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Use the collab server's admin API to retrieve the ID
|
|
||||||
// of the impersonated user.
|
|
||||||
let mut url = self.rpc_url(http.clone(), None).await?;
|
|
||||||
url.set_path("/user");
|
|
||||||
url.set_query(Some(
|
|
||||||
&query_params
|
|
||||||
.iter()
|
|
||||||
.map(|(key, value)| {
|
|
||||||
format!(
|
|
||||||
"{}={}",
|
|
||||||
key,
|
|
||||||
url::form_urlencoded::byte_serialize(value.as_bytes()).collect::<String>()
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("&"),
|
|
||||||
));
|
|
||||||
let request: http_client::Request<AsyncBody> = Request::get(url.as_str())
|
|
||||||
.header("Authorization", format!("token {api_token}"))
|
|
||||||
.body("".into())?;
|
|
||||||
|
|
||||||
let mut response = http.send(request).await?;
|
let mut response = http.send(request).await?;
|
||||||
let mut body = String::new();
|
let mut body = String::new();
|
||||||
|
@ -1465,18 +1470,17 @@ impl Client {
|
||||||
response.status().as_u16(),
|
response.status().as_u16(),
|
||||||
body,
|
body,
|
||||||
);
|
);
|
||||||
let response: AuthenticatedUserResponse = serde_json::from_str(&body)?;
|
let response: ImpersonateUserResponse = serde_json::from_str(&body)?;
|
||||||
|
|
||||||
// Use the admin API token to authenticate as the impersonated user.
|
|
||||||
api_token.insert_str(0, "ADMIN_TOKEN:");
|
|
||||||
Ok(Credentials {
|
Ok(Credentials {
|
||||||
user_id: response.user.id,
|
user_id: response.user_id,
|
||||||
access_token: api_token,
|
access_token: response.access_token,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn sign_out(self: &Arc<Self>, cx: &AsyncApp) {
|
pub async fn sign_out(self: &Arc<Self>, cx: &AsyncApp) {
|
||||||
self.state.write().credentials = None;
|
self.state.write().credentials = None;
|
||||||
|
self.cloud_client.clear_credentials();
|
||||||
self.disconnect(cx);
|
self.disconnect(cx);
|
||||||
|
|
||||||
if self.has_credentials(cx).await {
|
if self.has_credentials(cx).await {
|
||||||
|
@ -1786,7 +1790,7 @@ mod tests {
|
||||||
});
|
});
|
||||||
let auth_and_connect = cx.spawn({
|
let auth_and_connect = cx.spawn({
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
|cx| async move { client.authenticate_and_connect(false, &cx).await }
|
|cx| async move { client.connect(false, &cx).await }
|
||||||
});
|
});
|
||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
assert!(matches!(status.next().await, Some(Status::Connecting)));
|
assert!(matches!(status.next().await, Some(Status::Connecting)));
|
||||||
|
@ -1863,7 +1867,7 @@ mod tests {
|
||||||
|
|
||||||
let _authenticate = cx.spawn({
|
let _authenticate = cx.spawn({
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
move |cx| async move { client.authenticate_and_connect(false, &cx).await }
|
move |cx| async move { client.connect(false, &cx).await }
|
||||||
});
|
});
|
||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
assert_eq!(*auth_count.lock(), 1);
|
assert_eq!(*auth_count.lock(), 1);
|
||||||
|
@ -1871,7 +1875,7 @@ mod tests {
|
||||||
|
|
||||||
let _authenticate = cx.spawn({
|
let _authenticate = cx.spawn({
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
|cx| async move { client.authenticate_and_connect(false, &cx).await }
|
|cx| async move { client.connect(false, &cx).await }
|
||||||
});
|
});
|
||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
assert_eq!(*auth_count.lock(), 2);
|
assert_eq!(*auth_count.lock(), 2);
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
|
use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use chrono::Duration;
|
use chrono::Duration;
|
||||||
|
use cloud_api_client::{AuthenticatedUser, GetAuthenticatedUserResponse, PlanInfo};
|
||||||
|
use cloud_llm_client::{CurrentUsage, Plan, UsageData, UsageLimit};
|
||||||
use futures::{StreamExt, stream::BoxStream};
|
use futures::{StreamExt, stream::BoxStream};
|
||||||
use gpui::{AppContext as _, BackgroundExecutor, Entity, TestAppContext};
|
use gpui::{AppContext as _, BackgroundExecutor, Entity, TestAppContext};
|
||||||
|
use http_client::{AsyncBody, Method, Request, http};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rpc::{
|
use rpc::{
|
||||||
ConnectionId, Peer, Receipt, TypedEnvelope,
|
ConnectionId, Peer, Receipt, TypedEnvelope,
|
||||||
|
@ -39,6 +42,44 @@ impl FakeServer {
|
||||||
executor: cx.executor(),
|
executor: cx.executor(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
client.http_client().as_fake().replace_handler({
|
||||||
|
let state = server.state.clone();
|
||||||
|
move |old_handler, req| {
|
||||||
|
let state = state.clone();
|
||||||
|
let old_handler = old_handler.clone();
|
||||||
|
async move {
|
||||||
|
match (req.method(), req.uri().path()) {
|
||||||
|
(&Method::GET, "/client/users/me") => {
|
||||||
|
let credentials = parse_authorization_header(&req);
|
||||||
|
if credentials
|
||||||
|
!= Some(Credentials {
|
||||||
|
user_id: client_user_id,
|
||||||
|
access_token: state.lock().access_token.to_string(),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Ok(http_client::Response::builder()
|
||||||
|
.status(401)
|
||||||
|
.body("Unauthorized".into())
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(http_client::Response::builder()
|
||||||
|
.status(200)
|
||||||
|
.body(
|
||||||
|
serde_json::to_string(&make_get_authenticated_user_response(
|
||||||
|
client_user_id as i32,
|
||||||
|
format!("user-{client_user_id}"),
|
||||||
|
))
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
_ => old_handler(req).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
client
|
client
|
||||||
.override_authenticate({
|
.override_authenticate({
|
||||||
let state = Arc::downgrade(&server.state);
|
let state = Arc::downgrade(&server.state);
|
||||||
|
@ -105,7 +146,7 @@ impl FakeServer {
|
||||||
});
|
});
|
||||||
|
|
||||||
client
|
client
|
||||||
.authenticate_and_connect(false, &cx.to_async())
|
.connect(false, &cx.to_async())
|
||||||
.await
|
.await
|
||||||
.into_response()
|
.into_response()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -223,3 +264,54 @@ impl Drop for FakeServer {
|
||||||
self.disconnect();
|
self.disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_authorization_header(req: &Request<AsyncBody>) -> Option<Credentials> {
|
||||||
|
let mut auth_header = req
|
||||||
|
.headers()
|
||||||
|
.get(http::header::AUTHORIZATION)?
|
||||||
|
.to_str()
|
||||||
|
.ok()?
|
||||||
|
.split_whitespace();
|
||||||
|
let user_id = auth_header.next()?.parse().ok()?;
|
||||||
|
let access_token = auth_header.next()?;
|
||||||
|
Some(Credentials {
|
||||||
|
user_id,
|
||||||
|
access_token: access_token.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_get_authenticated_user_response(
|
||||||
|
user_id: i32,
|
||||||
|
github_login: String,
|
||||||
|
) -> GetAuthenticatedUserResponse {
|
||||||
|
GetAuthenticatedUserResponse {
|
||||||
|
user: AuthenticatedUser {
|
||||||
|
id: user_id,
|
||||||
|
metrics_id: format!("metrics-id-{user_id}"),
|
||||||
|
avatar_url: "".to_string(),
|
||||||
|
github_login,
|
||||||
|
name: None,
|
||||||
|
is_staff: false,
|
||||||
|
accepted_tos_at: None,
|
||||||
|
},
|
||||||
|
feature_flags: vec![],
|
||||||
|
plan: PlanInfo {
|
||||||
|
plan: Plan::ZedPro,
|
||||||
|
subscription_period: None,
|
||||||
|
usage: CurrentUsage {
|
||||||
|
model_requests: UsageData {
|
||||||
|
used: 0,
|
||||||
|
limit: UsageLimit::Limited(500),
|
||||||
|
},
|
||||||
|
edit_predictions: UsageData {
|
||||||
|
used: 250,
|
||||||
|
limit: UsageLimit::Unlimited,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
trial_started_at: None,
|
||||||
|
is_usage_based_billing_enabled: false,
|
||||||
|
is_account_too_young: false,
|
||||||
|
has_overdue_invoices: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
use super::{Client, Status, TypedEnvelope, proto};
|
use super::{Client, Status, TypedEnvelope, proto};
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use cloud_api_client::{GetAuthenticatedUserResponse, PlanInfo};
|
||||||
|
use cloud_llm_client::{
|
||||||
|
EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME, EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME,
|
||||||
|
MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME, MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME, UsageLimit,
|
||||||
|
};
|
||||||
use collections::{HashMap, HashSet, hash_map::Entry};
|
use collections::{HashMap, HashSet, hash_map::Entry};
|
||||||
use derive_more::Deref;
|
use derive_more::Deref;
|
||||||
use feature_flags::FeatureFlagAppExt;
|
use feature_flags::FeatureFlagAppExt;
|
||||||
|
@ -16,11 +21,7 @@ use std::{
|
||||||
sync::{Arc, Weak},
|
sync::{Arc, Weak},
|
||||||
};
|
};
|
||||||
use text::ReplicaId;
|
use text::ReplicaId;
|
||||||
use util::{TryFutureExt as _, maybe};
|
use util::{ResultExt, TryFutureExt as _};
|
||||||
use zed_llm_client::{
|
|
||||||
EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME, EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME,
|
|
||||||
MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME, MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME, UsageLimit,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub type UserId = u64;
|
pub type UserId = u64;
|
||||||
|
|
||||||
|
@ -55,7 +56,7 @@ pub struct ParticipantIndex(pub u32);
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: UserId,
|
pub id: UserId,
|
||||||
pub github_login: String,
|
pub github_login: SharedString,
|
||||||
pub avatar_uri: SharedUri,
|
pub avatar_uri: SharedUri,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
}
|
}
|
||||||
|
@ -107,19 +108,14 @@ pub enum ContactRequestStatus {
|
||||||
|
|
||||||
pub struct UserStore {
|
pub struct UserStore {
|
||||||
users: HashMap<u64, Arc<User>>,
|
users: HashMap<u64, Arc<User>>,
|
||||||
by_github_login: HashMap<String, u64>,
|
by_github_login: HashMap<SharedString, u64>,
|
||||||
participant_indices: HashMap<u64, ParticipantIndex>,
|
participant_indices: HashMap<u64, ParticipantIndex>,
|
||||||
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
|
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
|
||||||
current_plan: Option<proto::Plan>,
|
|
||||||
subscription_period: Option<(DateTime<Utc>, DateTime<Utc>)>,
|
|
||||||
trial_started_at: Option<DateTime<Utc>>,
|
|
||||||
model_request_usage: Option<ModelRequestUsage>,
|
model_request_usage: Option<ModelRequestUsage>,
|
||||||
edit_prediction_usage: Option<EditPredictionUsage>,
|
edit_prediction_usage: Option<EditPredictionUsage>,
|
||||||
is_usage_based_billing_enabled: Option<bool>,
|
plan_info: Option<PlanInfo>,
|
||||||
account_too_young: Option<bool>,
|
|
||||||
has_overdue_invoices: Option<bool>,
|
|
||||||
current_user: watch::Receiver<Option<Arc<User>>>,
|
current_user: watch::Receiver<Option<Arc<User>>>,
|
||||||
accepted_tos_at: Option<Option<DateTime<Utc>>>,
|
accepted_tos_at: Option<Option<cloud_api_client::Timestamp>>,
|
||||||
contacts: Vec<Arc<Contact>>,
|
contacts: Vec<Arc<Contact>>,
|
||||||
incoming_contact_requests: Vec<Arc<User>>,
|
incoming_contact_requests: Vec<Arc<User>>,
|
||||||
outgoing_contact_requests: Vec<Arc<User>>,
|
outgoing_contact_requests: Vec<Arc<User>>,
|
||||||
|
@ -145,6 +141,7 @@ pub enum Event {
|
||||||
ShowContacts,
|
ShowContacts,
|
||||||
ParticipantIndicesChanged,
|
ParticipantIndicesChanged,
|
||||||
PrivateUserInfoUpdated,
|
PrivateUserInfoUpdated,
|
||||||
|
PlanUpdated,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
|
@ -188,14 +185,9 @@ impl UserStore {
|
||||||
users: Default::default(),
|
users: Default::default(),
|
||||||
by_github_login: Default::default(),
|
by_github_login: Default::default(),
|
||||||
current_user: current_user_rx,
|
current_user: current_user_rx,
|
||||||
current_plan: None,
|
plan_info: None,
|
||||||
subscription_period: None,
|
|
||||||
trial_started_at: None,
|
|
||||||
model_request_usage: None,
|
model_request_usage: None,
|
||||||
edit_prediction_usage: None,
|
edit_prediction_usage: None,
|
||||||
is_usage_based_billing_enabled: None,
|
|
||||||
account_too_young: None,
|
|
||||||
has_overdue_invoices: None,
|
|
||||||
accepted_tos_at: None,
|
accepted_tos_at: None,
|
||||||
contacts: Default::default(),
|
contacts: Default::default(),
|
||||||
incoming_contact_requests: Default::default(),
|
incoming_contact_requests: Default::default(),
|
||||||
|
@ -225,53 +217,30 @@ impl UserStore {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
match status {
|
match status {
|
||||||
Status::Connected { .. } => {
|
Status::Authenticated | Status::Connected { .. } => {
|
||||||
if let Some(user_id) = client.user_id() {
|
if let Some(user_id) = client.user_id() {
|
||||||
let fetch_user = if let Ok(fetch_user) =
|
let response = client.cloud_client().get_authenticated_user().await;
|
||||||
this.update(cx, |this, cx| this.get_user(user_id, cx).log_err())
|
let mut current_user = None;
|
||||||
{
|
|
||||||
fetch_user
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
let fetch_private_user_info =
|
|
||||||
client.request(proto::GetPrivateUserInfo {}).log_err();
|
|
||||||
let (user, info) =
|
|
||||||
futures::join!(fetch_user, fetch_private_user_info);
|
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
if let Some(info) = info {
|
if let Some(response) = response.log_err() {
|
||||||
let staff =
|
let user = Arc::new(User {
|
||||||
info.staff && !*feature_flags::ZED_DISABLE_STAFF;
|
id: user_id,
|
||||||
cx.update_flags(staff, info.flags);
|
github_login: response.user.github_login.clone().into(),
|
||||||
client.telemetry.set_authenticated_user_info(
|
avatar_uri: response.user.avatar_url.clone().into(),
|
||||||
Some(info.metrics_id.clone()),
|
name: response.user.name.clone(),
|
||||||
staff,
|
});
|
||||||
);
|
current_user = Some(user.clone());
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
let accepted_tos_at = {
|
this.by_github_login
|
||||||
#[cfg(debug_assertions)]
|
.insert(user.github_login.clone(), user_id);
|
||||||
if std::env::var("ZED_IGNORE_ACCEPTED_TOS").is_ok()
|
this.users.insert(user_id, user);
|
||||||
{
|
this.update_authenticated_user(response, cx)
|
||||||
None
|
|
||||||
} else {
|
|
||||||
info.accepted_tos_at
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
info.accepted_tos_at
|
|
||||||
};
|
|
||||||
|
|
||||||
this.set_current_user_accepted_tos_at(accepted_tos_at);
|
|
||||||
cx.emit(Event::PrivateUserInfoUpdated);
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
})??;
|
})??;
|
||||||
|
current_user_tx.send(current_user).await.ok();
|
||||||
current_user_tx.send(user).await.ok();
|
|
||||||
|
|
||||||
this.update(cx, |_, cx| cx.notify())?;
|
this.update(cx, |_, cx| cx.notify())?;
|
||||||
}
|
}
|
||||||
|
@ -352,59 +321,22 @@ impl UserStore {
|
||||||
|
|
||||||
async fn handle_update_plan(
|
async fn handle_update_plan(
|
||||||
this: Entity<Self>,
|
this: Entity<Self>,
|
||||||
message: TypedEnvelope<proto::UpdateUserPlan>,
|
_message: TypedEnvelope<proto::UpdateUserPlan>,
|
||||||
mut cx: AsyncApp,
|
mut cx: AsyncApp,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let client = this
|
||||||
|
.read_with(&cx, |this, _| this.client.upgrade())?
|
||||||
|
.context("client was dropped")?;
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.cloud_client()
|
||||||
|
.get_authenticated_user()
|
||||||
|
.await
|
||||||
|
.context("failed to fetch authenticated user")?;
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.current_plan = Some(message.payload.plan());
|
this.update_authenticated_user(response, cx);
|
||||||
this.subscription_period = maybe!({
|
})
|
||||||
let period = message.payload.subscription_period?;
|
|
||||||
let started_at = DateTime::from_timestamp(period.started_at as i64, 0)?;
|
|
||||||
let ended_at = DateTime::from_timestamp(period.ended_at as i64, 0)?;
|
|
||||||
|
|
||||||
Some((started_at, ended_at))
|
|
||||||
});
|
|
||||||
this.trial_started_at = message
|
|
||||||
.payload
|
|
||||||
.trial_started_at
|
|
||||||
.and_then(|trial_started_at| DateTime::from_timestamp(trial_started_at as i64, 0));
|
|
||||||
this.is_usage_based_billing_enabled = message.payload.is_usage_based_billing_enabled;
|
|
||||||
this.account_too_young = message.payload.account_too_young;
|
|
||||||
this.has_overdue_invoices = message.payload.has_overdue_invoices;
|
|
||||||
|
|
||||||
if let Some(usage) = message.payload.usage {
|
|
||||||
// limits are always present even though they are wrapped in Option
|
|
||||||
this.model_request_usage = usage
|
|
||||||
.model_requests_usage_limit
|
|
||||||
.and_then(|limit| {
|
|
||||||
RequestUsage::from_proto(usage.model_requests_usage_amount, limit)
|
|
||||||
})
|
|
||||||
.map(ModelRequestUsage);
|
|
||||||
this.edit_prediction_usage = usage
|
|
||||||
.edit_predictions_usage_limit
|
|
||||||
.and_then(|limit| {
|
|
||||||
RequestUsage::from_proto(usage.model_requests_usage_amount, limit)
|
|
||||||
})
|
|
||||||
.map(EditPredictionUsage);
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
})?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_model_request_usage(&mut self, usage: ModelRequestUsage, cx: &mut Context<Self>) {
|
|
||||||
self.model_request_usage = Some(usage);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_edit_prediction_usage(
|
|
||||||
&mut self,
|
|
||||||
usage: EditPredictionUsage,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.edit_prediction_usage = Some(usage);
|
|
||||||
cx.notify();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_contacts(&mut self, message: UpdateContacts, cx: &Context<Self>) -> Task<Result<()>> {
|
fn update_contacts(&mut self, message: UpdateContacts, cx: &Context<Self>) -> Task<Result<()>> {
|
||||||
|
@ -763,59 +695,131 @@ impl UserStore {
|
||||||
self.current_user.borrow().clone()
|
self.current_user.borrow().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_plan(&self) -> Option<proto::Plan> {
|
pub fn plan(&self) -> Option<cloud_llm_client::Plan> {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
if let Ok(plan) = std::env::var("ZED_SIMULATE_PLAN").as_ref() {
|
if let Ok(plan) = std::env::var("ZED_SIMULATE_PLAN").as_ref() {
|
||||||
return match plan.as_str() {
|
return match plan.as_str() {
|
||||||
"free" => Some(proto::Plan::Free),
|
"free" => Some(cloud_llm_client::Plan::ZedFree),
|
||||||
"trial" => Some(proto::Plan::ZedProTrial),
|
"trial" => Some(cloud_llm_client::Plan::ZedProTrial),
|
||||||
"pro" => Some(proto::Plan::ZedPro),
|
"pro" => Some(cloud_llm_client::Plan::ZedPro),
|
||||||
_ => {
|
_ => {
|
||||||
panic!("ZED_SIMULATE_PLAN must be one of 'free', 'trial', or 'pro'");
|
panic!("ZED_SIMULATE_PLAN must be one of 'free', 'trial', or 'pro'");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
self.current_plan
|
self.plan_info.as_ref().map(|info| info.plan)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn subscription_period(&self) -> Option<(DateTime<Utc>, DateTime<Utc>)> {
|
pub fn subscription_period(&self) -> Option<(DateTime<Utc>, DateTime<Utc>)> {
|
||||||
self.subscription_period
|
self.plan_info
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|plan| plan.subscription_period)
|
||||||
|
.map(|subscription_period| {
|
||||||
|
(
|
||||||
|
subscription_period.started_at.0,
|
||||||
|
subscription_period.ended_at.0,
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn trial_started_at(&self) -> Option<DateTime<Utc>> {
|
pub fn trial_started_at(&self) -> Option<DateTime<Utc>> {
|
||||||
self.trial_started_at
|
self.plan_info
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|plan| plan.trial_started_at)
|
||||||
|
.map(|trial_started_at| trial_started_at.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn usage_based_billing_enabled(&self) -> Option<bool> {
|
/// Returns whether the user's account is too new to use the service.
|
||||||
self.is_usage_based_billing_enabled
|
pub fn account_too_young(&self) -> bool {
|
||||||
|
self.plan_info
|
||||||
|
.as_ref()
|
||||||
|
.map(|plan| plan.is_account_too_young)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the current user has overdue invoices and usage should be blocked.
|
||||||
|
pub fn has_overdue_invoices(&self) -> bool {
|
||||||
|
self.plan_info
|
||||||
|
.as_ref()
|
||||||
|
.map(|plan| plan.has_overdue_invoices)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_usage_based_billing_enabled(&self) -> bool {
|
||||||
|
self.plan_info
|
||||||
|
.as_ref()
|
||||||
|
.map(|plan| plan.is_usage_based_billing_enabled)
|
||||||
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn model_request_usage(&self) -> Option<ModelRequestUsage> {
|
pub fn model_request_usage(&self) -> Option<ModelRequestUsage> {
|
||||||
self.model_request_usage
|
self.model_request_usage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_model_request_usage(&mut self, usage: ModelRequestUsage, cx: &mut Context<Self>) {
|
||||||
|
self.model_request_usage = Some(usage);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn edit_prediction_usage(&self) -> Option<EditPredictionUsage> {
|
pub fn edit_prediction_usage(&self) -> Option<EditPredictionUsage> {
|
||||||
self.edit_prediction_usage
|
self.edit_prediction_usage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update_edit_prediction_usage(
|
||||||
|
&mut self,
|
||||||
|
usage: EditPredictionUsage,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.edit_prediction_usage = Some(usage);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_authenticated_user(
|
||||||
|
&mut self,
|
||||||
|
response: GetAuthenticatedUserResponse,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let staff = response.user.is_staff && !*feature_flags::ZED_DISABLE_STAFF;
|
||||||
|
cx.update_flags(staff, response.feature_flags);
|
||||||
|
if let Some(client) = self.client.upgrade() {
|
||||||
|
client
|
||||||
|
.telemetry
|
||||||
|
.set_authenticated_user_info(Some(response.user.metrics_id.clone()), staff);
|
||||||
|
}
|
||||||
|
|
||||||
|
let accepted_tos_at = {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
if std::env::var("ZED_IGNORE_ACCEPTED_TOS").is_ok() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
response.user.accepted_tos_at
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
response.user.accepted_tos_at
|
||||||
|
};
|
||||||
|
|
||||||
|
self.accepted_tos_at = Some(accepted_tos_at);
|
||||||
|
self.model_request_usage = Some(ModelRequestUsage(RequestUsage {
|
||||||
|
limit: response.plan.usage.model_requests.limit,
|
||||||
|
amount: response.plan.usage.model_requests.used as i32,
|
||||||
|
}));
|
||||||
|
self.edit_prediction_usage = Some(EditPredictionUsage(RequestUsage {
|
||||||
|
limit: response.plan.usage.edit_predictions.limit,
|
||||||
|
amount: response.plan.usage.edit_predictions.used as i32,
|
||||||
|
}));
|
||||||
|
self.plan_info = Some(response.plan);
|
||||||
|
cx.emit(Event::PrivateUserInfoUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn watch_current_user(&self) -> watch::Receiver<Option<Arc<User>>> {
|
pub fn watch_current_user(&self) -> watch::Receiver<Option<Arc<User>>> {
|
||||||
self.current_user.clone()
|
self.current_user.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether the user's account is too new to use the service.
|
pub fn has_accepted_terms_of_service(&self) -> bool {
|
||||||
pub fn account_too_young(&self) -> bool {
|
|
||||||
self.account_too_young.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether the current user has overdue invoices and usage should be blocked.
|
|
||||||
pub fn has_overdue_invoices(&self) -> bool {
|
|
||||||
self.has_overdue_invoices.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current_user_has_accepted_terms(&self) -> Option<bool> {
|
|
||||||
self.accepted_tos_at
|
self.accepted_tos_at
|
||||||
.map(|accepted_tos_at| accepted_tos_at.is_some())
|
.map_or(false, |accepted_tos_at| accepted_tos_at.is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn accept_terms_of_service(&self, cx: &Context<Self>) -> Task<Result<()>> {
|
pub fn accept_terms_of_service(&self, cx: &Context<Self>) -> Task<Result<()>> {
|
||||||
|
@ -827,23 +831,18 @@ impl UserStore {
|
||||||
cx.spawn(async move |this, cx| -> anyhow::Result<()> {
|
cx.spawn(async move |this, cx| -> anyhow::Result<()> {
|
||||||
let client = client.upgrade().context("client not found")?;
|
let client = client.upgrade().context("client not found")?;
|
||||||
let response = client
|
let response = client
|
||||||
.request(proto::AcceptTermsOfService {})
|
.cloud_client()
|
||||||
|
.accept_terms_of_service()
|
||||||
.await
|
.await
|
||||||
.context("error accepting tos")?;
|
.context("error accepting tos")?;
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_current_user_accepted_tos_at(Some(response.accepted_tos_at));
|
this.accepted_tos_at = Some(response.user.accepted_tos_at);
|
||||||
cx.emit(Event::PrivateUserInfoUpdated);
|
cx.emit(Event::PrivateUserInfoUpdated);
|
||||||
})?;
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_current_user_accepted_tos_at(&mut self, accepted_tos_at: Option<u64>) {
|
|
||||||
self.accepted_tos_at = Some(
|
|
||||||
accepted_tos_at.and_then(|timestamp| DateTime::from_timestamp(timestamp as i64, 0)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_users(
|
fn load_users(
|
||||||
&self,
|
&self,
|
||||||
request: impl RequestMessage<Response = UsersResponse>,
|
request: impl RequestMessage<Response = UsersResponse>,
|
||||||
|
@ -902,7 +901,7 @@ impl UserStore {
|
||||||
let mut missing_user_ids = Vec::new();
|
let mut missing_user_ids = Vec::new();
|
||||||
for id in user_ids {
|
for id in user_ids {
|
||||||
if let Some(github_login) = self.get_cached_user(id).map(|u| u.github_login.clone()) {
|
if let Some(github_login) = self.get_cached_user(id).map(|u| u.github_login.clone()) {
|
||||||
ret.insert(id, github_login.into());
|
ret.insert(id, github_login);
|
||||||
} else {
|
} else {
|
||||||
missing_user_ids.push(id)
|
missing_user_ids.push(id)
|
||||||
}
|
}
|
||||||
|
@ -923,7 +922,7 @@ impl User {
|
||||||
fn new(message: proto::User) -> Arc<Self> {
|
fn new(message: proto::User) -> Arc<Self> {
|
||||||
Arc::new(User {
|
Arc::new(User {
|
||||||
id: message.id,
|
id: message.id,
|
||||||
github_login: message.github_login,
|
github_login: message.github_login.into(),
|
||||||
avatar_uri: message.avatar_url.into(),
|
avatar_uri: message.avatar_url.into(),
|
||||||
name: message.name,
|
name: message.name,
|
||||||
})
|
})
|
||||||
|
|
21
crates/cloud_api_client/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "cloud_api_client"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
license = "Apache-2.0"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/cloud_api_client.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
cloud_api_types.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
http_client.workspace = true
|
||||||
|
parking_lot.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
workspace-hack.workspace = true
|
1
crates/cloud_api_client/LICENSE-APACHE
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../LICENSE-APACHE
|
155
crates/cloud_api_client/src/cloud_api_client.rs
Normal file
|
@ -0,0 +1,155 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::{Result, anyhow};
|
||||||
|
pub use cloud_api_types::*;
|
||||||
|
use futures::AsyncReadExt as _;
|
||||||
|
use http_client::http::request;
|
||||||
|
use http_client::{AsyncBody, HttpClientWithUrl, Method, Request};
|
||||||
|
use parking_lot::RwLock;
|
||||||
|
|
||||||
|
struct Credentials {
|
||||||
|
user_id: u32,
|
||||||
|
access_token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CloudApiClient {
|
||||||
|
credentials: RwLock<Option<Credentials>>,
|
||||||
|
http_client: Arc<HttpClientWithUrl>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CloudApiClient {
|
||||||
|
pub fn new(http_client: Arc<HttpClientWithUrl>) -> Self {
|
||||||
|
Self {
|
||||||
|
credentials: RwLock::new(None),
|
||||||
|
http_client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_credentials(&self) -> bool {
|
||||||
|
self.credentials.read().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_credentials(&self, user_id: u32, access_token: String) {
|
||||||
|
*self.credentials.write() = Some(Credentials {
|
||||||
|
user_id,
|
||||||
|
access_token,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_credentials(&self) {
|
||||||
|
*self.credentials.write() = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authorization_header(&self) -> Result<String> {
|
||||||
|
let guard = self.credentials.read();
|
||||||
|
let credentials = guard
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| anyhow!("No credentials provided"))?;
|
||||||
|
|
||||||
|
Ok(format!(
|
||||||
|
"{} {}",
|
||||||
|
credentials.user_id, credentials.access_token
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_request(
|
||||||
|
&self,
|
||||||
|
req: request::Builder,
|
||||||
|
body: impl Into<AsyncBody>,
|
||||||
|
) -> Result<Request<AsyncBody>> {
|
||||||
|
Ok(req
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Authorization", self.authorization_header()?)
|
||||||
|
.body(body.into())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_authenticated_user(&self) -> Result<GetAuthenticatedUserResponse> {
|
||||||
|
let request = self.build_request(
|
||||||
|
Request::builder().method(Method::GET).uri(
|
||||||
|
self.http_client
|
||||||
|
.build_zed_cloud_url("/client/users/me", &[])?
|
||||||
|
.as_ref(),
|
||||||
|
),
|
||||||
|
AsyncBody::default(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut response = self.http_client.send(request).await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let mut body = String::new();
|
||||||
|
response.body_mut().read_to_string(&mut body).await?;
|
||||||
|
|
||||||
|
anyhow::bail!(
|
||||||
|
"Failed to get authenticated user.\nStatus: {:?}\nBody: {body}",
|
||||||
|
response.status()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut body = String::new();
|
||||||
|
response.body_mut().read_to_string(&mut body).await?;
|
||||||
|
|
||||||
|
Ok(serde_json::from_str(&body)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn accept_terms_of_service(&self) -> Result<AcceptTermsOfServiceResponse> {
|
||||||
|
let request = self.build_request(
|
||||||
|
Request::builder().method(Method::POST).uri(
|
||||||
|
self.http_client
|
||||||
|
.build_zed_cloud_url("/client/terms_of_service/accept", &[])?
|
||||||
|
.as_ref(),
|
||||||
|
),
|
||||||
|
AsyncBody::default(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut response = self.http_client.send(request).await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let mut body = String::new();
|
||||||
|
response.body_mut().read_to_string(&mut body).await?;
|
||||||
|
|
||||||
|
anyhow::bail!(
|
||||||
|
"Failed to accept terms of service.\nStatus: {:?}\nBody: {body}",
|
||||||
|
response.status()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut body = String::new();
|
||||||
|
response.body_mut().read_to_string(&mut body).await?;
|
||||||
|
|
||||||
|
Ok(serde_json::from_str(&body)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_llm_token(
|
||||||
|
&self,
|
||||||
|
system_id: Option<String>,
|
||||||
|
) -> Result<CreateLlmTokenResponse> {
|
||||||
|
let mut request_builder = Request::builder().method(Method::POST).uri(
|
||||||
|
self.http_client
|
||||||
|
.build_zed_cloud_url("/client/llm_tokens", &[])?
|
||||||
|
.as_ref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(system_id) = system_id {
|
||||||
|
request_builder = request_builder.header(ZED_SYSTEM_ID_HEADER_NAME, system_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = self.build_request(request_builder, AsyncBody::default())?;
|
||||||
|
|
||||||
|
let mut response = self.http_client.send(request).await?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let mut body = String::new();
|
||||||
|
response.body_mut().read_to_string(&mut body).await?;
|
||||||
|
|
||||||
|
anyhow::bail!(
|
||||||
|
"Failed to create LLM token.\nStatus: {:?}\nBody: {body}",
|
||||||
|
response.status()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut body = String::new();
|
||||||
|
response.body_mut().read_to_string(&mut body).await?;
|
||||||
|
|
||||||
|
Ok(serde_json::from_str(&body)?)
|
||||||
|
}
|
||||||
|
}
|
22
crates/cloud_api_types/Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "cloud_api_types"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
license = "Apache-2.0"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/cloud_api_types.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono.workspace = true
|
||||||
|
cloud_llm_client.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
workspace-hack.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_assertions.workspace = true
|
||||||
|
serde_json.workspace = true
|
1
crates/cloud_api_types/LICENSE-APACHE
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../LICENSE-APACHE
|
55
crates/cloud_api_types/src/cloud_api_types.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
mod timestamp;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub use crate::timestamp::Timestamp;
|
||||||
|
|
||||||
|
pub const ZED_SYSTEM_ID_HEADER_NAME: &str = "x-zed-system-id";
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct GetAuthenticatedUserResponse {
|
||||||
|
pub user: AuthenticatedUser,
|
||||||
|
pub feature_flags: Vec<String>,
|
||||||
|
pub plan: PlanInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct AuthenticatedUser {
|
||||||
|
pub id: i32,
|
||||||
|
pub metrics_id: String,
|
||||||
|
pub avatar_url: String,
|
||||||
|
pub github_login: String,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub is_staff: bool,
|
||||||
|
pub accepted_tos_at: Option<Timestamp>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct PlanInfo {
|
||||||
|
pub plan: cloud_llm_client::Plan,
|
||||||
|
pub subscription_period: Option<SubscriptionPeriod>,
|
||||||
|
pub usage: cloud_llm_client::CurrentUsage,
|
||||||
|
pub trial_started_at: Option<Timestamp>,
|
||||||
|
pub is_usage_based_billing_enabled: bool,
|
||||||
|
pub is_account_too_young: bool,
|
||||||
|
pub has_overdue_invoices: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub struct SubscriptionPeriod {
|
||||||
|
pub started_at: Timestamp,
|
||||||
|
pub ended_at: Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct AcceptTermsOfServiceResponse {
|
||||||
|
pub user: AuthenticatedUser,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LlmToken(pub String);
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CreateLlmTokenResponse {
|
||||||
|
pub token: LlmToken,
|
||||||
|
}
|
166
crates/cloud_api_types/src/timestamp.rs
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
use chrono::{DateTime, NaiveDateTime, SecondsFormat, Utc};
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
|
/// A timestamp with a serialized representation in RFC 3339 format.
|
||||||
|
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
|
||||||
|
pub struct Timestamp(pub DateTime<Utc>);
|
||||||
|
|
||||||
|
impl Timestamp {
|
||||||
|
pub fn new(datetime: DateTime<Utc>) -> Self {
|
||||||
|
Self(datetime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DateTime<Utc>> for Timestamp {
|
||||||
|
fn from(value: DateTime<Utc>) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<NaiveDateTime> for Timestamp {
|
||||||
|
fn from(value: NaiveDateTime) -> Self {
|
||||||
|
Self(value.and_utc())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Timestamp {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let rfc3339_string = self.0.to_rfc3339_opts(SecondsFormat::Millis, true);
|
||||||
|
serializer.serialize_str(&rfc3339_string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Timestamp {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let value = String::deserialize(deserializer)?;
|
||||||
|
let datetime = DateTime::parse_from_rfc3339(&value)
|
||||||
|
.map_err(serde::de::Error::custom)?
|
||||||
|
.to_utc();
|
||||||
|
Ok(Self(datetime))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timestamp_serialization() {
|
||||||
|
let datetime = DateTime::parse_from_rfc3339("2023-12-25T14:30:45.123Z")
|
||||||
|
.unwrap()
|
||||||
|
.to_utc();
|
||||||
|
let timestamp = Timestamp::new(datetime);
|
||||||
|
|
||||||
|
let json = serde_json::to_string(×tamp).unwrap();
|
||||||
|
assert_eq!(json, "\"2023-12-25T14:30:45.123Z\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timestamp_deserialization() {
|
||||||
|
let json = "\"2023-12-25T14:30:45.123Z\"";
|
||||||
|
let timestamp: Timestamp = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
let expected = DateTime::parse_from_rfc3339("2023-12-25T14:30:45.123Z")
|
||||||
|
.unwrap()
|
||||||
|
.to_utc();
|
||||||
|
|
||||||
|
assert_eq!(timestamp.0, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timestamp_roundtrip() {
|
||||||
|
let original = DateTime::parse_from_rfc3339("2023-12-25T14:30:45.123Z")
|
||||||
|
.unwrap()
|
||||||
|
.to_utc();
|
||||||
|
|
||||||
|
let timestamp = Timestamp::new(original);
|
||||||
|
let json = serde_json::to_string(×tamp).unwrap();
|
||||||
|
let deserialized: Timestamp = serde_json::from_str(&json).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(deserialized.0, original);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timestamp_from_datetime_utc() {
|
||||||
|
let datetime = DateTime::parse_from_rfc3339("2023-12-25T14:30:45.123Z")
|
||||||
|
.unwrap()
|
||||||
|
.to_utc();
|
||||||
|
|
||||||
|
let timestamp = Timestamp::from(datetime);
|
||||||
|
assert_eq!(timestamp.0, datetime);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timestamp_from_naive_datetime() {
|
||||||
|
let naive_dt = NaiveDate::from_ymd_opt(2023, 12, 25)
|
||||||
|
.unwrap()
|
||||||
|
.and_hms_milli_opt(14, 30, 45, 123)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let timestamp = Timestamp::from(naive_dt);
|
||||||
|
let expected = naive_dt.and_utc();
|
||||||
|
|
||||||
|
assert_eq!(timestamp.0, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timestamp_serialization_with_microseconds() {
|
||||||
|
// Test that microseconds are truncated to milliseconds
|
||||||
|
let datetime = NaiveDate::from_ymd_opt(2023, 12, 25)
|
||||||
|
.unwrap()
|
||||||
|
.and_hms_micro_opt(14, 30, 45, 123456)
|
||||||
|
.unwrap()
|
||||||
|
.and_utc();
|
||||||
|
|
||||||
|
let timestamp = Timestamp::new(datetime);
|
||||||
|
let json = serde_json::to_string(×tamp).unwrap();
|
||||||
|
|
||||||
|
// Should be truncated to milliseconds
|
||||||
|
assert_eq!(json, "\"2023-12-25T14:30:45.123Z\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timestamp_deserialization_without_milliseconds() {
|
||||||
|
let json = "\"2023-12-25T14:30:45Z\"";
|
||||||
|
let timestamp: Timestamp = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
let expected = NaiveDate::from_ymd_opt(2023, 12, 25)
|
||||||
|
.unwrap()
|
||||||
|
.and_hms_opt(14, 30, 45)
|
||||||
|
.unwrap()
|
||||||
|
.and_utc();
|
||||||
|
|
||||||
|
assert_eq!(timestamp.0, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timestamp_deserialization_with_timezone() {
|
||||||
|
let json = "\"2023-12-25T14:30:45.123+05:30\"";
|
||||||
|
let timestamp: Timestamp = serde_json::from_str(json).unwrap();
|
||||||
|
|
||||||
|
// Should be converted to UTC
|
||||||
|
let expected = NaiveDate::from_ymd_opt(2023, 12, 25)
|
||||||
|
.unwrap()
|
||||||
|
.and_hms_milli_opt(9, 0, 45, 123) // 14:30:45 + 5:30 = 20:00:45, but we want UTC so subtract 5:30
|
||||||
|
.unwrap()
|
||||||
|
.and_utc();
|
||||||
|
|
||||||
|
assert_eq!(timestamp.0, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timestamp_deserialization_with_invalid_format() {
|
||||||
|
let json = "\"invalid-date\"";
|
||||||
|
let result: Result<Timestamp, _> = serde_json::from_str(json);
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
}
|
23
crates/cloud_llm_client/Cargo.toml
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
[package]
|
||||||
|
name = "cloud_llm_client"
|
||||||
|
version = "0.1.0"
|
||||||
|
publish.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license = "Apache-2.0"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/cloud_llm_client.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
serde = { workspace = true, features = ["derive", "rc"] }
|
||||||
|
serde_json.workspace = true
|
||||||
|
strum = { workspace = true, features = ["derive"] }
|
||||||
|
uuid = { workspace = true, features = ["serde"] }
|
||||||
|
workspace-hack.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_assertions.workspace = true
|
1
crates/cloud_llm_client/LICENSE-APACHE
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../LICENSE-APACHE
|
370
crates/cloud_llm_client/src/cloud_llm_client.rs
Normal file
|
@ -0,0 +1,370 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use anyhow::Context as _;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use strum::{Display, EnumIter, EnumString};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// The name of the header used to indicate which version of Zed the client is running.
|
||||||
|
pub const ZED_VERSION_HEADER_NAME: &str = "x-zed-version";
|
||||||
|
|
||||||
|
/// The name of the header used to indicate when a request failed due to an
|
||||||
|
/// expired LLM token.
|
||||||
|
///
|
||||||
|
/// The client may use this as a signal to refresh the token.
|
||||||
|
pub const EXPIRED_LLM_TOKEN_HEADER_NAME: &str = "x-zed-expired-token";
|
||||||
|
|
||||||
|
/// The name of the header used to indicate what plan the user is currently on.
|
||||||
|
pub const CURRENT_PLAN_HEADER_NAME: &str = "x-zed-plan";
|
||||||
|
|
||||||
|
/// The name of the header used to indicate the usage limit for model requests.
|
||||||
|
pub const MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME: &str = "x-zed-model-requests-usage-limit";
|
||||||
|
|
||||||
|
/// The name of the header used to indicate the usage amount for model requests.
|
||||||
|
pub const MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME: &str = "x-zed-model-requests-usage-amount";
|
||||||
|
|
||||||
|
/// The name of the header used to indicate the usage limit for edit predictions.
|
||||||
|
pub const EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME: &str = "x-zed-edit-predictions-usage-limit";
|
||||||
|
|
||||||
|
/// The name of the header used to indicate the usage amount for edit predictions.
|
||||||
|
pub const EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME: &str = "x-zed-edit-predictions-usage-amount";
|
||||||
|
|
||||||
|
/// The name of the header used to indicate the resource for which the subscription limit has been reached.
|
||||||
|
pub const SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME: &str = "x-zed-subscription-limit-resource";
|
||||||
|
|
||||||
|
pub const MODEL_REQUESTS_RESOURCE_HEADER_VALUE: &str = "model_requests";
|
||||||
|
pub const EDIT_PREDICTIONS_RESOURCE_HEADER_VALUE: &str = "edit_predictions";
|
||||||
|
|
||||||
|
/// The name of the header used to indicate that the maximum number of consecutive tool uses has been reached.
|
||||||
|
pub const TOOL_USE_LIMIT_REACHED_HEADER_NAME: &str = "x-zed-tool-use-limit-reached";
|
||||||
|
|
||||||
|
/// The name of the header used to indicate the the minimum required Zed version.
|
||||||
|
///
|
||||||
|
/// This can be used to force a Zed upgrade in order to continue communicating
|
||||||
|
/// with the LLM service.
|
||||||
|
pub const MINIMUM_REQUIRED_VERSION_HEADER_NAME: &str = "x-zed-minimum-required-version";
|
||||||
|
|
||||||
|
/// The name of the header used by the client to indicate to the server that it supports receiving status messages.
|
||||||
|
pub const CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME: &str =
|
||||||
|
"x-zed-client-supports-status-messages";
|
||||||
|
|
||||||
|
/// The name of the header used by the server to indicate to the client that it supports sending status messages.
|
||||||
|
pub const SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME: &str =
|
||||||
|
"x-zed-server-supports-status-messages";
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum UsageLimit {
|
||||||
|
Limited(i32),
|
||||||
|
Unlimited,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for UsageLimit {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||||
|
match value {
|
||||||
|
"unlimited" => Ok(Self::Unlimited),
|
||||||
|
limit => limit
|
||||||
|
.parse::<i32>()
|
||||||
|
.map(Self::Limited)
|
||||||
|
.context("failed to parse limit"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Plan {
|
||||||
|
#[default]
|
||||||
|
#[serde(alias = "Free")]
|
||||||
|
ZedFree,
|
||||||
|
#[serde(alias = "ZedPro")]
|
||||||
|
ZedPro,
|
||||||
|
#[serde(alias = "ZedProTrial")]
|
||||||
|
ZedProTrial,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Plan {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Plan::ZedFree => "zed_free",
|
||||||
|
Plan::ZedPro => "zed_pro",
|
||||||
|
Plan::ZedProTrial => "zed_pro_trial",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn model_requests_limit(&self) -> UsageLimit {
|
||||||
|
match self {
|
||||||
|
Plan::ZedPro => UsageLimit::Limited(500),
|
||||||
|
Plan::ZedProTrial => UsageLimit::Limited(150),
|
||||||
|
Plan::ZedFree => UsageLimit::Limited(50),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn edit_predictions_limit(&self) -> UsageLimit {
|
||||||
|
match self {
|
||||||
|
Plan::ZedPro => UsageLimit::Unlimited,
|
||||||
|
Plan::ZedProTrial => UsageLimit::Unlimited,
|
||||||
|
Plan::ZedFree => UsageLimit::Limited(2_000),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Plan {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||||
|
match value {
|
||||||
|
"zed_free" => Ok(Plan::ZedFree),
|
||||||
|
"zed_pro" => Ok(Plan::ZedPro),
|
||||||
|
"zed_pro_trial" => Ok(Plan::ZedProTrial),
|
||||||
|
plan => Err(anyhow::anyhow!("invalid plan: {plan:?}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize, EnumString, EnumIter, Display,
|
||||||
|
)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
#[strum(serialize_all = "snake_case")]
|
||||||
|
pub enum LanguageModelProvider {
|
||||||
|
Anthropic,
|
||||||
|
OpenAi,
|
||||||
|
Google,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PredictEditsBody {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||||
|
pub outline: Option<String>,
|
||||||
|
pub input_events: String,
|
||||||
|
pub input_excerpt: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||||
|
pub speculated_output: Option<String>,
|
||||||
|
/// Whether the user provided consent for sampling this interaction.
|
||||||
|
#[serde(default, alias = "data_collection_permission")]
|
||||||
|
pub can_collect_data: bool,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||||
|
pub diagnostic_groups: Option<Vec<(String, serde_json::Value)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PredictEditsResponse {
|
||||||
|
pub request_id: Uuid,
|
||||||
|
pub output_excerpt: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct AcceptEditPredictionBody {
|
||||||
|
pub request_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum CompletionMode {
|
||||||
|
Normal,
|
||||||
|
Max,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum CompletionIntent {
|
||||||
|
UserPrompt,
|
||||||
|
ToolResults,
|
||||||
|
ThreadSummarization,
|
||||||
|
ThreadContextSummarization,
|
||||||
|
CreateFile,
|
||||||
|
EditFile,
|
||||||
|
InlineAssist,
|
||||||
|
TerminalInlineAssist,
|
||||||
|
GenerateGitCommitMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct CompletionBody {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||||
|
pub thread_id: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||||
|
pub prompt_id: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||||
|
pub intent: Option<CompletionIntent>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||||
|
pub mode: Option<CompletionMode>,
|
||||||
|
pub provider: LanguageModelProvider,
|
||||||
|
pub model: String,
|
||||||
|
pub provider_request: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum CompletionRequestStatus {
|
||||||
|
Queued {
|
||||||
|
position: usize,
|
||||||
|
},
|
||||||
|
Started,
|
||||||
|
Failed {
|
||||||
|
code: String,
|
||||||
|
message: String,
|
||||||
|
request_id: Uuid,
|
||||||
|
/// Retry duration in seconds.
|
||||||
|
retry_after: Option<f64>,
|
||||||
|
},
|
||||||
|
UsageUpdated {
|
||||||
|
amount: usize,
|
||||||
|
limit: UsageLimit,
|
||||||
|
},
|
||||||
|
ToolUseLimitReached,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum CompletionEvent<T> {
|
||||||
|
Status(CompletionRequestStatus),
|
||||||
|
Event(T),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> CompletionEvent<T> {
|
||||||
|
pub fn into_status(self) -> Option<CompletionRequestStatus> {
|
||||||
|
match self {
|
||||||
|
Self::Status(status) => Some(status),
|
||||||
|
Self::Event(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_event(self) -> Option<T> {
|
||||||
|
match self {
|
||||||
|
Self::Event(event) => Some(event),
|
||||||
|
Self::Status(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct WebSearchBody {
|
||||||
|
pub query: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct WebSearchResponse {
|
||||||
|
pub results: Vec<WebSearchResult>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct WebSearchResult {
|
||||||
|
pub title: String,
|
||||||
|
pub url: String,
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct CountTokensBody {
|
||||||
|
pub provider: LanguageModelProvider,
|
||||||
|
pub model: String,
|
||||||
|
pub provider_request: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct CountTokensResponse {
|
||||||
|
pub tokens: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct LanguageModelId(pub Arc<str>);
|
||||||
|
|
||||||
|
impl std::fmt::Display for LanguageModelId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct LanguageModel {
|
||||||
|
pub provider: LanguageModelProvider,
|
||||||
|
pub id: LanguageModelId,
|
||||||
|
pub display_name: String,
|
||||||
|
pub max_token_count: usize,
|
||||||
|
pub max_token_count_in_max_mode: Option<usize>,
|
||||||
|
pub max_output_tokens: usize,
|
||||||
|
pub supports_tools: bool,
|
||||||
|
pub supports_images: bool,
|
||||||
|
pub supports_thinking: bool,
|
||||||
|
pub supports_max_mode: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ListModelsResponse {
|
||||||
|
pub models: Vec<LanguageModel>,
|
||||||
|
pub default_model: LanguageModelId,
|
||||||
|
pub default_fast_model: LanguageModelId,
|
||||||
|
pub recommended_models: Vec<LanguageModelId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct GetSubscriptionResponse {
|
||||||
|
pub plan: Plan,
|
||||||
|
pub usage: Option<CurrentUsage>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct CurrentUsage {
|
||||||
|
pub model_requests: UsageData,
|
||||||
|
pub edit_predictions: UsageData,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct UsageData {
|
||||||
|
pub used: u32,
|
||||||
|
pub limit: UsageLimit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_plan_deserialize_snake_case() {
|
||||||
|
let plan = serde_json::from_value::<Plan>(json!("zed_free")).unwrap();
|
||||||
|
assert_eq!(plan, Plan::ZedFree);
|
||||||
|
|
||||||
|
let plan = serde_json::from_value::<Plan>(json!("zed_pro")).unwrap();
|
||||||
|
assert_eq!(plan, Plan::ZedPro);
|
||||||
|
|
||||||
|
let plan = serde_json::from_value::<Plan>(json!("zed_pro_trial")).unwrap();
|
||||||
|
assert_eq!(plan, Plan::ZedProTrial);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_plan_deserialize_aliases() {
|
||||||
|
let plan = serde_json::from_value::<Plan>(json!("Free")).unwrap();
|
||||||
|
assert_eq!(plan, Plan::ZedFree);
|
||||||
|
|
||||||
|
let plan = serde_json::from_value::<Plan>(json!("ZedPro")).unwrap();
|
||||||
|
assert_eq!(plan, Plan::ZedPro);
|
||||||
|
|
||||||
|
let plan = serde_json::from_value::<Plan>(json!("ZedProTrial")).unwrap();
|
||||||
|
assert_eq!(plan, Plan::ZedProTrial);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_usage_limit_from_str() {
|
||||||
|
let limit = UsageLimit::from_str("unlimited").unwrap();
|
||||||
|
assert!(matches!(limit, UsageLimit::Unlimited));
|
||||||
|
|
||||||
|
let limit = UsageLimit::from_str(&0.to_string()).unwrap();
|
||||||
|
assert!(matches!(limit, UsageLimit::Limited(0)));
|
||||||
|
|
||||||
|
let limit = UsageLimit::from_str(&50.to_string()).unwrap();
|
||||||
|
assert!(matches!(limit, UsageLimit::Limited(50)));
|
||||||
|
|
||||||
|
for value in ["not_a_number", "50xyz"] {
|
||||||
|
let limit = UsageLimit::from_str(value);
|
||||||
|
assert!(limit.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,13 +23,14 @@ async-stripe.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
async-tungstenite.workspace = true
|
async-tungstenite.workspace = true
|
||||||
aws-config = { version = "1.1.5" }
|
aws-config = { version = "1.1.5" }
|
||||||
aws-sdk-s3 = { version = "1.15.0" }
|
|
||||||
aws-sdk-kinesis = "1.51.0"
|
aws-sdk-kinesis = "1.51.0"
|
||||||
|
aws-sdk-s3 = { version = "1.15.0" }
|
||||||
axum = { version = "0.6", features = ["json", "headers", "ws"] }
|
axum = { version = "0.6", features = ["json", "headers", "ws"] }
|
||||||
axum-extra = { version = "0.4", features = ["erased-json"] }
|
axum-extra = { version = "0.4", features = ["erased-json"] }
|
||||||
base64.workspace = true
|
base64.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
clock.workspace = true
|
clock.workspace = true
|
||||||
|
cloud_llm_client.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
dashmap.workspace = true
|
dashmap.workspace = true
|
||||||
derive_more.workspace = true
|
derive_more.workspace = true
|
||||||
|
@ -75,7 +76,6 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "re
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
zed_llm_client.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
agent_settings.workspace = true
|
agent_settings.workspace = true
|
||||||
|
|
|
@ -100,7 +100,6 @@ impl std::fmt::Display for SystemIdHeader {
|
||||||
|
|
||||||
pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
|
pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/user", get(update_or_create_authenticated_user))
|
|
||||||
.route("/users/look_up", get(look_up_user))
|
.route("/users/look_up", get(look_up_user))
|
||||||
.route("/users/:id/access_tokens", post(create_access_token))
|
.route("/users/:id/access_tokens", post(create_access_token))
|
||||||
.route("/users/:id/refresh_llm_tokens", post(refresh_llm_tokens))
|
.route("/users/:id/refresh_llm_tokens", post(refresh_llm_tokens))
|
||||||
|
@ -145,48 +144,6 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
|
||||||
Ok::<_, Error>(next.run(req).await)
|
Ok::<_, Error>(next.run(req).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct AuthenticatedUserParams {
|
|
||||||
github_user_id: i32,
|
|
||||||
github_login: String,
|
|
||||||
github_email: Option<String>,
|
|
||||||
github_name: Option<String>,
|
|
||||||
github_user_created_at: chrono::DateTime<chrono::Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
struct AuthenticatedUserResponse {
|
|
||||||
user: User,
|
|
||||||
metrics_id: String,
|
|
||||||
feature_flags: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_or_create_authenticated_user(
|
|
||||||
Query(params): Query<AuthenticatedUserParams>,
|
|
||||||
Extension(app): Extension<Arc<AppState>>,
|
|
||||||
) -> Result<Json<AuthenticatedUserResponse>> {
|
|
||||||
let initial_channel_id = app.config.auto_join_channel_id;
|
|
||||||
|
|
||||||
let user = app
|
|
||||||
.db
|
|
||||||
.update_or_create_user_by_github_account(
|
|
||||||
¶ms.github_login,
|
|
||||||
params.github_user_id,
|
|
||||||
params.github_email.as_deref(),
|
|
||||||
params.github_name.as_deref(),
|
|
||||||
params.github_user_created_at,
|
|
||||||
initial_channel_id,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
|
|
||||||
let feature_flags = app.db.get_user_flags(user.id).await?;
|
|
||||||
Ok(Json(AuthenticatedUserResponse {
|
|
||||||
user,
|
|
||||||
metrics_id,
|
|
||||||
feature_flags,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct LookUpUserParams {
|
struct LookUpUserParams {
|
||||||
identifier: String,
|
identifier: String,
|
||||||
|
@ -353,9 +310,9 @@ async fn refresh_llm_tokens(
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct UpdatePlanBody {
|
struct UpdatePlanBody {
|
||||||
pub plan: zed_llm_client::Plan,
|
pub plan: cloud_llm_client::Plan,
|
||||||
pub subscription_period: SubscriptionPeriod,
|
pub subscription_period: SubscriptionPeriod,
|
||||||
pub usage: zed_llm_client::CurrentUsage,
|
pub usage: cloud_llm_client::CurrentUsage,
|
||||||
pub trial_started_at: Option<DateTime<Utc>>,
|
pub trial_started_at: Option<DateTime<Utc>>,
|
||||||
pub is_usage_based_billing_enabled: bool,
|
pub is_usage_based_billing_enabled: bool,
|
||||||
pub is_account_too_young: bool,
|
pub is_account_too_young: bool,
|
||||||
|
@ -377,9 +334,9 @@ async fn update_plan(
|
||||||
extract::Json(body): extract::Json<UpdatePlanBody>,
|
extract::Json(body): extract::Json<UpdatePlanBody>,
|
||||||
) -> Result<Json<UpdatePlanResponse>> {
|
) -> Result<Json<UpdatePlanResponse>> {
|
||||||
let plan = match body.plan {
|
let plan = match body.plan {
|
||||||
zed_llm_client::Plan::ZedFree => proto::Plan::Free,
|
cloud_llm_client::Plan::ZedFree => proto::Plan::Free,
|
||||||
zed_llm_client::Plan::ZedPro => proto::Plan::ZedPro,
|
cloud_llm_client::Plan::ZedPro => proto::Plan::ZedPro,
|
||||||
zed_llm_client::Plan::ZedProTrial => proto::Plan::ZedProTrial,
|
cloud_llm_client::Plan::ZedProTrial => proto::Plan::ZedProTrial,
|
||||||
};
|
};
|
||||||
|
|
||||||
let update_user_plan = proto::UpdateUserPlan {
|
let update_user_plan = proto::UpdateUserPlan {
|
||||||
|
@ -411,15 +368,15 @@ async fn update_plan(
|
||||||
Ok(Json(UpdatePlanResponse {}))
|
Ok(Json(UpdatePlanResponse {}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn usage_limit_to_proto(limit: zed_llm_client::UsageLimit) -> proto::UsageLimit {
|
fn usage_limit_to_proto(limit: cloud_llm_client::UsageLimit) -> proto::UsageLimit {
|
||||||
proto::UsageLimit {
|
proto::UsageLimit {
|
||||||
variant: Some(match limit {
|
variant: Some(match limit {
|
||||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
cloud_llm_client::UsageLimit::Limited(limit) => {
|
||||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||||
limit: limit as u32,
|
limit: limit as u32,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
zed_llm_client::UsageLimit::Unlimited => {
|
cloud_llm_client::UsageLimit::Unlimited => {
|
||||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use anyhow::{Context as _, bail};
|
use anyhow::{Context as _, bail};
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use cloud_llm_client::LanguageModelProvider;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use sea_orm::ActiveValue;
|
use sea_orm::ActiveValue;
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
use stripe::{CancellationDetailsReason, EventObject, EventType, ListEvents, SubscriptionStatus};
|
use stripe::{CancellationDetailsReason, EventObject, EventType, ListEvents, SubscriptionStatus};
|
||||||
use util::{ResultExt, maybe};
|
use util::{ResultExt, maybe};
|
||||||
use zed_llm_client::LanguageModelProvider;
|
|
||||||
|
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use crate::db::billing_subscription::{
|
use crate::db::billing_subscription::{
|
||||||
|
@ -87,6 +87,14 @@ async fn poll_stripe_events(
|
||||||
stripe_client: &Arc<dyn StripeClient>,
|
stripe_client: &Arc<dyn StripeClient>,
|
||||||
real_stripe_client: &stripe::Client,
|
real_stripe_client: &stripe::Client,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
|
let feature_flags = app.db.list_feature_flags().await?;
|
||||||
|
let sync_events_using_cloud = feature_flags
|
||||||
|
.iter()
|
||||||
|
.any(|flag| flag.flag == "cloud-stripe-events-polling" && flag.enabled_for_all);
|
||||||
|
if sync_events_using_cloud {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
fn event_type_to_string(event_type: EventType) -> String {
|
fn event_type_to_string(event_type: EventType) -> String {
|
||||||
// Calling `to_string` on `stripe::EventType` members gives us a quoted string,
|
// Calling `to_string` on `stripe::EventType` members gives us a quoted string,
|
||||||
// so we need to unquote it.
|
// so we need to unquote it.
|
||||||
|
@ -569,6 +577,14 @@ async fn sync_model_request_usage_with_stripe(
|
||||||
llm_db: &Arc<LlmDatabase>,
|
llm_db: &Arc<LlmDatabase>,
|
||||||
stripe_billing: &Arc<StripeBilling>,
|
stripe_billing: &Arc<StripeBilling>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
|
let feature_flags = app.db.list_feature_flags().await?;
|
||||||
|
let sync_model_request_usage_using_cloud = feature_flags
|
||||||
|
.iter()
|
||||||
|
.any(|flag| flag.flag == "cloud-stripe-usage-meters-sync" && flag.enabled_for_all);
|
||||||
|
if sync_model_request_usage_using_cloud {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
log::info!("Stripe usage sync: Starting");
|
log::info!("Stripe usage sync: Starting");
|
||||||
let started_at = Utc::now();
|
let started_at = Utc::now();
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ use axum::{
|
||||||
use chrono::{NaiveDateTime, SecondsFormat};
|
use chrono::{NaiveDateTime, SecondsFormat};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::api::AuthenticatedUserParams;
|
|
||||||
use crate::db::ContributorSelector;
|
use crate::db::ContributorSelector;
|
||||||
use crate::{AppState, Result};
|
use crate::{AppState, Result};
|
||||||
|
|
||||||
|
@ -104,9 +103,18 @@ impl RenovateBot {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AddContributorBody {
|
||||||
|
github_user_id: i32,
|
||||||
|
github_login: String,
|
||||||
|
github_email: Option<String>,
|
||||||
|
github_name: Option<String>,
|
||||||
|
github_user_created_at: chrono::DateTime<chrono::Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
async fn add_contributor(
|
async fn add_contributor(
|
||||||
Extension(app): Extension<Arc<AppState>>,
|
Extension(app): Extension<Arc<AppState>>,
|
||||||
extract::Json(params): extract::Json<AuthenticatedUserParams>,
|
extract::Json(params): extract::Json<AddContributorBody>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let initial_channel_id = app.config.auto_join_channel_id;
|
let initial_channel_id = app.config.auto_join_channel_id;
|
||||||
app.db
|
app.db
|
||||||
|
|
|
@ -95,7 +95,7 @@ pub enum SubscriptionKind {
|
||||||
ZedFree,
|
ZedFree,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SubscriptionKind> for zed_llm_client::Plan {
|
impl From<SubscriptionKind> for cloud_llm_client::Plan {
|
||||||
fn from(value: SubscriptionKind) -> Self {
|
fn from(value: SubscriptionKind) -> Self {
|
||||||
match value {
|
match value {
|
||||||
SubscriptionKind::ZedPro => Self::ZedPro,
|
SubscriptionKind::ZedPro => Self::ZedPro,
|
||||||
|
|
|
@ -6,11 +6,11 @@ mod tables;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
|
use cloud_llm_client::LanguageModelProvider;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
pub use ids::*;
|
pub use ids::*;
|
||||||
pub use seed::*;
|
pub use seed::*;
|
||||||
pub use tables::*;
|
pub use tables::*;
|
||||||
use zed_llm_client::LanguageModelProvider;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub use tests::TestLlmDb;
|
pub use tests::TestLlmDb;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
use cloud_llm_client::LanguageModelProvider;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use zed_llm_client::LanguageModelProvider;
|
|
||||||
|
|
||||||
use crate::llm::db::LlmDatabase;
|
use crate::llm::db::LlmDatabase;
|
||||||
use crate::test_llm_db;
|
use crate::test_llm_db;
|
||||||
|
|
|
@ -4,12 +4,12 @@ use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEA
|
||||||
use crate::{Config, db::billing_preference};
|
use crate::{Config, db::billing_preference};
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use chrono::{NaiveDateTime, Utc};
|
use chrono::{NaiveDateTime, Utc};
|
||||||
|
use cloud_llm_client::Plan;
|
||||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
|
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use zed_llm_client::Plan;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
|
|
@ -23,6 +23,7 @@ use anyhow::{Context as _, anyhow, bail};
|
||||||
use async_tungstenite::tungstenite::{
|
use async_tungstenite::tungstenite::{
|
||||||
Message as TungsteniteMessage, protocol::CloseFrame as TungsteniteCloseFrame,
|
Message as TungsteniteMessage, protocol::CloseFrame as TungsteniteCloseFrame,
|
||||||
};
|
};
|
||||||
|
use axum::headers::UserAgent;
|
||||||
use axum::{
|
use axum::{
|
||||||
Extension, Router, TypedHeader,
|
Extension, Router, TypedHeader,
|
||||||
body::Body,
|
body::Body,
|
||||||
|
@ -41,7 +42,7 @@ use collections::{HashMap, HashSet};
|
||||||
pub use connection_pool::{ConnectionPool, ZedVersion};
|
pub use connection_pool::{ConnectionPool, ZedVersion};
|
||||||
use core::fmt::{self, Debug, Formatter};
|
use core::fmt::{self, Debug, Formatter};
|
||||||
use reqwest_client::ReqwestClient;
|
use reqwest_client::ReqwestClient;
|
||||||
use rpc::proto::split_repository_update;
|
use rpc::proto::{MultiLspQuery, split_repository_update};
|
||||||
use supermaven_api::{CreateExternalUserRequest, SupermavenAdminApi};
|
use supermaven_api::{CreateExternalUserRequest, SupermavenAdminApi};
|
||||||
|
|
||||||
use futures::{
|
use futures::{
|
||||||
|
@ -373,7 +374,7 @@ impl Server {
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::OnTypeFormatting>)
|
.add_request_handler(forward_mutating_project_request::<proto::OnTypeFormatting>)
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::SaveBuffer>)
|
.add_request_handler(forward_mutating_project_request::<proto::SaveBuffer>)
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::BlameBuffer>)
|
.add_request_handler(forward_mutating_project_request::<proto::BlameBuffer>)
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::MultiLspQuery>)
|
.add_request_handler(multi_lsp_query)
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::RestartLanguageServers>)
|
.add_request_handler(forward_mutating_project_request::<proto::RestartLanguageServers>)
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::StopLanguageServers>)
|
.add_request_handler(forward_mutating_project_request::<proto::StopLanguageServers>)
|
||||||
.add_request_handler(forward_mutating_project_request::<proto::LinkedEditingRange>)
|
.add_request_handler(forward_mutating_project_request::<proto::LinkedEditingRange>)
|
||||||
|
@ -750,6 +751,7 @@ impl Server {
|
||||||
address: String,
|
address: String,
|
||||||
principal: Principal,
|
principal: Principal,
|
||||||
zed_version: ZedVersion,
|
zed_version: ZedVersion,
|
||||||
|
user_agent: Option<String>,
|
||||||
geoip_country_code: Option<String>,
|
geoip_country_code: Option<String>,
|
||||||
system_id: Option<String>,
|
system_id: Option<String>,
|
||||||
send_connection_id: Option<oneshot::Sender<ConnectionId>>,
|
send_connection_id: Option<oneshot::Sender<ConnectionId>>,
|
||||||
|
@ -762,9 +764,14 @@ impl Server {
|
||||||
user_id=field::Empty,
|
user_id=field::Empty,
|
||||||
login=field::Empty,
|
login=field::Empty,
|
||||||
impersonator=field::Empty,
|
impersonator=field::Empty,
|
||||||
|
user_agent=field::Empty,
|
||||||
geoip_country_code=field::Empty
|
geoip_country_code=field::Empty
|
||||||
);
|
);
|
||||||
principal.update_span(&span);
|
principal.update_span(&span);
|
||||||
|
if let Some(user_agent) = user_agent {
|
||||||
|
span.record("user_agent", user_agent);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(country_code) = geoip_country_code.as_ref() {
|
if let Some(country_code) = geoip_country_code.as_ref() {
|
||||||
span.record("geoip_country_code", country_code);
|
span.record("geoip_country_code", country_code);
|
||||||
}
|
}
|
||||||
|
@ -831,7 +838,7 @@ impl Server {
|
||||||
// This arrangement ensures we will attempt to process earlier messages first, but fall
|
// This arrangement ensures we will attempt to process earlier messages first, but fall
|
||||||
// back to processing messages arrived later in the spirit of making progress.
|
// back to processing messages arrived later in the spirit of making progress.
|
||||||
let mut foreground_message_handlers = FuturesUnordered::new();
|
let mut foreground_message_handlers = FuturesUnordered::new();
|
||||||
let concurrent_handlers = Arc::new(Semaphore::new(512));
|
let concurrent_handlers = Arc::new(Semaphore::new(256));
|
||||||
loop {
|
loop {
|
||||||
let next_message = async {
|
let next_message = async {
|
||||||
let permit = concurrent_handlers.clone().acquire_owned().await.unwrap();
|
let permit = concurrent_handlers.clone().acquire_owned().await.unwrap();
|
||||||
|
@ -858,6 +865,7 @@ impl Server {
|
||||||
user_id=field::Empty,
|
user_id=field::Empty,
|
||||||
login=field::Empty,
|
login=field::Empty,
|
||||||
impersonator=field::Empty,
|
impersonator=field::Empty,
|
||||||
|
multi_lsp_query_request=field::Empty,
|
||||||
);
|
);
|
||||||
principal.update_span(&span);
|
principal.update_span(&span);
|
||||||
let span_enter = span.enter();
|
let span_enter = span.enter();
|
||||||
|
@ -1172,6 +1180,7 @@ pub async fn handle_websocket_request(
|
||||||
ConnectInfo(socket_address): ConnectInfo<SocketAddr>,
|
ConnectInfo(socket_address): ConnectInfo<SocketAddr>,
|
||||||
Extension(server): Extension<Arc<Server>>,
|
Extension(server): Extension<Arc<Server>>,
|
||||||
Extension(principal): Extension<Principal>,
|
Extension(principal): Extension<Principal>,
|
||||||
|
user_agent: Option<TypedHeader<UserAgent>>,
|
||||||
country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
|
country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
|
||||||
system_id_header: Option<TypedHeader<SystemIdHeader>>,
|
system_id_header: Option<TypedHeader<SystemIdHeader>>,
|
||||||
ws: WebSocketUpgrade,
|
ws: WebSocketUpgrade,
|
||||||
|
@ -1227,6 +1236,7 @@ pub async fn handle_websocket_request(
|
||||||
socket_address,
|
socket_address,
|
||||||
principal,
|
principal,
|
||||||
version,
|
version,
|
||||||
|
user_agent.map(|header| header.to_string()),
|
||||||
country_code_header.map(|header| header.to_string()),
|
country_code_header.map(|header| header.to_string()),
|
||||||
system_id_header.map(|header| header.to_string()),
|
system_id_header.map(|header| header.to_string()),
|
||||||
None,
|
None,
|
||||||
|
@ -2320,6 +2330,15 @@ where
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn multi_lsp_query(
|
||||||
|
request: MultiLspQuery,
|
||||||
|
response: Response<MultiLspQuery>,
|
||||||
|
session: Session,
|
||||||
|
) -> Result<()> {
|
||||||
|
tracing::Span::current().record("multi_lsp_query_request", request.request_str());
|
||||||
|
forward_mutating_project_request(request, response, session).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Notify other participants that a new buffer has been created
|
/// Notify other participants that a new buffer has been created
|
||||||
async fn create_buffer_for_peer(
|
async fn create_buffer_for_peer(
|
||||||
request: proto::CreateBufferForPeer,
|
request: proto::CreateBufferForPeer,
|
||||||
|
@ -2859,12 +2878,12 @@ async fn make_update_user_plan_message(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn model_requests_limit(
|
fn model_requests_limit(
|
||||||
plan: zed_llm_client::Plan,
|
plan: cloud_llm_client::Plan,
|
||||||
feature_flags: &Vec<String>,
|
feature_flags: &Vec<String>,
|
||||||
) -> zed_llm_client::UsageLimit {
|
) -> cloud_llm_client::UsageLimit {
|
||||||
match plan.model_requests_limit() {
|
match plan.model_requests_limit() {
|
||||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
cloud_llm_client::UsageLimit::Limited(limit) => {
|
||||||
let limit = if plan == zed_llm_client::Plan::ZedProTrial
|
let limit = if plan == cloud_llm_client::Plan::ZedProTrial
|
||||||
&& feature_flags
|
&& feature_flags
|
||||||
.iter()
|
.iter()
|
||||||
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG)
|
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG)
|
||||||
|
@ -2874,9 +2893,9 @@ fn model_requests_limit(
|
||||||
limit
|
limit
|
||||||
};
|
};
|
||||||
|
|
||||||
zed_llm_client::UsageLimit::Limited(limit)
|
cloud_llm_client::UsageLimit::Limited(limit)
|
||||||
}
|
}
|
||||||
zed_llm_client::UsageLimit::Unlimited => zed_llm_client::UsageLimit::Unlimited,
|
cloud_llm_client::UsageLimit::Unlimited => cloud_llm_client::UsageLimit::Unlimited,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2886,21 +2905,21 @@ fn subscription_usage_to_proto(
|
||||||
feature_flags: &Vec<String>,
|
feature_flags: &Vec<String>,
|
||||||
) -> proto::SubscriptionUsage {
|
) -> proto::SubscriptionUsage {
|
||||||
let plan = match plan {
|
let plan = match plan {
|
||||||
proto::Plan::Free => zed_llm_client::Plan::ZedFree,
|
proto::Plan::Free => cloud_llm_client::Plan::ZedFree,
|
||||||
proto::Plan::ZedPro => zed_llm_client::Plan::ZedPro,
|
proto::Plan::ZedPro => cloud_llm_client::Plan::ZedPro,
|
||||||
proto::Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
proto::Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial,
|
||||||
};
|
};
|
||||||
|
|
||||||
proto::SubscriptionUsage {
|
proto::SubscriptionUsage {
|
||||||
model_requests_usage_amount: usage.model_requests as u32,
|
model_requests_usage_amount: usage.model_requests as u32,
|
||||||
model_requests_usage_limit: Some(proto::UsageLimit {
|
model_requests_usage_limit: Some(proto::UsageLimit {
|
||||||
variant: Some(match model_requests_limit(plan, feature_flags) {
|
variant: Some(match model_requests_limit(plan, feature_flags) {
|
||||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
cloud_llm_client::UsageLimit::Limited(limit) => {
|
||||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||||
limit: limit as u32,
|
limit: limit as u32,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
zed_llm_client::UsageLimit::Unlimited => {
|
cloud_llm_client::UsageLimit::Unlimited => {
|
||||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -2908,12 +2927,12 @@ fn subscription_usage_to_proto(
|
||||||
edit_predictions_usage_amount: usage.edit_predictions as u32,
|
edit_predictions_usage_amount: usage.edit_predictions as u32,
|
||||||
edit_predictions_usage_limit: Some(proto::UsageLimit {
|
edit_predictions_usage_limit: Some(proto::UsageLimit {
|
||||||
variant: Some(match plan.edit_predictions_limit() {
|
variant: Some(match plan.edit_predictions_limit() {
|
||||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
cloud_llm_client::UsageLimit::Limited(limit) => {
|
||||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||||
limit: limit as u32,
|
limit: limit as u32,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
zed_llm_client::UsageLimit::Unlimited => {
|
cloud_llm_client::UsageLimit::Unlimited => {
|
||||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -2926,21 +2945,21 @@ fn make_default_subscription_usage(
|
||||||
feature_flags: &Vec<String>,
|
feature_flags: &Vec<String>,
|
||||||
) -> proto::SubscriptionUsage {
|
) -> proto::SubscriptionUsage {
|
||||||
let plan = match plan {
|
let plan = match plan {
|
||||||
proto::Plan::Free => zed_llm_client::Plan::ZedFree,
|
proto::Plan::Free => cloud_llm_client::Plan::ZedFree,
|
||||||
proto::Plan::ZedPro => zed_llm_client::Plan::ZedPro,
|
proto::Plan::ZedPro => cloud_llm_client::Plan::ZedPro,
|
||||||
proto::Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
proto::Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial,
|
||||||
};
|
};
|
||||||
|
|
||||||
proto::SubscriptionUsage {
|
proto::SubscriptionUsage {
|
||||||
model_requests_usage_amount: 0,
|
model_requests_usage_amount: 0,
|
||||||
model_requests_usage_limit: Some(proto::UsageLimit {
|
model_requests_usage_limit: Some(proto::UsageLimit {
|
||||||
variant: Some(match model_requests_limit(plan, feature_flags) {
|
variant: Some(match model_requests_limit(plan, feature_flags) {
|
||||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
cloud_llm_client::UsageLimit::Limited(limit) => {
|
||||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||||
limit: limit as u32,
|
limit: limit as u32,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
zed_llm_client::UsageLimit::Unlimited => {
|
cloud_llm_client::UsageLimit::Unlimited => {
|
||||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -2948,12 +2967,12 @@ fn make_default_subscription_usage(
|
||||||
edit_predictions_usage_amount: 0,
|
edit_predictions_usage_amount: 0,
|
||||||
edit_predictions_usage_limit: Some(proto::UsageLimit {
|
edit_predictions_usage_limit: Some(proto::UsageLimit {
|
||||||
variant: Some(match plan.edit_predictions_limit() {
|
variant: Some(match plan.edit_predictions_limit() {
|
||||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
cloud_llm_client::UsageLimit::Limited(limit) => {
|
||||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||||
limit: limit as u32,
|
limit: limit as u32,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
zed_llm_client::UsageLimit::Unlimited => {
|
cloud_llm_client::UsageLimit::Unlimited => {
|
||||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -38,12 +38,12 @@ fn room_participants(room: &Entity<Room>, cx: &mut TestAppContext) -> RoomPartic
|
||||||
let mut remote = room
|
let mut remote = room
|
||||||
.remote_participants()
|
.remote_participants()
|
||||||
.values()
|
.values()
|
||||||
.map(|participant| participant.user.github_login.clone())
|
.map(|participant| participant.user.github_login.clone().to_string())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let mut pending = room
|
let mut pending = room
|
||||||
.pending_participants()
|
.pending_participants()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|user| user.github_login.clone())
|
.map(|user| user.github_login.clone().to_string())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
remote.sort();
|
remote.sort();
|
||||||
pending.sort();
|
pending.sort();
|
||||||
|
|
|
@ -842,7 +842,7 @@ async fn test_client_disconnecting_from_room(
|
||||||
|
|
||||||
// Allow user A to reconnect to the server.
|
// Allow user A to reconnect to the server.
|
||||||
server.allow_connections();
|
server.allow_connections();
|
||||||
executor.advance_clock(RECEIVE_TIMEOUT);
|
executor.advance_clock(RECONNECT_TIMEOUT);
|
||||||
|
|
||||||
// Call user B again from client A.
|
// Call user B again from client A.
|
||||||
active_call_a
|
active_call_a
|
||||||
|
@ -1286,7 +1286,7 @@ async fn test_calls_on_multiple_connections(
|
||||||
client_b1.disconnect(&cx_b1.to_async());
|
client_b1.disconnect(&cx_b1.to_async());
|
||||||
executor.advance_clock(RECEIVE_TIMEOUT);
|
executor.advance_clock(RECEIVE_TIMEOUT);
|
||||||
client_b1
|
client_b1
|
||||||
.authenticate_and_connect(false, &cx_b1.to_async())
|
.connect(false, &cx_b1.to_async())
|
||||||
.await
|
.await
|
||||||
.into_response()
|
.into_response()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -1358,7 +1358,7 @@ async fn test_calls_on_multiple_connections(
|
||||||
|
|
||||||
// User A reconnects automatically, then calls user B again.
|
// User A reconnects automatically, then calls user B again.
|
||||||
server.allow_connections();
|
server.allow_connections();
|
||||||
executor.advance_clock(RECEIVE_TIMEOUT);
|
executor.advance_clock(RECONNECT_TIMEOUT);
|
||||||
active_call_a
|
active_call_a
|
||||||
.update(cx_a, |call, cx| {
|
.update(cx_a, |call, cx| {
|
||||||
call.invite(client_b1.user_id().unwrap(), None, cx)
|
call.invite(client_b1.user_id().unwrap(), None, cx)
|
||||||
|
@ -1667,7 +1667,7 @@ async fn test_project_reconnect(
|
||||||
// Client A reconnects. Their project is re-shared, and client B re-joins it.
|
// Client A reconnects. Their project is re-shared, and client B re-joins it.
|
||||||
server.allow_connections();
|
server.allow_connections();
|
||||||
client_a
|
client_a
|
||||||
.authenticate_and_connect(false, &cx_a.to_async())
|
.connect(false, &cx_a.to_async())
|
||||||
.await
|
.await
|
||||||
.into_response()
|
.into_response()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -1796,7 +1796,7 @@ async fn test_project_reconnect(
|
||||||
// Client B reconnects. They re-join the room and the remaining shared project.
|
// Client B reconnects. They re-join the room and the remaining shared project.
|
||||||
server.allow_connections();
|
server.allow_connections();
|
||||||
client_b
|
client_b
|
||||||
.authenticate_and_connect(false, &cx_b.to_async())
|
.connect(false, &cx_b.to_async())
|
||||||
.await
|
.await
|
||||||
.into_response()
|
.into_response()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -1881,7 +1881,7 @@ async fn test_active_call_events(
|
||||||
vec![room::Event::RemoteProjectShared {
|
vec![room::Event::RemoteProjectShared {
|
||||||
owner: Arc::new(User {
|
owner: Arc::new(User {
|
||||||
id: client_a.user_id().unwrap(),
|
id: client_a.user_id().unwrap(),
|
||||||
github_login: "user_a".to_string(),
|
github_login: "user_a".into(),
|
||||||
avatar_uri: "avatar_a".into(),
|
avatar_uri: "avatar_a".into(),
|
||||||
name: None,
|
name: None,
|
||||||
}),
|
}),
|
||||||
|
@ -1900,7 +1900,7 @@ async fn test_active_call_events(
|
||||||
vec![room::Event::RemoteProjectShared {
|
vec![room::Event::RemoteProjectShared {
|
||||||
owner: Arc::new(User {
|
owner: Arc::new(User {
|
||||||
id: client_b.user_id().unwrap(),
|
id: client_b.user_id().unwrap(),
|
||||||
github_login: "user_b".to_string(),
|
github_login: "user_b".into(),
|
||||||
avatar_uri: "avatar_b".into(),
|
avatar_uri: "avatar_b".into(),
|
||||||
name: None,
|
name: None,
|
||||||
}),
|
}),
|
||||||
|
@ -5738,7 +5738,7 @@ async fn test_contacts(
|
||||||
|
|
||||||
server.allow_connections();
|
server.allow_connections();
|
||||||
client_c
|
client_c
|
||||||
.authenticate_and_connect(false, &cx_c.to_async())
|
.connect(false, &cx_c.to_async())
|
||||||
.await
|
.await
|
||||||
.into_response()
|
.into_response()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -6079,7 +6079,7 @@ async fn test_contacts(
|
||||||
.iter()
|
.iter()
|
||||||
.map(|contact| {
|
.map(|contact| {
|
||||||
(
|
(
|
||||||
contact.user.github_login.clone(),
|
contact.user.github_login.clone().to_string(),
|
||||||
if contact.online { "online" } else { "offline" },
|
if contact.online { "online" } else { "offline" },
|
||||||
if contact.busy { "busy" } else { "free" },
|
if contact.busy { "busy" } else { "free" },
|
||||||
)
|
)
|
||||||
|
@ -6269,7 +6269,7 @@ async fn test_contact_requests(
|
||||||
client.disconnect(&cx.to_async());
|
client.disconnect(&cx.to_async());
|
||||||
client.clear_contacts(cx).await;
|
client.clear_contacts(cx).await;
|
||||||
client
|
client
|
||||||
.authenticate_and_connect(false, &cx.to_async())
|
.connect(false, &cx.to_async())
|
||||||
.await
|
.await
|
||||||
.into_response()
|
.into_response()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -3,6 +3,7 @@ use std::sync::Arc;
|
||||||
use gpui::{BackgroundExecutor, TestAppContext};
|
use gpui::{BackgroundExecutor, TestAppContext};
|
||||||
use notifications::NotificationEvent;
|
use notifications::NotificationEvent;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
use rpc::{Notification, proto};
|
use rpc::{Notification, proto};
|
||||||
|
|
||||||
use crate::tests::TestServer;
|
use crate::tests::TestServer;
|
||||||
|
@ -17,6 +18,9 @@ async fn test_notifications(
|
||||||
let client_a = server.create_client(cx_a, "user_a").await;
|
let client_a = server.create_client(cx_a, "user_a").await;
|
||||||
let client_b = server.create_client(cx_b, "user_b").await;
|
let client_b = server.create_client(cx_b, "user_b").await;
|
||||||
|
|
||||||
|
// Wait for authentication/connection to Collab to be established.
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
let notification_events_a = Arc::new(Mutex::new(Vec::new()));
|
let notification_events_a = Arc::new(Mutex::new(Vec::new()));
|
||||||
let notification_events_b = Arc::new(Mutex::new(Vec::new()));
|
let notification_events_b = Arc::new(Mutex::new(Vec::new()));
|
||||||
client_a.notification_store().update(cx_a, |_, cx| {
|
client_a.notification_store().update(cx_a, |_, cx| {
|
||||||
|
|
|
@ -8,6 +8,7 @@ use crate::{
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use channel::{ChannelBuffer, ChannelStore};
|
use channel::{ChannelBuffer, ChannelStore};
|
||||||
|
use client::test::{make_get_authenticated_user_response, parse_authorization_header};
|
||||||
use client::{
|
use client::{
|
||||||
self, ChannelId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
|
self, ChannelId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
|
||||||
proto::PeerId,
|
proto::PeerId,
|
||||||
|
@ -20,7 +21,7 @@ use fs::FakeFs;
|
||||||
use futures::{StreamExt as _, channel::oneshot};
|
use futures::{StreamExt as _, channel::oneshot};
|
||||||
use git::GitHostingProviderRegistry;
|
use git::GitHostingProviderRegistry;
|
||||||
use gpui::{AppContext as _, BackgroundExecutor, Entity, Task, TestAppContext, VisualTestContext};
|
use gpui::{AppContext as _, BackgroundExecutor, Entity, Task, TestAppContext, VisualTestContext};
|
||||||
use http_client::FakeHttpClient;
|
use http_client::{FakeHttpClient, Method};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
use notifications::NotificationStore;
|
use notifications::NotificationStore;
|
||||||
|
@ -161,6 +162,8 @@ impl TestServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
|
pub async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
|
||||||
|
const ACCESS_TOKEN: &str = "the-token";
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
|
@ -175,7 +178,7 @@ impl TestServer {
|
||||||
});
|
});
|
||||||
|
|
||||||
let clock = Arc::new(FakeSystemClock::new());
|
let clock = Arc::new(FakeSystemClock::new());
|
||||||
let http = FakeHttpClient::with_404_response();
|
|
||||||
let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
|
let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
|
||||||
{
|
{
|
||||||
user.id
|
user.id
|
||||||
|
@ -197,6 +200,47 @@ impl TestServer {
|
||||||
.expect("creating user failed")
|
.expect("creating user failed")
|
||||||
.user_id
|
.user_id
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let http = FakeHttpClient::create({
|
||||||
|
let name = name.to_string();
|
||||||
|
move |req| {
|
||||||
|
let name = name.clone();
|
||||||
|
async move {
|
||||||
|
match (req.method(), req.uri().path()) {
|
||||||
|
(&Method::GET, "/client/users/me") => {
|
||||||
|
let credentials = parse_authorization_header(&req);
|
||||||
|
if credentials
|
||||||
|
!= Some(Credentials {
|
||||||
|
user_id: user_id.to_proto(),
|
||||||
|
access_token: ACCESS_TOKEN.into(),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Ok(http_client::Response::builder()
|
||||||
|
.status(401)
|
||||||
|
.body("Unauthorized".into())
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(http_client::Response::builder()
|
||||||
|
.status(200)
|
||||||
|
.body(
|
||||||
|
serde_json::to_string(&make_get_authenticated_user_response(
|
||||||
|
user_id.0, name,
|
||||||
|
))
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
.unwrap())
|
||||||
|
}
|
||||||
|
_ => Ok(http_client::Response::builder()
|
||||||
|
.status(404)
|
||||||
|
.body("Not Found".into())
|
||||||
|
.unwrap()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let client_name = name.to_string();
|
let client_name = name.to_string();
|
||||||
let mut client = cx.update(|cx| Client::new(clock, http.clone(), cx));
|
let mut client = cx.update(|cx| Client::new(clock, http.clone(), cx));
|
||||||
let server = self.server.clone();
|
let server = self.server.clone();
|
||||||
|
@ -208,11 +252,10 @@ impl TestServer {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_id(user_id.to_proto())
|
.set_id(user_id.to_proto())
|
||||||
.override_authenticate(move |cx| {
|
.override_authenticate(move |cx| {
|
||||||
let access_token = "the-token".to_string();
|
|
||||||
cx.spawn(async move |_| {
|
cx.spawn(async move |_| {
|
||||||
Ok(Credentials {
|
Ok(Credentials {
|
||||||
user_id: user_id.to_proto(),
|
user_id: user_id.to_proto(),
|
||||||
access_token,
|
access_token: ACCESS_TOKEN.into(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -221,7 +264,7 @@ impl TestServer {
|
||||||
credentials,
|
credentials,
|
||||||
&Credentials {
|
&Credentials {
|
||||||
user_id: user_id.0 as u64,
|
user_id: user_id.0 as u64,
|
||||||
access_token: "the-token".into()
|
access_token: ACCESS_TOKEN.into(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -256,6 +299,7 @@ impl TestServer {
|
||||||
ZedVersion(SemanticVersion::new(1, 0, 0)),
|
ZedVersion(SemanticVersion::new(1, 0, 0)),
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
Some(connection_id_tx),
|
Some(connection_id_tx),
|
||||||
Executor::Deterministic(cx.background_executor().clone()),
|
Executor::Deterministic(cx.background_executor().clone()),
|
||||||
None,
|
None,
|
||||||
|
@ -318,7 +362,7 @@ impl TestServer {
|
||||||
});
|
});
|
||||||
|
|
||||||
client
|
client
|
||||||
.authenticate_and_connect(false, &cx.to_async())
|
.connect(false, &cx.to_async())
|
||||||
.await
|
.await
|
||||||
.into_response()
|
.into_response()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -691,17 +735,17 @@ impl TestClient {
|
||||||
current: store
|
current: store
|
||||||
.contacts()
|
.contacts()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|contact| contact.user.github_login.clone())
|
.map(|contact| contact.user.github_login.clone().to_string())
|
||||||
.collect(),
|
.collect(),
|
||||||
outgoing_requests: store
|
outgoing_requests: store
|
||||||
.outgoing_contact_requests()
|
.outgoing_contact_requests()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|user| user.github_login.clone())
|
.map(|user| user.github_login.clone().to_string())
|
||||||
.collect(),
|
.collect(),
|
||||||
incoming_requests: store
|
incoming_requests: store
|
||||||
.incoming_contact_requests()
|
.incoming_contact_requests()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|user| user.github_login.clone())
|
.map(|user| user.github_login.clone().to_string())
|
||||||
.collect(),
|
.collect(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -940,7 +940,7 @@ impl CollabPanel {
|
||||||
room.read(cx).local_participant().role == proto::ChannelRole::Admin
|
room.read(cx).local_participant().role == proto::ChannelRole::Admin
|
||||||
});
|
});
|
||||||
|
|
||||||
ListItem::new(SharedString::from(user.github_login.clone()))
|
ListItem::new(user.github_login.clone())
|
||||||
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||||
.child(Label::new(user.github_login.clone()))
|
.child(Label::new(user.github_login.clone()))
|
||||||
.toggle_state(is_selected)
|
.toggle_state(is_selected)
|
||||||
|
@ -2331,7 +2331,7 @@ impl CollabPanel {
|
||||||
let client = this.client.clone();
|
let client = this.client.clone();
|
||||||
cx.spawn_in(window, async move |_, cx| {
|
cx.spawn_in(window, async move |_, cx| {
|
||||||
client
|
client
|
||||||
.authenticate_and_connect(true, &cx)
|
.connect(true, &cx)
|
||||||
.await
|
.await
|
||||||
.into_response()
|
.into_response()
|
||||||
.notify_async_err(cx);
|
.notify_async_err(cx);
|
||||||
|
@ -2583,7 +2583,7 @@ impl CollabPanel {
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let online = contact.online;
|
let online = contact.online;
|
||||||
let busy = contact.busy || calling;
|
let busy = contact.busy || calling;
|
||||||
let github_login = SharedString::from(contact.user.github_login.clone());
|
let github_login = contact.user.github_login.clone();
|
||||||
let item = ListItem::new(github_login.clone())
|
let item = ListItem::new(github_login.clone())
|
||||||
.indent_level(1)
|
.indent_level(1)
|
||||||
.indent_step_size(px(20.))
|
.indent_step_size(px(20.))
|
||||||
|
@ -2662,7 +2662,7 @@ impl CollabPanel {
|
||||||
is_selected: bool,
|
is_selected: bool,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let github_login = SharedString::from(user.github_login.clone());
|
let github_login = user.github_login.clone();
|
||||||
let user_id = user.id;
|
let user_id = user.id;
|
||||||
let is_response_pending = self.user_store.read(cx).is_contact_request_pending(user);
|
let is_response_pending = self.user_store.read(cx).is_contact_request_pending(user);
|
||||||
let color = if is_response_pending {
|
let color = if is_response_pending {
|
||||||
|
|
|
@ -634,13 +634,13 @@ impl Render for NotificationPanel {
|
||||||
.child(Icon::new(IconName::Envelope)),
|
.child(Icon::new(IconName::Envelope)),
|
||||||
)
|
)
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if self.client.user_id().is_none() {
|
if !self.client.status().borrow().is_connected() {
|
||||||
this.child(
|
this.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.p_4()
|
.p_4()
|
||||||
.child(
|
.child(
|
||||||
Button::new("sign_in_prompt_button", "Sign in")
|
Button::new("connect_prompt_button", "Connect")
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
.icon(IconName::Github)
|
.icon(IconName::Github)
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
|
@ -652,10 +652,7 @@ impl Render for NotificationPanel {
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
window
|
window
|
||||||
.spawn(cx, async move |cx| {
|
.spawn(cx, async move |cx| {
|
||||||
match client
|
match client.connect(true, &cx).await {
|
||||||
.authenticate_and_connect(true, &cx)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
util::ConnectionResult::Timeout => {
|
util::ConnectionResult::Timeout => {
|
||||||
log::error!("Connection timeout");
|
log::error!("Connection timeout");
|
||||||
}
|
}
|
||||||
|
@ -673,7 +670,7 @@ impl Render for NotificationPanel {
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().flex().w_full().items_center().child(
|
div().flex().w_full().items_center().child(
|
||||||
Label::new("Sign in to view notifications.")
|
Label::new("Connect to view notifications.")
|
||||||
.color(Color::Muted)
|
.color(Color::Muted)
|
||||||
.size(LabelSize::Small),
|
.size(LabelSize::Small),
|
||||||
),
|
),
|
||||||
|
|
|
@ -158,6 +158,7 @@ impl Client {
|
||||||
pub fn stdio(
|
pub fn stdio(
|
||||||
server_id: ContextServerId,
|
server_id: ContextServerId,
|
||||||
binary: ModelContextServerBinary,
|
binary: ModelContextServerBinary,
|
||||||
|
working_directory: &Option<PathBuf>,
|
||||||
cx: AsyncApp,
|
cx: AsyncApp,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
log::info!(
|
log::info!(
|
||||||
|
@ -172,7 +173,7 @@ impl Client {
|
||||||
.map(|name| name.to_string_lossy().to_string())
|
.map(|name| name.to_string_lossy().to_string())
|
||||||
.unwrap_or_else(String::new);
|
.unwrap_or_else(String::new);
|
||||||
|
|
||||||
let transport = Arc::new(StdioTransport::new(binary, &cx)?);
|
let transport = Arc::new(StdioTransport::new(binary, working_directory, &cx)?);
|
||||||
Self::new(server_id, server_name.into(), transport, cx)
|
Self::new(server_id, server_name.into(), transport, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ impl std::fmt::Debug for ContextServerCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ContextServerTransport {
|
enum ContextServerTransport {
|
||||||
Stdio(ContextServerCommand),
|
Stdio(ContextServerCommand, Option<PathBuf>),
|
||||||
Custom(Arc<dyn crate::transport::Transport>),
|
Custom(Arc<dyn crate::transport::Transport>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,11 +64,18 @@ pub struct ContextServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextServer {
|
impl ContextServer {
|
||||||
pub fn stdio(id: ContextServerId, command: ContextServerCommand) -> Self {
|
pub fn stdio(
|
||||||
|
id: ContextServerId,
|
||||||
|
command: ContextServerCommand,
|
||||||
|
working_directory: Option<Arc<Path>>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
client: RwLock::new(None),
|
client: RwLock::new(None),
|
||||||
configuration: ContextServerTransport::Stdio(command),
|
configuration: ContextServerTransport::Stdio(
|
||||||
|
command,
|
||||||
|
working_directory.map(|directory| directory.to_path_buf()),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,13 +97,14 @@ impl ContextServer {
|
||||||
|
|
||||||
pub async fn start(self: Arc<Self>, cx: &AsyncApp) -> Result<()> {
|
pub async fn start(self: Arc<Self>, cx: &AsyncApp) -> Result<()> {
|
||||||
let client = match &self.configuration {
|
let client = match &self.configuration {
|
||||||
ContextServerTransport::Stdio(command) => Client::stdio(
|
ContextServerTransport::Stdio(command, working_directory) => Client::stdio(
|
||||||
client::ContextServerId(self.id.0.clone()),
|
client::ContextServerId(self.id.0.clone()),
|
||||||
client::ModelContextServerBinary {
|
client::ModelContextServerBinary {
|
||||||
executable: Path::new(&command.path).to_path_buf(),
|
executable: Path::new(&command.path).to_path_buf(),
|
||||||
args: command.args.clone(),
|
args: command.args.clone(),
|
||||||
env: command.env.clone(),
|
env: command.env.clone(),
|
||||||
},
|
},
|
||||||
|
working_directory,
|
||||||
cx.clone(),
|
cx.clone(),
|
||||||
)?,
|
)?,
|
||||||
ContextServerTransport::Custom(transport) => Client::new(
|
ContextServerTransport::Custom(transport) => Client::new(
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
|
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
|
@ -22,7 +23,11 @@ pub struct StdioTransport {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StdioTransport {
|
impl StdioTransport {
|
||||||
pub fn new(binary: ModelContextServerBinary, cx: &AsyncApp) -> Result<Self> {
|
pub fn new(
|
||||||
|
binary: ModelContextServerBinary,
|
||||||
|
working_directory: &Option<PathBuf>,
|
||||||
|
cx: &AsyncApp,
|
||||||
|
) -> Result<Self> {
|
||||||
let mut command = util::command::new_smol_command(&binary.executable);
|
let mut command = util::command::new_smol_command(&binary.executable);
|
||||||
command
|
command
|
||||||
.args(&binary.args)
|
.args(&binary.args)
|
||||||
|
@ -32,6 +37,10 @@ impl StdioTransport {
|
||||||
.stderr(std::process::Stdio::piped())
|
.stderr(std::process::Stdio::piped())
|
||||||
.kill_on_drop(true);
|
.kill_on_drop(true);
|
||||||
|
|
||||||
|
if let Some(working_directory) = working_directory {
|
||||||
|
command.current_dir(working_directory);
|
||||||
|
}
|
||||||
|
|
||||||
let mut server = command.spawn().with_context(|| {
|
let mut server = command.spawn().with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"failed to spawn command. (path={:?}, args={:?})",
|
"failed to spawn command. (path={:?}, args={:?})",
|
||||||
|
|
|
@ -85,45 +85,13 @@ pub fn init(
|
||||||
move |cx| Copilot::start(new_server_id, fs, node_runtime, cx)
|
move |cx| Copilot::start(new_server_id, fs, node_runtime, cx)
|
||||||
});
|
});
|
||||||
Copilot::set_global(copilot.clone(), cx);
|
Copilot::set_global(copilot.clone(), cx);
|
||||||
cx.observe(&copilot, |handle, cx| {
|
cx.observe(&copilot, |copilot, cx| {
|
||||||
let copilot_action_types = [
|
copilot.update(cx, |copilot, cx| copilot.update_action_visibilities(cx));
|
||||||
TypeId::of::<Suggest>(),
|
})
|
||||||
TypeId::of::<NextSuggestion>(),
|
.detach();
|
||||||
TypeId::of::<PreviousSuggestion>(),
|
cx.observe_global::<SettingsStore>(|cx| {
|
||||||
TypeId::of::<Reinstall>(),
|
if let Some(copilot) = Copilot::global(cx) {
|
||||||
];
|
copilot.update(cx, |copilot, cx| copilot.update_action_visibilities(cx));
|
||||||
let copilot_auth_action_types = [TypeId::of::<SignOut>()];
|
|
||||||
let copilot_no_auth_action_types = [TypeId::of::<SignIn>()];
|
|
||||||
let status = handle.read(cx).status();
|
|
||||||
|
|
||||||
let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
|
|
||||||
let filter = CommandPaletteFilter::global_mut(cx);
|
|
||||||
|
|
||||||
if is_ai_disabled {
|
|
||||||
filter.hide_action_types(&copilot_action_types);
|
|
||||||
filter.hide_action_types(&copilot_auth_action_types);
|
|
||||||
filter.hide_action_types(&copilot_no_auth_action_types);
|
|
||||||
} else {
|
|
||||||
match status {
|
|
||||||
Status::Disabled => {
|
|
||||||
filter.hide_action_types(&copilot_action_types);
|
|
||||||
filter.hide_action_types(&copilot_auth_action_types);
|
|
||||||
filter.hide_action_types(&copilot_no_auth_action_types);
|
|
||||||
}
|
|
||||||
Status::Authorized => {
|
|
||||||
filter.hide_action_types(&copilot_no_auth_action_types);
|
|
||||||
filter.show_action_types(
|
|
||||||
copilot_action_types
|
|
||||||
.iter()
|
|
||||||
.chain(&copilot_auth_action_types),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
filter.hide_action_types(&copilot_action_types);
|
|
||||||
filter.hide_action_types(&copilot_auth_action_types);
|
|
||||||
filter.show_action_types(copilot_no_auth_action_types.iter());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
@ -1131,6 +1099,44 @@ impl Copilot {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_action_visibilities(&self, cx: &mut App) {
|
||||||
|
let signed_in_actions = [
|
||||||
|
TypeId::of::<Suggest>(),
|
||||||
|
TypeId::of::<NextSuggestion>(),
|
||||||
|
TypeId::of::<PreviousSuggestion>(),
|
||||||
|
TypeId::of::<Reinstall>(),
|
||||||
|
];
|
||||||
|
let auth_actions = [TypeId::of::<SignOut>()];
|
||||||
|
let no_auth_actions = [TypeId::of::<SignIn>()];
|
||||||
|
let status = self.status();
|
||||||
|
|
||||||
|
let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
|
||||||
|
let filter = CommandPaletteFilter::global_mut(cx);
|
||||||
|
|
||||||
|
if is_ai_disabled {
|
||||||
|
filter.hide_action_types(&signed_in_actions);
|
||||||
|
filter.hide_action_types(&auth_actions);
|
||||||
|
filter.hide_action_types(&no_auth_actions);
|
||||||
|
} else {
|
||||||
|
match status {
|
||||||
|
Status::Disabled => {
|
||||||
|
filter.hide_action_types(&signed_in_actions);
|
||||||
|
filter.hide_action_types(&auth_actions);
|
||||||
|
filter.hide_action_types(&no_auth_actions);
|
||||||
|
}
|
||||||
|
Status::Authorized => {
|
||||||
|
filter.hide_action_types(&no_auth_actions);
|
||||||
|
filter.show_action_types(signed_in_actions.iter().chain(&auth_actions));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
filter.hide_action_types(&signed_in_actions);
|
||||||
|
filter.hide_action_types(&auth_actions);
|
||||||
|
filter.show_action_types(no_auth_actions.iter());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn id_for_language(language: Option<&Arc<Language>>) -> String {
|
fn id_for_language(language: Option<&Arc<Language>>) -> String {
|
||||||
|
|
|
@ -295,7 +295,7 @@ mod tests {
|
||||||
request: dap_types::StartDebuggingRequestArgumentsRequest::Launch,
|
request: dap_types::StartDebuggingRequestArgumentsRequest::Launch,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Box::new(|_| panic!("Did not expect to hit this code path")),
|
Box::new(|_| {}),
|
||||||
&mut cx.to_async(),
|
&mut cx.to_async(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -883,6 +883,7 @@ impl FakeTransport {
|
||||||
break Err(anyhow!("exit in response to request"));
|
break Err(anyhow!("exit in response to request"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let success = response.success;
|
||||||
let message =
|
let message =
|
||||||
serde_json::to_string(&Message::Response(response)).unwrap();
|
serde_json::to_string(&Message::Response(response)).unwrap();
|
||||||
|
|
||||||
|
@ -893,6 +894,25 @@ impl FakeTransport {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
if request.command == dap_types::requests::Initialize::COMMAND
|
||||||
|
&& success
|
||||||
|
{
|
||||||
|
let message = serde_json::to_string(&Message::Event(Box::new(
|
||||||
|
dap_types::messages::Events::Initialized(Some(
|
||||||
|
Default::default(),
|
||||||
|
)),
|
||||||
|
)))
|
||||||
|
.unwrap();
|
||||||
|
writer
|
||||||
|
.write_all(
|
||||||
|
TransportDelegate::build_rpc_message(message)
|
||||||
|
.as_bytes(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
writer.flush().await.unwrap();
|
writer.flush().await.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,17 +7,19 @@ license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
clap.workspace = true
|
command_palette.workspace = true
|
||||||
mdbook = "0.4.40"
|
gpui.workspace = true
|
||||||
|
# We are specifically pinning this version of mdbook, as later versions introduce issues with double-nested subdirectories.
|
||||||
|
# Ask @maxdeviant about this before bumping.
|
||||||
|
mdbook = "= 0.4.40"
|
||||||
|
regex.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
regex.workspace = true
|
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
zed.workspace = true
|
zed.workspace = true
|
||||||
gpui.workspace = true
|
zlog.workspace = true
|
||||||
command_palette.workspace = true
|
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use clap::{Arg, ArgMatches, Command};
|
|
||||||
use mdbook::BookItem;
|
use mdbook::BookItem;
|
||||||
use mdbook::book::{Book, Chapter};
|
use mdbook::book::{Book, Chapter};
|
||||||
use mdbook::preprocess::CmdPreprocessor;
|
use mdbook::preprocess::CmdPreprocessor;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use settings::KeymapFile;
|
use settings::KeymapFile;
|
||||||
use std::collections::HashSet;
|
use std::borrow::Cow;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::io::{self, Read};
|
use std::io::{self, Read};
|
||||||
use std::process;
|
use std::process;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
use util::paths::PathExt;
|
||||||
|
|
||||||
static KEYMAP_MACOS: LazyLock<KeymapFile> = LazyLock::new(|| {
|
static KEYMAP_MACOS: LazyLock<KeymapFile> = LazyLock::new(|| {
|
||||||
load_keymap("keymaps/default-macos.json").expect("Failed to load MacOS keymap")
|
load_keymap("keymaps/default-macos.json").expect("Failed to load MacOS keymap")
|
||||||
|
@ -20,60 +21,68 @@ static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
|
||||||
|
|
||||||
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
|
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
|
||||||
|
|
||||||
pub fn make_app() -> Command {
|
const FRONT_MATTER_COMMENT: &'static str = "<!-- ZED_META {} -->";
|
||||||
Command::new("zed-docs-preprocessor")
|
|
||||||
.about("Preprocesses Zed Docs content to provide rich action & keybinding support and more")
|
|
||||||
.subcommand(
|
|
||||||
Command::new("supports")
|
|
||||||
.arg(Arg::new("renderer").required(true))
|
|
||||||
.about("Check whether a renderer is supported by this preprocessor"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let matches = make_app().get_matches();
|
zlog::init();
|
||||||
|
zlog::init_output_stderr();
|
||||||
// call a zed:: function so everything in `zed` crate is linked and
|
// call a zed:: function so everything in `zed` crate is linked and
|
||||||
// all actions in the actual app are registered
|
// all actions in the actual app are registered
|
||||||
zed::stdout_is_a_pty();
|
zed::stdout_is_a_pty();
|
||||||
|
let args = std::env::args().skip(1).collect::<Vec<_>>();
|
||||||
|
|
||||||
if let Some(sub_args) = matches.subcommand_matches("supports") {
|
match args.get(0).map(String::as_str) {
|
||||||
handle_supports(sub_args);
|
Some("supports") => {
|
||||||
} else {
|
let renderer = args.get(1).expect("Required argument");
|
||||||
handle_preprocessing()?;
|
let supported = renderer != "not-supported";
|
||||||
|
if supported {
|
||||||
|
process::exit(0);
|
||||||
|
} else {
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("postprocess") => handle_postprocessing()?,
|
||||||
|
_ => handle_preprocessing()?,
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
enum Error {
|
enum PreprocessorError {
|
||||||
ActionNotFound { action_name: String },
|
ActionNotFound { action_name: String },
|
||||||
DeprecatedActionUsed { used: String, should_be: String },
|
DeprecatedActionUsed { used: String, should_be: String },
|
||||||
|
InvalidFrontmatterLine(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error {
|
impl PreprocessorError {
|
||||||
fn new_for_not_found_action(action_name: String) -> Self {
|
fn new_for_not_found_action(action_name: String) -> Self {
|
||||||
for action in &*ALL_ACTIONS {
|
for action in &*ALL_ACTIONS {
|
||||||
for alias in action.deprecated_aliases {
|
for alias in action.deprecated_aliases {
|
||||||
if alias == &action_name {
|
if alias == &action_name {
|
||||||
return Error::DeprecatedActionUsed {
|
return PreprocessorError::DeprecatedActionUsed {
|
||||||
used: action_name.clone(),
|
used: action_name.clone(),
|
||||||
should_be: action.name.to_string(),
|
should_be: action.name.to_string(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Error::ActionNotFound {
|
PreprocessorError::ActionNotFound {
|
||||||
action_name: action_name.to_string(),
|
action_name: action_name.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Error {
|
impl std::fmt::Display for PreprocessorError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Error::ActionNotFound { action_name } => write!(f, "Action not found: {}", action_name),
|
PreprocessorError::InvalidFrontmatterLine(line) => {
|
||||||
Error::DeprecatedActionUsed { used, should_be } => write!(
|
write!(f, "Invalid frontmatter line: {}", line)
|
||||||
|
}
|
||||||
|
PreprocessorError::ActionNotFound { action_name } => {
|
||||||
|
write!(f, "Action not found: {}", action_name)
|
||||||
|
}
|
||||||
|
PreprocessorError::DeprecatedActionUsed { used, should_be } => write!(
|
||||||
f,
|
f,
|
||||||
"Deprecated action used: {} should be {}",
|
"Deprecated action used: {} should be {}",
|
||||||
used, should_be
|
used, should_be
|
||||||
|
@ -89,8 +98,9 @@ fn handle_preprocessing() -> Result<()> {
|
||||||
|
|
||||||
let (_ctx, mut book) = CmdPreprocessor::parse_input(input.as_bytes())?;
|
let (_ctx, mut book) = CmdPreprocessor::parse_input(input.as_bytes())?;
|
||||||
|
|
||||||
let mut errors = HashSet::<Error>::new();
|
let mut errors = HashSet::<PreprocessorError>::new();
|
||||||
|
|
||||||
|
handle_frontmatter(&mut book, &mut errors);
|
||||||
template_and_validate_keybindings(&mut book, &mut errors);
|
template_and_validate_keybindings(&mut book, &mut errors);
|
||||||
template_and_validate_actions(&mut book, &mut errors);
|
template_and_validate_actions(&mut book, &mut errors);
|
||||||
|
|
||||||
|
@ -108,19 +118,41 @@ fn handle_preprocessing() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_supports(sub_args: &ArgMatches) -> ! {
|
fn handle_frontmatter(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
|
||||||
let renderer = sub_args
|
let frontmatter_regex = Regex::new(r"(?s)^\s*---(.*?)---").unwrap();
|
||||||
.get_one::<String>("renderer")
|
for_each_chapter_mut(book, |chapter| {
|
||||||
.expect("Required argument");
|
let new_content = frontmatter_regex.replace(&chapter.content, |caps: ®ex::Captures| {
|
||||||
let supported = renderer != "not-supported";
|
let frontmatter = caps[1].trim();
|
||||||
if supported {
|
let frontmatter = frontmatter.trim_matches(&[' ', '-', '\n']);
|
||||||
process::exit(0);
|
let mut metadata = HashMap::<String, String>::default();
|
||||||
} else {
|
for line in frontmatter.lines() {
|
||||||
process::exit(1);
|
let Some((name, value)) = line.split_once(':') else {
|
||||||
}
|
errors.insert(PreprocessorError::InvalidFrontmatterLine(format!(
|
||||||
|
"{}: {}",
|
||||||
|
chapter_breadcrumbs(&chapter),
|
||||||
|
line
|
||||||
|
)));
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let name = name.trim();
|
||||||
|
let value = value.trim();
|
||||||
|
metadata.insert(name.to_string(), value.to_string());
|
||||||
|
}
|
||||||
|
FRONT_MATTER_COMMENT.replace(
|
||||||
|
"{}",
|
||||||
|
&serde_json::to_string(&metadata).expect("Failed to serialize metadata"),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
match new_content {
|
||||||
|
Cow::Owned(content) => {
|
||||||
|
chapter.content = content;
|
||||||
|
}
|
||||||
|
Cow::Borrowed(_) => {}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<Error>) {
|
fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
|
||||||
let regex = Regex::new(r"\{#kb (.*?)\}").unwrap();
|
let regex = Regex::new(r"\{#kb (.*?)\}").unwrap();
|
||||||
|
|
||||||
for_each_chapter_mut(book, |chapter| {
|
for_each_chapter_mut(book, |chapter| {
|
||||||
|
@ -128,7 +160,9 @@ fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<Error
|
||||||
.replace_all(&chapter.content, |caps: ®ex::Captures| {
|
.replace_all(&chapter.content, |caps: ®ex::Captures| {
|
||||||
let action = caps[1].trim();
|
let action = caps[1].trim();
|
||||||
if find_action_by_name(action).is_none() {
|
if find_action_by_name(action).is_none() {
|
||||||
errors.insert(Error::new_for_not_found_action(action.to_string()));
|
errors.insert(PreprocessorError::new_for_not_found_action(
|
||||||
|
action.to_string(),
|
||||||
|
));
|
||||||
return String::new();
|
return String::new();
|
||||||
}
|
}
|
||||||
let macos_binding = find_binding("macos", action).unwrap_or_default();
|
let macos_binding = find_binding("macos", action).unwrap_or_default();
|
||||||
|
@ -144,7 +178,7 @@ fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<Error
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Error>) {
|
fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
|
||||||
let regex = Regex::new(r"\{#action (.*?)\}").unwrap();
|
let regex = Regex::new(r"\{#action (.*?)\}").unwrap();
|
||||||
|
|
||||||
for_each_chapter_mut(book, |chapter| {
|
for_each_chapter_mut(book, |chapter| {
|
||||||
|
@ -152,7 +186,9 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Error>) {
|
||||||
.replace_all(&chapter.content, |caps: ®ex::Captures| {
|
.replace_all(&chapter.content, |caps: ®ex::Captures| {
|
||||||
let name = caps[1].trim();
|
let name = caps[1].trim();
|
||||||
let Some(action) = find_action_by_name(name) else {
|
let Some(action) = find_action_by_name(name) else {
|
||||||
errors.insert(Error::new_for_not_found_action(name.to_string()));
|
errors.insert(PreprocessorError::new_for_not_found_action(
|
||||||
|
name.to_string(),
|
||||||
|
));
|
||||||
return String::new();
|
return String::new();
|
||||||
};
|
};
|
||||||
format!("<code class=\"hljs\">{}</code>", &action.human_name)
|
format!("<code class=\"hljs\">{}</code>", &action.human_name)
|
||||||
|
@ -217,6 +253,13 @@ fn name_for_action(action_as_str: String) -> String {
|
||||||
.unwrap_or(action_as_str)
|
.unwrap_or(action_as_str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn chapter_breadcrumbs(chapter: &Chapter) -> String {
|
||||||
|
let mut breadcrumbs = Vec::with_capacity(chapter.parent_names.len() + 1);
|
||||||
|
breadcrumbs.extend(chapter.parent_names.iter().map(String::as_str));
|
||||||
|
breadcrumbs.push(chapter.name.as_str());
|
||||||
|
format!("[{:?}] {}", chapter.source_path, breadcrumbs.join(" > "))
|
||||||
|
}
|
||||||
|
|
||||||
fn load_keymap(asset_path: &str) -> Result<KeymapFile> {
|
fn load_keymap(asset_path: &str) -> Result<KeymapFile> {
|
||||||
let content = util::asset_str::<settings::SettingsAssets>(asset_path);
|
let content = util::asset_str::<settings::SettingsAssets>(asset_path);
|
||||||
KeymapFile::parse(content.as_ref())
|
KeymapFile::parse(content.as_ref())
|
||||||
|
@ -254,3 +297,126 @@ fn dump_all_gpui_actions() -> Vec<ActionDef> {
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_postprocessing() -> Result<()> {
|
||||||
|
let logger = zlog::scoped!("render");
|
||||||
|
let mut ctx = mdbook::renderer::RenderContext::from_json(io::stdin())?;
|
||||||
|
let output = ctx
|
||||||
|
.config
|
||||||
|
.get_mut("output")
|
||||||
|
.expect("has output")
|
||||||
|
.as_table_mut()
|
||||||
|
.expect("output is table");
|
||||||
|
let zed_html = output.remove("zed-html").expect("zed-html output defined");
|
||||||
|
let default_description = zed_html
|
||||||
|
.get("default-description")
|
||||||
|
.expect("Default description not found")
|
||||||
|
.as_str()
|
||||||
|
.expect("Default description not a string")
|
||||||
|
.to_string();
|
||||||
|
let default_title = zed_html
|
||||||
|
.get("default-title")
|
||||||
|
.expect("Default title not found")
|
||||||
|
.as_str()
|
||||||
|
.expect("Default title not a string")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
output.insert("html".to_string(), zed_html);
|
||||||
|
mdbook::Renderer::render(&mdbook::renderer::HtmlHandlebars::new(), &ctx)?;
|
||||||
|
let ignore_list = ["toc.html"];
|
||||||
|
|
||||||
|
let root_dir = ctx.destination.clone();
|
||||||
|
let mut files = Vec::with_capacity(128);
|
||||||
|
let mut queue = Vec::with_capacity(64);
|
||||||
|
queue.push(root_dir.clone());
|
||||||
|
while let Some(dir) = queue.pop() {
|
||||||
|
for entry in std::fs::read_dir(&dir).context(dir.to_sanitized_string())? {
|
||||||
|
let Ok(entry) = entry else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let file_type = entry.file_type().context("Failed to determine file type")?;
|
||||||
|
if file_type.is_dir() {
|
||||||
|
queue.push(entry.path());
|
||||||
|
}
|
||||||
|
if file_type.is_file()
|
||||||
|
&& matches!(
|
||||||
|
entry.path().extension().and_then(std::ffi::OsStr::to_str),
|
||||||
|
Some("html")
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if ignore_list.contains(&&*entry.file_name().to_string_lossy()) {
|
||||||
|
zlog::info!(logger => "Ignoring {}", entry.path().to_string_lossy());
|
||||||
|
} else {
|
||||||
|
files.push(entry.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zlog::info!(logger => "Processing {} `.html` files", files.len());
|
||||||
|
let meta_regex = Regex::new(&FRONT_MATTER_COMMENT.replace("{}", "(.*)")).unwrap();
|
||||||
|
for file in files {
|
||||||
|
let contents = std::fs::read_to_string(&file)?;
|
||||||
|
let mut meta_description = None;
|
||||||
|
let mut meta_title = None;
|
||||||
|
let contents = meta_regex.replace(&contents, |caps: ®ex::Captures| {
|
||||||
|
let metadata: HashMap<String, String> = serde_json::from_str(&caps[1]).with_context(|| format!("JSON Metadata: {:?}", &caps[1])).expect("Failed to deserialize metadata");
|
||||||
|
for (kind, content) in metadata {
|
||||||
|
match kind.as_str() {
|
||||||
|
"description" => {
|
||||||
|
meta_description = Some(content);
|
||||||
|
}
|
||||||
|
"title" => {
|
||||||
|
meta_title = Some(content);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
zlog::warn!(logger => "Unrecognized frontmatter key: {} in {:?}", kind, pretty_path(&file, &root_dir));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String::new()
|
||||||
|
});
|
||||||
|
let meta_description = meta_description.as_ref().unwrap_or_else(|| {
|
||||||
|
zlog::warn!(logger => "No meta description found for {:?}", pretty_path(&file, &root_dir));
|
||||||
|
&default_description
|
||||||
|
});
|
||||||
|
let page_title = extract_title_from_page(&contents, pretty_path(&file, &root_dir));
|
||||||
|
let meta_title = meta_title.as_ref().unwrap_or_else(|| {
|
||||||
|
zlog::debug!(logger => "No meta title found for {:?}", pretty_path(&file, &root_dir));
|
||||||
|
&default_title
|
||||||
|
});
|
||||||
|
let meta_title = format!("{} | {}", page_title, meta_title);
|
||||||
|
zlog::trace!(logger => "Updating {:?}", pretty_path(&file, &root_dir));
|
||||||
|
let contents = contents.replace("#description#", meta_description);
|
||||||
|
let contents = TITLE_REGEX
|
||||||
|
.replace(&contents, |_: ®ex::Captures| {
|
||||||
|
format!("<title>{}</title>", meta_title)
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
// let contents = contents.replace("#title#", &meta_title);
|
||||||
|
std::fs::write(file, contents)?;
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
|
||||||
|
fn pretty_path<'a>(
|
||||||
|
path: &'a std::path::PathBuf,
|
||||||
|
root: &'a std::path::PathBuf,
|
||||||
|
) -> &'a std::path::Path {
|
||||||
|
&path.strip_prefix(&root).unwrap_or(&path)
|
||||||
|
}
|
||||||
|
const TITLE_REGEX: std::cell::LazyCell<Regex> =
|
||||||
|
std::cell::LazyCell::new(|| Regex::new(r"<title>\s*(.*?)\s*</title>").unwrap());
|
||||||
|
fn extract_title_from_page(contents: &str, pretty_path: &std::path::Path) -> String {
|
||||||
|
let title_tag_contents = &TITLE_REGEX
|
||||||
|
.captures(&contents)
|
||||||
|
.with_context(|| format!("Failed to find title in {:?}", pretty_path))
|
||||||
|
.expect("Page has <title> element")[1];
|
||||||
|
let title = title_tag_contents
|
||||||
|
.trim()
|
||||||
|
.strip_suffix("- Zed")
|
||||||
|
.unwrap_or(title_tag_contents)
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ test-support = [
|
||||||
"theme/test-support",
|
"theme/test-support",
|
||||||
"util/test-support",
|
"util/test-support",
|
||||||
"workspace/test-support",
|
"workspace/test-support",
|
||||||
|
"tree-sitter-c",
|
||||||
"tree-sitter-rust",
|
"tree-sitter-rust",
|
||||||
"tree-sitter-typescript",
|
"tree-sitter-typescript",
|
||||||
"tree-sitter-html",
|
"tree-sitter-html",
|
||||||
|
@ -76,6 +77,7 @@ telemetry.workspace = true
|
||||||
text.workspace = true
|
text.workspace = true
|
||||||
time.workspace = true
|
time.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
|
tree-sitter-c = { workspace = true, optional = true }
|
||||||
tree-sitter-html = { workspace = true, optional = true }
|
tree-sitter-html = { workspace = true, optional = true }
|
||||||
tree-sitter-rust = { workspace = true, optional = true }
|
tree-sitter-rust = { workspace = true, optional = true }
|
||||||
tree-sitter-typescript = { workspace = true, optional = true }
|
tree-sitter-typescript = { workspace = true, optional = true }
|
||||||
|
@ -106,6 +108,7 @@ settings = { workspace = true, features = ["test-support"] }
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
text = { workspace = true, features = ["test-support"] }
|
text = { workspace = true, features = ["test-support"] }
|
||||||
theme = { workspace = true, features = ["test-support"] }
|
theme = { workspace = true, features = ["test-support"] }
|
||||||
|
tree-sitter-c.workspace = true
|
||||||
tree-sitter-html.workspace = true
|
tree-sitter-html.workspace = true
|
||||||
tree-sitter-rust.workspace = true
|
tree-sitter-rust.workspace = true
|
||||||
tree-sitter-typescript.workspace = true
|
tree-sitter-typescript.workspace = true
|
||||||
|
|
|
@ -315,9 +315,8 @@ actions!(
|
||||||
[
|
[
|
||||||
/// Accepts the full edit prediction.
|
/// Accepts the full edit prediction.
|
||||||
AcceptEditPrediction,
|
AcceptEditPrediction,
|
||||||
/// Accepts a partial Copilot suggestion.
|
|
||||||
AcceptPartialCopilotSuggestion,
|
|
||||||
/// Accepts a partial edit prediction.
|
/// Accepts a partial edit prediction.
|
||||||
|
#[action(deprecated_aliases = ["editor::AcceptPartialCopilotSuggestion"])]
|
||||||
AcceptPartialEditPrediction,
|
AcceptPartialEditPrediction,
|
||||||
/// Adds a cursor above the current selection.
|
/// Adds a cursor above the current selection.
|
||||||
AddSelectionAbove,
|
AddSelectionAbove,
|
||||||
|
|
|
@ -51,42 +51,56 @@ mod signature_help;
|
||||||
pub mod test;
|
pub mod test;
|
||||||
|
|
||||||
pub(crate) use actions::*;
|
pub(crate) use actions::*;
|
||||||
pub use actions::{AcceptEditPrediction, OpenExcerpts, OpenExcerptsSplit};
|
pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder};
|
||||||
|
pub use editor_settings::{
|
||||||
|
CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode,
|
||||||
|
ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap, ShowScrollbar,
|
||||||
|
};
|
||||||
|
pub use editor_settings_controls::*;
|
||||||
|
pub use element::{
|
||||||
|
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
|
||||||
|
};
|
||||||
|
pub use git::blame::BlameRenderer;
|
||||||
|
pub use hover_popover::hover_markdown_style;
|
||||||
|
pub use inline_completion::Direction;
|
||||||
|
pub use items::MAX_TAB_TITLE_LEN;
|
||||||
|
pub use lsp::CompletionContext;
|
||||||
|
pub use lsp_ext::lsp_tasks;
|
||||||
|
pub use multi_buffer::{
|
||||||
|
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, PathKey,
|
||||||
|
RowInfo, ToOffset, ToPoint,
|
||||||
|
};
|
||||||
|
pub use proposed_changes_editor::{
|
||||||
|
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
|
||||||
|
};
|
||||||
|
pub use text::Bias;
|
||||||
|
|
||||||
|
use ::git::{
|
||||||
|
Restore,
|
||||||
|
blame::{BlameEntry, ParsedCommitMessage},
|
||||||
|
};
|
||||||
use aho_corasick::AhoCorasick;
|
use aho_corasick::AhoCorasick;
|
||||||
use anyhow::{Context as _, Result, anyhow};
|
use anyhow::{Context as _, Result, anyhow};
|
||||||
use blink_manager::BlinkManager;
|
use blink_manager::BlinkManager;
|
||||||
use buffer_diff::DiffHunkStatus;
|
use buffer_diff::DiffHunkStatus;
|
||||||
use client::{Collaborator, ParticipantIndex};
|
use client::{Collaborator, DisableAiSettings, ParticipantIndex};
|
||||||
use clock::{AGENT_REPLICA_ID, ReplicaId};
|
use clock::{AGENT_REPLICA_ID, ReplicaId};
|
||||||
|
use code_context_menus::{
|
||||||
|
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
|
||||||
|
CompletionsMenu, ContextMenuOrigin,
|
||||||
|
};
|
||||||
use collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
use collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
use dap::TelemetrySpawnLocation;
|
use dap::TelemetrySpawnLocation;
|
||||||
use display_map::*;
|
use display_map::*;
|
||||||
pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder};
|
|
||||||
pub use editor_settings::{
|
|
||||||
CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode,
|
|
||||||
ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowScrollbar,
|
|
||||||
};
|
|
||||||
use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings};
|
use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings};
|
||||||
pub use editor_settings_controls::*;
|
|
||||||
use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap, layout_line};
|
use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap, layout_line};
|
||||||
pub use element::{
|
|
||||||
CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition,
|
|
||||||
};
|
|
||||||
use futures::{
|
use futures::{
|
||||||
FutureExt, StreamExt as _,
|
FutureExt, StreamExt as _,
|
||||||
future::{self, Shared, join},
|
future::{self, Shared, join},
|
||||||
stream::FuturesUnordered,
|
stream::FuturesUnordered,
|
||||||
};
|
};
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use lsp_colors::LspColorData;
|
|
||||||
|
|
||||||
use ::git::blame::BlameEntry;
|
|
||||||
use ::git::{Restore, blame::ParsedCommitMessage};
|
|
||||||
use code_context_menus::{
|
|
||||||
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
|
|
||||||
CompletionsMenu, ContextMenuOrigin,
|
|
||||||
};
|
|
||||||
use git::blame::{GitBlame, GlobalBlameRenderer};
|
use git::blame::{GitBlame, GlobalBlameRenderer};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, Animation, AnimationExt, AnyElement, App, AppContext, AsyncWindowContext,
|
Action, Animation, AnimationExt, AnyElement, App, AppContext, AsyncWindowContext,
|
||||||
|
@ -100,32 +114,43 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||||
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file};
|
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file};
|
||||||
pub use hover_popover::hover_markdown_style;
|
|
||||||
use hover_popover::{HoverState, hide_hover};
|
use hover_popover::{HoverState, hide_hover};
|
||||||
use indent_guides::ActiveIndentGuidesState;
|
use indent_guides::ActiveIndentGuidesState;
|
||||||
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
||||||
pub use inline_completion::Direction;
|
|
||||||
use inline_completion::{EditPredictionProvider, InlineCompletionProviderHandle};
|
use inline_completion::{EditPredictionProvider, InlineCompletionProviderHandle};
|
||||||
pub use items::MAX_TAB_TITLE_LEN;
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, Capability, CharKind,
|
AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow,
|
||||||
CodeLabel, CursorShape, DiagnosticEntry, DiffOptions, EditPredictionsMode, EditPreview,
|
BufferSnapshot, Capability, CharClassifier, CharKind, CodeLabel, CursorShape, DiagnosticEntry,
|
||||||
HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection,
|
DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind, IndentSize,
|
||||||
SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
|
Language, OffsetRangeExt, Point, Runnable, RunnableRange, Selection, SelectionGoal, TextObject,
|
||||||
|
TransactionId, TreeSitterOptions, WordsQuery,
|
||||||
language_settings::{
|
language_settings::{
|
||||||
self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
|
self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
|
||||||
all_language_settings, language_settings,
|
all_language_settings, language_settings,
|
||||||
},
|
},
|
||||||
point_from_lsp, text_diff_with_options,
|
point_from_lsp, point_to_lsp, text_diff_with_options,
|
||||||
};
|
};
|
||||||
use language::{BufferRow, CharClassifier, Runnable, RunnableRange, point_to_lsp};
|
|
||||||
use linked_editing_ranges::refresh_linked_ranges;
|
use linked_editing_ranges::refresh_linked_ranges;
|
||||||
|
use lsp::{
|
||||||
|
CodeActionKind, CompletionItemKind, CompletionTriggerKind, InsertTextFormat, InsertTextMode,
|
||||||
|
LanguageServerId, LanguageServerName,
|
||||||
|
};
|
||||||
|
use lsp_colors::LspColorData;
|
||||||
use markdown::Markdown;
|
use markdown::Markdown;
|
||||||
use mouse_context_menu::MouseContextMenu;
|
use mouse_context_menu::MouseContextMenu;
|
||||||
|
use movement::TextLayoutDetails;
|
||||||
|
use multi_buffer::{
|
||||||
|
ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow,
|
||||||
|
MultiOrSingleBufferOffsetRange, ToOffsetUtf16,
|
||||||
|
};
|
||||||
|
use parking_lot::Mutex;
|
||||||
use persistence::DB;
|
use persistence::DB;
|
||||||
use project::{
|
use project::{
|
||||||
BreakpointWithPosition, CompletionResponse, ProjectPath,
|
BreakpointWithPosition, CodeAction, Completion, CompletionIntent, CompletionResponse,
|
||||||
|
CompletionSource, DocumentHighlight, InlayHint, Location, LocationLink, PrepareRenameResponse,
|
||||||
|
Project, ProjectItem, ProjectPath, ProjectTransaction, TaskSourceKind,
|
||||||
|
debugger::breakpoint_store::Breakpoint,
|
||||||
debugger::{
|
debugger::{
|
||||||
breakpoint_store::{
|
breakpoint_store::{
|
||||||
BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore,
|
BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore,
|
||||||
|
@ -134,44 +159,12 @@ use project::{
|
||||||
session::{Session, SessionEvent},
|
session::{Session, SessionEvent},
|
||||||
},
|
},
|
||||||
git_store::{GitStoreEvent, RepositoryEvent},
|
git_store::{GitStoreEvent, RepositoryEvent},
|
||||||
project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub use git::blame::BlameRenderer;
|
|
||||||
pub use proposed_changes_editor::{
|
|
||||||
ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar,
|
|
||||||
};
|
|
||||||
use std::{cell::OnceCell, iter::Peekable, ops::Not};
|
|
||||||
use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables};
|
|
||||||
|
|
||||||
pub use lsp::CompletionContext;
|
|
||||||
use lsp::{
|
|
||||||
CodeActionKind, CompletionItemKind, CompletionTriggerKind, InsertTextFormat, InsertTextMode,
|
|
||||||
LanguageServerId, LanguageServerName,
|
|
||||||
};
|
|
||||||
|
|
||||||
use language::BufferSnapshot;
|
|
||||||
pub use lsp_ext::lsp_tasks;
|
|
||||||
use movement::TextLayoutDetails;
|
|
||||||
pub use multi_buffer::{
|
|
||||||
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, PathKey,
|
|
||||||
RowInfo, ToOffset, ToPoint,
|
|
||||||
};
|
|
||||||
use multi_buffer::{
|
|
||||||
ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow,
|
|
||||||
MultiOrSingleBufferOffsetRange, ToOffsetUtf16,
|
|
||||||
};
|
|
||||||
use parking_lot::Mutex;
|
|
||||||
use project::{
|
|
||||||
CodeAction, Completion, CompletionIntent, CompletionSource, DocumentHighlight, InlayHint,
|
|
||||||
Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectTransaction,
|
|
||||||
TaskSourceKind,
|
|
||||||
debugger::breakpoint_store::Breakpoint,
|
|
||||||
lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
|
lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle},
|
||||||
|
project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter},
|
||||||
project_settings::{GitGutterSetting, ProjectSettings},
|
project_settings::{GitGutterSetting, ProjectSettings},
|
||||||
};
|
};
|
||||||
use rand::prelude::*;
|
use rand::{seq::SliceRandom, thread_rng};
|
||||||
use rpc::{ErrorExt, proto::*};
|
use rpc::{ErrorCode, ErrorExt, proto::PeerId};
|
||||||
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
|
use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide};
|
||||||
use selections_collection::{
|
use selections_collection::{
|
||||||
MutableSelectionsCollection, SelectionsCollection, resolve_selections,
|
MutableSelectionsCollection, SelectionsCollection, resolve_selections,
|
||||||
|
@ -180,21 +173,24 @@ use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsLocation, SettingsStore, update_settings_file};
|
use settings::{Settings, SettingsLocation, SettingsStore, update_settings_file};
|
||||||
use smallvec::{SmallVec, smallvec};
|
use smallvec::{SmallVec, smallvec};
|
||||||
use snippet::Snippet;
|
use snippet::Snippet;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
|
cell::OnceCell,
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
cmp::{self, Ordering, Reverse},
|
cmp::{self, Ordering, Reverse},
|
||||||
|
iter::Peekable,
|
||||||
mem,
|
mem,
|
||||||
num::NonZeroU32,
|
num::NonZeroU32,
|
||||||
|
ops::Not,
|
||||||
ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive},
|
ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
|
sync::Arc,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
pub use sum_tree::Bias;
|
|
||||||
use sum_tree::TreeMap;
|
use sum_tree::TreeMap;
|
||||||
|
use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables};
|
||||||
use text::{BufferId, FromAnchor, OffsetUtf16, Rope};
|
use text::{BufferId, FromAnchor, OffsetUtf16, Rope};
|
||||||
use theme::{
|
use theme::{
|
||||||
ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, Theme, ThemeSettings,
|
ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, Theme, ThemeSettings,
|
||||||
|
@ -213,14 +209,11 @@ use workspace::{
|
||||||
notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt},
|
notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt},
|
||||||
searchable::SearchEvent,
|
searchable::SearchEvent,
|
||||||
};
|
};
|
||||||
use zed_actions;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
code_context_menus::CompletionsMenuSource,
|
code_context_menus::CompletionsMenuSource,
|
||||||
hover_links::{find_url, find_url_from_range},
|
|
||||||
};
|
|
||||||
use crate::{
|
|
||||||
editor_settings::MultiCursorModifier,
|
editor_settings::MultiCursorModifier,
|
||||||
|
hover_links::{find_url, find_url_from_range},
|
||||||
signature_help::{SignatureHelpHiddenBy, SignatureHelpState},
|
signature_help::{SignatureHelpHiddenBy, SignatureHelpState},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1305,6 +1298,7 @@ impl Default for SelectionHistoryMode {
|
||||||
///
|
///
|
||||||
/// Similarly, you might want to disable scrolling if you don't want the viewport to
|
/// Similarly, you might want to disable scrolling if you don't want the viewport to
|
||||||
/// move.
|
/// move.
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct SelectionEffects {
|
pub struct SelectionEffects {
|
||||||
nav_history: Option<bool>,
|
nav_history: Option<bool>,
|
||||||
completions: bool,
|
completions: bool,
|
||||||
|
@ -2944,10 +2938,12 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let selection_anchors = self.selections.disjoint_anchors();
|
||||||
|
|
||||||
if self.focus_handle.is_focused(window) && self.leader_id.is_none() {
|
if self.focus_handle.is_focused(window) && self.leader_id.is_none() {
|
||||||
self.buffer.update(cx, |buffer, cx| {
|
self.buffer.update(cx, |buffer, cx| {
|
||||||
buffer.set_active_selections(
|
buffer.set_active_selections(
|
||||||
&self.selections.disjoint_anchors(),
|
&selection_anchors,
|
||||||
self.selections.line_mode,
|
self.selections.line_mode,
|
||||||
self.cursor_shape,
|
self.cursor_shape,
|
||||||
cx,
|
cx,
|
||||||
|
@ -2964,9 +2960,8 @@ impl Editor {
|
||||||
self.select_next_state = None;
|
self.select_next_state = None;
|
||||||
self.select_prev_state = None;
|
self.select_prev_state = None;
|
||||||
self.select_syntax_node_history.try_clear();
|
self.select_syntax_node_history.try_clear();
|
||||||
self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer);
|
self.invalidate_autoclose_regions(&selection_anchors, buffer);
|
||||||
self.snippet_stack
|
self.snippet_stack.invalidate(&selection_anchors, buffer);
|
||||||
.invalidate(&self.selections.disjoint_anchors(), buffer);
|
|
||||||
self.take_rename(false, window, cx);
|
self.take_rename(false, window, cx);
|
||||||
|
|
||||||
let newest_selection = self.selections.newest_anchor();
|
let newest_selection = self.selections.newest_anchor();
|
||||||
|
@ -4047,7 +4042,8 @@ impl Editor {
|
||||||
// then don't insert that closing bracket again; just move the selection
|
// then don't insert that closing bracket again; just move the selection
|
||||||
// past the closing bracket.
|
// past the closing bracket.
|
||||||
let should_skip = selection.end == region.range.end.to_point(&snapshot)
|
let should_skip = selection.end == region.range.end.to_point(&snapshot)
|
||||||
&& text.as_ref() == region.pair.end.as_str();
|
&& text.as_ref() == region.pair.end.as_str()
|
||||||
|
&& snapshot.contains_str_at(region.range.end, text.as_ref());
|
||||||
if should_skip {
|
if should_skip {
|
||||||
let anchor = snapshot.anchor_after(selection.end);
|
let anchor = snapshot.anchor_after(selection.end);
|
||||||
new_selections
|
new_selections
|
||||||
|
@ -4973,13 +4969,17 @@ impl Editor {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove any autoclose regions that no longer contain their selection.
|
/// Remove any autoclose regions that no longer contain their selection or have invalid anchors in ranges.
|
||||||
fn invalidate_autoclose_regions(
|
fn invalidate_autoclose_regions(
|
||||||
&mut self,
|
&mut self,
|
||||||
mut selections: &[Selection<Anchor>],
|
mut selections: &[Selection<Anchor>],
|
||||||
buffer: &MultiBufferSnapshot,
|
buffer: &MultiBufferSnapshot,
|
||||||
) {
|
) {
|
||||||
self.autoclose_regions.retain(|state| {
|
self.autoclose_regions.retain(|state| {
|
||||||
|
if !state.range.start.is_valid(buffer) || !state.range.end.is_valid(buffer) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
while let Some(selection) = selections.get(i) {
|
while let Some(selection) = selections.get(i) {
|
||||||
if selection.end.cmp(&state.range.start, buffer).is_lt() {
|
if selection.end.cmp(&state.range.start, buffer).is_lt() {
|
||||||
|
@ -5891,18 +5891,20 @@ impl Editor {
|
||||||
text: new_text[common_prefix_len..].into(),
|
text: new_text[common_prefix_len..].into(),
|
||||||
});
|
});
|
||||||
|
|
||||||
self.transact(window, cx, |this, window, cx| {
|
self.transact(window, cx, |editor, window, cx| {
|
||||||
if let Some(mut snippet) = snippet {
|
if let Some(mut snippet) = snippet {
|
||||||
snippet.text = new_text.to_string();
|
snippet.text = new_text.to_string();
|
||||||
this.insert_snippet(&ranges, snippet, window, cx).log_err();
|
editor
|
||||||
|
.insert_snippet(&ranges, snippet, window, cx)
|
||||||
|
.log_err();
|
||||||
} else {
|
} else {
|
||||||
this.buffer.update(cx, |buffer, cx| {
|
editor.buffer.update(cx, |multi_buffer, cx| {
|
||||||
let auto_indent = match completion.insert_text_mode {
|
let auto_indent = match completion.insert_text_mode {
|
||||||
Some(InsertTextMode::AS_IS) => None,
|
Some(InsertTextMode::AS_IS) => None,
|
||||||
_ => this.autoindent_mode.clone(),
|
_ => editor.autoindent_mode.clone(),
|
||||||
};
|
};
|
||||||
let edits = ranges.into_iter().map(|range| (range, new_text.as_str()));
|
let edits = ranges.into_iter().map(|range| (range, new_text.as_str()));
|
||||||
buffer.edit(edits, auto_indent, cx);
|
multi_buffer.edit(edits, auto_indent, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (buffer, edits) in linked_edits {
|
for (buffer, edits) in linked_edits {
|
||||||
|
@ -5921,8 +5923,9 @@ impl Editor {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.refresh_inline_completion(true, false, window, cx);
|
editor.refresh_inline_completion(true, false, window, cx);
|
||||||
});
|
});
|
||||||
|
self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), &snapshot);
|
||||||
|
|
||||||
let show_new_completions_on_confirm = completion
|
let show_new_completions_on_confirm = completion
|
||||||
.confirm
|
.confirm
|
||||||
|
@ -7048,7 +7051,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_edit_prediction_settings(&mut self, cx: &mut Context<Self>) {
|
pub fn update_edit_prediction_settings(&mut self, cx: &mut Context<Self>) {
|
||||||
if self.edit_prediction_provider.is_none() {
|
if self.edit_prediction_provider.is_none() || DisableAiSettings::get_global(cx).disable_ai {
|
||||||
self.edit_prediction_settings = EditPredictionSettings::Disabled;
|
self.edit_prediction_settings = EditPredictionSettings::Disabled;
|
||||||
} else {
|
} else {
|
||||||
let selection = self.selections.newest_anchor();
|
let selection = self.selections.newest_anchor();
|
||||||
|
@ -9562,27 +9565,46 @@ impl Editor {
|
||||||
// Check whether the just-entered snippet ends with an auto-closable bracket.
|
// Check whether the just-entered snippet ends with an auto-closable bracket.
|
||||||
if self.autoclose_regions.is_empty() {
|
if self.autoclose_regions.is_empty() {
|
||||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||||
for selection in &mut self.selections.all::<Point>(cx) {
|
let mut all_selections = self.selections.all::<Point>(cx);
|
||||||
|
for selection in &mut all_selections {
|
||||||
let selection_head = selection.head();
|
let selection_head = selection.head();
|
||||||
let Some(scope) = snapshot.language_scope_at(selection_head) else {
|
let Some(scope) = snapshot.language_scope_at(selection_head) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut bracket_pair = None;
|
let mut bracket_pair = None;
|
||||||
let next_chars = snapshot.chars_at(selection_head).collect::<String>();
|
let max_lookup_length = scope
|
||||||
let prev_chars = snapshot
|
.brackets()
|
||||||
.reversed_chars_at(selection_head)
|
.map(|(pair, _)| {
|
||||||
.collect::<String>();
|
pair.start
|
||||||
for (pair, enabled) in scope.brackets() {
|
.as_str()
|
||||||
if enabled
|
.chars()
|
||||||
&& pair.close
|
.count()
|
||||||
&& prev_chars.starts_with(pair.start.as_str())
|
.max(pair.end.as_str().chars().count())
|
||||||
&& next_chars.starts_with(pair.end.as_str())
|
})
|
||||||
{
|
.max();
|
||||||
bracket_pair = Some(pair.clone());
|
if let Some(max_lookup_length) = max_lookup_length {
|
||||||
break;
|
let next_text = snapshot
|
||||||
|
.chars_at(selection_head)
|
||||||
|
.take(max_lookup_length)
|
||||||
|
.collect::<String>();
|
||||||
|
let prev_text = snapshot
|
||||||
|
.reversed_chars_at(selection_head)
|
||||||
|
.take(max_lookup_length)
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
for (pair, enabled) in scope.brackets() {
|
||||||
|
if enabled
|
||||||
|
&& pair.close
|
||||||
|
&& prev_text.starts_with(pair.start.as_str())
|
||||||
|
&& next_text.starts_with(pair.end.as_str())
|
||||||
|
{
|
||||||
|
bracket_pair = Some(pair.clone());
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(pair) = bracket_pair {
|
if let Some(pair) = bracket_pair {
|
||||||
let snapshot_settings = snapshot.language_settings_at(selection_head, cx);
|
let snapshot_settings = snapshot.language_settings_at(selection_head, cx);
|
||||||
let autoclose_enabled =
|
let autoclose_enabled =
|
||||||
|
@ -21100,13 +21122,6 @@ fn process_completion_for_edit(
|
||||||
.is_le(),
|
.is_le(),
|
||||||
"replace_range should start before or at cursor position"
|
"replace_range should start before or at cursor position"
|
||||||
);
|
);
|
||||||
debug_assert!(
|
|
||||||
insert_range
|
|
||||||
.end
|
|
||||||
.cmp(&cursor_position, &buffer_snapshot)
|
|
||||||
.is_le(),
|
|
||||||
"insert_range should end before or at cursor position"
|
|
||||||
);
|
|
||||||
|
|
||||||
let should_replace = match intent {
|
let should_replace = match intent {
|
||||||
CompletionIntent::CompleteWithInsert => false,
|
CompletionIntent::CompleteWithInsert => false,
|
||||||
|
|
|
@ -8612,6 +8612,7 @@ async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
cx.language_registry().add(html_language.clone());
|
cx.language_registry().add(html_language.clone());
|
||||||
cx.language_registry().add(javascript_language.clone());
|
cx.language_registry().add(javascript_language.clone());
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
|
||||||
cx.update_buffer(|buffer, cx| {
|
cx.update_buffer(|buffer, cx| {
|
||||||
buffer.set_language(Some(html_language), cx);
|
buffer.set_language(Some(html_language), cx);
|
||||||
|
@ -13400,6 +13401,178 @@ async fn test_as_is_completions(cx: &mut TestAppContext) {
|
||||||
cx.assert_editor_state("fn a() {}\n unsafeˇ");
|
cx.assert_editor_state("fn a() {}\n unsafeˇ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx, |_| {});
|
||||||
|
let language =
|
||||||
|
Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
|
||||||
|
let mut cx = EditorLspTestContext::new(
|
||||||
|
language,
|
||||||
|
lsp::ServerCapabilities {
|
||||||
|
completion_provider: Some(lsp::CompletionOptions {
|
||||||
|
..lsp::CompletionOptions::default()
|
||||||
|
}),
|
||||||
|
..lsp::ServerCapabilities::default()
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cx.set_state(
|
||||||
|
"#ifndef BAR_H
|
||||||
|
#define BAR_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
int fn_branch(bool do_branch1, bool do_branch2);
|
||||||
|
|
||||||
|
#endif // BAR_H
|
||||||
|
ˇ",
|
||||||
|
);
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.handle_input("#", window, cx);
|
||||||
|
});
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.handle_input("i", window, cx);
|
||||||
|
});
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.handle_input("n", window, cx);
|
||||||
|
});
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
cx.assert_editor_state(
|
||||||
|
"#ifndef BAR_H
|
||||||
|
#define BAR_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
int fn_branch(bool do_branch1, bool do_branch2);
|
||||||
|
|
||||||
|
#endif // BAR_H
|
||||||
|
#inˇ",
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.lsp
|
||||||
|
.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
|
||||||
|
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||||
|
is_incomplete: false,
|
||||||
|
item_defaults: None,
|
||||||
|
items: vec![lsp::CompletionItem {
|
||||||
|
kind: Some(lsp::CompletionItemKind::SNIPPET),
|
||||||
|
label_details: Some(lsp::CompletionItemLabelDetails {
|
||||||
|
detail: Some("header".to_string()),
|
||||||
|
description: None,
|
||||||
|
}),
|
||||||
|
label: " include".to_string(),
|
||||||
|
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||||
|
range: lsp::Range {
|
||||||
|
start: lsp::Position {
|
||||||
|
line: 8,
|
||||||
|
character: 1,
|
||||||
|
},
|
||||||
|
end: lsp::Position {
|
||||||
|
line: 8,
|
||||||
|
character: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new_text: "include \"$0\"".to_string(),
|
||||||
|
})),
|
||||||
|
sort_text: Some("40b67681include".to_string()),
|
||||||
|
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
||||||
|
filter_text: Some("include".to_string()),
|
||||||
|
insert_text: Some("include \"$0\"".to_string()),
|
||||||
|
..lsp::CompletionItem::default()
|
||||||
|
}],
|
||||||
|
})))
|
||||||
|
});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||||
|
});
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
|
||||||
|
});
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
cx.assert_editor_state(
|
||||||
|
"#ifndef BAR_H
|
||||||
|
#define BAR_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
int fn_branch(bool do_branch1, bool do_branch2);
|
||||||
|
|
||||||
|
#endif // BAR_H
|
||||||
|
#include \"ˇ\"",
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.lsp
|
||||||
|
.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
|
||||||
|
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||||
|
is_incomplete: true,
|
||||||
|
item_defaults: None,
|
||||||
|
items: vec![lsp::CompletionItem {
|
||||||
|
kind: Some(lsp::CompletionItemKind::FILE),
|
||||||
|
label: "AGL/".to_string(),
|
||||||
|
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||||
|
range: lsp::Range {
|
||||||
|
start: lsp::Position {
|
||||||
|
line: 8,
|
||||||
|
character: 10,
|
||||||
|
},
|
||||||
|
end: lsp::Position {
|
||||||
|
line: 8,
|
||||||
|
character: 11,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
new_text: "AGL/".to_string(),
|
||||||
|
})),
|
||||||
|
sort_text: Some("40b67681AGL/".to_string()),
|
||||||
|
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
|
||||||
|
filter_text: Some("AGL/".to_string()),
|
||||||
|
insert_text: Some("AGL/".to_string()),
|
||||||
|
..lsp::CompletionItem::default()
|
||||||
|
}],
|
||||||
|
})))
|
||||||
|
});
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||||
|
});
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
|
||||||
|
});
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
cx.assert_editor_state(
|
||||||
|
r##"#ifndef BAR_H
|
||||||
|
#define BAR_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
int fn_branch(bool do_branch1, bool do_branch2);
|
||||||
|
|
||||||
|
#endif // BAR_H
|
||||||
|
#include "AGL/ˇ"##,
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.update_editor(|editor, window, cx| {
|
||||||
|
editor.handle_input("\"", window, cx);
|
||||||
|
});
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
cx.assert_editor_state(
|
||||||
|
r##"#ifndef BAR_H
|
||||||
|
#define BAR_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
int fn_branch(bool do_branch1, bool do_branch2);
|
||||||
|
|
||||||
|
#endif // BAR_H
|
||||||
|
#include "AGL/"ˇ"##,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
|
async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
|
|
@ -19,8 +19,8 @@ path = "src/explorer.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
agent.workspace = true
|
agent.workspace = true
|
||||||
agent_ui.workspace = true
|
|
||||||
agent_settings.workspace = true
|
agent_settings.workspace = true
|
||||||
|
agent_ui.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
assistant_tool.workspace = true
|
assistant_tool.workspace = true
|
||||||
assistant_tools.workspace = true
|
assistant_tools.workspace = true
|
||||||
|
@ -29,6 +29,7 @@ buffer_diff.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
clap.workspace = true
|
clap.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
|
cloud_llm_client.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
debug_adapter_extension.workspace = true
|
debug_adapter_extension.workspace = true
|
||||||
dirs.workspace = true
|
dirs.workspace = true
|
||||||
|
@ -68,4 +69,3 @@ util.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
watch.workspace = true
|
watch.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
zed_llm_client.workspace = true
|
|
||||||
|
|