Compare commits

...

202 Commits

Author SHA1 Message Date
Richard Feldman
5c563ec303 Delete fetch_fallible for now 2025-12-18 20:29:47 -05:00
Richard Feldman
050b95e429 Clean up imports 2025-12-18 20:29:41 -05:00
Richard Feldman
1385c49e59 Delete ExtensionOperation::AutoInstall for now 2025-12-18 20:29:09 -05:00
Richard Feldman
8a87a7f459 Delete unused fn 2025-12-18 20:28:54 -05:00
Richard Feldman
fc3de7f7f1 use and_then/filter_map over map 2025-12-18 20:28:47 -05:00
Richard Feldman
0482017a95 Restore language_model.rs to origin/main 2025-12-18 20:25:36 -05:00
Richard Feldman
6abc2c2d36 Restore since_v0_8_0.rs to origin/main 2025-12-18 20:24:46 -05:00
Richard Feldman
69bfcb0ed7 Delete openai_migration and open_router_migration 2025-12-18 20:24:25 -05:00
Richard Feldman
38544aef26 Delete open_router_migration.rs 2025-12-18 20:24:01 -05:00
Richard Feldman
ea713e95fe Restore extension_host.rs to origin/main 2025-12-18 20:23:27 -05:00
Richard Feldman
6505bdbb3f Delete other migrations for now 2025-12-18 20:23:17 -05:00
Richard Feldman
0dd773f591 Restore extension_host_proxy.rs to origin/main 2025-12-18 20:22:08 -05:00
Richard Feldman
a638946007 Delete copilot_migration.rs 2025-12-18 20:20:57 -05:00
Richard Feldman
d94ada12d0 Update Cargo.lock 2025-12-18 20:20:27 -05:00
Richard Feldman
387c428720 Restore extension_host Cargo.toml to origin/main 2025-12-18 20:20:22 -05:00
Richard Feldman
9e858de69b Delete llm-provider.wit 2025-12-18 20:19:24 -05:00
Richard Feldman
cb695ce71c Restore the since 0.8.0 stuff to origin/main 2025-12-18 20:18:17 -05:00
Richard Feldman
39cf37a154 Restore extension_settings.rs to origin/main 2025-12-18 20:18:17 -05:00
Richard Feldman
be37bd11a7 Restore wasm_host.rs to origin/main 2025-12-18 20:18:17 -05:00
Richard Feldman
aa8ee02367 Delete some llm provider and oauth stuff 2025-12-18 20:18:17 -05:00
Richard Feldman
005bfdbb96 Restore wit.rs to origin/main 2025-12-18 20:18:17 -05:00
Richard Feldman
253f7ac4a7 Restore since_v0_6_0.rs to origin/main 2025-12-18 20:18:17 -05:00
Richard Feldman
0808b636cf Delete since_v0_8_0.rs 2025-12-18 20:18:17 -05:00
Richard Feldman
73f42f0146 Restore test/platform.rs and text_context.rs to origin/main 2025-12-18 20:18:17 -05:00
Richard Feldman
b4a2dd4c8e Restore api_key.rs to origin/main 2025-12-18 20:18:17 -05:00
Richard Feldman
3373bad23b Delete ConfigurationViewTargetAgent::EditPrediction 2025-12-18 20:18:17 -05:00
Richard Feldman
0186e9bd42 Delete some unnecessary comments 2025-12-18 20:18:17 -05:00
Richard Feldman
14b337b2db Minimize a git diff 2025-12-18 20:18:17 -05:00
Richard Feldman
ee04ad637f Delete google_ai_api_key.rs 2025-12-18 20:18:17 -05:00
Richard Feldman
347825cc09 Restore anthropic.rs to origin/main 2025-12-18 20:18:17 -05:00
Richard Feldman
3f20635ff0 Restore google.rs to origin/main, modulo a comment 2025-12-18 20:18:17 -05:00
Richard Feldman
f2a4e081cc Delete allowed_env_var_providers for now 2025-12-18 20:18:17 -05:00
Richard Feldman
ca616d8824 Update Cargo.lock 2025-12-18 20:18:17 -05:00
Richard Feldman
09920413a6 Restore agent.rs to origin/main 2025-12-18 20:18:17 -05:00
Richard Feldman
3e5621833b Restore crates/settings_ui/Cargo.toml to origin/main 2025-12-18 20:18:17 -05:00
Richard Feldman
86f7066a1b Revert edit prediction provider changes for now 2025-12-18 20:18:17 -05:00
Richard Feldman
8768d46f2c Restore toggle.rs to origin/main 2025-12-18 20:18:17 -05:00
Richard Feldman
fe7a1aa921 Remove oauth device flow for now 2025-12-18 20:18:17 -05:00
Richard Feldman
dbc37ba469 Restore workspace.rs to origin/main 2025-12-18 20:18:17 -05:00
Richard Feldman
b2fe8a7afb Restore configuring-zed.md to origin/main 2025-12-18 20:18:17 -05:00
Richard Feldman
23b862c978 Remove google-ai extension for now 2025-12-18 20:18:17 -05:00
Richard Feldman
3012bb9293 Restore extension_api.rs to origin/main 2025-12-18 20:03:00 -05:00
Richard Feldman
0ed4a12ba7 Restore extension_manifest.rs to origin/main 2025-12-18 20:02:40 -05:00
Richard Feldman
f9a85025ea Restore gemini.rs to origin/main 2025-12-18 20:01:35 -05:00
Richard Feldman
7abca1ccbc restore anthropic.rs to origin/main 2025-12-18 20:00:33 -05:00
Richard Feldman
03a05d14ea Restore model_selector_components to origin/main 2025-12-18 20:00:13 -05:00
Richard Feldman
27719b4aad Combine to_selected_model and is_provider_authenticated 2025-12-18 19:58:35 -05:00
Richard Feldman
d6d7da567a Update Cargo.lock 2025-12-18 19:50:42 -05:00
Richard Feldman
3071e07205 Reset default.json to origin/main 2025-12-18 19:49:31 -05:00
Richard Feldman
acbe8aebfc Reset Cargo.toml to origin/main 2025-12-18 19:49:07 -05:00
Richard Feldman
46b5a6f763 Reset extensions dir to origin/main 2025-12-18 19:48:18 -05:00
Richard Feldman
c4c2a0a027 Merge remote-tracking branch 'origin/main' into register-language-model-extensions 2025-12-18 19:46:43 -05:00
Conrad Irwin
6976208e21 Move autofix stuff to zippy (#45304)
Although I wanted to avoid the dependency, it's hard to get github to do
what we want.

Release Notes:

- N/A
2025-12-18 15:23:09 -07:00
Richard Feldman
9d23e5733c Remove AgentModelIcon 2025-12-18 17:21:15 -05:00
Richard Feldman
3a6e91abcb Merge remote-tracking branch 'origin/main' into provider-extensions 2025-12-18 17:20:16 -05:00
Richard Feldman
6055b45ee1 Add support for provider extensions (but no extensions yet) (#45277)
This adds support for provider extensions but doesn't actually add any
yet.

Release Notes:

- N/A
2025-12-18 17:05:04 -05:00
Joseph T. Lyons
88f90c12ed Add language server version in a tooltip on language server hover (#45302)
I wanted a way to make it easy to figure out which version of a language
server Zed is running. Now, you get a tooltip when hovering on a
language server in the Language Servers popover.

<img width="498" height="168" alt="SCR-20251218-ovln"
src="https://github.com/user-attachments/assets/1ced4214-b868-4405-8881-eb7c0b75a53e"
/>

This PR also fixes a bug. We had existing code to open a tooltip on
these language server entrees and display the language server message,
which was never fully wired up for `CustomEntry`s. Now, in this PR, we
will show show either version, message, or both, in the documentation
aside, depending on what the server has given us.

Mostly done with Droid (using GPT-5.2), with manual review and multiple
follow ups to guide it into using existing patterns in the codebase,
when it did something abnormal.

Release Notes:

- Added language server version in a tooltip on language server hover

---------

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2025-12-18 21:59:21 +00:00
Marshall Bowers
0d74f982a5 danger: Upgrade danger-plugin-pr-hygiene to v0.7.1 (#45303)
This PR upgrades `danger-plugin-pr-hygiene` to v0.7.1.

Release Notes:

- N/A
2025-12-18 21:52:34 +00:00
Marshall Bowers
ca90b8555d docs: Remove local collaboration docs (#45301)
This PR removes the docs for running Collab locally, as they are
outdated and don't reflect the current state of affairs.

Release Notes:

- N/A
2025-12-18 21:42:28 +00:00
Richard Feldman
8516d81e13 Fix display name for Ollama models (#45287)
Closes #43646

Release Notes:

- Fixed display name for Ollama models
2025-12-18 16:32:59 -05:00
Danilo Leal
af589ff25f agent_ui: Simplify timestamp display (#45296)
This PR simplifies how we display thread timestamps in the agent panel's
history view. For threads that are older-than-yesterday, we just show
how many days ago that thread was had in. Hovering over the thread item
shows you both the title and the full date, if needed (time and date).

<img width="450" height="786" alt="Screenshot 2025-12-18 at 5  24@2x"
src="https://github.com/user-attachments/assets/11416e9b-f1b0-4307-9db0-988a95a316a1"
/>


Release Notes:

- N/A
2025-12-18 17:49:17 -03:00
Julia Ryan
d2bbfbb3bf lsp: Broadcast our capability for MessageActionItems (#45047)
Closes #37902

Release Notes:

- Enable LSP Message action items for more language servers. These are interactive prompts, often for things like downloading build inputs for a project.
2025-12-18 11:09:40 -08:00
Peter Tripp
413f4ea49c Redact environment variables from language server spawn errors (#44783)
Redact environment variables from zed logs when lsp fails to spawn.

Release Notes:

- N/A
2025-12-18 21:05:14 +02:00
Marshall Bowers
1b6d588413 danger: Deny conventional commits in PR titles (#45283)
This PR upgrades `danger-plugin-pr-hygiene` to v0.7.0 so that we can
have Danger deny conventional commits in PR titles.

Release Notes:

- N/A
2025-12-18 18:42:28 +00:00
Richard Feldman
4f22272b0d Alphabetize auto_install_extensions 2025-12-18 09:14:02 -05:00
Richard Feldman
20d7513c73 Re-add html to auto_install_extensions 2025-12-18 09:13:52 -05:00
Richard Feldman
493f8d59e6 Merge remote-tracking branch 'origin/main' into provider-extensions 2025-12-18 09:13:26 -05:00
Zed Zippy
65a395fa9a Autofix 2025-12-17 23:33:12 +00:00
Richard Feldman
ca8279ca79 More Gemini extension fixes 2025-12-17 18:30:07 -05:00
Richard Feldman
19833f0132 More minor Google AI fixes 2025-12-17 15:49:28 -05:00
Richard Feldman
ad0687a987 Merge remote-tracking branch 'origin/main' into provider-extensions 2025-12-17 15:18:53 -05:00
Richard Feldman
a51b99216d Revise Google AI extension 2025-12-17 15:14:45 -05:00
Richard Feldman
3de07eaf0c Fix extensions/google-ai/src/google_ai.rs
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-12-17 14:32:55 -05:00
Richard Feldman
5fa97e8da8 wip override the google_ai extension with the hardcoded implementation
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-12-17 13:53:20 -05:00
Richard Feldman
6acc4cc038 Merge remote-tracking branch 'origin/main' into provider-extensions
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-12-17 13:49:15 -05:00
Richard Feldman
6a07fe4e99 Revert "Replace extensions google_ai with the hardcoded one."
This reverts commit 6f05a4b6df.
2025-12-17 13:40:30 -05:00
Richard Feldman
6f05a4b6df Replace extensions google_ai with the hardcoded one.
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-12-17 13:37:36 -05:00
Richard Feldman
78f9f4a768 Go back to just Gemini for now - delete other extensions
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
2025-12-17 13:36:24 -05:00
Richard Feldman
46dedb3e13 wip fixing anthropic regressions 2025-12-17 13:27:23 -05:00
Richard Feldman
ea5800b322 Revise comment about tiktoken 2025-12-16 09:21:26 -05:00
Richard Feldman
b652196356 clippy 2025-12-16 09:15:11 -05:00
Richard Feldman
155a2d2a1e Merge remote-tracking branch 'origin/main' into provider-extensions 2025-12-16 09:10:54 -05:00
Richard Feldman
f182aa43bb Replace OAuth-specific HTTP WIT stuff with generic http alternatives. 2025-12-16 09:04:48 -05:00
Danilo Leal
f783f22e33 Remove unused editor dependency from extension_host 2025-12-15 20:59:41 -03:00
Danilo Leal
6811c57550 Adjust description markdown for built-in providers 2025-12-15 20:53:13 -03:00
Danilo Leal
5739fce607 Remove unnecessary comments from edit prediction setup page 2025-12-15 20:52:39 -03:00
Danilo Leal
f0fc578fe6 Start cleaning up the UI 2025-12-15 20:52:23 -03:00
Richard Feldman
7cbc6fb337 Revise layout of OAuth provider in settings 2025-12-15 15:44:57 -05:00
Richard Feldman
55c9113177 Support OAuth extensions in settings panel 2025-12-15 14:14:37 -05:00
Richard Feldman
98248d5a7a Hide hardcoded Copilot Chat setting if extension is installed 2025-12-15 13:24:03 -05:00
Richard Feldman
8d5b12a6be Don't show (Extension) in provider names 2025-12-15 12:50:04 -05:00
Richard Feldman
aa69a52685 Merge remote-tracking branch 'origin/main' into provider-extensions 2025-12-15 11:26:44 -05:00
Richard Feldman
d5e2a2a00c Migrate dev extension credentials too 2025-12-15 10:34:03 -05:00
Richard Feldman
094f514414 Minor change to Copilot visuals 2025-12-15 10:13:09 -05:00
Richard Feldman
5abf968748 Make Copilot login flow look like the builtin one 2025-12-14 21:47:17 -05:00
Richard Feldman
dd455306b2 Show "OpenAI-compatible models" notification 2025-12-14 21:14:24 -05:00
Richard Feldman
dd4d5b5b0c Fix multiple env var migrations 2025-12-14 21:14:04 -05:00
Richard Feldman
cc7799af38 Allow multiple API keys 2025-12-14 20:35:01 -05:00
Richard Feldman
13776b7898 Make provider extensions more closely match current styling 2025-12-14 20:04:30 -05:00
Richard Feldman
67f3b0987a Refactor out a var name 2025-12-12 13:20:38 -05:00
Richard Feldman
6a6b556143 Fix a warning 2025-12-12 12:57:26 -05:00
Richard Feldman
3debec1393 Simplify some path resolving logic 2025-12-12 12:55:57 -05:00
Richard Feldman
bde75bb11a Extract a big string into include_str! 2025-12-12 12:54:57 -05:00
Richard Feldman
eff0105c04 Add a comment explaining port behavior 2025-12-12 12:53:20 -05:00
Richard Feldman
4bbc53b0ee Remove obsolete request-credential from the interface 2025-12-12 12:34:44 -05:00
Richard Feldman
00a62555ec Restore since_v0_6_0.rs to origin/main 2025-12-12 12:28:21 -05:00
Richard Feldman
d1f085c063 Rename oauth to be at the front of oauth_send_http_request 2025-12-12 12:16:30 -05:00
Richard Feldman
73341e51ac Clarify sign-in docs 2025-12-12 11:59:15 -05:00
Richard Feldman
ed111bf528 Revise some docs 2025-12-12 11:58:23 -05:00
Richard Feldman
64966bbecc Require sign_in_button_label 2025-12-12 11:55:31 -05:00
Richard Feldman
fe895c7c97 Reorganize LanguageModelAuthConfig 2025-12-12 11:55:17 -05:00
Richard Feldman
9c2c9ea949 Don't make credential_label optional 2025-12-12 11:54:22 -05:00
Richard Feldman
f46b94635d Don't default some of the model options in the extension 2025-12-12 11:53:17 -05:00
Richard Feldman
b9c8f8b79e Deduplicate icon styling 2025-12-12 11:29:14 -05:00
Richard Feldman
6ac42dde0d Restore some origin/main logic 2025-12-12 11:27:50 -05:00
Richard Feldman
7f51ca3dbb Deduplicate some branches 2025-12-12 11:24:15 -05:00
Richard Feldman
c050b4225a Drop an unnecessary constant 2025-12-12 11:22:41 -05:00
Richard Feldman
b2073af63a Make a match exhaustive and use alternatives 2025-12-12 11:22:08 -05:00
Richard Feldman
a52e4af96d Further icon deduplication 2025-12-12 11:14:04 -05:00
Richard Feldman
35aa3f2207 More icon deduplication 2025-12-12 11:13:07 -05:00
Richard Feldman
1a808c4642 Reduce some icon code duplication 2025-12-12 11:11:52 -05:00
Richard Feldman
fda2688165 Merge remote-tracking branch 'origin/main' into provider-extensions 2025-12-12 11:11:43 -05:00
Richard Feldman
7881047432 prettier 2025-12-11 01:25:18 -05:00
Richard Feldman
da9281c4a4 Add missing licenses 2025-12-11 01:25:10 -05:00
Richard Feldman
9cc517e0dd Fix some extension auto install bugs 2025-12-11 00:52:08 -05:00
Richard Feldman
d1390a5b78 Merge remote-tracking branch 'origin/main' into migrate-provider-extensions 2025-12-11 00:26:09 -05:00
Richard Feldman
ee4faede38 Migrate on auto-load 2025-12-11 00:22:38 -05:00
Richard Feldman
8d96a699b3 Revise migration system some more 2025-12-11 00:13:11 -05:00
Richard Feldman
8cfb7471db Minimize how we're tracking migrations 2025-12-10 23:21:14 -05:00
Richard Feldman
def9c87837 Migrate credentials without touching settings 2025-12-10 22:29:48 -05:00
Richard Feldman
0313ab6d41 Change open-router to openrouter in default.json 2025-12-10 22:10:29 -05:00
Richard Feldman
c5329fdff2 Rename extension from open-router to openrouter 2025-12-10 22:09:59 -05:00
Richard Feldman
a676a6895b Remove redundant set_builtin_provider_hiding_fn call 2025-12-10 22:05:03 -05:00
Richard Feldman
3b5d7d7d89 Minor cleanups 2025-12-10 22:04:35 -05:00
Richard Feldman
91f01131b1 Introduce DEBUG_ALLOW_UNPUBLISHED_AUTO_EXTENSIONS 2025-12-10 21:29:10 -05:00
Richard Feldman
5fa5226286 Remove llm_provider_authenticate() 2025-12-10 21:28:58 -05:00
Richard Feldman
ae94007227 Merge remote-tracking branch 'origin/main' into migrate-provider-extensions 2025-12-10 21:13:57 -05:00
Richard Feldman
8f425a1bd5 Fix unused arg 2025-12-10 13:11:30 -05:00
Richard Feldman
743c414e7b Refresh models list after successful auth 2025-12-10 13:10:55 -05:00
Richard Feldman
0fe335efc5 Revise Copilot auth 2025-12-10 13:02:38 -05:00
Richard Feldman
36b95aac4b Debugging extension loading timing and fallbacks 2025-12-10 12:55:41 -05:00
Richard Feldman
b2df70ab58 Clean up extension markdown for settings 2025-12-10 12:55:23 -05:00
Richard Feldman
36293d7dd9 Debugging 2025-12-09 17:04:58 -05:00
Richard Feldman
3ae3e1fce8 Don't use a heuristic for icon path 2025-12-09 14:55:44 -05:00
Richard Feldman
e5f1fc7478 Fix some regressions 2025-12-09 14:48:31 -05:00
Richard Feldman
a4f6076da7 Migrate to extensions with fallback to builtin 2025-12-09 14:14:56 -05:00
Richard Feldman
43726b2620 Restore ai_anthropic icon svg 2025-12-09 12:00:36 -05:00
Richard Feldman
94980ffb49 Reduce duplication in compute_configured_providers 2025-12-09 11:55:37 -05:00
Richard Feldman
22cc731450 Remove some duplication from icon logic 2025-12-09 11:54:58 -05:00
Richard Feldman
d9396373e3 Eliminate more code duplication 2025-12-09 11:54:00 -05:00
Richard Feldman
48002be135 Use | instead of code duplication 2025-12-09 11:53:18 -05:00
Richard Feldman
58db83f8f5 more icon code cleanup 2025-12-09 11:48:06 -05:00
Richard Feldman
0243d5b542 Clean up some more icon code 2025-12-09 11:44:10 -05:00
Richard Feldman
06230327fa Clean up some icon code 2025-12-09 11:44:05 -05:00
Richard Feldman
ca5c8992f9 Merge remote-tracking branch 'origin/main' into migrate-provider-extensions 2025-12-08 20:23:32 -05:00
Richard Feldman
1038e1c2ef Clean up some duplicated code 2025-12-08 16:59:49 -05:00
Richard Feldman
e1fe0b3287 Restore providers, deduplicate if extensions are present 2025-12-08 16:25:41 -05:00
Richard Feldman
a0e10a91bf Merge remote-tracking branch 'origin/main' into migrate-provider-extensions 2025-12-08 15:35:44 -05:00
Richard Feldman
272b1aa4bc Remove obsolete llm_provider_authenticate 2025-12-08 14:46:04 -05:00
Richard Feldman
9ef0537b44 Add the other extensions to auto-install 2025-12-07 23:13:52 -05:00
Richard Feldman
77f1de742b delete hardcoded AI providers in favor of extnesions 2025-12-07 21:31:00 -05:00
Richard Feldman
e054cabd41 Migrate Google AI over to the extension 2025-12-07 20:57:00 -05:00
Richard Feldman
3b95cb5682 Migrate Copilot and Anthropic to extensions 2025-12-07 20:48:42 -05:00
Richard Feldman
c89653bd07 Fix bugs around logging out from provider extensions 2025-12-05 17:07:25 -05:00
Richard Feldman
b90ac2dc07 Fix Drop impl for WasmExtension 2025-12-05 16:21:53 -05:00
Marshall Bowers
c9998541f0 Revert spurious changes to default.json 2025-12-05 13:25:03 -05:00
Marshall Bowers
e2b49b3cd3 Restore blank lines from main 2025-12-05 13:08:30 -05:00
Marshall Bowers
d1e77397c6 Don't make v0.8.0 available on Stable/Preview yet 2025-12-05 13:07:36 -05:00
Richard Feldman
cc5f5e35e4 Clean up some comments 2025-12-05 13:00:19 -05:00
Richard Feldman
7183b8a1cd Fix API key bug 2025-12-05 12:59:19 -05:00
Richard Feldman
b1934fb712 Remove builtin Anthropic provider 2025-12-05 12:11:51 -05:00
Richard Feldman
a198b6c0d1 Use icon in more places 2025-12-05 11:48:11 -05:00
Richard Feldman
8b5b2712c8 Update Cargo.lock 2025-12-05 11:32:58 -05:00
Richard Feldman
4464392e8e Use kebab-case for open-router extension too. 2025-12-05 11:19:10 -05:00
Richard Feldman
a0d3bc31e9 Rename copilot_chat to copilot-chat 2025-12-05 11:15:43 -05:00
Richard Feldman
ccd6672d1a Revert "Remove builtin extensions for now"
This reverts commit 5559726fd7.
2025-12-05 11:13:29 -05:00
Richard Feldman
21de6d35dd Revert "Revert auto-install extensions for now"
This reverts commit 2031ca17e5.
2025-12-05 11:13:22 -05:00
Richard Feldman
2031ca17e5 Revert auto-install extensions for now 2025-12-05 11:06:12 -05:00
Richard Feldman
8b1ce75a57 Move wit extensions into their own module 2025-12-05 10:30:02 -05:00
Richard Feldman
5559726fd7 Remove builtin extensions for now 2025-12-04 17:20:47 -05:00
Richard Feldman
e1a9269921 Delete example provider extension 2025-12-04 17:20:47 -05:00
Richard Feldman
3b6b3ff504 Specify env vars for the builtin extensions 2025-12-04 17:19:35 -05:00
Richard Feldman
aabed94970 Add OAuth via web authentication to llm extensions, migrate copilot 2025-12-04 17:12:55 -05:00
Richard Feldman
2d3a3521ba Add OAuth Web Flow auth option for llm provider extensions 2025-12-04 17:12:55 -05:00
Richard Feldman
a48bd10da0 Add llm extensions to auto_install_extensions 2025-12-04 17:12:55 -05:00
Richard Feldman
fec9525be4 Add env var checkbox 2025-12-04 17:12:23 -05:00
Richard Feldman
bf2b8e999e use fill=black over fill=currentColor 2025-12-04 16:51:47 -05:00
Richard Feldman
63c35d2b00 Use local icons in llm extensions 2025-12-04 16:48:25 -05:00
Richard Feldman
1396c68010 Add svg icons to llm provider extensions 2025-12-04 16:43:49 -05:00
Richard Feldman
fcb3d3dec6 Update a comment 2025-12-04 16:28:29 -05:00
Richard Feldman
f54e7f8c9d Add trailing newlines 2025-12-04 16:18:43 -05:00
Richard Feldman
2a89529d7f Use named fields 2025-12-04 16:17:50 -05:00
Richard Feldman
58207325e2 restore impl Drop for WasmExtension 2025-12-04 16:12:21 -05:00
Richard Feldman
e08ab99e8d Add extensions for LLM providers 2025-12-04 16:03:51 -05:00
Richard Feldman
a95f3f33a4 Clean up debug logging 2025-12-04 12:38:06 -05:00
Richard Feldman
b0767c1b1f Merge remote-tracking branch 'origin/main' into provider-extensions 2025-12-04 12:27:15 -05:00
Richard Feldman
b200e10bc4 Clean up debug statements 2025-12-04 11:30:44 -05:00
Richard Feldman
948905d916 Revise provider extensions for Gemini API 2025-12-03 20:22:10 -05:00
Richard Feldman
04de456373 Use extension-llm- prefix for credential keys 2025-12-03 15:55:10 -05:00
Richard Feldman
e5ce32e936 Add provider extension API key in settings 2025-12-03 14:41:39 -05:00
Richard Feldman
d7caae30de Fix auth and subscriptions for provider extensions 2025-12-03 13:00:53 -05:00
Richard Feldman
c7e77674a1 Initial Claude Opus 4.5 implementation of Provider Extensions 2025-12-02 13:50:00 -05:00
65 changed files with 982 additions and 609 deletions

View File

@@ -61,8 +61,7 @@ jobs:
uses: namespacelabs/nscloud-cache-action@v1
with:
cache: rust
- id: cargo_fmt
name: steps::cargo_fmt
- name: steps::cargo_fmt
run: cargo fmt --all -- --check
shell: bash -euxo pipefail {0}
- name: extension_tests::run_clippy

View File

@@ -26,8 +26,7 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- name: steps::clear_target_dir_if_large
@@ -72,15 +71,9 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- id: record_clippy_failure
name: steps::record_clippy_failure
if: always()
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest
uses: taiki-e/install-action@nextest
- name: steps::clear_target_dir_if_large
@@ -94,8 +87,6 @@ jobs:
run: |
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}
outputs:
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
timeout-minutes: 60
run_tests_windows:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
@@ -114,8 +105,7 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy.ps1
shell: pwsh
- name: steps::clear_target_dir_if_large

View File

@@ -20,8 +20,7 @@ jobs:
with:
clean: false
fetch-depth: 0
- id: cargo_fmt
name: steps::cargo_fmt
- name: steps::cargo_fmt
run: cargo fmt --all -- --check
shell: bash -euxo pipefail {0}
- name: ./script/clippy
@@ -45,8 +44,7 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy.ps1
shell: pwsh
- name: steps::clear_target_dir_if_large

View File

@@ -74,19 +74,12 @@ jobs:
uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2
with:
version: '9'
- id: prettier
name: steps::prettier
- name: steps::prettier
run: ./script/prettier
shell: bash -euxo pipefail {0}
- id: cargo_fmt
name: steps::cargo_fmt
- name: steps::cargo_fmt
run: cargo fmt --all -- --check
shell: bash -euxo pipefail {0}
- id: record_style_failure
name: steps::record_style_failure
if: always()
run: echo "failed=${{ steps.prettier.outcome == 'failure' || steps.cargo_fmt.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
- name: ./script/check-todos
run: ./script/check-todos
shell: bash -euxo pipefail {0}
@@ -97,8 +90,6 @@ jobs:
uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06
with:
config: ./typos.toml
outputs:
style_failed: ${{ steps.record_style_failure.outputs.failed == 'true' }}
timeout-minutes: 60
run_tests_windows:
needs:
@@ -119,8 +110,7 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy.ps1
shell: pwsh
- name: steps::clear_target_dir_if_large
@@ -167,15 +157,9 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- id: record_clippy_failure
name: steps::record_clippy_failure
if: always()
run: echo "failed=${{ steps.clippy.outcome == 'failure' }}" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
- name: steps::cargo_install_nextest
uses: taiki-e/install-action@nextest
- name: steps::clear_target_dir_if_large
@@ -189,8 +173,6 @@ jobs:
run: |
rm -rf ./../.cargo
shell: bash -euxo pipefail {0}
outputs:
clippy_failed: ${{ steps.record_clippy_failure.outputs.failed == 'true' }}
timeout-minutes: 60
run_tests_mac:
needs:
@@ -211,8 +193,7 @@ jobs:
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
with:
node-version: '20'
- id: clippy
name: steps::clippy
- name: steps::clippy
run: ./script/clippy
shell: bash -euxo pipefail {0}
- name: steps::clear_target_dir_if_large
@@ -592,24 +573,6 @@ jobs:
exit $EXIT_CODE
shell: bash -euxo pipefail {0}
call_autofix:
needs:
- check_style
- run_tests_linux
if: always() && (needs.check_style.outputs.style_failed == 'true' || needs.run_tests_linux.outputs.clippy_failed == 'true') && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- id: get-app-token
name: steps::authenticate_as_zippy
uses: actions/create-github-app-token@bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1
with:
app-id: ${{ secrets.ZED_ZIPPY_APP_ID }}
private-key: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }}
- name: run_tests::call_autofix::dispatch_autofix
run: gh workflow run autofix_pr.yml -f pr_number=${{ github.event.pull_request.number }} -f run_clippy=${{ needs.run_tests_linux.outputs.clippy_failed == 'true' }}
shell: bash -euxo pipefail {0}
env:
GITHUB_TOKEN: ${{ steps.get-app-token.outputs.token }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}
cancel-in-progress: true

2
Cargo.lock generated
View File

@@ -8932,6 +8932,8 @@ dependencies = [
"credentials_provider",
"deepseek",
"editor",
"extension",
"extension_host",
"fs",
"futures 0.3.31",
"google_ai",

View File

@@ -20,7 +20,6 @@ Other platforms are not yet available:
- [Building Zed for macOS](./docs/src/development/macos.md)
- [Building Zed for Linux](./docs/src/development/linux.md)
- [Building Zed for Windows](./docs/src/development/windows.md)
- [Running Collaboration Locally](./docs/src/development/local-collaboration.md)
### Contributing

View File

@@ -3,11 +3,11 @@ use agent_client_protocol::{self as acp};
use anyhow::Result;
use collections::IndexMap;
use gpui::{Entity, SharedString, Task};
use language_model::LanguageModelProviderId;
use language_model::{IconOrSvg, LanguageModelProviderId};
use project::Project;
use serde::{Deserialize, Serialize};
use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc};
use ui::{App, IconName};
use ui::App;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
@@ -215,7 +215,7 @@ pub struct AgentModelInfo {
pub id: acp::ModelId,
pub name: SharedString,
pub description: Option<SharedString>,
pub icon: Option<IconName>,
pub icon: Option<IconOrSvg>,
}
impl From<acp::ModelInfo> for AgentModelInfo {

View File

@@ -93,7 +93,7 @@ impl LanguageModels {
fn refresh_list(&mut self, cx: &App) {
let providers = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.visible_providers()
.into_iter()
.filter(|provider| provider.is_authenticated(cx))
.collect::<Vec<_>>();
@@ -164,7 +164,7 @@ impl LanguageModels {
fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
let authenticate_all_providers = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.visible_providers()
.iter()
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
.collect::<Vec<_>>();
@@ -1630,7 +1630,7 @@ mod internal_tests {
id: acp::ModelId::new("fake/fake"),
name: "Fake".into(),
description: None,
icon: Some(ui::IconName::ZedAssistant),
icon: Some(language_model::IconOrSvg::Icon(ui::IconName::ZedAssistant)),
}]
)])
);

View File

@@ -13,6 +13,7 @@ use gpui::{
Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Task, WeakEntity,
};
use itertools::Itertools;
use language_model::IconOrSvg;
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
use settings::Settings;
@@ -350,7 +351,11 @@ impl PickerDelegate for AcpModelPickerDelegate {
})
.child(
ModelSelectorListItem::new(ix, model_info.name.clone())
.when_some(model_info.icon, |this, icon| this.icon(icon))
.map(|this| match &model_info.icon {
Some(IconOrSvg::Svg(path)) => this.icon_path(path.clone()),
Some(IconOrSvg::Icon(icon)) => this.icon(*icon),
None => this,
})
.is_selected(is_selected)
.is_focused(selected)
.when(supports_favorites, |this| {

View File

@@ -6,6 +6,7 @@ use agent_servers::AgentServer;
use agent_settings::AgentSettings;
use fs::Fs;
use gpui::{Entity, FocusHandle};
use language_model::IconOrSvg;
use picker::popover_menu::PickerPopoverMenu;
use settings::Settings as _;
use ui::{ButtonLike, KeyBinding, PopoverMenuHandle, TintColor, Tooltip, prelude::*};
@@ -70,7 +71,7 @@ impl Render for AcpModelSelectorPopover {
.map(|model| model.name.clone())
.unwrap_or_else(|| SharedString::from("Select a Model"));
let model_icon = model.as_ref().and_then(|model| model.icon);
let model_icon = model.as_ref().and_then(|model| model.icon.clone());
let focus_handle = self.focus_handle.clone();
@@ -125,7 +126,14 @@ impl Render for AcpModelSelectorPopover {
ButtonLike::new("active-model")
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.when_some(model_icon, |this, icon| {
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
this.child(
match icon {
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
IconOrSvg::Icon(icon_name) => Icon::new(icon_name),
}
.color(color)
.size(IconSize::XSmall),
)
})
.child(
Label::new(model_name)

View File

@@ -1,7 +1,7 @@
use crate::acp::AcpThreadView;
use crate::{AgentPanel, RemoveHistory, RemoveSelectedThread};
use agent::{HistoryEntry, HistoryStore};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
use editor::{Editor, EditorEvent};
use fuzzy::StringMatchCandidate;
use gpui::{
@@ -402,7 +402,22 @@ impl AcpThreadHistory {
let selected = ix == self.selected_index;
let hovered = Some(ix) == self.hovered_index;
let timestamp = entry.updated_at().timestamp();
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
let display_text = match format {
EntryTimeFormat::DateAndTime => {
let entry_time = entry.updated_at();
let now = Utc::now();
let duration = now.signed_duration_since(entry_time);
let days = duration.num_days();
format!("{}d", days)
}
EntryTimeFormat::TimeOnly => format.format_timestamp(timestamp, self.local_timezone),
};
let title = entry.title().clone();
let full_date =
EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
h_flex()
.w_full()
@@ -423,11 +438,14 @@ impl AcpThreadHistory {
.truncate(),
)
.child(
Label::new(thread_timestamp)
Label::new(display_text)
.color(Color::Muted)
.size(LabelSize::XSmall),
),
)
.tooltip(move |_, cx| {
Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
})
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
if *is_hovered {
this.hovered_index = Some(ix);

View File

@@ -22,7 +22,8 @@ use gpui::{
};
use language::LanguageRegistry;
use language_model::{
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
IconOrSvg, LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
ZED_CLOUD_PROVIDER_ID,
};
use language_models::AllLanguageModelSettings;
use notifications::status_toast::{StatusToast, ToastIcon};
@@ -117,7 +118,7 @@ impl AgentConfiguration {
}
fn build_provider_configuration_views(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let providers = LanguageModelRegistry::read_global(cx).providers();
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
for provider in providers {
self.add_provider_configuration_view(&provider, window, cx);
}
@@ -261,9 +262,12 @@ impl AgentConfiguration {
.w_full()
.gap_1p5()
.child(
Icon::new(provider.icon())
.size(IconSize::Small)
.color(Color::Muted),
match provider.icon() {
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
IconOrSvg::Icon(name) => Icon::new(name),
}
.size(IconSize::Small)
.color(Color::Muted),
)
.child(
h_flex()
@@ -416,7 +420,7 @@ impl AgentConfiguration {
&mut self,
cx: &mut Context<Self>,
) -> impl IntoElement {
let providers = LanguageModelRegistry::read_global(cx).providers();
let providers = LanguageModelRegistry::read_global(cx).visible_providers();
let popover_menu = PopoverMenu::new("add-provider-popover")
.trigger(

View File

@@ -4,6 +4,7 @@ use crate::{
};
use fs::Fs;
use gpui::{Entity, FocusHandle, SharedString};
use language_model::IconOrSvg;
use picker::popover_menu::PickerPopoverMenu;
use settings::update_settings_file;
use std::sync::Arc;
@@ -103,7 +104,14 @@ impl Render for AgentModelSelector {
self.selector.clone(),
ButtonLike::new("active-model")
.when_some(provider_icon, |this, icon| {
this.child(Icon::new(icon).color(color).size(IconSize::XSmall))
this.child(
match icon {
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
IconOrSvg::Icon(name) => Icon::new(name),
}
.color(color)
.size(IconSize::XSmall),
)
})
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.child(
@@ -115,7 +123,7 @@ impl Render for AgentModelSelector {
.child(
Icon::new(IconName::ChevronDown)
.color(color)
.size(IconSize::Small),
.size(IconSize::XSmall),
),
move |_window, cx| {
Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)

View File

@@ -2428,7 +2428,7 @@ impl AgentPanel {
let history_is_empty = self.history_store.read(cx).is_empty(cx);
let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
.providers()
.visible_providers()
.iter()
.any(|provider| {
provider.is_authenticated(cx)

View File

@@ -348,7 +348,8 @@ fn init_language_model_settings(cx: &mut App) {
|_, event: &language_model::Event, cx| match event {
language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => {
| language_model::Event::RemovedProvider(_)
| language_model::Event::ProvidersChanged => {
update_active_language_model_from_settings(cx);
}
_ => {}
@@ -360,33 +361,52 @@ fn init_language_model_settings(cx: &mut App) {
fn update_active_language_model_from_settings(cx: &mut App) {
let settings = AgentSettings::get_global(cx);
fn to_selected_model(selection: &LanguageModelSelection) -> language_model::SelectedModel {
language_model::SelectedModel {
provider: LanguageModelProviderId::from(selection.provider.0.clone()),
model: LanguageModelId::from(selection.model.clone()),
fn to_selected_model(
selection: &LanguageModelSelection,
registry: &LanguageModelRegistry,
cx: &App,
) -> Option<language_model::SelectedModel> {
let provider_id = LanguageModelProviderId::from(selection.provider.0.clone());
if registry
.provider(&provider_id)
.map_or(false, |provider| provider.is_authenticated(cx))
{
Some(language_model::SelectedModel {
provider: LanguageModelProviderId::from(selection.provider.0.clone()),
model: LanguageModelId::from(selection.model.clone()),
})
} else {
None
}
}
let default = settings.default_model.as_ref().map(to_selected_model);
let registry = LanguageModelRegistry::global(cx);
let registry_ref = registry.read(cx);
let default = settings
.default_model
.as_ref()
.and_then(|s| to_selected_model(s, registry_ref, cx));
let inline_assistant = settings
.inline_assistant_model
.as_ref()
.map(to_selected_model);
.and_then(|s| to_selected_model(s, registry_ref, cx));
let commit_message = settings
.commit_message_model
.as_ref()
.map(to_selected_model);
.and_then(|s| to_selected_model(s, registry_ref, cx));
let thread_summary = settings
.thread_summary_model
.as_ref()
.map(to_selected_model);
.and_then(|s| to_selected_model(s, registry_ref, cx));
let inline_alternatives = settings
.inline_alternatives
.iter()
.map(to_selected_model)
.filter_map(|s| to_selected_model(s, registry_ref, cx))
.collect::<Vec<_>>();
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry.update(cx, |registry, cx| {
registry.select_default_model(default.as_ref(), cx);
registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
registry.select_commit_message_model(commit_message.as_ref(), cx);

View File

@@ -2,13 +2,12 @@ use std::{cmp::Reverse, sync::Arc};
use agent_settings::AgentSettings;
use collections::{HashMap, HashSet, IndexMap};
use futures::{StreamExt, channel::mpsc};
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{
Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Subscription, Task,
};
use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, FocusHandle, Task};
use language_model::{
AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProvider,
LanguageModelProviderId, LanguageModelRegistry,
AuthenticateError, ConfiguredModel, IconOrSvg, LanguageModel, LanguageModelId,
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry,
};
use ordered_float::OrderedFloat;
use picker::{Picker, PickerDelegate};
@@ -55,7 +54,7 @@ pub fn language_model_selector(
fn all_models(cx: &App) -> GroupedModels {
let lm_registry = LanguageModelRegistry::global(cx).read(cx);
let providers = lm_registry.providers();
let providers = lm_registry.visible_providers();
let mut favorites_index = FavoritesIndex::default();
@@ -76,7 +75,7 @@ fn all_models(cx: &App) -> GroupedModels {
})
.collect();
let all = providers
let all: Vec<ModelInfo> = providers
.iter()
.flat_map(|provider| {
provider
@@ -94,7 +93,7 @@ type FavoritesIndex = HashMap<LanguageModelProviderId, HashSet<LanguageModelId>>
#[derive(Clone)]
struct ModelInfo {
model: Arc<dyn LanguageModel>,
icon: IconName,
icon: IconOrSvg,
is_favorite: bool,
}
@@ -124,7 +123,7 @@ pub struct LanguageModelPickerDelegate {
filtered_entries: Vec<LanguageModelPickerEntry>,
selected_index: usize,
_authenticate_all_providers_task: Task<()>,
_subscriptions: Vec<Subscription>,
_refresh_models_task: Task<()>,
popover_styles: bool,
focus_handle: FocusHandle,
}
@@ -151,24 +150,42 @@ impl LanguageModelPickerDelegate {
get_active_model: Arc::new(get_active_model),
on_toggle_favorite: Arc::new(on_toggle_favorite),
_authenticate_all_providers_task: Self::authenticate_all_providers(cx),
_subscriptions: vec![cx.subscribe_in(
&LanguageModelRegistry::global(cx),
window,
|picker, _, event, window, cx| {
match event {
language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => {
let query = picker.query(cx);
picker.delegate.all_models = Arc::new(all_models(cx));
// Update matches will automatically drop the previous task
// if we get a provider event again
picker.update_matches(query, window, cx)
}
_ => {}
_refresh_models_task: {
// Create a channel to signal when models need refreshing
let (refresh_tx, mut refresh_rx) = mpsc::unbounded::<()>();
// Subscribe to registry events and send refresh signals through the channel
let registry = LanguageModelRegistry::global(cx);
cx.subscribe(&registry, move |_picker, _, event, _cx| match event {
language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_)
| language_model::Event::ProvidersChanged => {
refresh_tx.unbounded_send(()).ok();
}
},
)],
language_model::Event::DefaultModelChanged
| language_model::Event::InlineAssistantModelChanged
| language_model::Event::CommitMessageModelChanged
| language_model::Event::ThreadSummaryModelChanged => {}
})
.detach();
// Spawn a task that listens for refresh signals and updates the picker
cx.spawn_in(window, async move |this, cx| {
while let Some(()) = refresh_rx.next().await {
if this
.update_in(cx, |picker, window, cx| {
picker.delegate.all_models = Arc::new(all_models(cx));
picker.refresh(window, cx);
})
.is_err()
{
// Picker was dropped, exit the loop
break;
}
}
})
},
popover_styles,
focus_handle,
}
@@ -203,7 +220,7 @@ impl LanguageModelPickerDelegate {
fn authenticate_all_providers(cx: &mut App) -> Task<()> {
let authenticate_all_providers = LanguageModelRegistry::global(cx)
.read(cx)
.providers()
.visible_providers()
.iter()
.map(|provider| (provider.id(), provider.name(), provider.authenticate(cx)))
.collect::<Vec<_>>();
@@ -474,7 +491,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
let configured_providers = language_model_registry
.read(cx)
.providers()
.visible_providers()
.into_iter()
.filter(|provider| provider.is_authenticated(cx))
.collect::<Vec<_>>();
@@ -566,7 +583,10 @@ impl PickerDelegate for LanguageModelPickerDelegate {
Some(
ModelSelectorListItem::new(ix, model_info.model.name().0)
.icon(model_info.icon)
.map(|this| match &model_info.icon {
IconOrSvg::Icon(icon_name) => this.icon(*icon_name),
IconOrSvg::Svg(icon_path) => this.icon_path(icon_path.clone()),
})
.is_selected(is_selected)
.is_focused(selected)
.is_favorite(is_favorite)
@@ -702,7 +722,7 @@ mod tests {
.any(|(fav_provider, fav_name)| *fav_provider == provider && *fav_name == name);
ModelInfo {
model: Arc::new(TestLanguageModel::new(name, provider)),
icon: IconName::Ai,
icon: IconOrSvg::Icon(IconName::Ai),
is_favorite,
}
})

View File

@@ -33,7 +33,8 @@ use language::{
language_settings::{SoftWrap, all_language_settings},
};
use language_model::{
ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelRegistry, Role,
ConfigurationError, IconOrSvg, LanguageModelExt, LanguageModelImage, LanguageModelRegistry,
Role,
};
use multi_buffer::MultiBufferRow;
use picker::{Picker, popover_menu::PickerPopoverMenu};
@@ -2231,10 +2232,10 @@ impl TextThreadEditor {
.default_model()
.map(|default| default.provider);
let provider_icon = match active_provider {
Some(provider) => provider.icon(),
None => IconName::Ai,
};
let provider_icon = active_provider
.as_ref()
.map(|p| p.icon())
.unwrap_or(IconOrSvg::Icon(IconName::Ai));
let focus_handle = self.editor().focus_handle(cx);
@@ -2244,6 +2245,13 @@ impl TextThreadEditor {
(Color::Muted, IconName::ChevronDown)
};
let provider_icon_element = match provider_icon {
IconOrSvg::Svg(path) => Icon::from_external_svg(path),
IconOrSvg::Icon(name) => Icon::new(name),
}
.color(color)
.size(IconSize::XSmall);
let tooltip = Tooltip::element({
move |_, cx| {
let focus_handle = focus_handle.clone();
@@ -2291,7 +2299,7 @@ impl TextThreadEditor {
.child(
h_flex()
.gap_0p5()
.child(Icon::new(provider_icon).color(color).size(IconSize::XSmall))
.child(provider_icon_element)
.child(
Label::new(model_name)
.color(color)

View File

@@ -1,6 +1,11 @@
use gpui::{Action, FocusHandle, prelude::*};
use ui::{ElevationIndex, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
enum ModelIcon {
Name(IconName),
Path(SharedString),
}
#[derive(IntoElement)]
pub struct ModelSelectorHeader {
title: SharedString,
@@ -39,7 +44,7 @@ impl RenderOnce for ModelSelectorHeader {
pub struct ModelSelectorListItem {
index: usize,
title: SharedString,
icon: Option<IconName>,
icon: Option<ModelIcon>,
is_selected: bool,
is_focused: bool,
is_favorite: bool,
@@ -60,7 +65,12 @@ impl ModelSelectorListItem {
}
pub fn icon(mut self, icon: IconName) -> Self {
self.icon = Some(icon);
self.icon = Some(ModelIcon::Name(icon));
self
}
pub fn icon_path(mut self, path: SharedString) -> Self {
self.icon = Some(ModelIcon::Path(path));
self
}
@@ -105,9 +115,12 @@ impl RenderOnce for ModelSelectorListItem {
.gap_1p5()
.when_some(self.icon, |this, icon| {
this.child(
Icon::new(icon)
.color(model_icon_color)
.size(IconSize::Small),
match icon {
ModelIcon::Name(icon_name) => Icon::new(icon_name),
ModelIcon::Path(icon_path) => Icon::from_external_svg(icon_path),
}
.color(model_icon_color)
.size(IconSize::Small),
)
})
.child(Label::new(self.title).truncate()),

View File

@@ -1,5 +1,5 @@
use agent::{HistoryEntry, HistoryStore};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta, Utc};
use editor::{Editor, EditorEvent};
use fuzzy::StringMatchCandidate;
use gpui::{
@@ -411,7 +411,22 @@ impl AcpThreadHistory {
let selected = ix == self.selected_index;
let hovered = Some(ix) == self.hovered_index;
let timestamp = entry.updated_at().timestamp();
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
let display_text = match format {
EntryTimeFormat::DateAndTime => {
let entry_time = entry.updated_at();
let now = Utc::now();
let duration = now.signed_duration_since(entry_time);
let days = duration.num_days();
format!("{}d", days)
}
EntryTimeFormat::TimeOnly => format.format_timestamp(timestamp, self.local_timezone),
};
let title = entry.title().clone();
let full_date =
EntryTimeFormat::DateAndTime.format_timestamp(timestamp, self.local_timezone);
h_flex()
.w_full()
@@ -432,11 +447,14 @@ impl AcpThreadHistory {
.truncate(),
)
.child(
Label::new(thread_timestamp)
Label::new(display_text)
.color(Color::Muted)
.size(LabelSize::XSmall),
),
)
.tooltip(move |_, cx| {
Tooltip::with_meta(title.clone(), None, full_date.clone(), cx)
})
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
if *is_hovered {
this.hovered_index = Some(ix);

View File

@@ -1,9 +1,9 @@
use gpui::{Action, IntoElement, ParentElement, RenderOnce, point};
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use language_model::{IconOrSvg, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use ui::{Divider, List, ListBulletItem, prelude::*};
pub struct ApiKeysWithProviders {
configured_providers: Vec<(IconName, SharedString)>,
configured_providers: Vec<(IconOrSvg, SharedString)>,
}
impl ApiKeysWithProviders {
@@ -13,7 +13,8 @@ impl ApiKeysWithProviders {
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => {
| language_model::Event::RemovedProvider(_)
| language_model::Event::ProvidersChanged => {
this.configured_providers = Self::compute_configured_providers(cx)
}
_ => {}
@@ -26,9 +27,9 @@ impl ApiKeysWithProviders {
}
}
fn compute_configured_providers(cx: &App) -> Vec<(IconName, SharedString)> {
fn compute_configured_providers(cx: &App) -> Vec<(IconOrSvg, SharedString)> {
LanguageModelRegistry::read_global(cx)
.providers()
.visible_providers()
.iter()
.filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
@@ -47,7 +48,14 @@ impl Render for ApiKeysWithProviders {
.map(|(icon, name)| {
h_flex()
.gap_1p5()
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
.child(
match icon {
IconOrSvg::Icon(icon_name) => Icon::new(icon_name),
IconOrSvg::Svg(icon_path) => Icon::from_external_svg(icon_path),
}
.size(IconSize::XSmall)
.color(Color::Muted),
)
.child(Label::new(name))
});
div()

View File

@@ -11,7 +11,7 @@ use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding};
pub struct AgentPanelOnboarding {
user_store: Entity<UserStore>,
client: Arc<Client>,
configured_providers: Vec<(IconName, SharedString)>,
has_configured_providers: bool,
continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
}
@@ -27,8 +27,9 @@ impl AgentPanelOnboarding {
|this: &mut Self, _registry, event: &language_model::Event, cx| match event {
language_model::Event::ProviderStateChanged(_)
| language_model::Event::AddedProvider(_)
| language_model::Event::RemovedProvider(_) => {
this.configured_providers = Self::compute_available_providers(cx)
| language_model::Event::RemovedProvider(_)
| language_model::Event::ProvidersChanged => {
this.has_configured_providers = Self::has_configured_providers(cx)
}
_ => {}
},
@@ -38,20 +39,16 @@ impl AgentPanelOnboarding {
Self {
user_store,
client,
configured_providers: Self::compute_available_providers(cx),
has_configured_providers: Self::has_configured_providers(cx),
continue_with_zed_ai: Arc::new(continue_with_zed_ai),
}
}
fn compute_available_providers(cx: &App) -> Vec<(IconName, SharedString)> {
fn has_configured_providers(cx: &App) -> bool {
LanguageModelRegistry::read_global(cx)
.providers()
.visible_providers()
.iter()
.filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
})
.map(|provider| (provider.icon(), provider.name().0))
.collect()
.any(|provider| provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID)
}
}
@@ -81,7 +78,7 @@ impl Render for AgentPanelOnboarding {
}),
)
.map(|this| {
if enrolled_in_trial || is_pro_user || !self.configured_providers.is_empty() {
if enrolled_in_trial || is_pro_user || self.has_configured_providers {
this
} else {
this.child(ApiKeysWithoutProviders::new())

View File

@@ -19,6 +19,9 @@ impl Global for GlobalExtensionHostProxy {}
///
/// This object implements each of the individual proxy types so that their
/// methods can be called directly on it.
/// Registration function for language model providers.
pub type LanguageModelProviderRegistration = Box<dyn FnOnce(&mut App) + Send>;
#[derive(Default)]
pub struct ExtensionHostProxy {
theme_proxy: RwLock<Option<Arc<dyn ExtensionThemeProxy>>>,
@@ -29,6 +32,7 @@ pub struct ExtensionHostProxy {
slash_command_proxy: RwLock<Option<Arc<dyn ExtensionSlashCommandProxy>>>,
context_server_proxy: RwLock<Option<Arc<dyn ExtensionContextServerProxy>>>,
debug_adapter_provider_proxy: RwLock<Option<Arc<dyn ExtensionDebugAdapterProviderProxy>>>,
language_model_provider_proxy: RwLock<Option<Arc<dyn ExtensionLanguageModelProviderProxy>>>,
}
impl ExtensionHostProxy {
@@ -54,6 +58,7 @@ impl ExtensionHostProxy {
slash_command_proxy: RwLock::default(),
context_server_proxy: RwLock::default(),
debug_adapter_provider_proxy: RwLock::default(),
language_model_provider_proxy: RwLock::default(),
}
}
@@ -90,6 +95,15 @@ impl ExtensionHostProxy {
.write()
.replace(Arc::new(proxy));
}
pub fn register_language_model_provider_proxy(
&self,
proxy: impl ExtensionLanguageModelProviderProxy,
) {
self.language_model_provider_proxy
.write()
.replace(Arc::new(proxy));
}
}
pub trait ExtensionThemeProxy: Send + Sync + 'static {
@@ -446,3 +460,37 @@ impl ExtensionDebugAdapterProviderProxy for ExtensionHostProxy {
proxy.unregister_debug_locator(locator_name)
}
}
pub trait ExtensionLanguageModelProviderProxy: Send + Sync + 'static {
fn register_language_model_provider(
&self,
provider_id: Arc<str>,
register_fn: LanguageModelProviderRegistration,
cx: &mut App,
);
fn unregister_language_model_provider(&self, provider_id: Arc<str>, cx: &mut App);
}
impl ExtensionLanguageModelProviderProxy for ExtensionHostProxy {
fn register_language_model_provider(
&self,
provider_id: Arc<str>,
register_fn: LanguageModelProviderRegistration,
cx: &mut App,
) {
let Some(proxy) = self.language_model_provider_proxy.read().clone() else {
return;
};
proxy.register_language_model_provider(provider_id, register_fn, cx)
}
fn unregister_language_model_provider(&self, provider_id: Arc<str>, cx: &mut App) {
let Some(proxy) = self.language_model_provider_proxy.read().clone() else {
return;
};
proxy.unregister_language_model_provider(provider_id, cx)
}
}

View File

@@ -93,6 +93,8 @@ pub struct ExtensionManifest {
pub debug_adapters: BTreeMap<Arc<str>, DebugAdapterManifestEntry>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub debug_locators: BTreeMap<Arc<str>, DebugLocatorManifestEntry>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub language_model_providers: BTreeMap<Arc<str>, LanguageModelProviderManifestEntry>,
}
impl ExtensionManifest {
@@ -288,6 +290,16 @@ pub struct DebugAdapterManifestEntry {
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct DebugLocatorManifestEntry {}
/// Manifest entry for a language model provider.
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct LanguageModelProviderManifestEntry {
/// Display name for the provider.
pub name: String,
/// Path to an SVG icon file relative to the extension root (e.g., "icons/provider.svg").
#[serde(default)]
pub icon: Option<String>,
}
impl ExtensionManifest {
pub async fn load(fs: Arc<dyn Fs>, extension_dir: &Path) -> Result<Self> {
let extension_name = extension_dir
@@ -358,6 +370,7 @@ fn manifest_from_old_manifest(
capabilities: Vec::new(),
debug_adapters: Default::default(),
debug_locators: Default::default(),
language_model_providers: Default::default(),
}
}
@@ -391,6 +404,7 @@ mod tests {
capabilities: vec![],
debug_adapters: Default::default(),
debug_locators: Default::default(),
language_model_providers: BTreeMap::default(),
}
}

View File

@@ -255,6 +255,21 @@ async fn copy_extension_resources(
}
}
for (_, provider_entry) in &manifest.language_model_providers {
if let Some(icon_path) = &provider_entry.icon {
let source_icon = extension_path.join(icon_path);
let dest_icon = output_dir.join(icon_path);
// Create parent directory if needed
if let Some(parent) = dest_icon.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(&source_icon, &dest_icon)
.with_context(|| format!("failed to copy LLM provider icon '{}'", icon_path))?;
}
}
if !manifest.languages.is_empty() {
let output_languages_dir = output_dir.join("languages");
fs::create_dir_all(&output_languages_dir)?;

View File

@@ -148,6 +148,7 @@ fn manifest() -> ExtensionManifest {
)],
debug_adapters: Default::default(),
debug_locators: Default::default(),
language_model_providers: BTreeMap::default(),
}
}

View File

@@ -113,6 +113,7 @@ mod tests {
capabilities: vec![],
debug_adapters: Default::default(),
debug_locators: Default::default(),
language_model_providers: BTreeMap::default(),
}
}

View File

@@ -165,6 +165,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
capabilities: Vec::new(),
debug_adapters: Default::default(),
debug_locators: Default::default(),
language_model_providers: BTreeMap::default(),
}),
dev: false,
},
@@ -196,6 +197,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
capabilities: Vec::new(),
debug_adapters: Default::default(),
debug_locators: Default::default(),
language_model_providers: BTreeMap::default(),
}),
dev: false,
},
@@ -376,6 +378,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
capabilities: Vec::new(),
debug_adapters: Default::default(),
debug_locators: Default::default(),
language_model_providers: BTreeMap::default(),
}),
dev: false,
},

View File

@@ -797,11 +797,26 @@ pub enum AuthenticateError {
Other(#[from] anyhow::Error),
}
/// Either a built-in icon name or a path to an external SVG.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IconOrSvg {
/// A built-in icon from Zed's icon set.
Icon(IconName),
/// Path to a custom SVG icon file.
Svg(SharedString),
}
impl Default for IconOrSvg {
fn default() -> Self {
Self::Icon(IconName::ZedAssistant)
}
}
pub trait LanguageModelProvider: 'static {
fn id(&self) -> LanguageModelProviderId;
fn name(&self) -> LanguageModelProviderName;
fn icon(&self) -> IconName {
IconName::ZedAssistant
fn icon(&self) -> IconOrSvg {
IconOrSvg::default()
}
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>>;
fn default_fast_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>>;
@@ -820,7 +835,7 @@ pub trait LanguageModelProvider: 'static {
fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>>;
}
#[derive(Default, Clone)]
#[derive(Default, Clone, PartialEq, Eq)]
pub enum ConfigurationViewTargetAgent {
#[default]
ZedAgent,

View File

@@ -2,12 +2,16 @@ use crate::{
LanguageModel, LanguageModelId, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderState,
};
use collections::BTreeMap;
use collections::{BTreeMap, HashSet};
use gpui::{App, Context, Entity, EventEmitter, Global, prelude::*};
use std::{str::FromStr, sync::Arc};
use thiserror::Error;
use util::maybe;
/// Function type for checking if a built-in provider should be hidden.
/// Returns Some(extension_id) if the provider should be hidden when that extension is installed.
pub type BuiltinProviderHidingFn = Box<dyn Fn(&str) -> Option<&'static str> + Send + Sync>;
pub fn init(cx: &mut App) {
let registry = cx.new(|_cx| LanguageModelRegistry::default());
cx.set_global(GlobalLanguageModelRegistry(registry));
@@ -48,6 +52,11 @@ pub struct LanguageModelRegistry {
thread_summary_model: Option<ConfiguredModel>,
providers: BTreeMap<LanguageModelProviderId, Arc<dyn LanguageModelProvider>>,
inline_alternatives: Vec<Arc<dyn LanguageModel>>,
/// Set of installed extension IDs that provide language models.
/// Used to determine which built-in providers should be hidden.
installed_llm_extension_ids: HashSet<Arc<str>>,
/// Function to check if a built-in provider should be hidden by an extension.
builtin_provider_hiding_fn: Option<BuiltinProviderHidingFn>,
}
#[derive(Debug)]
@@ -104,6 +113,8 @@ pub enum Event {
ProviderStateChanged(LanguageModelProviderId),
AddedProvider(LanguageModelProviderId),
RemovedProvider(LanguageModelProviderId),
/// Emitted when provider visibility changes due to extension install/uninstall.
ProvidersChanged,
}
impl EventEmitter<Event> for LanguageModelRegistry {}
@@ -183,6 +194,60 @@ impl LanguageModelRegistry {
providers
}
/// Returns providers, filtering out hidden built-in providers.
pub fn visible_providers(&self) -> Vec<Arc<dyn LanguageModelProvider>> {
self.providers()
.into_iter()
.filter(|p| !self.should_hide_provider(&p.id()))
.collect()
}
/// Sets the function used to check if a built-in provider should be hidden.
pub fn set_builtin_provider_hiding_fn(&mut self, hiding_fn: BuiltinProviderHidingFn) {
self.builtin_provider_hiding_fn = Some(hiding_fn);
}
/// Called when an extension is installed/loaded.
/// If the extension provides language models, track it so we can hide the corresponding built-in.
pub fn extension_installed(&mut self, extension_id: Arc<str>, cx: &mut Context<Self>) {
if self.installed_llm_extension_ids.insert(extension_id) {
cx.emit(Event::ProvidersChanged);
cx.notify();
}
}
/// Called when an extension is uninstalled/unloaded.
pub fn extension_uninstalled(&mut self, extension_id: &str, cx: &mut Context<Self>) {
if self.installed_llm_extension_ids.remove(extension_id) {
cx.emit(Event::ProvidersChanged);
cx.notify();
}
}
/// Sync the set of installed LLM extension IDs.
pub fn sync_installed_llm_extensions(
&mut self,
extension_ids: HashSet<Arc<str>>,
cx: &mut Context<Self>,
) {
if extension_ids != self.installed_llm_extension_ids {
self.installed_llm_extension_ids = extension_ids;
cx.emit(Event::ProvidersChanged);
cx.notify();
}
}
/// Returns true if a provider should be hidden from the UI.
/// Built-in providers are hidden when their corresponding extension is installed.
pub fn should_hide_provider(&self, provider_id: &LanguageModelProviderId) -> bool {
if let Some(ref hiding_fn) = self.builtin_provider_hiding_fn {
if let Some(extension_id) = hiding_fn(&provider_id.0) {
return self.installed_llm_extension_ids.contains(extension_id);
}
}
false
}
pub fn configuration_error(
&self,
model: Option<ConfiguredModel>,
@@ -416,4 +481,132 @@ mod tests {
let providers = registry.read(cx).providers();
assert!(providers.is_empty());
}
#[gpui::test]
fn test_provider_hiding_on_extension_install(cx: &mut App) {
let registry = cx.new(|_| LanguageModelRegistry::default());
let provider = Arc::new(FakeLanguageModelProvider::default());
let provider_id = provider.id();
registry.update(cx, |registry, cx| {
registry.register_provider(provider.clone(), cx);
registry.set_builtin_provider_hiding_fn(Box::new(|id| {
if id == "fake" {
Some("fake-extension")
} else {
None
}
}));
});
let visible = registry.read(cx).visible_providers();
assert_eq!(visible.len(), 1);
assert_eq!(visible[0].id(), provider_id);
registry.update(cx, |registry, cx| {
registry.extension_installed("fake-extension".into(), cx);
});
let visible = registry.read(cx).visible_providers();
assert!(visible.is_empty());
let all = registry.read(cx).providers();
assert_eq!(all.len(), 1);
}
#[gpui::test]
fn test_provider_unhiding_on_extension_uninstall(cx: &mut App) {
let registry = cx.new(|_| LanguageModelRegistry::default());
let provider = Arc::new(FakeLanguageModelProvider::default());
let provider_id = provider.id();
registry.update(cx, |registry, cx| {
registry.register_provider(provider.clone(), cx);
registry.set_builtin_provider_hiding_fn(Box::new(|id| {
if id == "fake" {
Some("fake-extension")
} else {
None
}
}));
registry.extension_installed("fake-extension".into(), cx);
});
let visible = registry.read(cx).visible_providers();
assert!(visible.is_empty());
registry.update(cx, |registry, cx| {
registry.extension_uninstalled("fake-extension", cx);
});
let visible = registry.read(cx).visible_providers();
assert_eq!(visible.len(), 1);
assert_eq!(visible[0].id(), provider_id);
}
#[gpui::test]
fn test_should_hide_provider(cx: &mut App) {
let registry = cx.new(|_| LanguageModelRegistry::default());
registry.update(cx, |registry, cx| {
registry.set_builtin_provider_hiding_fn(Box::new(|id| {
if id == "anthropic" {
Some("anthropic")
} else if id == "openai" {
Some("openai")
} else {
None
}
}));
registry.extension_installed("anthropic".into(), cx);
});
let registry_read = registry.read(cx);
assert!(registry_read.should_hide_provider(&LanguageModelProviderId("anthropic".into())));
assert!(!registry_read.should_hide_provider(&LanguageModelProviderId("openai".into())));
assert!(!registry_read.should_hide_provider(&LanguageModelProviderId("unknown".into())));
}
#[gpui::test]
fn test_sync_installed_llm_extensions(cx: &mut App) {
let registry = cx.new(|_| LanguageModelRegistry::default());
let provider = Arc::new(FakeLanguageModelProvider::default());
registry.update(cx, |registry, cx| {
registry.register_provider(provider.clone(), cx);
registry.set_builtin_provider_hiding_fn(Box::new(|id| {
if id == "fake" {
Some("fake-extension")
} else {
None
}
}));
});
let mut extension_ids = HashSet::default();
extension_ids.insert(Arc::from("fake-extension"));
registry.update(cx, |registry, cx| {
registry.sync_installed_llm_extensions(extension_ids, cx);
});
assert!(registry.read(cx).visible_providers().is_empty());
registry.update(cx, |registry, cx| {
registry.sync_installed_llm_extensions(HashSet::default(), cx);
});
assert_eq!(registry.read(cx).visible_providers().len(), 1);
}
}

View File

@@ -28,6 +28,8 @@ convert_case.workspace = true
copilot.workspace = true
credentials_provider.workspace = true
deepseek = { workspace = true, features = ["schemars"] }
extension.workspace = true
extension_host.workspace = true
fs.workspace = true
futures.workspace = true
google_ai = { workspace = true, features = ["schemars"] }

View File

@@ -0,0 +1,67 @@
use collections::HashMap;
use extension::{
ExtensionHostProxy, ExtensionLanguageModelProviderProxy, LanguageModelProviderRegistration,
};
use gpui::{App, Entity};
use language_model::{LanguageModelProviderId, LanguageModelRegistry};
use std::sync::{Arc, LazyLock};
/// Maps built-in provider IDs to their corresponding extension IDs.
/// When an extension with this ID is installed, the built-in provider should be hidden.
static BUILTIN_TO_EXTENSION_MAP: LazyLock<HashMap<&'static str, &'static str>> =
LazyLock::new(|| {
let mut map = HashMap::default();
map.insert("anthropic", "anthropic");
map.insert("openai", "openai");
map.insert("google", "google-ai");
map.insert("openrouter", "openrouter");
map.insert("copilot_chat", "copilot-chat");
map
});
/// Returns the extension ID that should hide the given built-in provider.
pub fn extension_for_builtin_provider(provider_id: &str) -> Option<&'static str> {
BUILTIN_TO_EXTENSION_MAP.get(provider_id).copied()
}
/// Proxy that registers extension language model providers with the LanguageModelRegistry.
pub struct LanguageModelProviderRegistryProxy {
registry: Entity<LanguageModelRegistry>,
}
impl LanguageModelProviderRegistryProxy {
pub fn new(registry: Entity<LanguageModelRegistry>) -> Self {
Self { registry }
}
}
impl ExtensionLanguageModelProviderProxy for LanguageModelProviderRegistryProxy {
fn register_language_model_provider(
&self,
_provider_id: Arc<str>,
register_fn: LanguageModelProviderRegistration,
cx: &mut App,
) {
register_fn(cx);
}
fn unregister_language_model_provider(&self, provider_id: Arc<str>, cx: &mut App) {
self.registry.update(cx, |registry, cx| {
registry.unregister_provider(LanguageModelProviderId::from(provider_id), cx);
});
}
}
/// Initialize the extension language model provider proxy.
/// This must be called BEFORE extension_host::init to ensure the proxy is available
/// when extensions try to register their language model providers.
pub fn init_proxy(cx: &mut App) {
let proxy = ExtensionHostProxy::default_global(cx);
let registry = LanguageModelRegistry::global(cx);
registry.update(cx, |registry, _cx| {
registry.set_builtin_provider_hiding_fn(Box::new(extension_for_builtin_provider));
});
proxy.register_language_model_provider_proxy(LanguageModelProviderRegistryProxy::new(registry));
}

View File

@@ -7,14 +7,16 @@ use gpui::{App, Context, Entity};
use language_model::{LanguageModelProviderId, LanguageModelRegistry};
use provider::deepseek::DeepSeekLanguageModelProvider;
pub mod extension;
pub mod provider;
mod settings;
pub use crate::extension::init_proxy as init_extension_proxy;
use crate::provider::anthropic::AnthropicLanguageModelProvider;
use crate::provider::bedrock::BedrockLanguageModelProvider;
use crate::provider::cloud::CloudLanguageModelProvider;
use crate::provider::copilot_chat::CopilotChatLanguageModelProvider;
use crate::provider::google::GoogleLanguageModelProvider;
pub use crate::provider::google::GoogleLanguageModelProvider;
use crate::provider::lmstudio::LmStudioLanguageModelProvider;
pub use crate::provider::mistral::MistralLanguageModelProvider;
use crate::provider::ollama::OllamaLanguageModelProvider;
@@ -31,6 +33,61 @@ pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) {
register_language_model_providers(registry, user_store, client.clone(), cx);
});
// Subscribe to extension store events to track LLM extension installations
if let Some(extension_store) = extension_host::ExtensionStore::try_global(cx) {
cx.subscribe(&extension_store, {
let registry = registry.clone();
move |extension_store, event, cx| {
match event {
extension_host::Event::ExtensionInstalled(extension_id) => {
// Check if this extension has language_model_providers
if let Some(manifest) = extension_store
.read(cx)
.extension_manifest_for_id(extension_id)
{
if !manifest.language_model_providers.is_empty() {
registry.update(cx, |registry, cx| {
registry.extension_installed(extension_id.clone(), cx);
});
}
}
}
extension_host::Event::ExtensionUninstalled(extension_id) => {
registry.update(cx, |registry, cx| {
registry.extension_uninstalled(extension_id, cx);
});
}
extension_host::Event::ExtensionsUpdated => {
// Re-sync installed extensions on bulk updates
let mut new_ids = HashSet::default();
for (extension_id, entry) in extension_store.read(cx).installed_extensions()
{
if !entry.manifest.language_model_providers.is_empty() {
new_ids.insert(extension_id.clone());
}
}
registry.update(cx, |registry, cx| {
registry.sync_installed_llm_extensions(new_ids, cx);
});
}
_ => {}
}
}
})
.detach();
// Initialize with currently installed extensions
registry.update(cx, |registry, cx| {
let mut initial_ids = HashSet::default();
for (extension_id, entry) in extension_store.read(cx).installed_extensions() {
if !entry.manifest.language_model_providers.is_empty() {
initial_ids.insert(extension_id.clone());
}
}
registry.sync_installed_llm_extensions(initial_ids, cx);
});
}
let mut openai_compatible_providers = AllLanguageModelSettings::get_global(cx)
.openai_compatible
.keys()

View File

@@ -8,7 +8,7 @@ use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture, stream::B
use gpui::{AnyView, App, AsyncApp, Context, Entity, Task};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, ConfigurationViewTargetAgent, EnvVar, LanguageModel,
ApiKeyState, AuthenticateError, ConfigurationViewTargetAgent, EnvVar, IconOrSvg, LanguageModel,
LanguageModelCacheConfiguration, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
@@ -125,8 +125,8 @@ impl LanguageModelProvider for AnthropicLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiAnthropic
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiAnthropic)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -30,7 +30,7 @@ use gpui::{
use gpui_tokio::Tokio;
use http_client::HttpClient;
use language_model::{
AuthenticateError, EnvVar, LanguageModel, LanguageModelCacheConfiguration,
AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCacheConfiguration,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
@@ -426,8 +426,8 @@ impl LanguageModelProvider for BedrockLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiBedrock
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiBedrock)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
@@ -996,7 +996,7 @@ pub fn get_bedrock_tokens(
}
}
// Tiktoken doesn't yet support these models, so we manually use the
// Tiktoken doesn't support these models, so we manually use the
// same tokenizer as GPT-4.
tiktoken_rs::num_tokens_from_messages("gpt-4", &string_messages)
.map(|tokens| (tokens + tokens_from_images) as u64)

View File

@@ -19,7 +19,7 @@ use gpui::{AnyElement, AnyView, App, AsyncApp, Context, Entity, Subscription, Ta
use http_client::http::{HeaderMap, HeaderValue};
use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Response, StatusCode};
use language_model::{
AuthenticateError, LanguageModel, LanguageModelCacheConfiguration,
AuthenticateError, IconOrSvg, LanguageModel, LanguageModelCacheConfiguration,
LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
@@ -304,8 +304,8 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiZed
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiZed)
}
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -18,12 +18,12 @@ use gpui::{AnyView, App, AsyncApp, Entity, Subscription, Task};
use http_client::StatusCode;
use language::language_settings::all_language_settings;
use language_model::{
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
LanguageModelRequestMessage, LanguageModelToolChoice, LanguageModelToolResultContent,
LanguageModelToolSchemaFormat, LanguageModelToolUse, MessageContent, RateLimiter, Role,
StopReason, TokenUsage,
AuthenticateError, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelRequestMessage, LanguageModelToolChoice,
LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse,
MessageContent, RateLimiter, Role, StopReason, TokenUsage,
};
use settings::SettingsStore;
use ui::prelude::*;
@@ -104,8 +104,8 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::Copilot
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::Copilot)
}
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -7,7 +7,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture, stream::BoxStream
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
@@ -127,8 +127,8 @@ impl LanguageModelProvider for DeepSeekLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiDeepSeek
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiDeepSeek)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -14,7 +14,7 @@ use language_model::{
LanguageModelToolUse, LanguageModelToolUseId, MessageContent, StopReason,
};
use language_model::{
LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
IconOrSvg, LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, RateLimiter, Role,
};
@@ -164,8 +164,8 @@ impl LanguageModelProvider for GoogleLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiGoogle
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiGoogle)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {
@@ -707,7 +707,7 @@ pub fn count_google_tokens(
})
.collect::<Vec<_>>();
// Tiktoken doesn't yet support these models, so we manually use the
// Tiktoken doesn't support these models, so we manually use the
// same tokenizer as GPT-4.
tiktoken_rs::num_tokens_from_messages("gpt-4", &messages).map(|tokens| tokens as u64)
})

View File

@@ -10,7 +10,7 @@ use language_model::{
StopReason, TokenUsage,
};
use language_model::{
LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
IconOrSvg, LanguageModel, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, RateLimiter, Role,
};
@@ -175,8 +175,8 @@ impl LanguageModelProvider for LmStudioLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiLmStudio
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiLmStudio)
}
fn default_model(&self, _: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -5,7 +5,7 @@ use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture, stream::B
use gpui::{AnyView, App, AsyncApp, Context, Entity, Global, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
@@ -176,8 +176,8 @@ impl LanguageModelProvider for MistralLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiMistral
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiMistral)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -5,7 +5,7 @@ use futures::{Stream, TryFutureExt, stream};
use gpui::{AnyView, App, AsyncApp, Context, CursorStyle, Entity, Task};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelRequestTool, LanguageModelToolChoice, LanguageModelToolUse,
@@ -221,8 +221,8 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiOllama
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiOllama)
}
fn default_model(&self, _: &App) -> Option<Arc<dyn LanguageModel>> {
@@ -249,33 +249,7 @@ impl LanguageModelProvider for OllamaLanguageModelProvider {
}
// Override with available models from settings
for setting_model in &OllamaLanguageModelProvider::settings(cx).available_models {
let setting_base = setting_model.name.split(':').next().unwrap();
if let Some(model) = models
.values_mut()
.find(|m| m.name.split(':').next().unwrap() == setting_base)
{
model.max_tokens = setting_model.max_tokens;
model.display_name = setting_model.display_name.clone();
model.keep_alive = setting_model.keep_alive.clone();
model.supports_tools = setting_model.supports_tools;
model.supports_vision = setting_model.supports_images;
model.supports_thinking = setting_model.supports_thinking;
} else {
models.insert(
setting_model.name.clone(),
ollama::Model {
name: setting_model.name.clone(),
display_name: setting_model.display_name.clone(),
max_tokens: setting_model.max_tokens,
keep_alive: setting_model.keep_alive.clone(),
supports_tools: setting_model.supports_tools,
supports_vision: setting_model.supports_images,
supports_thinking: setting_model.supports_thinking,
},
);
}
}
merge_settings_into_models(&mut models, &settings.available_models);
let mut models = models
.into_values()
@@ -921,6 +895,35 @@ impl Render for ConfigurationView {
}
}
fn merge_settings_into_models(
models: &mut HashMap<String, ollama::Model>,
available_models: &[AvailableModel],
) {
for setting_model in available_models {
if let Some(model) = models.get_mut(&setting_model.name) {
model.max_tokens = setting_model.max_tokens;
model.display_name = setting_model.display_name.clone();
model.keep_alive = setting_model.keep_alive.clone();
model.supports_tools = setting_model.supports_tools;
model.supports_vision = setting_model.supports_images;
model.supports_thinking = setting_model.supports_thinking;
} else {
models.insert(
setting_model.name.clone(),
ollama::Model {
name: setting_model.name.clone(),
display_name: setting_model.display_name.clone(),
max_tokens: setting_model.max_tokens,
keep_alive: setting_model.keep_alive.clone(),
supports_tools: setting_model.supports_tools,
supports_vision: setting_model.supports_images,
supports_thinking: setting_model.supports_thinking,
},
);
}
}
}
fn tool_into_ollama(tool: LanguageModelRequestTool) -> ollama::OllamaTool {
ollama::OllamaTool::Function {
function: OllamaFunctionTool {
@@ -930,3 +933,83 @@ fn tool_into_ollama(tool: LanguageModelRequestTool) -> ollama::OllamaTool {
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_merge_settings_preserves_display_names_for_similar_models() {
// Regression test for https://github.com/zed-industries/zed/issues/43646
// When multiple models share the same base name (e.g., qwen2.5-coder:1.5b and qwen2.5-coder:3b),
// each model should get its own display_name from settings, not a random one.
let mut models: HashMap<String, ollama::Model> = HashMap::new();
models.insert(
"qwen2.5-coder:1.5b".to_string(),
ollama::Model {
name: "qwen2.5-coder:1.5b".to_string(),
display_name: None,
max_tokens: 4096,
keep_alive: None,
supports_tools: None,
supports_vision: None,
supports_thinking: None,
},
);
models.insert(
"qwen2.5-coder:3b".to_string(),
ollama::Model {
name: "qwen2.5-coder:3b".to_string(),
display_name: None,
max_tokens: 4096,
keep_alive: None,
supports_tools: None,
supports_vision: None,
supports_thinking: None,
},
);
let available_models = vec![
AvailableModel {
name: "qwen2.5-coder:1.5b".to_string(),
display_name: Some("QWEN2.5 Coder 1.5B".to_string()),
max_tokens: 5000,
keep_alive: None,
supports_tools: Some(true),
supports_images: None,
supports_thinking: None,
},
AvailableModel {
name: "qwen2.5-coder:3b".to_string(),
display_name: Some("QWEN2.5 Coder 3B".to_string()),
max_tokens: 6000,
keep_alive: None,
supports_tools: Some(true),
supports_images: None,
supports_thinking: None,
},
];
merge_settings_into_models(&mut models, &available_models);
let model_1_5b = models
.get("qwen2.5-coder:1.5b")
.expect("1.5b model missing");
let model_3b = models.get("qwen2.5-coder:3b").expect("3b model missing");
assert_eq!(
model_1_5b.display_name,
Some("QWEN2.5 Coder 1.5B".to_string()),
"1.5b model should have its own display_name"
);
assert_eq!(model_1_5b.max_tokens, 5000);
assert_eq!(
model_3b.display_name,
Some("QWEN2.5 Coder 3B".to_string()),
"3b model should have its own display_name"
);
assert_eq!(model_3b.max_tokens, 6000);
}
}

View File

@@ -5,7 +5,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
@@ -122,8 +122,8 @@ impl LanguageModelProvider for OpenAiLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiOpenAi
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiOpenAi)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -4,7 +4,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
@@ -133,8 +133,8 @@ impl LanguageModelProvider for OpenAiCompatibleLanguageModelProvider {
self.name.clone()
}
fn icon(&self) -> IconName {
IconName::AiOpenAiCompat
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiOpenAiCompat)
}
fn default_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -4,7 +4,7 @@ use futures::{FutureExt, Stream, StreamExt, future, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolResultContent,
@@ -180,8 +180,8 @@ impl LanguageModelProvider for OpenRouterLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiOpenRouter
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiOpenRouter)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -4,7 +4,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, SharedString, Task, Window};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, RateLimiter, Role, env_var,
@@ -117,8 +117,8 @@ impl LanguageModelProvider for VercelLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiVZero
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiVZero)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -4,7 +4,7 @@ use futures::{FutureExt, StreamExt, future, future::BoxFuture};
use gpui::{AnyView, App, AsyncApp, Context, Entity, Task, Window};
use http_client::HttpClient;
use language_model::{
ApiKeyState, AuthenticateError, EnvVar, LanguageModel, LanguageModelCompletionError,
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
@@ -118,8 +118,8 @@ impl LanguageModelProvider for XAiLanguageModelProvider {
PROVIDER_NAME
}
fn icon(&self) -> IconName {
IconName::AiXAi
fn icon(&self) -> IconOrSvg {
IconOrSvg::Icon(IconName::AiXAi)
}
fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> {

View File

@@ -127,6 +127,16 @@ impl LanguageServerState {
return menu;
};
let server_versions = self
.lsp_store
.update(cx, |lsp_store, _| {
lsp_store
.language_server_statuses()
.map(|(server_id, status)| (server_id, status.server_version.clone()))
.collect::<HashMap<_, _>>()
})
.unwrap_or_default();
let mut first_button_encountered = false;
for item in &self.items {
if let LspMenuItem::ToggleServersButton { restart } = item {
@@ -254,6 +264,22 @@ impl LanguageServerState {
};
let server_name = server_info.name.clone();
let server_version = server_versions
.get(&server_info.id)
.and_then(|version| version.clone());
let tooltip_text = match (&server_version, &message) {
(None, None) => None,
(Some(version), None) => {
Some(SharedString::from(format!("Version: {}", version.as_ref())))
}
(None, Some(message)) => Some(message.clone()),
(Some(version), Some(message)) => Some(SharedString::from(format!(
"Version: {}\n\n{}",
version.as_ref(),
message.as_ref()
))),
};
menu = menu.item(ContextMenuItem::custom_entry(
move |_, _| {
h_flex()
@@ -355,11 +381,11 @@ impl LanguageServerState {
}
}
},
message.map(|server_message| {
tooltip_text.map(|tooltip_text| {
DocumentationAside::new(
DocumentationSide::Right,
DocumentationEdge::Bottom,
Rc::new(move |_| Label::new(server_message.clone()).into_any_element()),
DocumentationEdge::Top,
Rc::new(move |_| Label::new(tooltip_text.clone()).into_any_element()),
)
}),
));

View File

@@ -330,6 +330,8 @@ impl LspLogView {
let server_info = format!(
"* Server: {NAME} (id {ID})
* Version: {VERSION}
* Binary: {BINARY}
* Registered workspace folders:
@@ -340,6 +342,12 @@ impl LspLogView {
* Configuration: {CONFIGURATION}",
NAME = info.status.name,
ID = info.id,
VERSION = info
.status
.server_version
.as_ref()
.map(|version| version.as_ref())
.unwrap_or("Unknown"),
BINARY = info
.status
.binary
@@ -1334,6 +1342,7 @@ impl ServerInfo {
capabilities: server.capabilities(),
status: LanguageServerStatus {
name: server.name(),
server_version: server.version(),
pending_work: Default::default(),
has_pending_diagnostic_updates: false,
progress_tokens: Default::default(),

View File

@@ -89,6 +89,7 @@ pub struct LanguageServer {
outbound_tx: channel::Sender<String>,
notification_tx: channel::Sender<NotificationSerializer>,
name: LanguageServerName,
version: Option<SharedString>,
process_name: Arc<str>,
binary: LanguageServerBinary,
capabilities: RwLock<ServerCapabilities>,
@@ -501,6 +502,7 @@ impl LanguageServer {
response_handlers,
io_handlers,
name: server_name,
version: None,
process_name: binary
.path
.file_name()
@@ -882,7 +884,9 @@ impl LanguageServer {
window: Some(WindowClientCapabilities {
work_done_progress: Some(true),
show_message: Some(ShowMessageRequestClientCapabilities {
message_action_item: None,
message_action_item: Some(MessageActionItemCapabilities {
additional_properties_support: Some(true),
}),
}),
..WindowClientCapabilities::default()
}),
@@ -923,6 +927,7 @@ impl LanguageServer {
)
})?;
if let Some(info) = response.server_info {
self.version = info.version.map(SharedString::from);
self.process_name = info.name.into();
}
self.capabilities = RwLock::new(response.capabilities);
@@ -1153,6 +1158,11 @@ impl LanguageServer {
self.name.clone()
}
/// Get the version of the running language server.
pub fn version(&self) -> Option<SharedString> {
self.version.clone()
}
pub fn process_name(&self) -> &str {
&self.process_name
}

View File

@@ -128,6 +128,7 @@ use util::{
ConnectionResult, ResultExt as _, debug_panic, defer, maybe, merge_json_value_into,
paths::{PathStyle, SanitizedPath},
post_inc,
redact::redact_command,
rel_path::RelPath,
};
@@ -577,9 +578,12 @@ impl LocalLspStore {
},
},
);
log::error!("Failed to start language server {server_name:?}: {err:?}");
log::error!(
"Failed to start language server {server_name:?}: {}",
redact_command(&format!("{err:?}"))
);
if !log.is_empty() {
log::error!("server stderr: {log}");
log::error!("server stderr: {}", redact_command(&log));
}
None
}
@@ -3860,6 +3864,7 @@ pub enum LspStoreEvent {
#[derive(Clone, Debug, Serialize)]
pub struct LanguageServerStatus {
pub name: LanguageServerName,
pub server_version: Option<SharedString>,
pub pending_work: BTreeMap<ProgressToken, LanguageServerProgress>,
pub has_pending_diagnostic_updates: bool,
pub progress_tokens: HashSet<ProgressToken>,
@@ -8350,6 +8355,7 @@ impl LspStore {
server_id,
LanguageServerStatus {
name,
server_version: None,
pending_work: Default::default(),
has_pending_diagnostic_updates: false,
progress_tokens: Default::default(),
@@ -9385,6 +9391,7 @@ impl LspStore {
server_id,
LanguageServerStatus {
name: server_name.clone(),
server_version: None,
pending_work: Default::default(),
has_pending_diagnostic_updates: false,
progress_tokens: Default::default(),
@@ -11415,6 +11422,7 @@ impl LspStore {
server_id,
LanguageServerStatus {
name: language_server.name(),
server_version: language_server.version(),
pending_work: Default::default(),
has_pending_diagnostic_updates: false,
progress_tokens: Default::default(),

View File

@@ -893,39 +893,57 @@ impl ContextMenu {
entry_render,
handler,
selectable,
documentation_aside,
..
} => {
let handler = handler.clone();
let menu = cx.entity().downgrade();
let selectable = *selectable;
ListItem::new(ix)
.inset(true)
.toggle_state(if selectable {
Some(ix) == self.selected_index
} else {
false
})
.selectable(selectable)
.when(selectable, |item| {
item.on_click({
let context = self.action_context.clone();
let keep_open_on_confirm = self.keep_open_on_confirm;
move |_, window, cx| {
handler(context.as_ref(), window, cx);
menu.update(cx, |menu, cx| {
menu.clicked = true;
if keep_open_on_confirm {
menu.rebuild(window, cx);
} else {
cx.emit(DismissEvent);
div()
.id(("context-menu-child", ix))
.when_some(documentation_aside.clone(), |this, documentation_aside| {
this.occlude()
.on_hover(cx.listener(move |menu, hovered, _, cx| {
if *hovered {
menu.documentation_aside = Some((ix, documentation_aside.clone()));
} else if matches!(menu.documentation_aside, Some((id, _)) if id == ix)
{
menu.documentation_aside = None;
}
cx.notify();
}))
})
.child(
ListItem::new(ix)
.inset(true)
.toggle_state(if selectable {
Some(ix) == self.selected_index
} else {
false
})
.selectable(selectable)
.when(selectable, |item| {
item.on_click({
let context = self.action_context.clone();
let keep_open_on_confirm = self.keep_open_on_confirm;
move |_, window, cx| {
handler(context.as_ref(), window, cx);
menu.update(cx, |menu, cx| {
menu.clicked = true;
if keep_open_on_confirm {
menu.rebuild(window, cx);
} else {
cx.emit(DismissEvent);
}
})
.ok();
}
})
.ok();
}
})
})
.child(entry_render(window, cx))
})
.child(entry_render(window, cx)),
)
.into_any_element()
}
}

View File

@@ -126,17 +126,6 @@ enum IconSource {
ExternalSvg(SharedString),
}
impl IconSource {
fn from_path(path: impl Into<SharedString>) -> Self {
let path = path.into();
if path.starts_with("icons/") {
Self::Embedded(path)
} else {
Self::External(Arc::from(PathBuf::from(path.as_ref())))
}
}
}
#[derive(IntoElement, RegisterComponent)]
pub struct Icon {
source: IconSource,
@@ -155,9 +144,18 @@ impl Icon {
}
}
/// Create an icon from a path. Uses a heuristic to determine if it's embedded or external:
/// - Paths starting with "icons/" are treated as embedded SVGs
/// - Other paths are treated as external raster images (from icon themes)
pub fn from_path(path: impl Into<SharedString>) -> Self {
let path = path.into();
let source = if path.starts_with("icons/") {
IconSource::Embedded(path)
} else {
IconSource::External(Arc::from(PathBuf::from(path.as_ref())))
};
Self {
source: IconSource::from_path(path),
source,
color: Color::default(),
size: IconSize::default().rems(),
transformation: Transformation::default(),

View File

@@ -1,3 +1,9 @@
use std::sync::LazyLock;
static REDACT_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| {
regex::Regex::new(r#"([A-Z_][A-Z0-9_]*)=("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\S+)"#).unwrap()
});
/// Whether a given environment variable name should have its value redacted
pub fn should_redact(env_var_name: &str) -> bool {
const REDACTED_SUFFIXES: &[&str] = &[
@@ -13,3 +19,31 @@ pub fn should_redact(env_var_name: &str) -> bool {
.iter()
.any(|suffix| env_var_name.ends_with(suffix))
}
/// Redact a string which could include a command with environment variables
pub fn redact_command(command: &str) -> String {
REDACT_REGEX
.replace_all(command, |caps: &regex::Captures| {
let var_name = &caps[1];
let value = &caps[2];
if should_redact(var_name) {
format!(r#"{}="[REDACTED]""#, var_name)
} else {
format!("{}={}", var_name, value)
}
})
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_redact_string_with_multiple_env_vars() {
let input = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="sk-ant-api03-WOOOO" COMMAND_MODE="unix2003" GEMINI_API_KEY="AIGEMINIFACE" HOME="/Users/foo""#;
let result = redact_command(input);
let expected = r#"failed to spawn command cd "/code/something" && ANTHROPIC_API_KEY="[REDACTED]" COMMAND_MODE="unix2003" GEMINI_API_KEY="[REDACTED]" HOME="/Users/foo""#;
assert_eq!(result, expected);
}
}

View File

@@ -564,6 +564,11 @@ pub fn main() {
dap_adapters::init(cx);
auto_update_ui::init(cx);
reliability::init(client.clone(), cx);
// Initialize the language model registry first, then set up the extension proxy
// BEFORE extension_host::init so that extensions can register their LLM providers
// when they load.
language_model::init(app_state.client.clone(), cx);
language_models::init_extension_proxy(cx);
extension_host::init(
extension_host_proxy.clone(),
app_state.fs.clone(),
@@ -589,7 +594,6 @@ pub fn main() {
cx,
);
supermaven::init(app_state.client.clone(), cx);
language_model::init(app_state.client.clone(), cx);
language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);
acp_tools::init(cx);
edit_prediction_ui::init(cx);

View File

@@ -177,7 +177,6 @@
- [Linux](./development/linux.md)
- [Windows](./development/windows.md)
- [FreeBSD](./development/freebsd.md)
- [Local Collaboration](./development/local-collaboration.md)
- [Using Debuggers](./development/debuggers.md)
- [Performance](./performance.md)
- [Glossary](./development/glossary.md)

View File

@@ -6,10 +6,6 @@ See the platform-specific instructions for building Zed from source:
- [Linux](./development/linux.md)
- [Windows](./development/windows.md)
If you'd like to develop collaboration features, additionally see:
- [Local Collaboration](./development/local-collaboration.md)
## Keychain access
Zed stores secrets in the system keychain.

View File

@@ -16,10 +16,6 @@ Clone down the [Zed repository](https://github.com/zed-industries/zed).
If you prefer to install the system libraries manually, you can find the list of required packages in the `script/linux` file.
### Backend Dependencies (optional) {#backend-dependencies}
If you are looking to develop Zed collaboration features using a local collaboration server, please see: [Local Collaboration](./local-collaboration.md) docs.
### Linkers {#linker}
On Linux, Rust's default linker is [LLVM's `lld`](https://blog.rust-lang.org/2025/09/18/Rust-1.90.0/). Alternative linkers, especially [Wild](https://github.com/davidlattimore/wild) and [Mold](https://github.com/rui314/mold) can significantly improve clean and incremental build time.

View File

@@ -1,207 +0,0 @@
# Local Collaboration
1. Ensure you have access to our cloud infrastructure. If you don't have access, you can't collaborate locally at this time.
2. Make sure you've installed Zed's dependencies for your platform:
- [macOS](#macos)
- [Linux](#linux)
- [Windows](#backend-windows)
Note that `collab` can be compiled only with MSVC toolchain on Windows
3. Clone down our cloud repository and follow the instructions in the cloud README
4. Setup the local database for your platform:
- [macOS & Linux](#database-unix)
- [Windows](#database-windows)
5. Run collab:
- [macOS & Linux](#run-collab-unix)
- [Windows](#run-collab-windows)
## Backend Dependencies
If you are developing collaborative features of Zed, you'll need to install the dependencies of zed's `collab` server:
- PostgreSQL
- LiveKit
- Foreman
You can install these dependencies natively or run them under Docker.
### macOS
1. Install [Postgres.app](https://postgresapp.com) or [postgresql via homebrew](https://formulae.brew.sh/formula/postgresql@15):
```sh
brew install postgresql@15
```
2. Install [Livekit](https://formulae.brew.sh/formula/livekit) and [Foreman](https://formulae.brew.sh/formula/foreman)
```sh
brew install livekit foreman
```
- Follow the steps in the [collab README](https://github.com/zed-industries/zed/blob/main/crates/collab/README.md) to configure the Postgres database for integration tests
Alternatively, if you have [Docker](https://www.docker.com/) installed you can bring up all the `collab` dependencies using Docker Compose.
### Linux
1. Install [Postgres](https://www.postgresql.org/download/linux/)
```sh
sudo apt-get install postgresql # Ubuntu/Debian
sudo pacman -S postgresql # Arch Linux
sudo dnf install postgresql postgresql-server # RHEL/Fedora
sudo zypper install postgresql postgresql-server # OpenSUSE
```
2. Install [Livekit](https://github.com/livekit/livekit-cli)
```sh
curl -sSL https://get.livekit.io/cli | bash
```
3. Install [Foreman](https://theforeman.org/manuals/3.15/quickstart_guide.html)
### Windows {#backend-windows}
> This section is still in development. The instructions are not yet complete.
- Install [Postgres](https://www.postgresql.org/download/windows/)
- Install [Livekit](https://github.com/livekit/livekit), optionally you can add the `livekit-server` binary to your `PATH`.
Alternatively, if you have [Docker](https://www.docker.com/) installed you can bring up all the `collab` dependencies using Docker Compose.
### Docker {#Docker}
If you have docker or podman available, you can run the backend dependencies inside containers with Docker Compose:
```sh
docker compose up -d
```
## Database setup
Before you can run the `collab` server locally, you'll need to set up a `zed` Postgres database.
### On macOS and Linux {#database-unix}
```sh
script/bootstrap
```
This script will set up the `zed` Postgres database, and populate it with some users. It requires internet access, because it fetches some users from the GitHub API.
The script will seed the database with various content defined by:
```sh
cat crates/collab/seed.default.json
```
To use a different set of admin users, you can create your own version of that json file and export the `SEED_PATH` environment variable. Note that the usernames listed in the admins list currently must correspond to valid GitHub users.
```json [settings]
{
"admins": ["admin1", "admin2"],
"channels": ["zed"]
}
```
### On Windows {#database-windows}
```powershell
.\script\bootstrap.ps1
```
## Testing collaborative features locally
### On macOS and Linux {#run-collab-unix}
Ensure that Postgres is configured and running, then run Zed's collaboration server and the `livekit` dev server:
```sh
foreman start
# OR
docker compose up
```
Alternatively, if you're not testing voice and screenshare, you can just run `collab` and `cloud`, and not the `livekit` dev server:
```sh
cargo run -p collab -- serve all
```
```sh
cd ../cloud; cargo make dev
```
In a new terminal, run two or more instances of Zed.
```sh
script/zed-local -3
```
This script starts one to four instances of Zed, depending on the `-2`, `-3` or `-4` flags. Each instance will be connected to the local `collab` server, signed in as a different user from `.admins.json` or `.admins.default.json`.
### On Windows {#run-collab-windows}
Since `foreman` is not available on Windows, you can run the following commands in separate terminals:
```powershell
cargo run --package=collab -- serve all
```
If you have added the `livekit-server` binary to your `PATH`, you can run:
```powershell
livekit-server --dev
```
Otherwise,
```powershell
.\path\to\livekit-serve.exe --dev
```
You'll also need to start the cloud server:
```powershell
cd ..\cloud; cargo make dev
```
In a new terminal, run two or more instances of Zed.
```powershell
node .\script\zed-local -2
```
Note that this requires `node.exe` to be in your `PATH`.
## Running a local collab server
> [!NOTE]
> Because of recent changes to our authentication system, Zed will not be able to authenticate itself with, and therefore use, a local collab server.
If you want to run your own version of the zed collaboration service, you can, but note that this is still under development, and there is no support for authentication nor extensions.
Configuration is done through environment variables. By default it will read the configuration from [`.env.toml`](https://github.com/zed-industries/zed/blob/main/crates/collab/.env.toml) and you should use that as a guide for setting this up.
By default Zed assumes that the DATABASE_URL is a Postgres database, but you can make it use Sqlite by compiling with `--features sqlite` and using a sqlite DATABASE_URL with `?mode=rwc`.
To authenticate you must first configure the server by creating a seed.json file that contains at a minimum your github handle. This will be used to create the user on demand.
```json [settings]
{
"admins": ["nathansobo"]
}
```
By default the collab server will seed the database when first creating it, but if you want to add more users you can explicitly reseed them with `SEED_PATH=./seed.json cargo run -p collab seed`
Then when running the zed client you must specify two environment variables, `ZED_ADMIN_API_TOKEN` (which should match the value of `API_TOKEN` in .env.toml) and `ZED_IMPERSONATE` (which should match one of the users in your seed.json)

View File

@@ -31,10 +31,6 @@ Clone down the [Zed repository](https://github.com/zed-industries/zed).
brew install cmake
```
### Backend Dependencies (optional) {#backend-dependencies}
If you are looking to develop Zed collaboration features using a local collaboration server, please see: [Local Collaboration](./local-collaboration.md) docs.
## Building Zed from Source
Once you have the dependencies installed, you can build Zed using [Cargo](https://doc.rust-lang.org/cargo/).

View File

@@ -66,10 +66,6 @@ The list can be obtained as follows:
- Click on `More` in the `Installed` tab
- Click on `Export configuration`
### Backend Dependencies (optional) {#backend-dependencies}
If you are looking to develop Zed collaboration features using a local collaboration server, please see: [Local Collaboration](./local-collaboration.md) docs.
### Notes
You should modify the `pg_hba.conf` file in the `data` directory to use `trust` instead of `scram-sha-256` for the `host` method. Otherwise, the connection will fail with the error `password authentication failed`. The `pg_hba.conf` file typically locates at `C:\Program Files\PostgreSQL\17\data\pg_hba.conf`. After the modification, the file should look like this:

View File

@@ -6,6 +6,9 @@ prHygiene({
rules: {
// Don't enable this rule just yet, as it can have false positives.
useImperativeMood: "off",
noConventionalCommits: {
bannedTypes: ["feat", "fix", "style", "refactor", "perf", "test", "chore", "build", "revert"],
},
},
});

View File

@@ -8,6 +8,6 @@
},
"devDependencies": {
"danger": "13.0.4",
"danger-plugin-pr-hygiene": "0.6.1"
"danger-plugin-pr-hygiene": "0.7.1"
}
}

View File

@@ -12,8 +12,8 @@ importers:
specifier: 13.0.4
version: 13.0.4
danger-plugin-pr-hygiene:
specifier: 0.6.1
version: 0.6.1
specifier: 0.7.1
version: 0.7.1
packages:
@@ -134,8 +134,8 @@ packages:
core-js@3.45.1:
resolution: {integrity: sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==}
danger-plugin-pr-hygiene@0.6.1:
resolution: {integrity: sha512-nb+iUQvirE3BlKXI1WoOND6sujyGzHar590mJm5tt4RLi65HXFaU5hqONxgDoWFujJNHYnXse9yaZdxnxEi4QA==}
danger-plugin-pr-hygiene@0.7.1:
resolution: {integrity: sha512-ll070nNaL3OeO2nooYWflPE/CRKLeq8GiH2C68u5zM3gW4gepH89GhVv0sYNNGLx4cYwa1zZ/TuiYYhC49z06Q==}
danger@13.0.4:
resolution: {integrity: sha512-IAdQ5nSJyIs4zKj6AN35ixt2B0Ce3WZUm3IFe/CMnL/Op7wV7IGg4D348U0EKNaNPP58QgXbdSk9pM+IXP1QXg==}
@@ -573,7 +573,7 @@ snapshots:
core-js@3.45.1: {}
danger-plugin-pr-hygiene@0.6.1: {}
danger-plugin-pr-hygiene@0.7.1: {}
danger@13.0.4:
dependencies:

View File

@@ -45,15 +45,11 @@ pub(crate) fn run_tests() -> Workflow {
&should_run_tests,
]);
let check_style = check_style();
let run_tests_linux = run_platform_tests(Platform::Linux);
let call_autofix = call_autofix(&check_style, &run_tests_linux);
let mut jobs = vec![
orchestrate,
check_style,
check_style(),
should_run_tests.guard(run_platform_tests(Platform::Windows)),
should_run_tests.guard(run_tests_linux),
should_run_tests.guard(run_platform_tests(Platform::Linux)),
should_run_tests.guard(run_platform_tests(Platform::Mac)),
should_run_tests.guard(doctests()),
should_run_tests.guard(check_workspace_binaries()),
@@ -110,7 +106,6 @@ pub(crate) fn run_tests() -> Workflow {
workflow
})
.add_job(tests_pass.name, tests_pass.job)
.add_job(call_autofix.name, call_autofix.job)
}
// Generates a bash script that checks changed files against regex patterns
@@ -226,8 +221,6 @@ pub fn tests_pass(jobs: &[NamedJob]) -> NamedJob {
named::job(job)
}
pub const STYLE_FAILED_OUTPUT: &str = "style_failed";
fn check_style() -> NamedJob {
fn check_for_typos() -> Step<Use> {
named::uses(
@@ -245,56 +238,12 @@ fn check_style() -> NamedJob {
.add_step(steps::setup_pnpm())
.add_step(steps::prettier())
.add_step(steps::cargo_fmt())
.add_step(steps::record_style_failure())
.add_step(steps::script("./script/check-todos"))
.add_step(steps::script("./script/check-keymaps"))
.add_step(check_for_typos())
.outputs([(
STYLE_FAILED_OUTPUT.to_owned(),
format!(
"${{{{ steps.{}.outputs.failed == 'true' }}}}",
steps::RECORD_STYLE_FAILURE_STEP_ID
),
)]),
.add_step(check_for_typos()),
)
}
fn call_autofix(check_style: &NamedJob, run_tests_linux: &NamedJob) -> NamedJob {
fn dispatch_autofix(run_tests_linux_name: &str) -> Step<Run> {
let clippy_failed_expr = format!(
"needs.{}.outputs.{} == 'true'",
run_tests_linux_name, CLIPPY_FAILED_OUTPUT
);
named::bash(format!(
"gh workflow run autofix_pr.yml -f pr_number=${{{{ github.event.pull_request.number }}}} -f run_clippy=${{{{ {} }}}}",
clippy_failed_expr
))
.add_env(("GITHUB_TOKEN", "${{ steps.get-app-token.outputs.token }}"))
}
let style_failed_expr = format!(
"needs.{}.outputs.{} == 'true'",
check_style.name, STYLE_FAILED_OUTPUT
);
let clippy_failed_expr = format!(
"needs.{}.outputs.{} == 'true'",
run_tests_linux.name, CLIPPY_FAILED_OUTPUT
);
let (authenticate, _token) = steps::authenticate_as_zippy();
let job = Job::default()
.runs_on(runners::LINUX_SMALL)
.cond(Expression::new(format!(
"always() && ({} || {}) && github.event_name == 'pull_request' && github.actor != 'zed-zippy[bot]'",
style_failed_expr, clippy_failed_expr
)))
.needs(vec![check_style.name.clone(), run_tests_linux.name.clone()])
.add_step(authenticate)
.add_step(dispatch_autofix(&run_tests_linux.name));
named::job(job)
}
fn check_dependencies() -> NamedJob {
fn install_cargo_machete() -> Step<Use> {
named::uses(
@@ -355,8 +304,6 @@ fn check_workspace_binaries() -> NamedJob {
)
}
pub const CLIPPY_FAILED_OUTPUT: &str = "clippy_failed";
pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob {
let runner = match platform {
Platform::Windows => runners::WINDOWS_DEFAULT,
@@ -378,24 +325,12 @@ pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob {
)
.add_step(steps::setup_node())
.add_step(steps::clippy(platform))
.when(platform == Platform::Linux, |job| {
job.add_step(steps::record_clippy_failure())
})
.when(platform == Platform::Linux, |job| {
job.add_step(steps::cargo_install_nextest())
})
.add_step(steps::clear_target_dir_if_large(platform))
.add_step(steps::cargo_nextest(platform))
.add_step(steps::cleanup_cargo_config(platform))
.when(platform == Platform::Linux, |job| {
job.outputs([(
CLIPPY_FAILED_OUTPUT.to_owned(),
format!(
"${{{{ steps.{}.outputs.failed == 'true' }}}}",
steps::RECORD_CLIPPY_FAILURE_STEP_ID
),
)])
}),
.add_step(steps::cleanup_cargo_config(platform)),
}
}

View File

@@ -54,25 +54,12 @@ pub fn setup_sentry() -> Step<Use> {
.add_with(("token", vars::SENTRY_AUTH_TOKEN))
}
pub const PRETTIER_STEP_ID: &str = "prettier";
pub const CARGO_FMT_STEP_ID: &str = "cargo_fmt";
pub const RECORD_STYLE_FAILURE_STEP_ID: &str = "record_style_failure";
pub fn prettier() -> Step<Run> {
named::bash("./script/prettier").id(PRETTIER_STEP_ID)
named::bash("./script/prettier")
}
pub fn cargo_fmt() -> Step<Run> {
named::bash("cargo fmt --all -- --check").id(CARGO_FMT_STEP_ID)
}
pub fn record_style_failure() -> Step<Run> {
named::bash(format!(
"echo \"failed=${{{{ steps.{}.outcome == 'failure' || steps.{}.outcome == 'failure' }}}}\" >> \"$GITHUB_OUTPUT\"",
PRETTIER_STEP_ID, CARGO_FMT_STEP_ID
))
.id(RECORD_STYLE_FAILURE_STEP_ID)
.if_condition(Expression::new("always()"))
named::bash("cargo fmt --all -- --check")
}
pub fn cargo_install_nextest() -> Step<Use> {
@@ -118,25 +105,13 @@ pub fn clear_target_dir_if_large(platform: Platform) -> Step<Run> {
}
}
pub const CLIPPY_STEP_ID: &str = "clippy";
pub const RECORD_CLIPPY_FAILURE_STEP_ID: &str = "record_clippy_failure";
pub fn clippy(platform: Platform) -> Step<Run> {
match platform {
Platform::Windows => named::pwsh("./script/clippy.ps1").id(CLIPPY_STEP_ID),
_ => named::bash("./script/clippy").id(CLIPPY_STEP_ID),
Platform::Windows => named::pwsh("./script/clippy.ps1"),
_ => named::bash("./script/clippy"),
}
}
pub fn record_clippy_failure() -> Step<Run> {
named::bash(format!(
"echo \"failed=${{{{ steps.{}.outcome == 'failure' }}}}\" >> \"$GITHUB_OUTPUT\"",
CLIPPY_STEP_ID
))
.id(RECORD_CLIPPY_FAILURE_STEP_ID)
.if_condition(Expression::new("always()"))
}
pub fn cache_rust_dependencies_namespace() -> Step<Use> {
named::uses("namespacelabs", "nscloud-cache-action", "v1").add_with(("cache", "rust"))
}