Compare commits
6 Commits
tailwind-c
...
add-channe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e2d6d2a75 | ||
|
|
9a85806aca | ||
|
|
4174c4c9d9 | ||
|
|
0e12e31edc | ||
|
|
a8287e4289 | ||
|
|
8019a3925d |
6
.rules
6
.rules
@@ -5,6 +5,12 @@
|
||||
* Prefer implementing functionality in existing files unless it is a new logical component. Avoid creating many small files.
|
||||
* Avoid using functions that panic like `unwrap()`, instead use mechanisms like `?` to propagate errors.
|
||||
* Be careful with operations like indexing which may panic if the indexes are out of bounds.
|
||||
* Never silently discard errors with `let _ =` on fallible operations. Always handle errors appropriately:
|
||||
- Propagate errors with `?` when the calling function should handle them
|
||||
- Use `.log_err()` or similar when you need to ignore errors but want visibility
|
||||
- Use explicit error handling with `match` or `if let Err(...)` when you need custom logic
|
||||
- Example: avoid `let _ = client.request(...).await?;` - use `client.request(...).await?;` instead
|
||||
* When implementing async operations that may fail, ensure errors propagate to the UI layer so users get meaningful feedback.
|
||||
* Never create files with `mod.rs` paths - prefer `src/some_module.rs` instead of `src/some_module/mod.rs`.
|
||||
|
||||
# GPUI
|
||||
|
||||
52
Cargo.lock
generated
52
Cargo.lock
generated
@@ -678,6 +678,7 @@ dependencies = [
|
||||
"handlebars 4.5.0",
|
||||
"html_to_markdown",
|
||||
"http_client",
|
||||
"icons",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
@@ -686,6 +687,7 @@ dependencies = [
|
||||
"log",
|
||||
"lsp",
|
||||
"markdown",
|
||||
"node_runtime",
|
||||
"open",
|
||||
"paths",
|
||||
"portable-pty",
|
||||
@@ -694,7 +696,9 @@ dependencies = [
|
||||
"prompt_store",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"reqwest_client",
|
||||
"rpc",
|
||||
"rust-embed",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -707,6 +711,7 @@ dependencies = [
|
||||
"tempfile",
|
||||
"terminal",
|
||||
"terminal_view",
|
||||
"text",
|
||||
"theme",
|
||||
"tree-sitter-rust",
|
||||
"ui",
|
||||
@@ -1280,9 +1285,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.13.1"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7"
|
||||
checksum = "19b756939cb2f8dc900aa6dcd505e6e2428e9cae7ff7b028c49e3946efa70878"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
@@ -1290,9 +1295,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.29.0"
|
||||
version = "0.28.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079"
|
||||
checksum = "bfa9b6986f250236c27e5a204062434a773a13243d2ffc2955f37bdba4c5c6a1"
|
||||
dependencies = [
|
||||
"bindgen 0.69.5",
|
||||
"cc",
|
||||
@@ -2663,6 +2668,40 @@ dependencies = [
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "channel_tools"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assistant_tool",
|
||||
"assistant_tools",
|
||||
"channel",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"editor",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
"icons",
|
||||
"language",
|
||||
"language_model",
|
||||
"log",
|
||||
"node_runtime",
|
||||
"project",
|
||||
"release_channel",
|
||||
"rpc",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"tempfile",
|
||||
"text",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.41"
|
||||
@@ -3545,9 +3584,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cosmic-text"
|
||||
version = "0.14.0"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e1ecbb5db9a4c2ee642df67bcfa8f044dd867dbbaa21bfab139cbc204ffbf67"
|
||||
checksum = "da46a9d5a8905cc538a4a5bceb6a4510de7a51049c5588c0114efce102bcbbe8"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"fontdb 0.16.2",
|
||||
@@ -19707,6 +19746,7 @@ dependencies = [
|
||||
"breadcrumbs",
|
||||
"call",
|
||||
"channel",
|
||||
"channel_tools",
|
||||
"chrono",
|
||||
"clap",
|
||||
"cli",
|
||||
|
||||
@@ -22,6 +22,7 @@ members = [
|
||||
"crates/buffer_diff",
|
||||
"crates/call",
|
||||
"crates/channel",
|
||||
"crates/channel_tools",
|
||||
"crates/cli",
|
||||
"crates/client",
|
||||
"crates/clock",
|
||||
@@ -231,6 +232,7 @@ breadcrumbs = { path = "crates/breadcrumbs" }
|
||||
buffer_diff = { path = "crates/buffer_diff" }
|
||||
call = { path = "crates/call" }
|
||||
channel = { path = "crates/channel" }
|
||||
channel_tools = { path = "crates/channel_tools" }
|
||||
cli = { path = "crates/cli" }
|
||||
client = { path = "crates/client" }
|
||||
clock = { path = "crates/clock" }
|
||||
|
||||
@@ -897,7 +897,9 @@
|
||||
"context": "CollabPanel && not_editing",
|
||||
"bindings": {
|
||||
"ctrl-backspace": "collab_panel::Remove",
|
||||
"space": "menu::Confirm"
|
||||
"space": "menu::Confirm",
|
||||
"ctrl-up": "collab_panel::MoveChannelUp",
|
||||
"ctrl-down": "collab_panel::MoveChannelDown"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -951,7 +951,9 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-backspace": "collab_panel::Remove",
|
||||
"space": "menu::Confirm"
|
||||
"space": "menu::Confirm",
|
||||
"cmd-up": "collab_panel::MoveChannelUp",
|
||||
"cmd-down": "collab_panel::MoveChannelDown"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -21,6 +21,7 @@ assistant_tool.workspace = true
|
||||
async-watch.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
derive_more.workspace = true
|
||||
@@ -31,6 +32,7 @@ gpui.workspace = true
|
||||
handlebars = { workspace = true, features = ["rust-embed"] }
|
||||
html_to_markdown.workspace = true
|
||||
http_client.workspace = true
|
||||
icons.workspace = true
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
@@ -44,6 +46,8 @@ portable-pty.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
regex.workspace = true
|
||||
release_channel.workspace = true
|
||||
rpc.workspace = true
|
||||
rust-embed.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
@@ -55,6 +59,7 @@ strsim.workspace = true
|
||||
task.workspace = true
|
||||
terminal.workspace = true
|
||||
terminal_view.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
@@ -72,13 +77,16 @@ collections = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
gpui_tokio.workspace = true
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
language_models.workspace = true
|
||||
node_runtime = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
task = { workspace = true, features = ["test-support"]}
|
||||
tempfile.workspace = true
|
||||
|
||||
@@ -2,7 +2,7 @@ mod copy_path_tool;
|
||||
mod create_directory_tool;
|
||||
mod delete_path_tool;
|
||||
mod diagnostics_tool;
|
||||
mod edit_agent;
|
||||
pub mod edit_agent;
|
||||
mod edit_file_tool;
|
||||
mod fetch_tool;
|
||||
mod find_path_tool;
|
||||
@@ -29,7 +29,7 @@ use language_model::LanguageModelRegistry;
|
||||
use move_path_tool::MovePathTool;
|
||||
use web_search_tool::WebSearchTool;
|
||||
|
||||
pub(crate) use templates::*;
|
||||
pub use templates::*;
|
||||
|
||||
use crate::create_directory_tool::CreateDirectoryTool;
|
||||
use crate::delete_path_tool::DeletePathTool;
|
||||
@@ -42,6 +42,7 @@ use crate::list_directory_tool::ListDirectoryTool;
|
||||
use crate::now_tool::NowTool;
|
||||
use crate::thinking_tool::ThinkingTool;
|
||||
|
||||
pub use edit_agent::{EditAgent, EditAgentOutputEvent};
|
||||
pub use edit_file_tool::{EditFileMode, EditFileToolInput};
|
||||
pub use find_path_tool::FindPathToolInput;
|
||||
pub use open_tool::OpenTool;
|
||||
|
||||
@@ -56,6 +56,7 @@ pub struct Channel {
|
||||
pub name: SharedString,
|
||||
pub visibility: proto::ChannelVisibility,
|
||||
pub parent_path: Vec<ChannelId>,
|
||||
pub channel_order: i32,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
@@ -614,7 +615,24 @@ impl ChannelStore {
|
||||
to: to.0,
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn reorder_channel(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
direction: proto::reorder_channel::Direction,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.clone();
|
||||
cx.spawn(async move |_, _| {
|
||||
client
|
||||
.request(proto::ReorderChannel {
|
||||
channel_id: channel_id.0,
|
||||
direction: direction.into(),
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -1050,6 +1068,7 @@ impl ChannelStore {
|
||||
visibility: channel.visibility(),
|
||||
name: channel.name.into(),
|
||||
parent_path: channel.parent_path.into_iter().map(ChannelId).collect(),
|
||||
channel_order: channel.channel_order,
|
||||
}),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -61,11 +61,13 @@ impl ChannelPathsInsertGuard<'_> {
|
||||
|
||||
ret = existing_channel.visibility != channel_proto.visibility()
|
||||
|| existing_channel.name != channel_proto.name
|
||||
|| existing_channel.parent_path != parent_path;
|
||||
|| existing_channel.parent_path != parent_path
|
||||
|| existing_channel.channel_order != channel_proto.channel_order;
|
||||
|
||||
existing_channel.visibility = channel_proto.visibility();
|
||||
existing_channel.name = channel_proto.name.into();
|
||||
existing_channel.parent_path = parent_path;
|
||||
existing_channel.channel_order = channel_proto.channel_order;
|
||||
} else {
|
||||
self.channels_by_id.insert(
|
||||
ChannelId(channel_proto.id),
|
||||
@@ -74,6 +76,7 @@ impl ChannelPathsInsertGuard<'_> {
|
||||
visibility: channel_proto.visibility(),
|
||||
name: channel_proto.name.into(),
|
||||
parent_path,
|
||||
channel_order: channel_proto.channel_order,
|
||||
}),
|
||||
);
|
||||
self.insert_root(ChannelId(channel_proto.id));
|
||||
@@ -100,17 +103,18 @@ impl Drop for ChannelPathsInsertGuard<'_> {
|
||||
fn channel_path_sorting_key(
|
||||
id: ChannelId,
|
||||
channels_by_id: &BTreeMap<ChannelId, Arc<Channel>>,
|
||||
) -> impl Iterator<Item = (&str, ChannelId)> {
|
||||
let (parent_path, name) = channels_by_id
|
||||
.get(&id)
|
||||
.map_or((&[] as &[_], None), |channel| {
|
||||
(
|
||||
channel.parent_path.as_slice(),
|
||||
Some((channel.name.as_ref(), channel.id)),
|
||||
)
|
||||
});
|
||||
) -> impl Iterator<Item = (i32, ChannelId)> {
|
||||
let (parent_path, order_and_id) =
|
||||
channels_by_id
|
||||
.get(&id)
|
||||
.map_or((&[] as &[_], None), |channel| {
|
||||
(
|
||||
channel.parent_path.as_slice(),
|
||||
Some((channel.channel_order, channel.id)),
|
||||
)
|
||||
});
|
||||
parent_path
|
||||
.iter()
|
||||
.filter_map(|id| Some((channels_by_id.get(id)?.name.as_ref(), *id)))
|
||||
.chain(name)
|
||||
.filter_map(|id| Some((channels_by_id.get(id)?.channel_order, *id)))
|
||||
.chain(order_and_id)
|
||||
}
|
||||
|
||||
@@ -21,12 +21,14 @@ fn test_update_channels(cx: &mut App) {
|
||||
name: "b".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
parent_path: Vec::new(),
|
||||
channel_order: 1,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 2,
|
||||
name: "a".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
parent_path: Vec::new(),
|
||||
channel_order: 2,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
@@ -37,8 +39,8 @@ fn test_update_channels(cx: &mut App) {
|
||||
&channel_store,
|
||||
&[
|
||||
//
|
||||
(0, "a".to_string()),
|
||||
(0, "b".to_string()),
|
||||
(0, "a".to_string()),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
@@ -52,12 +54,14 @@ fn test_update_channels(cx: &mut App) {
|
||||
name: "x".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
parent_path: vec![1],
|
||||
channel_order: 1,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 4,
|
||||
name: "y".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
parent_path: vec![2],
|
||||
channel_order: 1,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
@@ -67,10 +71,10 @@ fn test_update_channels(cx: &mut App) {
|
||||
assert_channels(
|
||||
&channel_store,
|
||||
&[
|
||||
(0, "a".to_string()),
|
||||
(1, "y".to_string()),
|
||||
(0, "b".to_string()),
|
||||
(1, "x".to_string()),
|
||||
(0, "a".to_string()),
|
||||
(1, "y".to_string()),
|
||||
],
|
||||
cx,
|
||||
);
|
||||
@@ -89,18 +93,21 @@ fn test_dangling_channel_paths(cx: &mut App) {
|
||||
name: "a".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
parent_path: vec![],
|
||||
channel_order: 1,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 1,
|
||||
name: "b".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
parent_path: vec![0],
|
||||
channel_order: 1,
|
||||
},
|
||||
proto::Channel {
|
||||
id: 2,
|
||||
name: "c".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
parent_path: vec![0, 1],
|
||||
channel_order: 1,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
@@ -147,6 +154,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
name: "the-channel".to_string(),
|
||||
visibility: proto::ChannelVisibility::Members as i32,
|
||||
parent_path: vec![],
|
||||
channel_order: 1,
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
51
crates/channel_tools/Cargo.toml
Normal file
51
crates/channel_tools/Cargo.toml
Normal file
@@ -0,0 +1,51 @@
|
||||
[package]
|
||||
name = "channel_tools"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/channel_tools.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
assistant_tools.workspace = true
|
||||
channel.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
icons.workspace = true
|
||||
language_model.workspace = true
|
||||
log.workspace = true
|
||||
project.workspace = true
|
||||
rpc.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
text.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
channel = { workspace = true, features = ["test-support"] }
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
http_client = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
node_runtime = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
release_channel.workspace = true
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
tempfile.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
62
crates/channel_tools/MIGRATION.md
Normal file
62
crates/channel_tools/MIGRATION.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Channel Tools Migration
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the migration of channel tools from the `assistant_tools` crate to a separate `channel_tools` crate to resolve initialization order issues.
|
||||
|
||||
## Problem
|
||||
|
||||
The channel tools were originally part of the `assistant_tools` crate, but this created an initialization order problem:
|
||||
- `assistant_tools::init()` was called before `channel::init()` in `main.rs`
|
||||
- Channel tools require `ChannelStore::global()` to be available
|
||||
- Attempting to access the global channel store before initialization caused a panic
|
||||
|
||||
## Solution
|
||||
|
||||
Created a separate `channel_tools` crate that is initialized after the channel system is ready.
|
||||
|
||||
### Changes Made
|
||||
|
||||
1. **Created new `channel_tools` crate** (`crates/channel_tools/`)
|
||||
- Moved all channel tool implementations from `assistant_tools`
|
||||
- Added proper dependencies in `Cargo.toml`
|
||||
- Maintained the same public API
|
||||
|
||||
2. **Updated `assistant_tools` crate**
|
||||
- Removed channel tools module
|
||||
- Removed channel dependency
|
||||
- Removed commented-out initialization code
|
||||
|
||||
3. **Updated `zed` main crate**
|
||||
- Added `channel_tools` dependency
|
||||
- Added `channel_tools::init()` call after `channel::init()`
|
||||
- Passes the global `ChannelStore` explicitly to channel tools
|
||||
|
||||
4. **Updated workspace configuration**
|
||||
- Added `channel_tools` to workspace members
|
||||
- Added `channel_tools` to workspace dependencies
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Clean separation of concerns**: Channel-specific tools are now in their own crate
|
||||
2. **Proper initialization order**: Channel tools are initialized only after the channel system is ready
|
||||
3. **No runtime panics**: The explicit dependency on `ChannelStore` is satisfied
|
||||
4. **Maintainability**: Future channel-related tools can be added to this dedicated crate
|
||||
|
||||
## Usage
|
||||
|
||||
The channel tools are now initialized in `main.rs` after the channel system:
|
||||
|
||||
```rust
|
||||
channel::init(&app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||
channel_tools::init(channel::ChannelStore::global(cx), cx);
|
||||
```
|
||||
|
||||
The tools remain available through the global `ToolRegistry` just as before, so no changes are needed in code that uses these tools.
|
||||
|
||||
## Tools Included
|
||||
|
||||
- `CreateChannelTool`: Creates new channels
|
||||
- `MoveChannelTool`: Moves channels to different parents
|
||||
- `ReorderChannelTool`: Changes channel order within a parent
|
||||
- `EditChannelNotesTool`: Edits channel notes collaboratively
|
||||
169
crates/channel_tools/README.md
Normal file
169
crates/channel_tools/README.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# Channel Tools for AI Agents
|
||||
|
||||
This module provides AI agent tools for interacting with Zed's channel system. These tools enable agents to manage channels programmatically, including creating channels, organizing them, and editing channel notes collaboratively.
|
||||
|
||||
## Overview
|
||||
|
||||
The channel tools allow AI agents to:
|
||||
- Create new channels and subchannels
|
||||
- Move channels to different parent channels
|
||||
- Reorder channels within their parent
|
||||
- Edit channel notes using collaborative editing
|
||||
|
||||
## Tools
|
||||
|
||||
### CreateChannelTool
|
||||
|
||||
Creates a new channel in the workspace.
|
||||
|
||||
**Input Schema:**
|
||||
```json
|
||||
{
|
||||
"name": "channel-name",
|
||||
"parent": "parent-channel-name", // optional
|
||||
"visibility": "members" // or "public"
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Creates root channels when no parent is specified
|
||||
- Supports creating subchannels under existing channels
|
||||
- Allows setting channel visibility (members-only or public)
|
||||
- Does not require user confirmation
|
||||
|
||||
### MoveChannelTool
|
||||
|
||||
Moves a channel to a different parent channel.
|
||||
|
||||
**Input Schema:**
|
||||
```json
|
||||
{
|
||||
"channel": "channel-to-move",
|
||||
"to": "new-parent-channel" // or null for root
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Moves channels between different parents
|
||||
- Currently does not support moving channels to root (limitation)
|
||||
- Validates against circular dependencies (can't move to descendants)
|
||||
- Requires user confirmation before executing
|
||||
|
||||
### ReorderChannelTool
|
||||
|
||||
Changes the order of a channel among its siblings.
|
||||
|
||||
**Input Schema:**
|
||||
```json
|
||||
{
|
||||
"channel": "channel-name",
|
||||
"direction": "up" // or "down"
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Moves channels up or down within their sibling list
|
||||
- Uses the native channel reordering API
|
||||
- Does not require user confirmation
|
||||
|
||||
### EditChannelNotesTool
|
||||
|
||||
Edits channel notes using collaborative editing to avoid conflicts.
|
||||
|
||||
**Input Schema:**
|
||||
```json
|
||||
{
|
||||
"channel": "channel-name",
|
||||
"edits": [
|
||||
{
|
||||
"kind": "create", // or "edit" or "append"
|
||||
"content": "Note content",
|
||||
"range": { // optional, for "edit" kind
|
||||
"start_line": 0,
|
||||
"start_column": 0,
|
||||
"end_line": 10,
|
||||
"end_column": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Supports creating new notes, editing existing content, or appending
|
||||
- Uses collaborative editing through channel buffers
|
||||
- Automatically handles buffer synchronization
|
||||
- Supports multiple edits in a single operation
|
||||
- Does not require user confirmation
|
||||
|
||||
## Collaborative Editing
|
||||
|
||||
The EditChannelNotesTool uses Zed's collaborative editing infrastructure:
|
||||
|
||||
1. Opens a channel buffer (same as when users edit notes)
|
||||
2. Applies edits through the buffer's collaborative editing system
|
||||
3. Acknowledges buffer versions to ensure synchronization
|
||||
4. Avoids conflicts with other users editing simultaneously
|
||||
|
||||
This approach ensures that agent edits integrate seamlessly with human edits and maintain consistency across all connected clients.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Architecture
|
||||
- Tools implement the `Tool` trait from the assistant_tool crate
|
||||
- Each tool maintains a reference to the global ChannelStore
|
||||
- Operations are performed asynchronously using GPUI's task system
|
||||
- Channel lookups are done by name for user-friendliness
|
||||
|
||||
### Error Handling
|
||||
- Invalid channel names result in descriptive error messages
|
||||
- Network failures are propagated as errors
|
||||
- Validation prevents invalid operations (e.g., circular moves)
|
||||
|
||||
### Testing
|
||||
All tools have comprehensive test coverage including:
|
||||
- Schema validation
|
||||
- UI text generation
|
||||
- Confirmation requirements
|
||||
- Basic operation validation
|
||||
|
||||
## Limitations
|
||||
|
||||
1. **Moving to Root**: The MoveChannelTool cannot currently move channels to the root level due to API limitations
|
||||
2. **Channel Deletion**: No tool for deleting channels (intentional safety measure)
|
||||
3. **Permissions**: Tools operate with the current user's permissions
|
||||
4. **Name Conflicts**: No automatic handling of duplicate channel names
|
||||
|
||||
## Usage Example
|
||||
|
||||
An agent might use these tools in sequence to organize a project:
|
||||
|
||||
```
|
||||
1. Create main channels:
|
||||
- "frontend" (public)
|
||||
- "backend" (members)
|
||||
- "docs" (public)
|
||||
|
||||
2. Create subchannels:
|
||||
- "frontend/components"
|
||||
- "frontend/styles"
|
||||
- "backend/api"
|
||||
- "backend/database"
|
||||
|
||||
3. Edit channel notes:
|
||||
- Add README content to each channel
|
||||
- Include guidelines and links
|
||||
|
||||
4. Reorder for clarity:
|
||||
- Move "docs" to the top
|
||||
- Organize subchannels alphabetically
|
||||
```
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements could include:
|
||||
- Channel deletion tool (with strong safety measures)
|
||||
- Bulk operations for efficiency
|
||||
- Channel templates for common structures
|
||||
- Integration with channel permissions/roles
|
||||
- Search functionality for finding channels
|
||||
65
crates/channel_tools/src/channel_tools.rs
Normal file
65
crates/channel_tools/src/channel_tools.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
mod create_channel_tool;
|
||||
mod edit_channel_notes_tool;
|
||||
mod list_channels_tool;
|
||||
mod move_channel_tool;
|
||||
mod reorder_channel_tool;
|
||||
mod schema;
|
||||
mod streaming_edit_channel_notes_tool;
|
||||
|
||||
#[cfg(test)]
|
||||
mod channel_tools_tests;
|
||||
|
||||
pub use create_channel_tool::CreateChannelTool;
|
||||
pub use edit_channel_notes_tool::EditChannelNotesTool;
|
||||
pub use list_channels_tool::ListChannelsTool;
|
||||
pub use move_channel_tool::MoveChannelTool;
|
||||
pub use reorder_channel_tool::ReorderChannelTool;
|
||||
pub use streaming_edit_channel_notes_tool::StreamingEditChannelNotesTool;
|
||||
|
||||
use assistant_tool::ToolRegistry;
|
||||
use channel::{Channel, ChannelStore};
|
||||
use client::ChannelId;
|
||||
use gpui::{App, Entity};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Initialize channel tools by registering them with the global ToolRegistry.
|
||||
/// This should be called after channel::init to ensure ChannelStore is available.
|
||||
pub fn init(channel_store: Entity<ChannelStore>, cx: &mut App) {
|
||||
let registry = ToolRegistry::global(cx);
|
||||
registry.register_tool(ListChannelsTool::new(channel_store.clone()));
|
||||
registry.register_tool(CreateChannelTool::new(channel_store.clone()));
|
||||
registry.register_tool(MoveChannelTool::new(channel_store.clone()));
|
||||
registry.register_tool(ReorderChannelTool::new(channel_store.clone()));
|
||||
registry.register_tool(EditChannelNotesTool::new(channel_store.clone()));
|
||||
registry.register_tool(StreamingEditChannelNotesTool::new(channel_store));
|
||||
}
|
||||
|
||||
/// Helper function to find a channel by name
|
||||
fn find_channel_by_name(
|
||||
channel_store: &Entity<ChannelStore>,
|
||||
name: &str,
|
||||
cx: &App,
|
||||
) -> Option<(ChannelId, Arc<Channel>)> {
|
||||
let store = channel_store.read(cx);
|
||||
store
|
||||
.channels()
|
||||
.find(|channel| channel.name == name)
|
||||
.map(|channel| (channel.id, channel.clone()))
|
||||
}
|
||||
|
||||
/// Visibility options for channels
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ChannelVisibility {
|
||||
Members,
|
||||
Public,
|
||||
}
|
||||
|
||||
impl ChannelVisibility {
|
||||
fn from_str(s: &str) -> Option<Self> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"members" | "private" => Some(Self::Members),
|
||||
"public" => Some(Self::Public),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
335
crates/channel_tools/src/channel_tools_tests.rs
Normal file
335
crates/channel_tools/src/channel_tools_tests.rs
Normal file
@@ -0,0 +1,335 @@
|
||||
use crate::*;
|
||||
use assistant_tool::Tool;
|
||||
use client::{Client, UserStore};
|
||||
use gpui::{AppContext, TestAppContext};
|
||||
use project::Project;
|
||||
use serde_json::json;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_create_channel_tool(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = settings::SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
release_channel::init(gpui::SemanticVersion::default(), cx);
|
||||
client::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
|
||||
// Create a minimal test setup without server
|
||||
let http_client = http_client::FakeHttpClient::create(|_| async { unreachable!() });
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(clock::FakeSystemClock::new()),
|
||||
http_client.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
cx.update(|cx| {
|
||||
channel::init(&client, user_store.clone(), cx);
|
||||
});
|
||||
let channel_store = cx.update(|cx| channel::ChannelStore::global(cx));
|
||||
|
||||
let _project = cx.update(|cx| {
|
||||
Project::local(
|
||||
client.clone(),
|
||||
node_runtime::NodeRuntime::unavailable(),
|
||||
user_store.clone(),
|
||||
Arc::new(language::LanguageRegistry::new(
|
||||
cx.background_executor().clone(),
|
||||
)),
|
||||
fs::FakeFs::new(cx.background_executor().clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let tool = Arc::new(CreateChannelTool::new(channel_store.clone()));
|
||||
|
||||
// Test tool schema
|
||||
let schema = tool
|
||||
.input_schema(language_model::LanguageModelToolSchemaFormat::JsonSchema)
|
||||
.unwrap();
|
||||
assert!(schema.is_object());
|
||||
|
||||
// Test UI text generation
|
||||
let input = json!({
|
||||
"name": "test-channel",
|
||||
"visibility": "members"
|
||||
});
|
||||
|
||||
let ui_text = tool.ui_text(&input);
|
||||
assert_eq!(
|
||||
ui_text,
|
||||
"Create channel 'test-channel' (visibility: members)"
|
||||
);
|
||||
|
||||
// Test needs_confirmation
|
||||
cx.update(|cx| {
|
||||
assert!(!tool.needs_confirmation(&input, cx));
|
||||
});
|
||||
|
||||
// Test with parent channel
|
||||
let input_with_parent = json!({
|
||||
"name": "sub-channel",
|
||||
"parent": "parent-channel",
|
||||
"visibility": "public"
|
||||
});
|
||||
|
||||
let ui_text = tool.ui_text(&input_with_parent);
|
||||
assert_eq!(
|
||||
ui_text,
|
||||
"Create channel 'sub-channel' under 'parent-channel' (visibility: public)"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_move_channel_tool(cx: &mut TestAppContext) {
|
||||
// Create minimal setup
|
||||
cx.update(|cx| {
|
||||
let settings_store = settings::SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
release_channel::init(gpui::SemanticVersion::default(), cx);
|
||||
client::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
|
||||
let http_client = http_client::FakeHttpClient::create(|_| async { unreachable!() });
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(clock::FakeSystemClock::new()),
|
||||
http_client.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
cx.update(|cx| {
|
||||
channel::init(&client, user_store.clone(), cx);
|
||||
});
|
||||
let channel_store = cx.update(|cx| channel::ChannelStore::global(cx));
|
||||
|
||||
let tool = Arc::new(MoveChannelTool::new(channel_store));
|
||||
|
||||
// Test schema
|
||||
let schema = tool
|
||||
.input_schema(language_model::LanguageModelToolSchemaFormat::JsonSchema)
|
||||
.unwrap();
|
||||
assert!(schema.is_object());
|
||||
|
||||
// Test UI text generation
|
||||
let input = json!({
|
||||
"channel": "child",
|
||||
"to": "new-parent"
|
||||
});
|
||||
|
||||
let ui_text = tool.ui_text(&input);
|
||||
assert_eq!(ui_text, "Move channel 'child' to 'new-parent'");
|
||||
|
||||
// Test moving to root
|
||||
let input_to_root = json!({
|
||||
"channel": "child",
|
||||
"to": null
|
||||
});
|
||||
|
||||
let ui_text = tool.ui_text(&input_to_root);
|
||||
assert_eq!(ui_text, "Move channel 'child' to root");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_reorder_channel_tool(cx: &mut TestAppContext) {
|
||||
// Create minimal setup
|
||||
cx.update(|cx| {
|
||||
let settings_store = settings::SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
release_channel::init(gpui::SemanticVersion::default(), cx);
|
||||
client::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
|
||||
let http_client = http_client::FakeHttpClient::create(|_| async { unreachable!() });
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(clock::FakeSystemClock::new()),
|
||||
http_client.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
cx.update(|cx| {
|
||||
channel::init(&client, user_store.clone(), cx);
|
||||
});
|
||||
let channel_store = cx.update(|cx| channel::ChannelStore::global(cx));
|
||||
|
||||
let tool = Arc::new(ReorderChannelTool::new(channel_store));
|
||||
|
||||
// Test schema
|
||||
let schema = tool
|
||||
.input_schema(language_model::LanguageModelToolSchemaFormat::JsonSchema)
|
||||
.unwrap();
|
||||
assert!(schema.is_object());
|
||||
|
||||
// Test UI text generation
|
||||
let input = json!({
|
||||
"channel": "test-channel",
|
||||
"direction": "up"
|
||||
});
|
||||
|
||||
let ui_text = tool.ui_text(&input);
|
||||
assert_eq!(ui_text, "Move channel 'test-channel' up");
|
||||
|
||||
// Test with down direction
|
||||
let input_down = json!({
|
||||
"channel": "test-channel",
|
||||
"direction": "down"
|
||||
});
|
||||
|
||||
let ui_text = tool.ui_text(&input_down);
|
||||
assert_eq!(ui_text, "Move channel 'test-channel' down");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edit_channel_notes_tool(cx: &mut TestAppContext) {
|
||||
// Create minimal setup
|
||||
cx.update(|cx| {
|
||||
let settings_store = settings::SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
release_channel::init(gpui::SemanticVersion::default(), cx);
|
||||
client::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
|
||||
let http_client = http_client::FakeHttpClient::create(|_| async { unreachable!() });
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(clock::FakeSystemClock::new()),
|
||||
http_client.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
cx.update(|cx| {
|
||||
channel::init(&client, user_store.clone(), cx);
|
||||
});
|
||||
let channel_store = cx.update(|cx| channel::ChannelStore::global(cx));
|
||||
|
||||
let tool = Arc::new(EditChannelNotesTool::new(channel_store));
|
||||
|
||||
// Test schema
|
||||
let schema = tool
|
||||
.input_schema(language_model::LanguageModelToolSchemaFormat::JsonSchema)
|
||||
.unwrap();
|
||||
assert!(schema.is_object());
|
||||
|
||||
// Test UI text generation - create
|
||||
let input_create = json!({
|
||||
"channel": "test-channel",
|
||||
"edits": [
|
||||
{
|
||||
"kind": "create",
|
||||
"content": "# Welcome\n\nChannel notes here."
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let ui_text = tool.ui_text(&input_create);
|
||||
assert_eq!(ui_text, "Create notes for channel 'test-channel'");
|
||||
|
||||
// Test UI text generation - edit
|
||||
let input_edit = json!({
|
||||
"channel": "test-channel",
|
||||
"edits": [
|
||||
{
|
||||
"kind": "edit",
|
||||
"content": "Updated content"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let ui_text = tool.ui_text(&input_edit);
|
||||
assert_eq!(ui_text, "Edit notes for channel 'test-channel'");
|
||||
|
||||
// Test UI text generation - append
|
||||
let input_append = json!({
|
||||
"channel": "test-channel",
|
||||
"edits": [
|
||||
{
|
||||
"kind": "append",
|
||||
"content": "\n\n## New Section"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let ui_text = tool.ui_text(&input_append);
|
||||
assert_eq!(ui_text, "Append to notes for channel 'test-channel'");
|
||||
|
||||
// Test multiple edits
|
||||
let input_multiple = json!({
|
||||
"channel": "test-channel",
|
||||
"edits": [
|
||||
{
|
||||
"kind": "edit",
|
||||
"content": "Edit 1"
|
||||
},
|
||||
{
|
||||
"kind": "append",
|
||||
"content": "Edit 2"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
let ui_text = tool.ui_text(&input_multiple);
|
||||
assert_eq!(
|
||||
ui_text,
|
||||
"Apply multiple edits to notes for channel 'test-channel'"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_channel_tools_confirmation(cx: &mut TestAppContext) {
|
||||
// Create minimal setup
|
||||
cx.update(|cx| {
|
||||
let settings_store = settings::SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
release_channel::init(gpui::SemanticVersion::default(), cx);
|
||||
client::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
|
||||
let http_client = http_client::FakeHttpClient::create(|_| async { unreachable!() });
|
||||
let client = cx.update(|cx| {
|
||||
Client::new(
|
||||
Arc::new(clock::FakeSystemClock::new()),
|
||||
http_client.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
|
||||
cx.update(|cx| {
|
||||
channel::init(&client, user_store.clone(), cx);
|
||||
});
|
||||
let channel_store = cx.update(|cx| channel::ChannelStore::global(cx));
|
||||
|
||||
// Test that move tool requires confirmation
|
||||
let move_tool = Arc::new(MoveChannelTool::new(channel_store.clone()));
|
||||
let input = json!({
|
||||
"channel": "important-channel",
|
||||
"to": "new-location"
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
assert!(move_tool.needs_confirmation(&input, cx));
|
||||
});
|
||||
|
||||
// Test that create tool doesn't require confirmation
|
||||
let create_tool = Arc::new(CreateChannelTool::new(channel_store));
|
||||
let input = json!({
|
||||
"name": "new-channel",
|
||||
"visibility": "members"
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
assert!(!create_tool.needs_confirmation(&input, cx));
|
||||
});
|
||||
}
|
||||
165
crates/channel_tools/src/create_channel_tool.rs
Normal file
165
crates/channel_tools/src/create_channel_tool.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
use super::{ChannelVisibility, find_channel_by_name};
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{Tool, ToolResult, ToolResultOutput};
|
||||
use channel::ChannelStore;
|
||||
use gpui::{App, Entity, Task};
|
||||
use icons::IconName;
|
||||
use language_model::{LanguageModelRequest, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct CreateChannelTool {
|
||||
channel_store: Entity<ChannelStore>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
struct CreateChannelInput {
|
||||
/// The name of the channel to create
|
||||
name: String,
|
||||
/// The name of the parent channel (optional, if not provided creates a root channel)
|
||||
#[serde(default)]
|
||||
parent: Option<String>,
|
||||
/// The visibility of the channel: "members" (default) or "public"
|
||||
#[serde(default = "default_visibility")]
|
||||
visibility: String,
|
||||
}
|
||||
|
||||
fn default_visibility() -> String {
|
||||
"members".to_string()
|
||||
}
|
||||
|
||||
impl CreateChannelTool {
|
||||
pub fn new(channel_store: Entity<ChannelStore>) -> Self {
|
||||
Self { channel_store }
|
||||
}
|
||||
}
|
||||
|
||||
impl Tool for CreateChannelTool {
|
||||
fn name(&self) -> String {
|
||||
"create_channel".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Create a new channel in the workspace".to_string()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Hash
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
let schema = schemars::schema_for!(CreateChannelInput);
|
||||
let mut json = serde_json::to_value(schema)?;
|
||||
|
||||
match format {
|
||||
LanguageModelToolSchemaFormat::JsonSchema => Ok(json),
|
||||
LanguageModelToolSchemaFormat::JsonSchemaSubset => {
|
||||
assistant_tool::adapt_schema_to_format(&mut json, format)?;
|
||||
Ok(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
let Ok(input) = serde_json::from_value::<CreateChannelInput>(input.clone()) else {
|
||||
return "Create channel (invalid input)".to_string();
|
||||
};
|
||||
|
||||
if let Some(parent) = &input.parent {
|
||||
format!(
|
||||
"Create channel '{}' under '{}' (visibility: {})",
|
||||
input.name, parent, input.visibility
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Create channel '{}' (visibility: {})",
|
||||
input.name, input.visibility
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_request: Arc<LanguageModelRequest>,
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<assistant_tool::ActionLog>,
|
||||
_model: Arc<dyn language_model::LanguageModel>,
|
||||
_window: Option<gpui::AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input: CreateChannelInput = match serde_json::from_value(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => {
|
||||
return ToolResult::from(Task::ready(Err(anyhow!("Invalid input: {}", err))));
|
||||
}
|
||||
};
|
||||
|
||||
let visibility = match ChannelVisibility::from_str(&input.visibility) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return ToolResult::from(Task::ready(Err(anyhow!(
|
||||
"Invalid visibility '{}'. Use 'members' or 'public'",
|
||||
input.visibility
|
||||
))));
|
||||
}
|
||||
};
|
||||
|
||||
let channel_store = self.channel_store.clone();
|
||||
let name = input.name.clone();
|
||||
let parent_name = input.parent.clone();
|
||||
|
||||
// Find parent channel if specified
|
||||
let parent_id = if let Some(parent_name) = &parent_name {
|
||||
match find_channel_by_name(&channel_store, parent_name, cx) {
|
||||
Some((id, _)) => Some(id),
|
||||
None => {
|
||||
return ToolResult::from(Task::ready(Err(anyhow!(
|
||||
"Parent channel '{}' not found",
|
||||
parent_name
|
||||
))));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let task = cx.spawn(async move |cx| {
|
||||
let create_task = cx.update(|cx| {
|
||||
channel_store.update(cx, |store, cx| store.create_channel(&name, parent_id, cx))
|
||||
})?;
|
||||
|
||||
let channel_id = create_task.await?;
|
||||
|
||||
// Set visibility if not default
|
||||
if visibility == ChannelVisibility::Public {
|
||||
let visibility_task = cx.update(|cx| {
|
||||
channel_store.update(cx, |store, cx| {
|
||||
store.set_channel_visibility(
|
||||
channel_id,
|
||||
rpc::proto::ChannelVisibility::Public,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?;
|
||||
visibility_task.await?;
|
||||
}
|
||||
|
||||
let message = if let Some(parent_name) = parent_name {
|
||||
format!("Created channel '{}' under '{}'", name, parent_name)
|
||||
} else {
|
||||
format!("Created channel '{}'", name)
|
||||
};
|
||||
|
||||
Ok(ToolResultOutput::from(message))
|
||||
});
|
||||
|
||||
ToolResult::from(task)
|
||||
}
|
||||
}
|
||||
211
crates/channel_tools/src/edit_channel_notes_tool.rs
Normal file
211
crates/channel_tools/src/edit_channel_notes_tool.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
use super::find_channel_by_name;
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{Tool, ToolResult, ToolResultContent, ToolResultOutput};
|
||||
use channel::{ChannelBuffer, ChannelStore};
|
||||
use gpui::{App, Entity, Task};
|
||||
use icons::IconName;
|
||||
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use text::Point;
|
||||
|
||||
pub struct EditChannelNotesTool {
|
||||
channel_store: Entity<ChannelStore>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
struct EditChannelNotesInput {
|
||||
/// The name of the channel whose notes to edit
|
||||
channel: String,
|
||||
/// The edits to apply to the channel notes
|
||||
edits: Vec<ChannelNotesEdit>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
struct ChannelNotesEdit {
|
||||
/// The kind of edit: "create", "edit", or "append"
|
||||
kind: EditKind,
|
||||
/// The content to insert, replace, or append
|
||||
content: String,
|
||||
/// For "edit" kind: the range to replace (optional, defaults to entire buffer)
|
||||
#[serde(default)]
|
||||
range: Option<EditRange>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum EditKind {
|
||||
Create,
|
||||
Edit,
|
||||
Append,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
struct EditRange {
|
||||
/// Starting line (0-based)
|
||||
start_line: u32,
|
||||
/// Starting column (0-based)
|
||||
start_column: u32,
|
||||
/// Ending line (0-based)
|
||||
end_line: u32,
|
||||
/// Ending column (0-based)
|
||||
end_column: u32,
|
||||
}
|
||||
|
||||
impl EditChannelNotesTool {
|
||||
pub fn new(channel_store: Entity<ChannelStore>) -> Self {
|
||||
Self { channel_store }
|
||||
}
|
||||
|
||||
fn apply_edits(
|
||||
channel_buffer: &Entity<ChannelBuffer>,
|
||||
edits: Vec<ChannelNotesEdit>,
|
||||
cx: &mut App,
|
||||
) -> Result<()> {
|
||||
channel_buffer.update(cx, |channel_buffer, cx| {
|
||||
let buffer = channel_buffer.buffer();
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
for edit in edits {
|
||||
match edit.kind {
|
||||
EditKind::Create => {
|
||||
// Replace entire content
|
||||
let len = buffer.len();
|
||||
buffer.edit([(0..len, edit.content)], None, cx);
|
||||
}
|
||||
EditKind::Edit => {
|
||||
if let Some(range) = edit.range {
|
||||
let start = Point::new(range.start_line, range.start_column);
|
||||
let end = Point::new(range.end_line, range.end_column);
|
||||
let start_offset = buffer.point_to_offset(start);
|
||||
let end_offset = buffer.point_to_offset(end);
|
||||
buffer.edit([(start_offset..end_offset, edit.content)], None, cx);
|
||||
} else {
|
||||
// Replace entire content if no range specified
|
||||
let len = buffer.len();
|
||||
buffer.edit([(0..len, edit.content)], None, cx);
|
||||
}
|
||||
}
|
||||
EditKind::Append => {
|
||||
let len = buffer.len();
|
||||
buffer.edit([(len..len, edit.content)], None, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Acknowledge the buffer version to sync changes
|
||||
channel_buffer.acknowledge_buffer_version(cx);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Tool for EditChannelNotesTool {
|
||||
fn name(&self) -> String {
|
||||
"edit_channel_notes".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Edit the notes of a channel collaboratively".to_string()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::FileText
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
json_schema_for::<EditChannelNotesInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
let Ok(input) = serde_json::from_value::<EditChannelNotesInput>(input.clone()) else {
|
||||
return "Edit channel notes (invalid input)".to_string();
|
||||
};
|
||||
|
||||
let action = if input.edits.len() == 1 {
|
||||
match &input.edits[0].kind {
|
||||
EditKind::Create => "Create notes for",
|
||||
EditKind::Edit => "Edit notes for",
|
||||
EditKind::Append => "Append to notes for",
|
||||
}
|
||||
} else {
|
||||
"Apply multiple edits to notes for"
|
||||
};
|
||||
|
||||
format!("{} channel '{}'", action, input.channel)
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_request: Arc<LanguageModelRequest>,
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<assistant_tool::ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<gpui::AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input: EditChannelNotesInput = match serde_json::from_value(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => {
|
||||
return ToolResult::from(Task::ready(Err(anyhow!("Invalid input: {}", err))));
|
||||
}
|
||||
};
|
||||
|
||||
if input.edits.is_empty() {
|
||||
return ToolResult::from(Task::ready(Err(anyhow!("No edits provided"))));
|
||||
}
|
||||
|
||||
let channel_store = self.channel_store.clone();
|
||||
let channel_name = input.channel.clone();
|
||||
let edits = input.edits;
|
||||
|
||||
// Find the channel
|
||||
let (channel_id, _) = match find_channel_by_name(&channel_store, &channel_name, cx) {
|
||||
Some(channel) => channel,
|
||||
None => {
|
||||
return ToolResult::from(Task::ready(Err(anyhow!(
|
||||
"Channel '{}' not found",
|
||||
channel_name
|
||||
))));
|
||||
}
|
||||
};
|
||||
|
||||
let task = cx.spawn(async move |cx| {
|
||||
// Open the channel buffer
|
||||
let channel_buffer = cx
|
||||
.update(|cx| {
|
||||
channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx))
|
||||
})?
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to open channel buffer: {}", e))?;
|
||||
|
||||
// Check if the buffer is connected
|
||||
cx.update(|cx| {
|
||||
if !channel_buffer.read(cx).is_connected() {
|
||||
return Err(anyhow!("Channel buffer is not connected"));
|
||||
}
|
||||
Ok(())
|
||||
})??;
|
||||
|
||||
// Apply the edits
|
||||
cx.update(|cx| Self::apply_edits(&channel_buffer, edits, cx))??;
|
||||
|
||||
let message = format!("Edited notes for channel '{}'", channel_name);
|
||||
Ok(ToolResultOutput {
|
||||
content: ToolResultContent::Text(message),
|
||||
output: None,
|
||||
})
|
||||
});
|
||||
|
||||
ToolResult::from(task)
|
||||
}
|
||||
}
|
||||
177
crates/channel_tools/src/list_channels_tool.rs
Normal file
177
crates/channel_tools/src/list_channels_tool.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{Tool, ToolResult, ToolResultContent, ToolResultOutput};
|
||||
use channel::ChannelStore;
|
||||
use gpui::{App, Entity};
|
||||
use icons::IconName;
|
||||
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct ListChannelsTool {
|
||||
channel_store: Entity<ChannelStore>,
|
||||
}
|
||||
|
||||
impl ListChannelsTool {
|
||||
pub fn new(channel_store: Entity<ChannelStore>) -> Self {
|
||||
Self { channel_store }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ListChannelsInput {
|
||||
/// Optional filter to show only channels with names containing this string (case-insensitive)
|
||||
#[serde(default)]
|
||||
filter: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ChannelInfo {
|
||||
id: u64,
|
||||
name: String,
|
||||
parent_path: Vec<String>,
|
||||
visibility: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
participant_count: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ListChannelsOutput {
|
||||
channels: Vec<ChannelInfo>,
|
||||
total_count: usize,
|
||||
}
|
||||
|
||||
impl Tool for ListChannelsTool {
|
||||
fn name(&self) -> String {
|
||||
"list_channels".to_string()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"List available channels in the workspace".to_string()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Hash
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
json_schema_for::<ListChannelsInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
let input = serde_json::from_value::<ListChannelsInput>(input.clone())
|
||||
.unwrap_or(ListChannelsInput { filter: None });
|
||||
|
||||
let mut text = "Listing channels".to_string();
|
||||
if let Some(ref filter) = input.filter {
|
||||
text.push_str(&format!(" containing '{}'", filter));
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_request: Arc<LanguageModelRequest>,
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<assistant_tool::ActionLog>,
|
||||
_model: Arc<dyn LanguageModel>,
|
||||
_window: Option<gpui::AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let channel_store = self.channel_store.clone();
|
||||
|
||||
let Ok(input) = serde_json::from_value::<ListChannelsInput>(input) else {
|
||||
return ToolResult::from(gpui::Task::ready(Err(anyhow::anyhow!(
|
||||
"Invalid input parameters"
|
||||
))));
|
||||
};
|
||||
|
||||
let filter = input.filter.clone();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let channels = cx
|
||||
.update(|cx| {
|
||||
let store = channel_store.read(cx);
|
||||
let mut channel_infos = Vec::new();
|
||||
|
||||
for channel in store.channels() {
|
||||
// Apply filter if provided
|
||||
if let Some(ref filter_text) = filter {
|
||||
if !channel
|
||||
.name
|
||||
.to_lowercase()
|
||||
.contains(&filter_text.to_lowercase())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Build parent path names
|
||||
let mut parent_path_names = Vec::new();
|
||||
for parent_id in &channel.parent_path {
|
||||
if let Some(parent) = store.channel_for_id(*parent_id) {
|
||||
parent_path_names.push(parent.name.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let visibility = match channel.visibility {
|
||||
rpc::proto::ChannelVisibility::Public => "public",
|
||||
rpc::proto::ChannelVisibility::Members => "members",
|
||||
};
|
||||
|
||||
let participant_count = Some(store.channel_participants(channel.id).len());
|
||||
|
||||
channel_infos.push(ChannelInfo {
|
||||
id: channel.id.0,
|
||||
name: channel.name.to_string(),
|
||||
parent_path: parent_path_names,
|
||||
visibility: visibility.to_string(),
|
||||
participant_count,
|
||||
});
|
||||
}
|
||||
|
||||
channel_infos
|
||||
})
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
let total_count = channels.len();
|
||||
let output = ListChannelsOutput {
|
||||
channels,
|
||||
total_count,
|
||||
};
|
||||
|
||||
let mut text = String::new();
|
||||
if output.channels.is_empty() {
|
||||
text.push_str("No channels found");
|
||||
} else {
|
||||
text.push_str(&format!("Found {} channels:\n\n", output.total_count));
|
||||
for channel in &output.channels {
|
||||
if !channel.parent_path.is_empty() {
|
||||
text.push_str(&channel.parent_path.join(" > "));
|
||||
text.push_str(" > ");
|
||||
}
|
||||
text.push_str(&channel.name);
|
||||
text.push_str(&format!(" ({})", channel.visibility));
|
||||
if let Some(count) = channel.participant_count {
|
||||
text.push_str(&format!(" - {} participants", count));
|
||||
}
|
||||
text.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ToolResultOutput {
|
||||
content: ToolResultContent::Text(text),
|
||||
output: Some(serde_json::to_value(output).unwrap_or(serde_json::Value::Null)),
|
||||
})
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
160
crates/channel_tools/src/move_channel_tool.rs
Normal file
160
crates/channel_tools/src/move_channel_tool.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use super::find_channel_by_name;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{Tool, ToolResult, ToolResultOutput};
|
||||
use channel::ChannelStore;
|
||||
use gpui::{App, Entity, Task};
|
||||
use icons::IconName;
|
||||
use language_model::{LanguageModelRequest, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct MoveChannelTool {
|
||||
channel_store: Entity<ChannelStore>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
struct MoveChannelInput {
|
||||
/// The name of the channel to move
|
||||
channel: String,
|
||||
/// The name of the new parent channel (null to move to root)
|
||||
to: Option<String>,
|
||||
}
|
||||
|
||||
impl MoveChannelTool {
|
||||
pub fn new(channel_store: Entity<ChannelStore>) -> Self {
|
||||
Self { channel_store }
|
||||
}
|
||||
}
|
||||
|
||||
impl Tool for MoveChannelTool {
|
||||
fn name(&self) -> String {
|
||||
"move_channel".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Move a channel to a different parent or to the root level".to_string()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::FolderOpen
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
|
||||
// Moving channels is a significant operation that should be confirmed
|
||||
true
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
let schema = schemars::schema_for!(MoveChannelInput);
|
||||
let mut json = serde_json::to_value(schema)?;
|
||||
|
||||
match format {
|
||||
LanguageModelToolSchemaFormat::JsonSchema => Ok(json),
|
||||
LanguageModelToolSchemaFormat::JsonSchemaSubset => {
|
||||
assistant_tool::adapt_schema_to_format(&mut json, format)?;
|
||||
Ok(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
let Ok(input) = serde_json::from_value::<MoveChannelInput>(input.clone()) else {
|
||||
return "Move channel (invalid input)".to_string();
|
||||
};
|
||||
|
||||
if let Some(to) = &input.to {
|
||||
format!("Move channel '{}' to '{}'", input.channel, to)
|
||||
} else {
|
||||
format!("Move channel '{}' to root", input.channel)
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_request: Arc<LanguageModelRequest>,
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<assistant_tool::ActionLog>,
|
||||
_model: Arc<dyn language_model::LanguageModel>,
|
||||
_window: Option<gpui::AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input: MoveChannelInput = match serde_json::from_value(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => {
|
||||
return ToolResult::from(Task::ready(Err(anyhow!("Invalid input: {}", err))));
|
||||
}
|
||||
};
|
||||
|
||||
let channel_store = self.channel_store.clone();
|
||||
let channel_name = input.channel.clone();
|
||||
let to_name = input.to.clone();
|
||||
|
||||
// Find the channel to move
|
||||
let (channel_id, _) = match find_channel_by_name(&channel_store, &channel_name, cx) {
|
||||
Some(channel) => channel,
|
||||
None => {
|
||||
return ToolResult::from(Task::ready(Err(anyhow!(
|
||||
"Channel '{}' not found",
|
||||
channel_name
|
||||
))));
|
||||
}
|
||||
};
|
||||
|
||||
// Find the target parent channel if specified
|
||||
let new_parent_id = if let Some(to_name) = &to_name {
|
||||
match find_channel_by_name(&channel_store, to_name, cx) {
|
||||
Some((id, _)) => Some(id),
|
||||
None => {
|
||||
return ToolResult::from(Task::ready(Err(anyhow!(
|
||||
"Target channel '{}' not found",
|
||||
to_name
|
||||
))));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Check if we're trying to move a channel to itself or its descendant
|
||||
if let Some(new_parent_id) = new_parent_id {
|
||||
if channel_id == new_parent_id {
|
||||
return ToolResult::from(Task::ready(Err(anyhow!(
|
||||
"Cannot move channel to itself"
|
||||
))));
|
||||
}
|
||||
|
||||
// Check if new parent is a descendant of the channel being moved
|
||||
let store = channel_store.read(cx);
|
||||
if let Some(new_parent) = store.channel_for_id(new_parent_id) {
|
||||
if new_parent.parent_path.contains(&channel_id) {
|
||||
return ToolResult::from(Task::ready(Err(anyhow!(
|
||||
"Cannot move channel to one of its descendants"
|
||||
))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let task = cx.spawn(async move |cx| {
|
||||
// Check if we're trying to move to root
|
||||
let new_parent_id = new_parent_id
|
||||
.ok_or_else(|| anyhow!("Moving channels to root is not currently supported"))?;
|
||||
|
||||
let move_task = cx.update(|cx| {
|
||||
channel_store.update(cx, |store, cx| {
|
||||
store.move_channel(channel_id, new_parent_id, cx)
|
||||
})
|
||||
})?;
|
||||
|
||||
move_task.await?;
|
||||
|
||||
let message = format!("Moved channel '{}' to '{}'", channel_name, to_name.unwrap());
|
||||
|
||||
Ok(ToolResultOutput::from(message))
|
||||
});
|
||||
|
||||
ToolResult::from(task)
|
||||
}
|
||||
}
|
||||
129
crates/channel_tools/src/reorder_channel_tool.rs
Normal file
129
crates/channel_tools/src/reorder_channel_tool.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
use super::find_channel_by_name;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{Tool, ToolResult, ToolResultOutput};
|
||||
use channel::ChannelStore;
|
||||
use gpui::{App, Entity, Task};
|
||||
use icons::IconName;
|
||||
use language_model::{LanguageModelRequest, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct ReorderChannelTool {
|
||||
channel_store: Entity<ChannelStore>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
struct ReorderChannelInput {
|
||||
/// The name of the channel to reorder
|
||||
channel: String,
|
||||
/// The direction to move the channel: "up" or "down"
|
||||
direction: String,
|
||||
}
|
||||
|
||||
impl ReorderChannelTool {
|
||||
pub fn new(channel_store: Entity<ChannelStore>) -> Self {
|
||||
Self { channel_store }
|
||||
}
|
||||
}
|
||||
|
||||
impl Tool for ReorderChannelTool {
|
||||
fn name(&self) -> String {
|
||||
"reorder_channel".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Move a channel up or down in the list among its siblings".to_string()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::ListTree
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
let schema = schemars::schema_for!(ReorderChannelInput);
|
||||
let mut json = serde_json::to_value(schema)?;
|
||||
|
||||
match format {
|
||||
LanguageModelToolSchemaFormat::JsonSchema => Ok(json),
|
||||
LanguageModelToolSchemaFormat::JsonSchemaSubset => {
|
||||
assistant_tool::adapt_schema_to_format(&mut json, format)?;
|
||||
Ok(json)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
let Ok(input) = serde_json::from_value::<ReorderChannelInput>(input.clone()) else {
|
||||
return "Reorder channel (invalid input)".to_string();
|
||||
};
|
||||
|
||||
format!("Move channel '{}' {}", input.channel, input.direction)
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_request: Arc<LanguageModelRequest>,
|
||||
_project: Entity<Project>,
|
||||
_action_log: Entity<assistant_tool::ActionLog>,
|
||||
_model: Arc<dyn language_model::LanguageModel>,
|
||||
_window: Option<gpui::AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input: ReorderChannelInput = match serde_json::from_value(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => {
|
||||
return ToolResult::from(Task::ready(Err(anyhow!("Invalid input: {}", err))));
|
||||
}
|
||||
};
|
||||
|
||||
let channel_store = self.channel_store.clone();
|
||||
let channel_name = input.channel.clone();
|
||||
let direction_str = input.direction.to_lowercase();
|
||||
|
||||
// Parse direction
|
||||
let direction = match direction_str.as_str() {
|
||||
"up" => rpc::proto::reorder_channel::Direction::Up,
|
||||
"down" => rpc::proto::reorder_channel::Direction::Down,
|
||||
_ => {
|
||||
return ToolResult::from(Task::ready(Err(anyhow!(
|
||||
"Invalid direction '{}'. Use 'up' or 'down'",
|
||||
input.direction
|
||||
))));
|
||||
}
|
||||
};
|
||||
|
||||
// Find the channel to reorder
|
||||
let (channel_id, _) = match find_channel_by_name(&channel_store, &channel_name, cx) {
|
||||
Some(channel) => channel,
|
||||
None => {
|
||||
return ToolResult::from(Task::ready(Err(anyhow!(
|
||||
"Channel '{}' not found",
|
||||
channel_name
|
||||
))));
|
||||
}
|
||||
};
|
||||
|
||||
let task = cx.spawn(async move |cx| {
|
||||
let reorder_task = cx.update(|cx| {
|
||||
channel_store.update(cx, |store, cx| {
|
||||
store.reorder_channel(channel_id, direction, cx)
|
||||
})
|
||||
})?;
|
||||
|
||||
reorder_task.await?;
|
||||
|
||||
let message = format!("Moved channel '{}' {}", channel_name, direction_str);
|
||||
|
||||
Ok(ToolResultOutput::from(message))
|
||||
});
|
||||
|
||||
ToolResult::from(task)
|
||||
}
|
||||
}
|
||||
78
crates/channel_tools/src/schema.rs
Normal file
78
crates/channel_tools/src/schema.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use anyhow::Result;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
use schemars::{
|
||||
JsonSchema,
|
||||
schema::{RootSchema, Schema, SchemaObject},
|
||||
};
|
||||
|
||||
pub fn json_schema_for<T: JsonSchema>(
|
||||
format: LanguageModelToolSchemaFormat,
|
||||
) -> Result<serde_json::Value> {
|
||||
let schema = root_schema_for::<T>(format);
|
||||
schema_to_json(&schema, format)
|
||||
}
|
||||
|
||||
fn schema_to_json(
|
||||
schema: &RootSchema,
|
||||
format: LanguageModelToolSchemaFormat,
|
||||
) -> Result<serde_json::Value> {
|
||||
let mut value = serde_json::to_value(schema)?;
|
||||
assistant_tool::adapt_schema_to_format(&mut value, format)?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> RootSchema {
|
||||
let mut generator = match format {
|
||||
LanguageModelToolSchemaFormat::JsonSchema => schemars::SchemaGenerator::default(),
|
||||
LanguageModelToolSchemaFormat::JsonSchemaSubset => {
|
||||
schemars::r#gen::SchemaSettings::default()
|
||||
.with(|settings| {
|
||||
settings.meta_schema = None;
|
||||
settings.inline_subschemas = true;
|
||||
settings
|
||||
.visitors
|
||||
.push(Box::new(TransformToJsonSchemaSubsetVisitor));
|
||||
})
|
||||
.into_generator()
|
||||
}
|
||||
};
|
||||
generator.root_schema_for::<T>()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TransformToJsonSchemaSubsetVisitor;
|
||||
|
||||
impl schemars::visit::Visitor for TransformToJsonSchemaSubsetVisitor {
|
||||
fn visit_root_schema(&mut self, root: &mut RootSchema) {
|
||||
schemars::visit::visit_root_schema(self, root)
|
||||
}
|
||||
|
||||
fn visit_schema(&mut self, schema: &mut Schema) {
|
||||
schemars::visit::visit_schema(self, schema)
|
||||
}
|
||||
|
||||
fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
|
||||
// Ensure that the type field is not an array, this happens when we use
|
||||
// Option<T>, the type will be [T, "null"].
|
||||
if let Some(instance_type) = schema.instance_type.take() {
|
||||
schema.instance_type = match instance_type {
|
||||
schemars::schema::SingleOrVec::Single(t) => {
|
||||
Some(schemars::schema::SingleOrVec::Single(t))
|
||||
}
|
||||
schemars::schema::SingleOrVec::Vec(items) => items
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(schemars::schema::SingleOrVec::from),
|
||||
};
|
||||
}
|
||||
|
||||
// One of is not supported, use anyOf instead.
|
||||
if let Some(subschema) = schema.subschemas.as_mut() {
|
||||
if let Some(one_of) = subschema.one_of.take() {
|
||||
subschema.any_of = Some(one_of);
|
||||
}
|
||||
}
|
||||
|
||||
schemars::visit::visit_schema_object(self, schema)
|
||||
}
|
||||
}
|
||||
211
crates/channel_tools/src/streaming_edit_channel_notes_tool.rs
Normal file
211
crates/channel_tools/src/streaming_edit_channel_notes_tool.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
use super::find_channel_by_name;
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Result, anyhow};
|
||||
use assistant_tool::{Tool, ToolResult, ToolResultContent, ToolResultOutput};
|
||||
use assistant_tools::{EditAgent, EditAgentOutputEvent, Templates};
|
||||
use channel::ChannelStore;
|
||||
use futures::StreamExt;
|
||||
use gpui::{App, Entity, Task};
|
||||
use icons::IconName;
|
||||
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct StreamingEditChannelNotesTool {
|
||||
channel_store: Entity<ChannelStore>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
struct StreamingEditChannelNotesInput {
|
||||
/// The name of the channel whose notes to edit
|
||||
channel: String,
|
||||
/// A one-line, user-friendly markdown description of the edit.
|
||||
/// Be terse, but also descriptive in what you want to achieve with this edit.
|
||||
display_description: String,
|
||||
/// The edit mode: "edit" to modify existing content, "overwrite" to replace entire content
|
||||
#[serde(default)]
|
||||
mode: EditMode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema, Default, Clone, Copy)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
enum EditMode {
|
||||
#[default]
|
||||
Edit,
|
||||
Overwrite,
|
||||
}
|
||||
|
||||
impl StreamingEditChannelNotesTool {
|
||||
pub fn new(channel_store: Entity<ChannelStore>) -> Self {
|
||||
Self { channel_store }
|
||||
}
|
||||
}
|
||||
|
||||
impl Tool for StreamingEditChannelNotesTool {
|
||||
fn name(&self) -> String {
|
||||
"streaming_edit_channel_notes".to_string()
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
"Edit channel notes using AI-powered streaming edits for efficient collaborative editing"
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::FileText
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
json_schema_for::<StreamingEditChannelNotesInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
let Ok(input) = serde_json::from_value::<StreamingEditChannelNotesInput>(input.clone())
|
||||
else {
|
||||
return "Streaming edit channel notes (invalid input)".to_string();
|
||||
};
|
||||
|
||||
if input.display_description.is_empty() {
|
||||
format!("Streaming edit notes for channel '{}'", input.channel)
|
||||
} else {
|
||||
format!(
|
||||
"{} in channel '{}'",
|
||||
input.display_description, input.channel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
request: Arc<LanguageModelRequest>,
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<assistant_tool::ActionLog>,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
_window: Option<gpui::AnyWindowHandle>,
|
||||
cx: &mut App,
|
||||
) -> ToolResult {
|
||||
let input: StreamingEditChannelNotesInput = match serde_json::from_value(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => {
|
||||
return ToolResult::from(Task::ready(Err(anyhow!("Invalid input: {}", err))));
|
||||
}
|
||||
};
|
||||
|
||||
let channel_store = self.channel_store.clone();
|
||||
let channel_name = input.channel.clone();
|
||||
|
||||
// Find the channel
|
||||
let (channel_id, _) = match find_channel_by_name(&channel_store, &channel_name, cx) {
|
||||
Some(channel) => channel,
|
||||
None => {
|
||||
return ToolResult::from(Task::ready(Err(anyhow!(
|
||||
"Channel '{}' not found",
|
||||
channel_name
|
||||
))));
|
||||
}
|
||||
};
|
||||
|
||||
let task = cx.spawn(async move |mut cx| {
|
||||
// Open the channel buffer
|
||||
let channel_buffer = cx
|
||||
.update(|cx| {
|
||||
channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx))
|
||||
})?
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to open channel buffer: {}", e))?;
|
||||
|
||||
// Check if the buffer is connected
|
||||
cx.update(|cx| {
|
||||
if !channel_buffer.read(cx).is_connected() {
|
||||
return Err(anyhow!("Channel buffer is not connected"));
|
||||
}
|
||||
Ok(())
|
||||
})??;
|
||||
|
||||
// Get the language buffer from the channel buffer
|
||||
let buffer = cx.update(|cx| {
|
||||
channel_buffer.read_with(cx, |channel_buffer, _| channel_buffer.buffer().clone())
|
||||
})?;
|
||||
|
||||
// Create an EditAgent for streaming edits
|
||||
let edit_agent = EditAgent::new(model, project, action_log, Templates::new());
|
||||
|
||||
// Perform the edit using EditAgent
|
||||
let (output, mut events) = match input.mode {
|
||||
EditMode::Edit => {
|
||||
edit_agent.edit(buffer.clone(), input.display_description, &request, &mut cx)
|
||||
}
|
||||
EditMode::Overwrite => edit_agent.overwrite(
|
||||
buffer.clone(),
|
||||
input.display_description,
|
||||
&request,
|
||||
&mut cx,
|
||||
),
|
||||
};
|
||||
|
||||
// Process streaming events
|
||||
let mut edit_completed = false;
|
||||
let mut unresolved_range = false;
|
||||
while let Some(event) = events.next().await {
|
||||
match event {
|
||||
EditAgentOutputEvent::Edited => {
|
||||
edit_completed = true;
|
||||
// Acknowledge the buffer version to sync changes
|
||||
cx.update(|cx| {
|
||||
channel_buffer.update(cx, |channel_buffer, cx| {
|
||||
channel_buffer.acknowledge_buffer_version(cx);
|
||||
})
|
||||
})?;
|
||||
}
|
||||
EditAgentOutputEvent::UnresolvedEditRange => {
|
||||
unresolved_range = true;
|
||||
log::warn!("AI couldn't resolve edit range in channel notes");
|
||||
}
|
||||
EditAgentOutputEvent::ResolvingEditRange(_) => {
|
||||
// This is primarily for UI updates, which we don't need here
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _result = output.await?;
|
||||
|
||||
if !edit_completed && !unresolved_range {
|
||||
return Err(anyhow!("Edit was not completed successfully"));
|
||||
}
|
||||
|
||||
let message = if unresolved_range {
|
||||
format!(
|
||||
"Applied {} to channel '{}' (some ranges could not be resolved)",
|
||||
match input.mode {
|
||||
EditMode::Edit => "streaming edits",
|
||||
EditMode::Overwrite => "streaming overwrite",
|
||||
},
|
||||
channel_name
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Applied {} to notes for channel '{}'",
|
||||
match input.mode {
|
||||
EditMode::Edit => "streaming edits",
|
||||
EditMode::Overwrite => "streaming overwrite",
|
||||
},
|
||||
channel_name
|
||||
)
|
||||
};
|
||||
|
||||
Ok(ToolResultOutput {
|
||||
content: ToolResultContent::Text(message),
|
||||
output: None,
|
||||
})
|
||||
});
|
||||
|
||||
ToolResult::from(task)
|
||||
}
|
||||
}
|
||||
@@ -266,11 +266,14 @@ CREATE TABLE "channels" (
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"visibility" VARCHAR NOT NULL,
|
||||
"parent_path" TEXT NOT NULL,
|
||||
"requires_zed_cla" BOOLEAN NOT NULL DEFAULT FALSE
|
||||
"requires_zed_cla" BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
"channel_order" INTEGER NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE INDEX "index_channels_on_parent_path" ON "channels" ("parent_path");
|
||||
|
||||
CREATE INDEX "index_channels_on_parent_path_and_order" ON "channels" ("parent_path", "channel_order");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "channel_chat_participants" (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
"user_id" INTEGER NOT NULL REFERENCES users (id),
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
-- Add channel_order column to channels table with default value
|
||||
ALTER TABLE channels ADD COLUMN channel_order INTEGER NOT NULL DEFAULT 1;
|
||||
|
||||
-- Update channel_order for existing channels using ROW_NUMBER for deterministic ordering
|
||||
UPDATE channels
|
||||
SET channel_order = (
|
||||
SELECT ROW_NUMBER() OVER (
|
||||
PARTITION BY parent_path
|
||||
ORDER BY name, id
|
||||
)
|
||||
FROM channels c2
|
||||
WHERE c2.id = channels.id
|
||||
);
|
||||
|
||||
-- Create index for efficient ordering queries
|
||||
CREATE INDEX "index_channels_on_parent_path_and_order" ON "channels" ("parent_path", "channel_order");
|
||||
@@ -582,6 +582,7 @@ pub struct Channel {
|
||||
pub visibility: ChannelVisibility,
|
||||
/// parent_path is the channel ids from the root to this one (not including this one)
|
||||
pub parent_path: Vec<ChannelId>,
|
||||
pub channel_order: i32,
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
@@ -591,6 +592,7 @@ impl Channel {
|
||||
visibility: value.visibility,
|
||||
name: value.clone().name,
|
||||
parent_path: value.ancestors().collect(),
|
||||
channel_order: value.channel_order,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -600,8 +602,13 @@ impl Channel {
|
||||
name: self.name.clone(),
|
||||
visibility: self.visibility.into(),
|
||||
parent_path: self.parent_path.iter().map(|c| c.to_proto()).collect(),
|
||||
channel_order: self.channel_order,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn root_id(&self) -> ChannelId {
|
||||
self.parent_path.first().copied().unwrap_or(self.id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
|
||||
@@ -4,7 +4,7 @@ use rpc::{
|
||||
ErrorCode, ErrorCodeExt,
|
||||
proto::{ChannelBufferVersion, VectorClockEntry, channel_member::Kind},
|
||||
};
|
||||
use sea_orm::{DbBackend, TryGetableMany};
|
||||
use sea_orm::{ActiveValue, DbBackend, TryGetableMany};
|
||||
|
||||
impl Database {
|
||||
#[cfg(test)]
|
||||
@@ -59,16 +59,36 @@ impl Database {
|
||||
parent = Some(parent_channel);
|
||||
}
|
||||
|
||||
let parent_path = parent
|
||||
.as_ref()
|
||||
.map_or(String::new(), |parent| parent.path());
|
||||
|
||||
// Find the maximum channel_order among siblings to set the new channel at the end
|
||||
let max_order = channel::Entity::find()
|
||||
.filter(channel::Column::ParentPath.eq(&parent_path))
|
||||
.select_only()
|
||||
.column_as(channel::Column::ChannelOrder.max(), "max_order")
|
||||
.into_tuple::<Option<i32>>()
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.flatten()
|
||||
.unwrap_or(0);
|
||||
|
||||
log::info!(
|
||||
"Creating channel '{}' with parent_path='{}', max_order={}, new_order={}",
|
||||
name,
|
||||
parent_path,
|
||||
max_order,
|
||||
max_order + 1
|
||||
);
|
||||
|
||||
let channel = channel::ActiveModel {
|
||||
id: ActiveValue::NotSet,
|
||||
name: ActiveValue::Set(name.to_string()),
|
||||
visibility: ActiveValue::Set(ChannelVisibility::Members),
|
||||
parent_path: ActiveValue::Set(
|
||||
parent
|
||||
.as_ref()
|
||||
.map_or(String::new(), |parent| parent.path()),
|
||||
),
|
||||
parent_path: ActiveValue::Set(parent_path),
|
||||
requires_zed_cla: ActiveValue::NotSet,
|
||||
channel_order: ActiveValue::Set(max_order + 1),
|
||||
}
|
||||
.insert(&*tx)
|
||||
.await?;
|
||||
@@ -554,6 +574,42 @@ impl Database {
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Build a map of channel_id -> channel_order for lookup
|
||||
let channel_order_map: std::collections::HashMap<ChannelId, i32> =
|
||||
channels.iter().map(|c| (c.id, c.channel_order)).collect();
|
||||
|
||||
// Pre-compute sort keys for efficient O(n log n) sorting instead of O(n²)
|
||||
let mut channels_with_keys: Vec<(Vec<i32>, Channel)> = channels
|
||||
.into_iter()
|
||||
.map(|channel| {
|
||||
let mut sort_key = Vec::with_capacity(channel.parent_path.len() + 1);
|
||||
|
||||
// Build sort key from parent path orders
|
||||
for parent_id in &channel.parent_path {
|
||||
match channel_order_map.get(parent_id) {
|
||||
Some(&order) => sort_key.push(order),
|
||||
None => {
|
||||
// Missing parent - this should not happen in a well-formed tree
|
||||
// Put orphaned channels at the end with a high sort value
|
||||
sort_key.push(i32::MAX);
|
||||
}
|
||||
}
|
||||
}
|
||||
sort_key.push(channel.channel_order);
|
||||
|
||||
(sort_key, channel)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sort by pre-computed keys (stable sort for deterministic ordering)
|
||||
channels_with_keys.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
|
||||
// Extract the sorted channels
|
||||
let channels: Vec<Channel> = channels_with_keys
|
||||
.into_iter()
|
||||
.map(|(_, channel)| channel)
|
||||
.collect();
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
enum QueryUserIdsAndChannelIds {
|
||||
ChannelId,
|
||||
@@ -986,6 +1042,174 @@ impl Database {
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn reorder_channel(
|
||||
&self,
|
||||
channel_id: ChannelId,
|
||||
direction: proto::reorder_channel::Direction,
|
||||
user_id: UserId,
|
||||
) -> Result<Vec<Channel>> {
|
||||
self.transaction(|tx| async move {
|
||||
let channel = self.get_channel_internal(channel_id, &tx).await?;
|
||||
|
||||
log::info!(
|
||||
"Reordering channel {} (parent_path: '{}', order: {})",
|
||||
channel.id,
|
||||
channel.parent_path,
|
||||
channel.channel_order
|
||||
);
|
||||
|
||||
// Check if user is admin of the channel
|
||||
self.check_user_is_channel_admin(&channel, user_id, &tx)
|
||||
.await?;
|
||||
|
||||
// Find the sibling channel to swap with
|
||||
let sibling_channel = match direction {
|
||||
proto::reorder_channel::Direction::Up => {
|
||||
log::info!(
|
||||
"Looking for sibling with parent_path='{}' and order < {}",
|
||||
channel.parent_path,
|
||||
channel.channel_order
|
||||
);
|
||||
// Find channel with highest order less than current
|
||||
channel::Entity::find()
|
||||
.filter(
|
||||
channel::Column::ParentPath
|
||||
.eq(&channel.parent_path)
|
||||
.and(channel::Column::ChannelOrder.lt(channel.channel_order)),
|
||||
)
|
||||
.order_by_desc(channel::Column::ChannelOrder)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
}
|
||||
proto::reorder_channel::Direction::Down => {
|
||||
log::info!(
|
||||
"Looking for sibling with parent_path='{}' and order > {}",
|
||||
channel.parent_path,
|
||||
channel.channel_order
|
||||
);
|
||||
// Find channel with lowest order greater than current
|
||||
channel::Entity::find()
|
||||
.filter(
|
||||
channel::Column::ParentPath
|
||||
.eq(&channel.parent_path)
|
||||
.and(channel::Column::ChannelOrder.gt(channel.channel_order)),
|
||||
)
|
||||
.order_by_asc(channel::Column::ChannelOrder)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
let sibling_channel = match sibling_channel {
|
||||
Some(sibling) => {
|
||||
log::info!(
|
||||
"Found sibling {} (parent_path: '{}', order: {})",
|
||||
sibling.id,
|
||||
sibling.parent_path,
|
||||
sibling.channel_order
|
||||
);
|
||||
sibling
|
||||
}
|
||||
None => {
|
||||
log::warn!("No sibling found to swap with");
|
||||
// No sibling to swap with, return current channel unchanged
|
||||
return Ok(vec![Channel::from_model(channel)]);
|
||||
}
|
||||
};
|
||||
|
||||
// Validate that we're not swapping identical orders
|
||||
if channel.channel_order == sibling_channel.channel_order {
|
||||
return Err(anyhow!("Channels have identical order values, cannot swap").into());
|
||||
}
|
||||
|
||||
// Bounds checking for channel_order values
|
||||
const MIN_CHANNEL_ORDER: i32 = 1;
|
||||
const MAX_CHANNEL_ORDER: i32 = 999_999;
|
||||
|
||||
if channel.channel_order < MIN_CHANNEL_ORDER
|
||||
|| channel.channel_order > MAX_CHANNEL_ORDER
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"Channel order {} is out of valid range ({}-{})",
|
||||
channel.channel_order,
|
||||
MIN_CHANNEL_ORDER,
|
||||
MAX_CHANNEL_ORDER
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
if sibling_channel.channel_order < MIN_CHANNEL_ORDER
|
||||
|| sibling_channel.channel_order > MAX_CHANNEL_ORDER
|
||||
{
|
||||
return Err(anyhow!(
|
||||
"Sibling channel order {} is out of valid range ({}-{})",
|
||||
sibling_channel.channel_order,
|
||||
MIN_CHANNEL_ORDER,
|
||||
MAX_CHANNEL_ORDER
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
// Swap the channel_order values atomically using a temporary value to avoid conflicts
|
||||
let current_order = channel.channel_order;
|
||||
let sibling_order = sibling_channel.channel_order;
|
||||
let temp_order = i32::MAX; // Use max value as temporary to avoid conflicts
|
||||
|
||||
// Three-step swap to avoid unique constraint violations
|
||||
// Step 1: Set current channel to temp value
|
||||
channel::ActiveModel {
|
||||
id: ActiveValue::Unchanged(channel.id),
|
||||
channel_order: ActiveValue::Set(temp_order),
|
||||
..Default::default()
|
||||
}
|
||||
.update(&*tx)
|
||||
.await?;
|
||||
|
||||
// Step 2: Set sibling to current's original value
|
||||
channel::ActiveModel {
|
||||
id: ActiveValue::Unchanged(sibling_channel.id),
|
||||
channel_order: ActiveValue::Set(current_order),
|
||||
..Default::default()
|
||||
}
|
||||
.update(&*tx)
|
||||
.await?;
|
||||
|
||||
// Step 3: Set current to sibling's original value
|
||||
channel::ActiveModel {
|
||||
id: ActiveValue::Unchanged(channel.id),
|
||||
channel_order: ActiveValue::Set(sibling_order),
|
||||
..Default::default()
|
||||
}
|
||||
.update(&*tx)
|
||||
.await?;
|
||||
|
||||
// Return only the two channels that were swapped
|
||||
let updated_channel = channel::Entity::find_by_id(channel.id)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("Channel not found after update"))?;
|
||||
|
||||
let updated_sibling = channel::Entity::find_by_id(sibling_channel.id)
|
||||
.one(&*tx)
|
||||
.await?
|
||||
.ok_or_else(|| anyhow!("Sibling channel not found after update"))?;
|
||||
|
||||
let swapped_channels = vec![
|
||||
Channel::from_model(updated_channel),
|
||||
Channel::from_model(updated_sibling),
|
||||
];
|
||||
|
||||
log::info!(
|
||||
"Reorder complete. Swapped channels {} and {}",
|
||||
channel.id,
|
||||
sibling_channel.id
|
||||
);
|
||||
|
||||
Ok(swapped_channels)
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||
|
||||
@@ -10,6 +10,9 @@ pub struct Model {
|
||||
pub visibility: ChannelVisibility,
|
||||
pub parent_path: String,
|
||||
pub requires_zed_cla: bool,
|
||||
/// The order of this channel relative to its siblings within the same parent.
|
||||
/// Lower values appear first. Channels are sorted by parent_path first, then by channel_order.
|
||||
pub channel_order: i32,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
|
||||
@@ -173,15 +173,28 @@ impl Drop for TestDb {
|
||||
}
|
||||
|
||||
fn channel_tree(channels: &[(ChannelId, &[ChannelId], &'static str)]) -> Vec<Channel> {
|
||||
channels
|
||||
.iter()
|
||||
.map(|(id, parent_path, name)| Channel {
|
||||
use std::collections::HashMap;
|
||||
|
||||
let mut result = Vec::new();
|
||||
let mut order_by_parent: HashMap<Vec<ChannelId>, i32> = HashMap::new();
|
||||
|
||||
for (id, parent_path, name) in channels {
|
||||
let parent_key = parent_path.to_vec();
|
||||
let order = *order_by_parent
|
||||
.entry(parent_key.clone())
|
||||
.and_modify(|e| *e += 1)
|
||||
.or_insert(1);
|
||||
|
||||
result.push(Channel {
|
||||
id: *id,
|
||||
name: name.to_string(),
|
||||
visibility: ChannelVisibility::Members,
|
||||
parent_path: parent_path.to_vec(),
|
||||
})
|
||||
.collect()
|
||||
parent_path: parent_key,
|
||||
channel_order: order,
|
||||
});
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5);
|
||||
|
||||
@@ -7,7 +7,7 @@ use crate::{
|
||||
};
|
||||
use rpc::{
|
||||
ConnectionId,
|
||||
proto::{self},
|
||||
proto::{self, reorder_channel},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -64,11 +64,11 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
channel_tree(&[
|
||||
(zed_id, &[], "zed"),
|
||||
(crdb_id, &[zed_id], "crdb"),
|
||||
(livestreaming_id, &[zed_id], "livestreaming",),
|
||||
(livestreaming_id, &[zed_id], "livestreaming"),
|
||||
(replace_id, &[zed_id], "replace"),
|
||||
(rust_id, &[], "rust"),
|
||||
(cargo_id, &[rust_id], "cargo"),
|
||||
(cargo_ra_id, &[rust_id, cargo_id], "cargo-ra",)
|
||||
(cargo_ra_id, &[rust_id, cargo_id], "cargo-ra")
|
||||
],)
|
||||
);
|
||||
|
||||
@@ -78,7 +78,7 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
channel_tree(&[
|
||||
(zed_id, &[], "zed"),
|
||||
(crdb_id, &[zed_id], "crdb"),
|
||||
(livestreaming_id, &[zed_id], "livestreaming",),
|
||||
(livestreaming_id, &[zed_id], "livestreaming"),
|
||||
(replace_id, &[zed_id], "replace")
|
||||
],)
|
||||
);
|
||||
@@ -99,7 +99,7 @@ async fn test_channels(db: &Arc<Database>) {
|
||||
channel_tree(&[
|
||||
(zed_id, &[], "zed"),
|
||||
(crdb_id, &[zed_id], "crdb"),
|
||||
(livestreaming_id, &[zed_id], "livestreaming",),
|
||||
(livestreaming_id, &[zed_id], "livestreaming"),
|
||||
(replace_id, &[zed_id], "replace")
|
||||
],)
|
||||
);
|
||||
@@ -366,6 +366,152 @@ async fn test_db_channel_moving(db: &Arc<Database>) {
|
||||
);
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_channel_reordering,
|
||||
test_channel_reordering_postgres,
|
||||
test_channel_reordering_sqlite
|
||||
);
|
||||
|
||||
async fn test_channel_reordering(db: &Arc<Database>) {
|
||||
let admin_id = db
|
||||
.create_user(
|
||||
"admin@example.com",
|
||||
None,
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "admin".into(),
|
||||
github_user_id: 1,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
let user_id = db
|
||||
.create_user(
|
||||
"user@example.com",
|
||||
None,
|
||||
false,
|
||||
NewUserParams {
|
||||
github_login: "user".into(),
|
||||
github_user_id: 2,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.user_id;
|
||||
|
||||
// Create a root channel with some sub-channels
|
||||
let root_id = db.create_root_channel("root", admin_id).await.unwrap();
|
||||
|
||||
// Invite user to root channel so they can see the sub-channels
|
||||
db.invite_channel_member(root_id, user_id, admin_id, ChannelRole::Member)
|
||||
.await
|
||||
.unwrap();
|
||||
db.respond_to_channel_invite(root_id, user_id, true)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let alpha_id = db
|
||||
.create_sub_channel("alpha", root_id, admin_id)
|
||||
.await
|
||||
.unwrap();
|
||||
let beta_id = db
|
||||
.create_sub_channel("beta", root_id, admin_id)
|
||||
.await
|
||||
.unwrap();
|
||||
let gamma_id = db
|
||||
.create_sub_channel("gamma", root_id, admin_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Initial order should be: root, alpha (order=1), beta (order=2), gamma (order=3)
|
||||
let result = db.get_channels_for_user(admin_id).await.unwrap();
|
||||
assert_channel_tree(
|
||||
result.channels,
|
||||
&[
|
||||
(root_id, &[]),
|
||||
(alpha_id, &[root_id]),
|
||||
(beta_id, &[root_id]),
|
||||
(gamma_id, &[root_id]),
|
||||
],
|
||||
);
|
||||
|
||||
// Test moving beta up (should swap with alpha)
|
||||
let updated_channels = db
|
||||
.reorder_channel(beta_id, reorder_channel::Direction::Up, admin_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify that beta and alpha were returned as updated
|
||||
assert_eq!(updated_channels.len(), 2);
|
||||
let updated_ids: std::collections::HashSet<_> = updated_channels.iter().map(|c| c.id).collect();
|
||||
assert!(updated_ids.contains(&alpha_id));
|
||||
assert!(updated_ids.contains(&beta_id));
|
||||
|
||||
// Now order should be: root, beta (order=1), alpha (order=2), gamma (order=3)
|
||||
let result = db.get_channels_for_user(admin_id).await.unwrap();
|
||||
assert_channel_tree(
|
||||
result.channels,
|
||||
&[
|
||||
(root_id, &[]),
|
||||
(beta_id, &[root_id]),
|
||||
(alpha_id, &[root_id]),
|
||||
(gamma_id, &[root_id]),
|
||||
],
|
||||
);
|
||||
|
||||
// Test moving gamma down (should be no-op since it's already last)
|
||||
let updated_channels = db
|
||||
.reorder_channel(gamma_id, reorder_channel::Direction::Down, admin_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Should return just gamma (no change)
|
||||
assert_eq!(updated_channels.len(), 1);
|
||||
assert_eq!(updated_channels[0].id, gamma_id);
|
||||
|
||||
// Test moving alpha down (should swap with gamma)
|
||||
let updated_channels = db
|
||||
.reorder_channel(alpha_id, reorder_channel::Direction::Down, admin_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Verify that alpha and gamma were returned as updated
|
||||
assert_eq!(updated_channels.len(), 2);
|
||||
let updated_ids: std::collections::HashSet<_> = updated_channels.iter().map(|c| c.id).collect();
|
||||
assert!(updated_ids.contains(&alpha_id));
|
||||
assert!(updated_ids.contains(&gamma_id));
|
||||
|
||||
// Now order should be: root, beta (order=1), gamma (order=2), alpha (order=3)
|
||||
let result = db.get_channels_for_user(admin_id).await.unwrap();
|
||||
assert_channel_tree(
|
||||
result.channels,
|
||||
&[
|
||||
(root_id, &[]),
|
||||
(beta_id, &[root_id]),
|
||||
(gamma_id, &[root_id]),
|
||||
(alpha_id, &[root_id]),
|
||||
],
|
||||
);
|
||||
|
||||
// Test that non-admin cannot reorder
|
||||
let reorder_result = db
|
||||
.reorder_channel(beta_id, reorder_channel::Direction::Up, user_id)
|
||||
.await;
|
||||
assert!(reorder_result.is_err());
|
||||
|
||||
// Test moving beta up (should be no-op since it's already first)
|
||||
let updated_channels = db
|
||||
.reorder_channel(beta_id, reorder_channel::Direction::Up, admin_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Should return just beta (no change)
|
||||
assert_eq!(updated_channels.len(), 1);
|
||||
assert_eq!(updated_channels[0].id, beta_id);
|
||||
}
|
||||
|
||||
test_both_dbs!(
|
||||
test_db_channel_moving_bugs,
|
||||
test_db_channel_moving_bugs_postgres,
|
||||
|
||||
@@ -384,6 +384,7 @@ impl Server {
|
||||
.add_request_handler(get_notifications)
|
||||
.add_request_handler(mark_notification_as_read)
|
||||
.add_request_handler(move_channel)
|
||||
.add_request_handler(reorder_channel)
|
||||
.add_request_handler(follow)
|
||||
.add_message_handler(unfollow)
|
||||
.add_message_handler(update_followers)
|
||||
@@ -3195,6 +3196,55 @@ async fn move_channel(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn reorder_channel(
|
||||
request: proto::ReorderChannel,
|
||||
response: Response<proto::ReorderChannel>,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||
let direction = request.direction();
|
||||
|
||||
let updated_channels = session
|
||||
.db()
|
||||
.await
|
||||
.reorder_channel(channel_id, direction, session.user_id())
|
||||
.await?;
|
||||
|
||||
// Get the root channel to find all connections that need updates
|
||||
let root_id = updated_channels
|
||||
.first()
|
||||
.map(|channel| channel.root_id())
|
||||
.unwrap_or(channel_id);
|
||||
|
||||
let connection_pool = session.connection_pool().await;
|
||||
for (connection_id, role) in connection_pool.channel_connection_ids(root_id) {
|
||||
let channels = updated_channels
|
||||
.iter()
|
||||
.filter_map(|channel| {
|
||||
if role.can_see_channel(channel.visibility) {
|
||||
Some(channel.to_proto())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if channels.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let update = proto::UpdateChannels {
|
||||
channels,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
session.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
|
||||
response.send(Ack {})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the list of channel members
|
||||
async fn get_channel_members(
|
||||
request: proto::GetChannelMembers,
|
||||
|
||||
@@ -14,9 +14,9 @@ use fuzzy::{StringMatchCandidate, match_strings};
|
||||
use gpui::{
|
||||
AnyElement, App, AsyncWindowContext, Bounds, ClickEvent, ClipboardItem, Context, DismissEvent,
|
||||
Div, Entity, EventEmitter, FocusHandle, Focusable, FontStyle, InteractiveElement, IntoElement,
|
||||
ListOffset, ListState, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render,
|
||||
SharedString, Styled, Subscription, Task, TextStyle, WeakEntity, Window, actions, anchored,
|
||||
canvas, deferred, div, fill, list, point, prelude::*, px,
|
||||
KeyContext, ListOffset, ListState, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel,
|
||||
Render, SharedString, Styled, Subscription, Task, TextStyle, WeakEntity, Window, actions,
|
||||
anchored, canvas, deferred, div, fill, list, point, prelude::*, px,
|
||||
};
|
||||
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrevious};
|
||||
use project::{Fs, Project};
|
||||
@@ -52,6 +52,8 @@ actions!(
|
||||
StartMoveChannel,
|
||||
MoveSelected,
|
||||
InsertSpace,
|
||||
MoveChannelUp,
|
||||
MoveChannelDown,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -1961,6 +1963,33 @@ impl CollabPanel {
|
||||
})
|
||||
}
|
||||
|
||||
fn move_channel_up(&mut self, _: &MoveChannelUp, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(channel) = self.selected_channel() {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
store
|
||||
.reorder_channel(channel.id, proto::reorder_channel::Direction::Up, cx)
|
||||
.detach_and_prompt_err("Failed to move channel up", window, cx, |_, _, _| None)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn move_channel_down(
|
||||
&mut self,
|
||||
_: &MoveChannelDown,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(channel) = self.selected_channel() {
|
||||
self.channel_store.update(cx, |store, cx| {
|
||||
store
|
||||
.reorder_channel(channel.id, proto::reorder_channel::Direction::Down, cx)
|
||||
.detach_and_prompt_err("Failed to move channel down", window, cx, |_, _, _| {
|
||||
None
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn open_channel_notes(
|
||||
&mut self,
|
||||
channel_id: ChannelId,
|
||||
@@ -1974,7 +2003,7 @@ impl CollabPanel {
|
||||
|
||||
fn show_inline_context_menu(
|
||||
&mut self,
|
||||
_: &menu::SecondaryConfirm,
|
||||
_: &Secondary,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -2003,6 +2032,21 @@ impl CollabPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_context(&self, window: &Window, cx: &Context<Self>) -> KeyContext {
|
||||
let mut dispatch_context = KeyContext::new_with_defaults();
|
||||
dispatch_context.add("CollabPanel");
|
||||
dispatch_context.add("menu");
|
||||
|
||||
let identifier = if self.channel_name_editor.focus_handle(cx).is_focused(window) {
|
||||
"editing"
|
||||
} else {
|
||||
"not_editing"
|
||||
};
|
||||
|
||||
dispatch_context.add(identifier);
|
||||
dispatch_context
|
||||
}
|
||||
|
||||
fn selected_channel(&self) -> Option<&Arc<Channel>> {
|
||||
self.selection
|
||||
.and_then(|ix| self.entries.get(ix))
|
||||
@@ -2965,7 +3009,7 @@ fn render_tree_branch(
|
||||
impl Render for CollabPanel {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("CollabPanel")
|
||||
.key_context(self.dispatch_context(window, cx))
|
||||
.on_action(cx.listener(CollabPanel::cancel))
|
||||
.on_action(cx.listener(CollabPanel::select_next))
|
||||
.on_action(cx.listener(CollabPanel::select_previous))
|
||||
@@ -2977,6 +3021,8 @@ impl Render for CollabPanel {
|
||||
.on_action(cx.listener(CollabPanel::collapse_selected_channel))
|
||||
.on_action(cx.listener(CollabPanel::expand_selected_channel))
|
||||
.on_action(cx.listener(CollabPanel::start_move_selected_channel))
|
||||
.on_action(cx.listener(CollabPanel::move_channel_up))
|
||||
.on_action(cx.listener(CollabPanel::move_channel_down))
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.size_full()
|
||||
.child(if self.user_store.read(cx).current_user().is_none() {
|
||||
|
||||
@@ -8,6 +8,7 @@ message Channel {
|
||||
uint64 id = 1;
|
||||
string name = 2;
|
||||
ChannelVisibility visibility = 3;
|
||||
int32 channel_order = 4;
|
||||
repeated uint64 parent_path = 5;
|
||||
}
|
||||
|
||||
@@ -207,6 +208,15 @@ message MoveChannel {
|
||||
uint64 to = 2;
|
||||
}
|
||||
|
||||
message ReorderChannel {
|
||||
uint64 channel_id = 1;
|
||||
enum Direction {
|
||||
Up = 0;
|
||||
Down = 1;
|
||||
}
|
||||
Direction direction = 2;
|
||||
}
|
||||
|
||||
message JoinChannelBuffer {
|
||||
uint64 channel_id = 1;
|
||||
}
|
||||
|
||||
@@ -190,6 +190,7 @@ message Envelope {
|
||||
GetChannelMessagesById get_channel_messages_by_id = 144;
|
||||
|
||||
MoveChannel move_channel = 147;
|
||||
ReorderChannel reorder_channel = 349;
|
||||
SetChannelVisibility set_channel_visibility = 148;
|
||||
|
||||
AddNotification add_notification = 149;
|
||||
|
||||
@@ -176,6 +176,7 @@ messages!(
|
||||
(LspExtClearFlycheck, Background),
|
||||
(MarkNotificationRead, Foreground),
|
||||
(MoveChannel, Foreground),
|
||||
(ReorderChannel, Foreground),
|
||||
(MultiLspQuery, Background),
|
||||
(MultiLspQueryResponse, Background),
|
||||
(OnTypeFormatting, Background),
|
||||
@@ -389,6 +390,7 @@ request_messages!(
|
||||
(RemoveContact, Ack),
|
||||
(RenameChannel, RenameChannelResponse),
|
||||
(RenameProjectEntry, ProjectEntryResponse),
|
||||
(ReorderChannel, Ack),
|
||||
(RequestContact, Ack),
|
||||
(
|
||||
ResolveCompletionDocumentation,
|
||||
|
||||
@@ -32,6 +32,7 @@ backtrace = "0.3"
|
||||
breadcrumbs.workspace = true
|
||||
call.workspace = true
|
||||
channel.workspace = true
|
||||
channel_tools.workspace = true
|
||||
chrono.workspace = true
|
||||
clap.workspace = true
|
||||
cli.workspace = true
|
||||
|
||||
@@ -555,6 +555,7 @@ fn main() {
|
||||
tasks_ui::init(cx);
|
||||
snippets_ui::init(cx);
|
||||
channel::init(&app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||
channel_tools::init(channel::ChannelStore::global(cx), cx);
|
||||
search::init(cx);
|
||||
vim::init(cx);
|
||||
terminal_view::init(cx);
|
||||
|
||||
Reference in New Issue
Block a user