Factor tool definitions out of assistant
(#21189)
This PR factors the tool definitions out of the `assistant` crate so that they can be shared between `assistant` and `assistant2`. `ToolWorkingSet` now lives in `assistant_tool`. The tool definitions themselves live in `assistant_tools`, with the exception of the `ContextServerTool`, which has been moved to the `context_server` crate. As part of this refactoring I needed to extract the `ContextServerSettings` to a separate `context_server_settings` crate so that the `extension_host`—which is referenced by the `remote_server`—can name the `ContextServerSettings` type without pulling in some undesired dependencies. Release Notes: - N/A
This commit is contained in:
parent
321fd19763
commit
3901d46101
35 changed files with 219 additions and 113 deletions
42
Cargo.lock
generated
42
Cargo.lock
generated
|
@ -383,7 +383,7 @@ dependencies = [
|
||||||
"clock",
|
"clock",
|
||||||
"collections",
|
"collections",
|
||||||
"command_palette_hooks",
|
"command_palette_hooks",
|
||||||
"context_servers",
|
"context_server",
|
||||||
"ctor",
|
"ctor",
|
||||||
"db",
|
"db",
|
||||||
"editor",
|
"editor",
|
||||||
|
@ -506,6 +506,20 @@ dependencies = [
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "assistant_tools"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"assistant_tool",
|
||||||
|
"chrono",
|
||||||
|
"gpui",
|
||||||
|
"schemars",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"workspace",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-attributes"
|
name = "async-attributes"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
@ -2613,6 +2627,7 @@ dependencies = [
|
||||||
"anthropic",
|
"anthropic",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"assistant",
|
"assistant",
|
||||||
|
"assistant_tool",
|
||||||
"async-stripe",
|
"async-stripe",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"async-tungstenite 0.28.0",
|
"async-tungstenite 0.28.0",
|
||||||
|
@ -2631,7 +2646,7 @@ dependencies = [
|
||||||
"clock",
|
"clock",
|
||||||
"collab_ui",
|
"collab_ui",
|
||||||
"collections",
|
"collections",
|
||||||
"context_servers",
|
"context_server",
|
||||||
"ctor",
|
"ctor",
|
||||||
"dashmap 6.1.0",
|
"dashmap 6.1.0",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
|
@ -2874,12 +2889,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "context_servers"
|
name = "context_server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"assistant_tool",
|
||||||
"collections",
|
"collections",
|
||||||
"command_palette_hooks",
|
"command_palette_hooks",
|
||||||
|
"context_server_settings",
|
||||||
"extension",
|
"extension",
|
||||||
"futures 0.3.31",
|
"futures 0.3.31",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
@ -2887,13 +2904,27 @@ dependencies = [
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
"schemars",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
"smol",
|
"smol",
|
||||||
|
"ui",
|
||||||
"url",
|
"url",
|
||||||
"util",
|
"util",
|
||||||
|
"workspace",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "context_server_settings"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"collections",
|
||||||
|
"gpui",
|
||||||
|
"schemars",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"settings",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -4209,7 +4240,7 @@ dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"client",
|
"client",
|
||||||
"collections",
|
"collections",
|
||||||
"context_servers",
|
"context_server_settings",
|
||||||
"ctor",
|
"ctor",
|
||||||
"env_logger 0.11.5",
|
"env_logger 0.11.5",
|
||||||
"extension",
|
"extension",
|
||||||
|
@ -15586,6 +15617,7 @@ dependencies = [
|
||||||
"assets",
|
"assets",
|
||||||
"assistant",
|
"assistant",
|
||||||
"assistant2",
|
"assistant2",
|
||||||
|
"assistant_tools",
|
||||||
"async-watch",
|
"async-watch",
|
||||||
"audio",
|
"audio",
|
||||||
"auto_update",
|
"auto_update",
|
||||||
|
|
|
@ -8,6 +8,7 @@ members = [
|
||||||
"crates/assistant2",
|
"crates/assistant2",
|
||||||
"crates/assistant_slash_command",
|
"crates/assistant_slash_command",
|
||||||
"crates/assistant_tool",
|
"crates/assistant_tool",
|
||||||
|
"crates/assistant_tools",
|
||||||
"crates/audio",
|
"crates/audio",
|
||||||
"crates/auto_update",
|
"crates/auto_update",
|
||||||
"crates/auto_update_ui",
|
"crates/auto_update_ui",
|
||||||
|
@ -22,7 +23,8 @@ members = [
|
||||||
"crates/collections",
|
"crates/collections",
|
||||||
"crates/command_palette",
|
"crates/command_palette",
|
||||||
"crates/command_palette_hooks",
|
"crates/command_palette_hooks",
|
||||||
"crates/context_servers",
|
"crates/context_server",
|
||||||
|
"crates/context_server_settings",
|
||||||
"crates/copilot",
|
"crates/copilot",
|
||||||
"crates/db",
|
"crates/db",
|
||||||
"crates/diagnostics",
|
"crates/diagnostics",
|
||||||
|
@ -191,6 +193,7 @@ assistant = { path = "crates/assistant" }
|
||||||
assistant2 = { path = "crates/assistant2" }
|
assistant2 = { path = "crates/assistant2" }
|
||||||
assistant_slash_command = { path = "crates/assistant_slash_command" }
|
assistant_slash_command = { path = "crates/assistant_slash_command" }
|
||||||
assistant_tool = { path = "crates/assistant_tool" }
|
assistant_tool = { path = "crates/assistant_tool" }
|
||||||
|
assistant_tools = { path = "crates/assistant_tools" }
|
||||||
audio = { path = "crates/audio" }
|
audio = { path = "crates/audio" }
|
||||||
auto_update = { path = "crates/auto_update" }
|
auto_update = { path = "crates/auto_update" }
|
||||||
auto_update_ui = { path = "crates/auto_update_ui" }
|
auto_update_ui = { path = "crates/auto_update_ui" }
|
||||||
|
@ -205,7 +208,8 @@ collab_ui = { path = "crates/collab_ui" }
|
||||||
collections = { path = "crates/collections" }
|
collections = { path = "crates/collections" }
|
||||||
command_palette = { path = "crates/command_palette" }
|
command_palette = { path = "crates/command_palette" }
|
||||||
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
command_palette_hooks = { path = "crates/command_palette_hooks" }
|
||||||
context_servers = { path = "crates/context_servers" }
|
context_server = { path = "crates/context_server" }
|
||||||
|
context_server_settings = { path = "crates/context_server_settings" }
|
||||||
copilot = { path = "crates/copilot" }
|
copilot = { path = "crates/copilot" }
|
||||||
db = { path = "crates/db" }
|
db = { path = "crates/db" }
|
||||||
diagnostics = { path = "crates/diagnostics" }
|
diagnostics = { path = "crates/diagnostics" }
|
||||||
|
|
|
@ -33,7 +33,7 @@ client.workspace = true
|
||||||
clock.workspace = true
|
clock.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
command_palette_hooks.workspace = true
|
command_palette_hooks.workspace = true
|
||||||
context_servers.workspace = true
|
context_server.workspace = true
|
||||||
db.workspace = true
|
db.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
feature_flags.workspace = true
|
feature_flags.workspace = true
|
||||||
|
|
|
@ -14,16 +14,12 @@ pub mod slash_command_settings;
|
||||||
mod slash_command_working_set;
|
mod slash_command_working_set;
|
||||||
mod streaming_diff;
|
mod streaming_diff;
|
||||||
mod terminal_inline_assistant;
|
mod terminal_inline_assistant;
|
||||||
mod tool_working_set;
|
|
||||||
mod tools;
|
|
||||||
|
|
||||||
use crate::slash_command::project_command::ProjectSlashCommandFeatureFlag;
|
use crate::slash_command::project_command::ProjectSlashCommandFeatureFlag;
|
||||||
pub use crate::slash_command_working_set::{SlashCommandId, SlashCommandWorkingSet};
|
pub use crate::slash_command_working_set::{SlashCommandId, SlashCommandWorkingSet};
|
||||||
pub use crate::tool_working_set::{ToolId, ToolWorkingSet};
|
|
||||||
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
|
pub use assistant_panel::{AssistantPanel, AssistantPanelEvent};
|
||||||
use assistant_settings::AssistantSettings;
|
use assistant_settings::AssistantSettings;
|
||||||
use assistant_slash_command::SlashCommandRegistry;
|
use assistant_slash_command::SlashCommandRegistry;
|
||||||
use assistant_tool::ToolRegistry;
|
|
||||||
use client::{proto, Client};
|
use client::{proto, Client};
|
||||||
use command_palette_hooks::CommandPaletteFilter;
|
use command_palette_hooks::CommandPaletteFilter;
|
||||||
pub use context::*;
|
pub use context::*;
|
||||||
|
@ -246,7 +242,7 @@ pub fn init(
|
||||||
assistant_slash_command::init(cx);
|
assistant_slash_command::init(cx);
|
||||||
assistant_tool::init(cx);
|
assistant_tool::init(cx);
|
||||||
assistant_panel::init(cx);
|
assistant_panel::init(cx);
|
||||||
context_servers::init(cx);
|
context_server::init(cx);
|
||||||
|
|
||||||
let prompt_builder = prompts::PromptBuilder::new(Some(PromptLoadingParams {
|
let prompt_builder = prompts::PromptBuilder::new(Some(PromptLoadingParams {
|
||||||
fs: fs.clone(),
|
fs: fs.clone(),
|
||||||
|
@ -259,7 +255,6 @@ pub fn init(
|
||||||
.map(Arc::new)
|
.map(Arc::new)
|
||||||
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
|
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
|
||||||
register_slash_commands(Some(prompt_builder.clone()), cx);
|
register_slash_commands(Some(prompt_builder.clone()), cx);
|
||||||
register_tools(cx);
|
|
||||||
inline_assistant::init(
|
inline_assistant::init(
|
||||||
fs.clone(),
|
fs.clone(),
|
||||||
prompt_builder.clone(),
|
prompt_builder.clone(),
|
||||||
|
@ -423,11 +418,6 @@ fn update_slash_commands_from_settings(cx: &mut AppContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn register_tools(cx: &mut AppContext) {
|
|
||||||
let tool_registry = ToolRegistry::global(cx);
|
|
||||||
tool_registry.register_tool(tools::now_tool::NowTool);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn humanize_token_count(count: usize) -> String {
|
pub fn humanize_token_count(count: usize) -> String {
|
||||||
match count {
|
match count {
|
||||||
0..=999 => count.to_string(),
|
0..=999 => count.to_string(),
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use crate::slash_command::file_command::codeblock_fence_for_path;
|
use crate::slash_command::file_command::codeblock_fence_for_path;
|
||||||
use crate::slash_command_working_set::SlashCommandWorkingSet;
|
use crate::slash_command_working_set::SlashCommandWorkingSet;
|
||||||
use crate::ToolWorkingSet;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
assistant_settings::{AssistantDockPosition, AssistantSettings},
|
assistant_settings::{AssistantDockPosition, AssistantSettings},
|
||||||
humanize_token_count,
|
humanize_token_count,
|
||||||
|
@ -23,6 +22,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
|
use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
|
||||||
|
use assistant_tool::ToolWorkingSet;
|
||||||
use client::{proto, zed_urls, Client, Status};
|
use client::{proto, zed_urls, Client, Status};
|
||||||
use collections::{hash_map, BTreeSet, HashMap, HashSet};
|
use collections::{hash_map, BTreeSet, HashMap, HashSet};
|
||||||
use editor::{
|
use editor::{
|
||||||
|
@ -1316,7 +1316,7 @@ impl AssistantPanel {
|
||||||
|
|
||||||
fn restart_context_servers(
|
fn restart_context_servers(
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
_action: &context_servers::Restart,
|
_action: &context_server::Restart,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
|
let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
mod context_tests;
|
mod context_tests;
|
||||||
|
|
||||||
use crate::slash_command_working_set::SlashCommandWorkingSet;
|
use crate::slash_command_working_set::SlashCommandWorkingSet;
|
||||||
use crate::ToolWorkingSet;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
prompts::PromptBuilder,
|
prompts::PromptBuilder,
|
||||||
slash_command::{file_command::FileCommandMetadata, SlashCommandLine},
|
slash_command::{file_command::FileCommandMetadata, SlashCommandLine},
|
||||||
|
@ -12,6 +11,7 @@ use anyhow::{anyhow, Context as _, Result};
|
||||||
use assistant_slash_command::{
|
use assistant_slash_command::{
|
||||||
SlashCommandContent, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult,
|
SlashCommandContent, SlashCommandEvent, SlashCommandOutputSection, SlashCommandResult,
|
||||||
};
|
};
|
||||||
|
use assistant_tool::ToolWorkingSet;
|
||||||
use client::{self, proto, telemetry::Telemetry};
|
use client::{self, proto, telemetry::Telemetry};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use super::{AssistantEdit, MessageCacheMetadata};
|
use super::{AssistantEdit, MessageCacheMetadata};
|
||||||
use crate::slash_command_working_set::SlashCommandWorkingSet;
|
use crate::slash_command_working_set::SlashCommandWorkingSet;
|
||||||
use crate::ToolWorkingSet;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
assistant_panel, prompt_library, slash_command::file_command, AssistantEditKind, CacheStatus,
|
assistant_panel, prompt_library, slash_command::file_command, AssistantEditKind, CacheStatus,
|
||||||
Context, ContextEvent, ContextId, ContextOperation, InvokedSlashCommandId, MessageId,
|
Context, ContextEvent, ContextId, ContextOperation, InvokedSlashCommandId, MessageId,
|
||||||
|
@ -11,6 +10,7 @@ use assistant_slash_command::{
|
||||||
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, SlashCommandOutput,
|
ArgumentCompletion, SlashCommand, SlashCommandContent, SlashCommandEvent, SlashCommandOutput,
|
||||||
SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult,
|
SlashCommandOutputSection, SlashCommandRegistry, SlashCommandResult,
|
||||||
};
|
};
|
||||||
|
use assistant_tool::ToolWorkingSet;
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use fs::FakeFs;
|
use fs::FakeFs;
|
||||||
use futures::{
|
use futures::{
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
use crate::slash_command::context_server_command;
|
use crate::slash_command::context_server_command;
|
||||||
|
use crate::SlashCommandId;
|
||||||
use crate::{
|
use crate::{
|
||||||
prompts::PromptBuilder, slash_command_working_set::SlashCommandWorkingSet, Context,
|
prompts::PromptBuilder, slash_command_working_set::SlashCommandWorkingSet, Context,
|
||||||
ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext, SavedContextMetadata,
|
ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext, SavedContextMetadata,
|
||||||
};
|
};
|
||||||
use crate::{tools, SlashCommandId, ToolId, ToolWorkingSet};
|
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
|
use assistant_tool::{ToolId, ToolWorkingSet};
|
||||||
use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
|
use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
|
||||||
use clock::ReplicaId;
|
use clock::ReplicaId;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use context_servers::manager::ContextServerManager;
|
use context_server::manager::ContextServerManager;
|
||||||
use context_servers::ContextServerFactoryRegistry;
|
use context_server::{ContextServerFactoryRegistry, ContextServerTool};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use fuzzy::StringMatchCandidate;
|
use fuzzy::StringMatchCandidate;
|
||||||
|
@ -808,13 +809,13 @@ impl ContextStore {
|
||||||
fn handle_context_server_event(
|
fn handle_context_server_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
context_server_manager: Model<ContextServerManager>,
|
context_server_manager: Model<ContextServerManager>,
|
||||||
event: &context_servers::manager::Event,
|
event: &context_server::manager::Event,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) {
|
) {
|
||||||
let slash_command_working_set = self.slash_commands.clone();
|
let slash_command_working_set = self.slash_commands.clone();
|
||||||
let tool_working_set = self.tools.clone();
|
let tool_working_set = self.tools.clone();
|
||||||
match event {
|
match event {
|
||||||
context_servers::manager::Event::ServerStarted { server_id } => {
|
context_server::manager::Event::ServerStarted { server_id } => {
|
||||||
if let Some(server) = context_server_manager.read(cx).get_server(server_id) {
|
if let Some(server) = context_server_manager.read(cx).get_server(server_id) {
|
||||||
let context_server_manager = context_server_manager.clone();
|
let context_server_manager = context_server_manager.clone();
|
||||||
cx.spawn({
|
cx.spawn({
|
||||||
|
@ -825,7 +826,7 @@ impl ContextStore {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if protocol.capable(context_servers::protocol::ServerCapability::Prompts) {
|
if protocol.capable(context_server::protocol::ServerCapability::Prompts) {
|
||||||
if let Some(prompts) = protocol.list_prompts().await.log_err() {
|
if let Some(prompts) = protocol.list_prompts().await.log_err() {
|
||||||
let slash_command_ids = prompts
|
let slash_command_ids = prompts
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -853,12 +854,12 @@ impl ContextStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if protocol.capable(context_servers::protocol::ServerCapability::Tools) {
|
if protocol.capable(context_server::protocol::ServerCapability::Tools) {
|
||||||
if let Some(tools) = protocol.list_tools().await.log_err() {
|
if let Some(tools) = protocol.list_tools().await.log_err() {
|
||||||
let tool_ids = tools.tools.into_iter().map(|tool| {
|
let tool_ids = tools.tools.into_iter().map(|tool| {
|
||||||
log::info!("registering context server tool: {:?}", tool.name);
|
log::info!("registering context server tool: {:?}", tool.name);
|
||||||
tool_working_set.insert(
|
tool_working_set.insert(
|
||||||
Arc::new(tools::context_server_tool::ContextServerTool::new(
|
Arc::new(ContextServerTool::new(
|
||||||
context_server_manager.clone(),
|
context_server_manager.clone(),
|
||||||
server.id(),
|
server.id(),
|
||||||
tool,
|
tool,
|
||||||
|
@ -880,7 +881,7 @@ impl ContextStore {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
context_servers::manager::Event::ServerStopped { server_id } => {
|
context_server::manager::Event::ServerStopped { server_id } => {
|
||||||
if let Some(slash_command_ids) =
|
if let Some(slash_command_ids) =
|
||||||
self.context_server_slash_command_ids.remove(server_id)
|
self.context_server_slash_command_ids.remove(server_id)
|
||||||
{
|
{
|
||||||
|
|
|
@ -4,7 +4,7 @@ use assistant_slash_command::{
|
||||||
SlashCommandOutputSection, SlashCommandResult,
|
SlashCommandOutputSection, SlashCommandResult,
|
||||||
};
|
};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use context_servers::{
|
use context_server::{
|
||||||
manager::{ContextServer, ContextServerManager},
|
manager::{ContextServer, ContextServerManager},
|
||||||
types::Prompt,
|
types::Prompt,
|
||||||
};
|
};
|
||||||
|
@ -95,9 +95,9 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||||
|
|
||||||
let completion_result = protocol
|
let completion_result = protocol
|
||||||
.completion(
|
.completion(
|
||||||
context_servers::types::CompletionReference::Prompt(
|
context_server::types::CompletionReference::Prompt(
|
||||||
context_servers::types::PromptReference {
|
context_server::types::PromptReference {
|
||||||
r#type: context_servers::types::PromptReferenceType::Prompt,
|
r#type: context_server::types::PromptReferenceType::Prompt,
|
||||||
name: prompt_name,
|
name: prompt_name,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -152,7 +152,7 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||||
if result
|
if result
|
||||||
.messages
|
.messages
|
||||||
.iter()
|
.iter()
|
||||||
.any(|msg| !matches!(msg.role, context_servers::types::Role::User))
|
.any(|msg| !matches!(msg.role, context_server::types::Role::User))
|
||||||
{
|
{
|
||||||
return Err(anyhow!(
|
return Err(anyhow!(
|
||||||
"Prompt contains non-user roles, which is not supported"
|
"Prompt contains non-user roles, which is not supported"
|
||||||
|
@ -164,7 +164,7 @@ impl SlashCommand for ContextServerSlashCommand {
|
||||||
.messages
|
.messages
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|msg| match msg.content {
|
.filter_map(|msg| match msg.content {
|
||||||
context_servers::types::MessageContent::Text { text } => Some(text),
|
context_server::types::MessageContent::Text { text } => Some(text),
|
||||||
_ => None,
|
_ => None,
|
||||||
})
|
})
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
|
|
|
@ -1,2 +0,0 @@
|
||||||
pub mod context_server_tool;
|
|
||||||
pub mod now_tool;
|
|
|
@ -1,4 +1,5 @@
|
||||||
mod tool_registry;
|
mod tool_registry;
|
||||||
|
mod tool_working_set;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
@ -6,7 +7,8 @@ use anyhow::Result;
|
||||||
use gpui::{AppContext, Task, WeakView, WindowContext};
|
use gpui::{AppContext, Task, WeakView, WindowContext};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
pub use tool_registry::*;
|
pub use crate::tool_registry::*;
|
||||||
|
pub use crate::tool_working_set::*;
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
ToolRegistry::default_global(cx);
|
ToolRegistry::default_global(cx);
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
use assistant_tool::{Tool, ToolRegistry};
|
use std::sync::Arc;
|
||||||
|
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use gpui::AppContext;
|
use gpui::AppContext;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::sync::Arc;
|
|
||||||
|
use crate::{Tool, ToolRegistry};
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Default)]
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, Default)]
|
||||||
pub struct ToolId(usize);
|
pub struct ToolId(usize);
|
22
crates/assistant_tools/Cargo.toml
Normal file
22
crates/assistant_tools/Cargo.toml
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "assistant_tools"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/assistant_tools.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
assistant_tool.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
gpui.workspace = true
|
||||||
|
schemars.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
workspace.workspace = true
|
13
crates/assistant_tools/src/assistant_tools.rs
Normal file
13
crates/assistant_tools/src/assistant_tools.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
mod now_tool;
|
||||||
|
|
||||||
|
use assistant_tool::ToolRegistry;
|
||||||
|
use gpui::AppContext;
|
||||||
|
|
||||||
|
use crate::now_tool::NowTool;
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
assistant_tool::init(cx);
|
||||||
|
|
||||||
|
let registry = ToolRegistry::global(cx);
|
||||||
|
registry.register_tool(NowTool);
|
||||||
|
}
|
|
@ -79,7 +79,8 @@ uuid.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assistant = { workspace = true, features = ["test-support"] }
|
assistant = { workspace = true, features = ["test-support"] }
|
||||||
context_servers.workspace = true
|
assistant_tool.workspace = true
|
||||||
|
context_server.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
audio.workspace = true
|
audio.workspace = true
|
||||||
call = { workspace = true, features = ["test-support"] }
|
call = { workspace = true, features = ["test-support"] }
|
||||||
|
|
|
@ -6,7 +6,8 @@ use crate::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use assistant::{ContextStore, PromptBuilder, SlashCommandWorkingSet, ToolWorkingSet};
|
use assistant::{ContextStore, PromptBuilder, SlashCommandWorkingSet};
|
||||||
|
use assistant_tool::ToolWorkingSet;
|
||||||
use call::{room, ActiveCall, ParticipantLocation, Room};
|
use call::{room, ActiveCall, ParticipantLocation, Room};
|
||||||
use client::{User, RECEIVE_TIMEOUT};
|
use client::{User, RECEIVE_TIMEOUT};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
|
@ -6486,8 +6487,8 @@ async fn test_context_collaboration_with_reconnect(
|
||||||
assert_eq!(project.collaborators().len(), 1);
|
assert_eq!(project.collaborators().len(), 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
cx_a.update(context_servers::init);
|
cx_a.update(context_server::init);
|
||||||
cx_b.update(context_servers::init);
|
cx_b.update(context_server::init);
|
||||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||||
let context_store_a = cx_a
|
let context_store_a = cx_a
|
||||||
.update(|cx| {
|
.update(|cx| {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "context_servers"
|
name = "context_server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
publish = false
|
publish = false
|
||||||
|
@ -9,12 +9,14 @@ license = "GPL-3.0-or-later"
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
path = "src/context_servers.rs"
|
path = "src/context_server.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
assistant_tool.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
command_palette_hooks.workspace = true
|
command_palette_hooks.workspace = true
|
||||||
|
context_server_settings.workspace = true
|
||||||
extension.workspace = true
|
extension.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
@ -22,10 +24,11 @@ log.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
schemars.workspace = true
|
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
|
ui.workspace = true
|
||||||
url = { workspace = true, features = ["serde"] }
|
url = { workspace = true, features = ["serde"] }
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
|
workspace.workspace = true
|
1
crates/context_server/LICENSE-GPL
Symbolic link
1
crates/context_server/LICENSE-GPL
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../LICENSE-GPL
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod client;
|
pub mod client;
|
||||||
|
mod context_server_tool;
|
||||||
mod extension_context_server;
|
mod extension_context_server;
|
||||||
pub mod manager;
|
pub mod manager;
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
|
@ -6,10 +7,10 @@ mod registry;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
use command_palette_hooks::CommandPaletteFilter;
|
use command_palette_hooks::CommandPaletteFilter;
|
||||||
|
pub use context_server_settings::{ContextServerSettings, ServerCommand, ServerConfig};
|
||||||
use gpui::{actions, AppContext};
|
use gpui::{actions, AppContext};
|
||||||
use settings::Settings;
|
|
||||||
|
|
||||||
use crate::manager::ContextServerSettings;
|
pub use crate::context_server_tool::ContextServerTool;
|
||||||
pub use crate::registry::ContextServerFactoryRegistry;
|
pub use crate::registry::ContextServerFactoryRegistry;
|
||||||
|
|
||||||
actions!(context_servers, [Restart]);
|
actions!(context_servers, [Restart]);
|
||||||
|
@ -18,7 +19,7 @@ actions!(context_servers, [Restart]);
|
||||||
pub const CONTEXT_SERVERS_NAMESPACE: &'static str = "context_servers";
|
pub const CONTEXT_SERVERS_NAMESPACE: &'static str = "context_servers";
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
ContextServerSettings::register(cx);
|
context_server_settings::init(cx);
|
||||||
ContextServerFactoryRegistry::default_global(cx);
|
ContextServerFactoryRegistry::default_global(cx);
|
||||||
extension_context_server::init(cx);
|
extension_context_server::init(cx);
|
||||||
|
|
|
@ -2,10 +2,11 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail};
|
use anyhow::{anyhow, bail};
|
||||||
use assistant_tool::Tool;
|
use assistant_tool::Tool;
|
||||||
use context_servers::manager::ContextServerManager;
|
|
||||||
use context_servers::types;
|
|
||||||
use gpui::{Model, Task};
|
use gpui::{Model, Task};
|
||||||
|
|
||||||
|
use crate::manager::ContextServerManager;
|
||||||
|
use crate::types;
|
||||||
|
|
||||||
pub struct ContextServerTool {
|
pub struct ContextServerTool {
|
||||||
server_manager: Model<ContextServerManager>,
|
server_manager: Model<ContextServerManager>,
|
||||||
server_id: Arc<str>,
|
server_id: Arc<str>,
|
|
@ -3,8 +3,7 @@ use std::sync::Arc;
|
||||||
use extension::{Extension, ExtensionContextServerProxy, ExtensionHostProxy, ProjectDelegate};
|
use extension::{Extension, ExtensionContextServerProxy, ExtensionHostProxy, ProjectDelegate};
|
||||||
use gpui::{AppContext, Model};
|
use gpui::{AppContext, Model};
|
||||||
|
|
||||||
use crate::manager::ServerCommand;
|
use crate::{ContextServerFactoryRegistry, ServerCommand};
|
||||||
use crate::ContextServerFactoryRegistry;
|
|
||||||
|
|
||||||
struct ExtensionProject {
|
struct ExtensionProject {
|
||||||
worktree_ids: Vec<u64>,
|
worktree_ids: Vec<u64>,
|
|
@ -24,66 +24,16 @@ use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, Subscription, Tas
|
||||||
use log;
|
use log;
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use schemars::gen::SchemaGenerator;
|
use settings::{Settings, SettingsStore};
|
||||||
use schemars::schema::{InstanceType, Schema, SchemaObject};
|
|
||||||
use schemars::JsonSchema;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use settings::{Settings, SettingsSources, SettingsStore};
|
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
|
|
||||||
|
use crate::{ContextServerSettings, ServerConfig};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
client::{self, Client},
|
client::{self, Client},
|
||||||
types, ContextServerFactoryRegistry, CONTEXT_SERVERS_NAMESPACE,
|
types, ContextServerFactoryRegistry, CONTEXT_SERVERS_NAMESPACE,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Default, Clone, PartialEq, Eq, JsonSchema, Debug)]
|
|
||||||
pub struct ContextServerSettings {
|
|
||||||
/// Settings for context servers used in the Assistant.
|
|
||||||
#[serde(default)]
|
|
||||||
pub context_servers: HashMap<Arc<str>, ServerConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)]
|
|
||||||
pub struct ServerConfig {
|
|
||||||
/// The command to run this context server.
|
|
||||||
///
|
|
||||||
/// This will override the command set by an extension.
|
|
||||||
pub command: Option<ServerCommand>,
|
|
||||||
/// The settings for this context server.
|
|
||||||
///
|
|
||||||
/// Consult the documentation for the context server to see what settings
|
|
||||||
/// are supported.
|
|
||||||
#[schemars(schema_with = "server_config_settings_json_schema")]
|
|
||||||
pub settings: Option<serde_json::Value>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn server_config_settings_json_schema(_generator: &mut SchemaGenerator) -> Schema {
|
|
||||||
Schema::Object(SchemaObject {
|
|
||||||
instance_type: Some(InstanceType::Object.into()),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
|
|
||||||
pub struct ServerCommand {
|
|
||||||
pub path: String,
|
|
||||||
pub args: Vec<String>,
|
|
||||||
pub env: Option<HashMap<String, String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Settings for ContextServerSettings {
|
|
||||||
const KEY: Option<&'static str> = None;
|
|
||||||
|
|
||||||
type FileContent = Self;
|
|
||||||
|
|
||||||
fn load(
|
|
||||||
sources: SettingsSources<Self::FileContent>,
|
|
||||||
_: &mut gpui::AppContext,
|
|
||||||
) -> anyhow::Result<Self> {
|
|
||||||
sources.json_merge()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ContextServer {
|
pub struct ContextServer {
|
||||||
pub id: Arc<str>,
|
pub id: Arc<str>,
|
||||||
pub config: Arc<ServerConfig>,
|
pub config: Arc<ServerConfig>,
|
|
@ -5,7 +5,7 @@ use collections::HashMap;
|
||||||
use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ReadGlobal, Task};
|
use gpui::{AppContext, AsyncAppContext, Context, Global, Model, ReadGlobal, Task};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
|
|
||||||
use crate::manager::ServerCommand;
|
use crate::ServerCommand;
|
||||||
|
|
||||||
pub type ContextServerFactory = Arc<
|
pub type ContextServerFactory = Arc<
|
||||||
dyn Fn(Model<Project>, &AsyncAppContext) -> Task<Result<ServerCommand>> + Send + Sync + 'static,
|
dyn Fn(Model<Project>, &AsyncAppContext) -> Task<Result<ServerCommand>> + Send + Sync + 'static,
|
21
crates/context_server_settings/Cargo.toml
Normal file
21
crates/context_server_settings/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "context_server_settings"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/context_server_settings.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
collections.workspace = true
|
||||||
|
gpui.workspace = true
|
||||||
|
schemars.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
settings.workspace = true
|
1
crates/context_server_settings/LICENSE-GPL
Symbolic link
1
crates/context_server_settings/LICENSE-GPL
Symbolic link
|
@ -0,0 +1 @@
|
||||||
|
../../LICENSE-GPL
|
|
@ -0,0 +1,61 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use collections::HashMap;
|
||||||
|
use gpui::AppContext;
|
||||||
|
use schemars::gen::SchemaGenerator;
|
||||||
|
use schemars::schema::{InstanceType, Schema, SchemaObject};
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use settings::{Settings, SettingsSources};
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
ContextServerSettings::register(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)]
|
||||||
|
pub struct ServerConfig {
|
||||||
|
/// The command to run this context server.
|
||||||
|
///
|
||||||
|
/// This will override the command set by an extension.
|
||||||
|
pub command: Option<ServerCommand>,
|
||||||
|
/// The settings for this context server.
|
||||||
|
///
|
||||||
|
/// Consult the documentation for the context server to see what settings
|
||||||
|
/// are supported.
|
||||||
|
#[schemars(schema_with = "server_config_settings_json_schema")]
|
||||||
|
pub settings: Option<serde_json::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_config_settings_json_schema(_generator: &mut SchemaGenerator) -> Schema {
|
||||||
|
Schema::Object(SchemaObject {
|
||||||
|
instance_type: Some(InstanceType::Object.into()),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
|
||||||
|
pub struct ServerCommand {
|
||||||
|
pub path: String,
|
||||||
|
pub args: Vec<String>,
|
||||||
|
pub env: Option<HashMap<String, String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Default, Clone, PartialEq, Eq, JsonSchema, Debug)]
|
||||||
|
pub struct ContextServerSettings {
|
||||||
|
/// Settings for context servers used in the Assistant.
|
||||||
|
#[serde(default)]
|
||||||
|
pub context_servers: HashMap<Arc<str>, ServerConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings for ContextServerSettings {
|
||||||
|
const KEY: Option<&'static str> = None;
|
||||||
|
|
||||||
|
type FileContent = Self;
|
||||||
|
|
||||||
|
fn load(
|
||||||
|
sources: SettingsSources<Self::FileContent>,
|
||||||
|
_: &mut gpui::AppContext,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
sources.json_merge()
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ async-tar.workspace = true
|
||||||
async-trait.workspace = true
|
async-trait.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
context_servers.workspace = true
|
context_server_settings.workspace = true
|
||||||
extension.workspace = true
|
extension.workspace = true
|
||||||
fs.workspace = true
|
fs.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
|
|
|
@ -7,7 +7,7 @@ use anyhow::{anyhow, bail, Context, Result};
|
||||||
use async_compression::futures::bufread::GzipDecoder;
|
use async_compression::futures::bufread::GzipDecoder;
|
||||||
use async_tar::Archive;
|
use async_tar::Archive;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use context_servers::manager::ContextServerSettings;
|
use context_server_settings::ContextServerSettings;
|
||||||
use extension::{
|
use extension::{
|
||||||
ExtensionLanguageServerProxy, KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate,
|
ExtensionLanguageServerProxy, KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate,
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,6 +20,7 @@ anyhow.workspace = true
|
||||||
assets.workspace = true
|
assets.workspace = true
|
||||||
assistant.workspace = true
|
assistant.workspace = true
|
||||||
assistant2.workspace = true
|
assistant2.workspace = true
|
||||||
|
assistant_tools.workspace = true
|
||||||
async-watch.workspace = true
|
async-watch.workspace = true
|
||||||
audio.workspace = true
|
audio.workspace = true
|
||||||
auto_update.workspace = true
|
auto_update.workspace = true
|
||||||
|
|
|
@ -407,6 +407,7 @@ fn main() {
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
assistant2::init(cx);
|
assistant2::init(cx);
|
||||||
|
assistant_tools::init(cx);
|
||||||
repl::init(
|
repl::init(
|
||||||
app_state.fs.clone(),
|
app_state.fs.clone(),
|
||||||
app_state.client.telemetry().clone(),
|
app_state.client.telemetry().clone(),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue