From 3d222998979be6296572ed186c1b7c5b649a047a Mon Sep 17 00:00:00 2001 From: "Jason \"Jay\" Smith" Date: Thu, 21 May 2026 18:21:11 -0700 Subject: [PATCH 1/4] paddleboard_ai_dock: introduce AI Dock and absorb MCP Servers page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new `paddleboard_ai_dock` crate that ships a single browse-surface for the three things agents talk to. Replaces the hardcoded 5-card "Agent Setup" row on the Welcome screen and absorbs the standalone MCP Servers pane into one of its tabs. What lands: - `paddleboard_ai_dock` crate with a `ModalView`-implementing `AiDock` exposing three tabs (Agents / Skills / MCP Servers), backed by a static in-repo catalog at `assets/ai_dock/catalog.json` (5 agents, 6 skills, 5 MCP servers). - `crates/agent_ui/src/mcp_servers_ui.rs`: `McpServersPage` → `McpServersView`. Dropped the `Item` + `EventEmitter` impls and tab-metadata methods; kept `Render` + `Focusable`. Re-exported publicly via `pub use crate::mcp_servers_ui::McpServersView` in `agent_ui.rs`. The dock hosts an `Entity` constructed lazily on first MCP-tab activation, so the view keeps using the private `agent_configuration::*` modals without lifting them. - Action wiring: new `paddleboard_actions::ai_dock::Open` (Agents tab); legacy `paddleboard_actions::McpServers` keeps working but now routes to the dock on the MCP tab. The old pane-item handler in `agent_ui.rs:575-593` was removed in favor of a divergence comment. - `crates/onboarding/src/basics_page.rs::render_ai_section` collapsed from a 5-card grid to a single "Open the AI Dock" button. The now-dead `render_zed_agent_button` / `render_registry_agent_button` helpers and ~6 unused imports removed. `FEATURED_AGENT_IDS` retained for `onboarding.rs:245` telemetry. - Detection is per-tab rather than a generic strategy: agents check `project::agent_server_store::AllAgentServersSettings`; skills check `/.claude/commands/.md` and `~/.claude/commands/.md`; MCP defers entirely to the absorbed view. Originally scaffolded as "Store"; renamed to "AI Dock" mid-session because nothing is being purchased and the nautical metaphor fits the PaddleBoard theme. The rename is total — crate, action namespace, types, asset path, UI strings. The dock renders as a `ModalView` despite the name; the "Dock" framing is the paddleboard metaphor, not the GPUI dock concept. Verification: `./script/clippy` (release, all targets, deny warnings) clean; `cargo build -p paddleboard` clean. UI smoke test deferred — binary launches without crash but Screen Recording permission blocked screenshot evidence in this session. WELCOME.md and the in-app tour (`crates/workspace/src/tour.md`) updated; RECAPS 2026-05-21 entry documents the build + the rename. Release Notes: - Added the AI Dock — a single modal for browsing and installing agents, skills, and MCP servers. Replaces the old Welcome-screen "Agent Setup" row and absorbs the MCP Servers page (the `zed: Mcp Servers` action now opens the dock on the MCP tab). Co-Authored-By: Claude Opus 4.7 --- Cargo.lock | 26 +++ Cargo.toml | 2 + RECAPS.md | 35 +++ WELCOME.md | 15 +- assets/ai_dock/catalog.json | 132 +++++++++++ crates/agent_ui/src/agent_ui.rs | 24 +- crates/agent_ui/src/mcp_servers_ui.rs | 42 +--- crates/onboarding/src/basics_page.rs | 198 +++-------------- crates/paddleboard/Cargo.toml | 1 + crates/paddleboard/src/main.rs | 1 + crates/paddleboard_actions/src/lib.rs | 14 ++ crates/paddleboard_ai_dock/Cargo.toml | 33 +++ crates/paddleboard_ai_dock/src/ai_dock.rs | 207 ++++++++++++++++++ .../src/ai_dock/agents_tab.rs | 173 +++++++++++++++ .../src/ai_dock/mcp_tab.rs | 33 +++ .../src/ai_dock/skills_tab.rs | 141 ++++++++++++ crates/paddleboard_ai_dock/src/catalog.rs | 105 +++++++++ .../src/paddleboard_ai_dock.rs | 39 ++++ crates/workspace/src/tour.md | 19 +- paddleboard-5.png | Bin 0 -> 19947 bytes 20 files changed, 1005 insertions(+), 235 deletions(-) create mode 100644 assets/ai_dock/catalog.json create mode 100644 crates/paddleboard_ai_dock/Cargo.toml create mode 100644 crates/paddleboard_ai_dock/src/ai_dock.rs create mode 100644 crates/paddleboard_ai_dock/src/ai_dock/agents_tab.rs create mode 100644 crates/paddleboard_ai_dock/src/ai_dock/mcp_tab.rs create mode 100644 crates/paddleboard_ai_dock/src/ai_dock/skills_tab.rs create mode 100644 crates/paddleboard_ai_dock/src/catalog.rs create mode 100644 crates/paddleboard_ai_dock/src/paddleboard_ai_dock.rs create mode 100644 paddleboard-5.png diff --git a/Cargo.lock b/Cargo.lock index 232fbd3078..8633173166 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12866,6 +12866,7 @@ dependencies = [ "outline", "outline_panel", "paddleboard_actions", + "paddleboard_ai_dock", "paddleboard_env_vars", "paddleboard_sandbox_prereqs", "paddleboard_sandbox_prereqs_ui", @@ -12953,6 +12954,31 @@ dependencies = [ "uuid", ] +[[package]] +name = "paddleboard_ai_dock" +version = "0.1.0" +dependencies = [ + "agent_settings", + "agent_ui", + "anyhow", + "client", + "collections", + "fs", + "gpui", + "language", + "log", + "menu", + "paddleboard_actions", + "project", + "serde", + "serde_json", + "settings", + "ui", + "util", + "which 6.0.3", + "workspace", +] + [[package]] name = "paddleboard_credentials_provider" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 542c727352..d901b0cdef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -236,6 +236,7 @@ members = [ "crates/paddleboard_sandbox_prereqs_state", "crates/paddleboard_sandbox_prereqs_ui", "crates/paddleboard_sandbox_settings", + "crates/paddleboard_ai_dock", "crates/windows_resources", "crates/zeta_prompt", "crates/zlog", @@ -501,6 +502,7 @@ paddleboard_sandbox_prereqs = { path = "crates/paddleboard_sandbox_prereqs" } paddleboard_sandbox_prereqs_state = { path = "crates/paddleboard_sandbox_prereqs_state" } paddleboard_sandbox_prereqs_ui = { path = "crates/paddleboard_sandbox_prereqs_ui" } paddleboard_sandbox_settings = { path = "crates/paddleboard_sandbox_settings" } +paddleboard_ai_dock = { path = "crates/paddleboard_ai_dock" } edit_prediction = { path = "crates/edit_prediction" } edit_prediction_metrics = { path = "crates/edit_prediction_metrics" } zeta_prompt = { path = "crates/zeta_prompt" } diff --git a/RECAPS.md b/RECAPS.md index 74d4111a92..eb2e0599f7 100644 --- a/RECAPS.md +++ b/RECAPS.md @@ -4,8 +4,43 @@ Running log of completed work sessions, newest first. Each entry summarizes a co --- +## 2026-05-21 + +### AI Dock — built, McpServersPage absorbed +- Promoted yesterday's "Store" sketch into a shipping feature. **Renamed `Store` → `AI Dock` mid-session** at the user's request (no one's buying anything; "Dock" plays off the PaddleBoard theme). The rename was total: crate `paddleboard_store` → `paddleboard_ai_dock`, action `paddleboard_actions::store::OpenStore` → `paddleboard_actions::ai_dock::Open`, types `StoreModal`/`StoreTab` → `AiDock`/`AiDockTab`, catalog path `assets/store/catalog.json` → `assets/ai_dock/catalog.json`, plus every UI string. Nothing was committed under the old name, so it's clean. +- New crate `paddleboard_ai_dock` with an `AiDock` modal that ships three tabs (Agents / Skills / MCP Servers), backed by a static in-repo catalog (5 agents, 6 skills, 5 MCP servers — adds are PRs, not fetches). +- **Two open questions from yesterday locked in:** + - Panel home → **modal** (transient browse-then-leave; doesn't eat persistent dock space). Note: it's called "AI Dock" semantically but renders as a `ModalView`. The name is a paddleboard metaphor, not the GPUI dock concept. + - `McpServersPage` → **absorbed**, not linked. Pragmatic absorb: renamed `McpServersPage` → `McpServersView` in `crates/agent_ui/src/mcp_servers_ui.rs`, dropped the `Item`/`EventEmitter` impls + tab metadata, kept `Render`+`Focusable`, made it `pub` via re-export. The AI Dock's MCP tab hosts an `Entity` constructed lazily on first activation, so the existing `crate::agent_configuration::*` modal wiring keeps working without lifting it into the new crate. +- **Action plumbing**: kept the legacy `paddleboard_actions::McpServers` working — `paddleboard_ai_dock::init` now owns that handler and opens the dock on the MCP tab. Removed the old `agent_ui.rs:575-593` pane-item handler entirely (replaced with a PaddleBoard divergence comment). Added one new action, `paddleboard_actions::ai_dock::Open`, which lands on the Agents tab. +- **Welcome row swap**: `crates/onboarding/src/basics_page.rs::render_ai_section` no longer renders the 5-card grid; it's a single **Open the AI Dock** button (with the `ArrowUpRight` end-icon) dispatching the new action. `FEATURED_AGENT_IDS` stayed because telemetry in `onboarding.rs:245` still counts it, but the helpers (`render_zed_agent_button`, `render_registry_agent_button`) and ~6 now-unused imports were deleted. +- **Detection per tab** (no generic strategy yet — each tab does what fits): + - Agents → cross-reference `project::agent_server_store::AllAgentServersSettings` from `SettingsStore`. Install button writes `settings::CustomAgentServerSettings::Registry { … }` via `update_settings_file` — same shape as the old onboarding card. + - Skills → `cwd/.claude/commands/.md` (project) and `~/.claude/commands/.md` (user); shows scope label. No bundled content yet, so "not installed" routes to homepage when the catalog entry has one. + - MCP → defers entirely to the absorbed `McpServersView`. +- **Docs + tour**: added a new "AI Dock" section to `WELCOME.md` (between Sandboxed MCP Servers and Step-Through Mode), updated the MCP section to mention the new entry point, and synced `crates/workspace/src/tour.md` with the same section as `### 5. AI Dock`. Renumbered downstream tour sections (6–10). +- **Verification**: `./script/clippy` (release, all targets, deny warnings) clean. `cargo check -p paddleboard` clean after the rename. **No UI smoke test yet** — the binary builds but the modal hasn't been clicked through in the running app; that's the next step for the user (or a `/verify` invocation). +- **Yesterday's `script/bundle-mac` side note still stands** — debug builds exit 1 on the unguarded `gzip target/.../remote_server`. Untouched this session. + +--- + ## 2026-05-20 +### PaddleBoard Store panel — design sketched, implementation deferred +- User reviewed `paddleboard-5.png` (the hardcoded "Agent Setup" 5-card row on the Welcome screen — Zed Agent / Claude Agent / Codex CLI / GitHub Copilot / Cursor) and proposed replacing it with a unified "agent store" or "skills store." +- Agreed framing: one panel, three tabs (**Agents / Skills / MCP Servers**), backed by a static in-repo catalog. Solves the discoverability gap that the MCP orchestrator already hit (it's buried in the command palette), and consolidates three surfaces that drift apart today (agents = long-lived processes, skills = markdown files, MCP = subprocess+config). +- **Sketched in conversation, not yet built.** Captured in `project_store_idea.md` memory. Key choices already made: + - New crate `paddleboard_store` (fork hygiene — net-new feature, no upstream-file edits). + - Catalog at `assets/store/catalog.json` in-repo. PR-reviewed adds, not fetched. Can graduate to a hosted catalog later if community contributions outgrow PR review. + - Per-tab action verbs diverge intentionally because "install" means different things: Agents → Install/Sign in/Open/Configure; Skills → Add to user/Add to project/Remove; MCP → Add Server (delegate to existing `McpServersPage`). + - `enum StoreItem { Agent(AgentManifest), Skill(SkillManifest), Mcp(McpManifest) }` with per-item `detect: DetectStrategy` (`BinaryOnPath` / `FileExists`) driving the Installed/Available badge. + - Welcome screen replaces the 5-card row with a single "Browse the Store →" button plus a small Featured strip for first-run. +- **Three open questions left for user to decide** before any code: + 1. Panel home: modal vs. dock panel vs. top-level workspace item. Claude leaned modal (discrete interactions, not "always docked"). + 2. Catalog source: in-repo JSON to start; revisit fetched later if needed. + 3. `McpServersPage`: absorb into the Store's MCP tab, or stay separate and just be linked? Absorb = cleaner but real refactor; link = cheaper but two surfaces. +- **Side observation, not acted on:** `./script/bundle-mac -d -o -i` (the `/build bundle install` path) exits with code 1 on debug builds because the script tries to `gzip target/.../release/remote_server` after the install step — a release-only operation that runs unconditionally. The install + open both succeed before the failure, so it's cosmetic, but a real fix would be a one-liner guarding the gzip with the same `if [ -z "$DEBUG_BUILD" ]` check used elsewhere in the script. Not done; flagging for whoever next touches `script/bundle-mac`. + ### `/build bundle` — debug `.app` with the paddle icon - Triggered by user observation that `cargo build -p paddleboard` produces a binary that macOS shows in the dock as a generic "exec" entry with no logo. Root cause: the raw `target/debug/paddleboard` binary has no `.app` wrapper, so there's no `Info.plist` / `CFBundleName` / `CFBundleIconFile` / `AppIcon.icns` for macOS to read. - Extended `.claude/commands/build.md` (the `/build` slash command) with three new arguments: diff --git a/WELCOME.md b/WELCOME.md index 07f27059d5..e438919f93 100644 --- a/WELCOME.md +++ b/WELCOME.md @@ -78,7 +78,7 @@ Most editors run **MCP (Model Context Protocol) servers** directly on your host. PaddleBoard adds a fourth context-server transport, `sandboxed_stdio`, that runs the MCP server inside a `podman run -i --rm --runtime=runsc` container. Stdin and stdout are proxied transparently, so the JSON-RPC framing keeps working without any change on the agent side. -**The MCP Servers settings page** (command palette → `zed: Mcp Servers`) gives you a UI for adding, filtering (All / Running / Stopped / Error), and inspecting servers without hand-editing JSON. Use the "Add Server" popover to declare a new server; the page surfaces live status as the connection comes up. +**Manage servers in the AI Dock** — `zed: Mcp Servers` (or `ai_dock: Open` then the **MCP Servers** tab) opens the PaddleBoard AI Dock with the absorbed server view. You get the full add/filter (All / Running / Stopped / Error) / inspect surface plus a side-by-side **Available** catalog of well-known servers without hand-editing JSON. You can still configure servers by hand in `settings.json` if you prefer: @@ -103,6 +103,19 @@ The original `stdio` transport (which runs the binary directly on your host) is --- +### AI Dock + +One place to browse and install everything the agent talks to. Think of it as the marina where every external collaborator your PaddleBoard talks to ties up. + +- Open it from the command palette (`ai_dock: Open`) or the **Open the AI Dock** button on the Welcome screen. +- Three tabs: **Agents** (Zed, Claude, Codex, Copilot, Cursor, …), **Skills** (slash commands shipped with the project or installed in `~/.claude/commands/`), and **MCP Servers** (the absorbed management page plus a catalog of common servers). +- Installed items show a green badge; missing ones show an **Install / Sign In / Learn More** action that does the right thing for the category — agent installs are a one-click settings write, sign-in flows route to your existing identity, and MCP server adds delegate to the existing setup machinery. +- The catalog itself is `assets/ai_dock/catalog.json` in this repo — adding an entry is a PR, not a network fetch, so what shows up in the Dock is exactly what the team has reviewed. + +The AI Dock replaces the old hardcoded 5-card "Agent Setup" row on the Welcome screen and the standalone MCP Servers pane — both routes now land here. + +--- + ### Step-through mode Step-through mode lets you approve every tool call before the agent executes it — useful when you want to watch exactly what the agent is doing or sanity-check a risky operation. diff --git a/assets/ai_dock/catalog.json b/assets/ai_dock/catalog.json new file mode 100644 index 0000000000..1e9029ab61 --- /dev/null +++ b/assets/ai_dock/catalog.json @@ -0,0 +1,132 @@ +{ + "$schema_note": "PaddleBoard Store catalog. Edit via PR. See crates/paddleboard_store/src/catalog.rs for the schema.", + "agents": [ + { + "id": "zed-agent", + "name": "Zed Agent", + "description": "First-party agent backed by Anthropic, OpenAI, and Google models. Sign in to use.", + "homepage": "https://zed.dev/agent", + "featured": true, + "builtin_zed": true + }, + { + "id": "claude-acp", + "name": "Claude Agent", + "description": "Anthropic's Claude agent over ACP. Streaming tool use, persistent threads, multi-modal.", + "homepage": "https://docs.claude.com/en/docs/claude-code/overview", + "featured": true + }, + { + "id": "codex-acp", + "name": "Codex CLI", + "description": "OpenAI's terminal coding agent. Runs locally; bring your own API key.", + "homepage": "https://github.com/openai/codex", + "featured": true + }, + { + "id": "github-copilot-cli", + "name": "GitHub Copilot", + "description": "GitHub's coding assistant. Sign in with your GitHub account.", + "homepage": "https://github.com/features/copilot", + "featured": true + }, + { + "id": "cursor", + "name": "Cursor", + "description": "Cursor's agent over ACP. Connects to the Cursor backend.", + "homepage": "https://www.cursor.com/", + "featured": false + } + ], + "skills": [ + { + "id": "build", + "name": "/build", + "description": "Build PaddleBoard via cargo. Use `bundle` for a macOS .app, `install` to drop it into /Applications.", + "homepage": null, + "featured": true + }, + { + "id": "update-tour", + "name": "/update-tour", + "description": "Sync the in-app first-launch tour with the latest WELCOME.md after a user-facing change.", + "homepage": null, + "featured": true + }, + { + "id": "verify", + "name": "/verify", + "description": "Run the app and confirm a change actually does what it's supposed to do.", + "homepage": null, + "featured": false + }, + { + "id": "simplify", + "name": "/simplify", + "description": "Review changed code for reuse, quality, and efficiency, then fix any issues found.", + "homepage": null, + "featured": false + }, + { + "id": "review", + "name": "/review", + "description": "Review a pull request.", + "homepage": null, + "featured": false + }, + { + "id": "security-review", + "name": "/security-review", + "description": "Run a complete security review of pending changes on the current branch.", + "homepage": null, + "featured": false + } + ], + "mcp_servers": [ + { + "id": "filesystem", + "name": "Filesystem", + "description": "Read, write, and list files under a directory you allow.", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "."], + "homepage": "https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem", + "featured": true + }, + { + "id": "fetch", + "name": "Fetch", + "description": "HTTP fetch for URLs, returning markdown-converted text.", + "command": "uvx", + "args": ["mcp-server-fetch"], + "homepage": "https://github.com/modelcontextprotocol/servers/tree/main/src/fetch", + "featured": true + }, + { + "id": "git", + "name": "Git", + "description": "Inspect git history, branches, diffs, and blame for a repository.", + "command": "uvx", + "args": ["mcp-server-git", "--repository", "."], + "homepage": "https://github.com/modelcontextprotocol/servers/tree/main/src/git", + "featured": true + }, + { + "id": "github", + "name": "GitHub", + "description": "Read and write to GitHub repos, issues, and PRs via the GitHub API.", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-github"], + "homepage": "https://github.com/modelcontextprotocol/servers/tree/main/src/github", + "featured": false + }, + { + "id": "puppeteer", + "name": "Puppeteer", + "description": "Headless browser automation: navigate pages, take screenshots, scrape content.", + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-puppeteer"], + "homepage": "https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer", + "featured": false + } + ] +} diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 94889b5d8e..d1c64c8471 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -76,7 +76,7 @@ pub use crate::agent_panel::{ }; pub use crate::orchestration_panel::OrchestrationPanel; use crate::agent_registry_ui::AgentRegistryPage; -use crate::mcp_servers_ui::McpServersPage; +pub use crate::mcp_servers_ui::McpServersView; pub use crate::inline_assistant::InlineAssistant; pub use crate::thread_metadata_store::ThreadId; pub use agent_diff::{AgentDiffPane, AgentDiffToolbar}; @@ -572,25 +572,9 @@ pub fn init( } }, ); - workspace.register_action( - move |workspace: &mut Workspace, - _: &paddleboard_actions::McpServers, - window: &mut Window, - cx: &mut Context| { - let existing = workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()); - - if let Some(existing) = existing { - workspace.activate_item(&existing, true, true, window, cx); - } else { - let page = McpServersPage::new(workspace, window, cx); - workspace.add_item_to_active_pane(Box::new(page), None, true, window, cx); - } - }, - ); + // PaddleBoard: `McpServers` is registered by `paddleboard_store::init` + // — opening the MCP Servers UI now routes through the Store modal, so + // the legacy pane-item handler that used to live here has moved. }) .detach(); cx.observe_new(|workspace: &mut Workspace, _window, _cx| { diff --git a/crates/agent_ui/src/mcp_servers_ui.rs b/crates/agent_ui/src/mcp_servers_ui.rs index e1ad9ed5fc..1b81b5d828 100644 --- a/crates/agent_ui/src/mcp_servers_ui.rs +++ b/crates/agent_ui/src/mcp_servers_ui.rs @@ -7,8 +7,7 @@ use editor::{Editor, EditorElement, EditorStyle}; use extension_host::ExtensionStore; use fs::Fs; use gpui::{ - Action, Anchor as Corner, AnyElement, App, Context, Entity, EventEmitter, Focusable, - KeyContext, + Action, Anchor as Corner, AnyElement, App, Context, Entity, Focusable, KeyContext, ParentElement, Render, RenderOnce, SharedString, Styled, Task, TextStyle, UniformListScrollHandle, WeakEntity, Window, point, uniform_list, }; @@ -24,10 +23,7 @@ use ui::{ WithScrollbar, prelude::*, }; use util::ResultExt as _; -use workspace::{ - Workspace, - item::{Item, ItemEvent}, -}; +use workspace::Workspace; use crate::agent_configuration::{ ConfigureContextServerModal, ConfigureContextServerToolsModal, @@ -81,7 +77,7 @@ impl RenderOnce for McpServerCard { } } -pub struct McpServersPage { +pub struct McpServersView { fs: Arc, language_registry: Arc, workspace: WeakEntity, @@ -95,7 +91,7 @@ pub struct McpServersPage { _subscriptions: Vec, } -impl McpServersPage { +impl McpServersView { pub fn new( workspace: &Workspace, window: &mut Window, @@ -751,7 +747,7 @@ impl McpServersPage { } } -impl Render for McpServersPage { +impl Render for McpServersView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let total = self.server_ids.len(); let running = self @@ -864,34 +860,8 @@ impl Render for McpServersPage { } } -impl EventEmitter for McpServersPage {} - -impl Focusable for McpServersPage { +impl Focusable for McpServersView { fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { self.query_editor.read(cx).focus_handle(cx) } } - -impl Item for McpServersPage { - type Event = ItemEvent; - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "MCP Servers".into() - } - - fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { - Some(Icon::new(IconName::Server)) - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - Some("MCP Servers Page Opened") - } - - fn show_toolbar(&self) -> bool { - false - } - - fn to_item_events(event: &Self::Event, f: &mut dyn FnMut(workspace::item::ItemEvent)) { - f(*event) - } -} diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 8b29be32d7..828c99b0cd 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -1,22 +1,15 @@ use std::sync::Arc; -use std::time::Duration; -use client::{Client, UserStore, zed_urls}; -use cloud_api_types::Plan; -use collections::HashMap; +use client::UserStore; use fs::Fs; -use gpui::{Action, Animation, AnimationExt, App, Entity, IntoElement, TaskExt, pulsating_between}; -use project::agent_server_store::AllAgentServersSettings; +use gpui::{Action, App, Entity, IntoElement}; use project::project_settings::ProjectSettings; -use project::{AgentRegistryStore, RegistryAgent}; -use settings::{ - BaseKeymap, CustomAgentServerSettings, Settings, SettingsStore, update_settings_file, -}; +use settings::{BaseKeymap, Settings, update_settings_file}; use theme::{Appearance, SystemAppearance, ThemeRegistry}; use theme_settings::{ThemeAppearanceMode, ThemeName, ThemeSelection, ThemeSettings}; use ui::{ - AgentSetupButton, StatefulInteractiveElement, SwitchField, TintColor, ToggleButtonGroup, - ToggleButtonGroupSize, ToggleButtonSimple, ToggleButtonWithIcon, Tooltip, prelude::*, + StatefulInteractiveElement, SwitchField, TintColor, ToggleButtonGroup, ToggleButtonGroupSize, + ToggleButtonSimple, ToggleButtonWithIcon, Tooltip, prelude::*, }; use vim_mode_setting::VimModeSetting; @@ -442,171 +435,32 @@ fn render_import_settings_section(tab_index: &mut isize, cx: &mut App) -> impl I pub(crate) const FEATURED_AGENT_IDS: &[&str] = &["claude-acp", "codex-acp", "github-copilot-cli", "cursor"]; -fn render_registry_agent_button( - agent: &RegistryAgent, - installed: bool, - cx: &mut App, -) -> impl IntoElement { - let agent_id = agent.id().to_string(); - let element_id = format!("{}-onboarding", agent_id); - - let icon = match agent.icon_path() { - Some(icon_path) => Icon::from_external_svg(icon_path.clone()), - None => Icon::new(IconName::Sparkle), - } - .size(IconSize::XSmall) - .color(Color::Muted); - - let fs = ::global(cx); - - let state_element = if installed { - Icon::new(IconName::Check) - .size(IconSize::Small) - .color(Color::Success) - .into_any_element() - } else { - Label::new("Install") - .size(LabelSize::XSmall) - .color(Color::Muted) - .into_any_element() - }; - - AgentSetupButton::new(element_id) - .icon(icon) - .name(agent.name().clone()) - .state(state_element) - .disabled(installed) - .on_click(move |_, _, cx| { - telemetry::event!("Welcome Agent Install Clicked", agent = agent_id.as_str()); - let agent_id = agent_id.clone(); - update_settings_file(fs.clone(), cx, move |settings, _| { - let agent_servers = settings.agent_servers.get_or_insert_default(); - agent_servers.entry(agent_id).or_insert_with(|| { - CustomAgentServerSettings::Registry { - env: Default::default(), - default_mode: None, - default_model: None, - favorite_models: Vec::new(), - default_config_options: HashMap::default(), - favorite_config_option_values: HashMap::default(), - } - }); - }); - }) -} - -fn render_zed_agent_button(user_store: &Entity, cx: &mut App) -> impl IntoElement { - let client = Client::global(cx); - let status = *client.status().borrow(); - - let plan = user_store.read(cx).plan(); - let is_free = matches!(plan, Some(Plan::ZedFree) | None); - let is_pro = matches!(plan, Some(Plan::ZedPro)); - let is_trial = matches!(plan, Some(Plan::ZedProTrial)); - - let is_signed_out = status.is_signed_out() - || matches!( - status, - client::Status::AuthenticationError | client::Status::ConnectionError - ); - let is_signing_in = status.is_signing_in(); - let is_signed_in = !is_signed_out; - - let state_element = if is_signed_out { - Label::new("Sign In") - .size(LabelSize::XSmall) - .color(Color::Muted) - .into_any_element() - } else if is_signing_in { - Label::new("Signing In…") - .size(LabelSize::XSmall) - .color(Color::Muted) - .with_animation( - "signing-in", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.4, 0.8)), - |label, delta| label.alpha(delta), - ) - .into_any_element() - } else if is_signed_in && is_free { - Label::new("Start Free Trial") - .size(LabelSize::XSmall) - .color(Color::Muted) - .into_any_element() - } else { - Icon::new(IconName::Check) - .size(IconSize::Small) - .color(Color::Success) - .into_any_element() - }; - - AgentSetupButton::new("zed-agent-onboarding") - .icon( - Icon::new(IconName::ZedAgent) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .name("Zed Agent") - .state(state_element) - .disabled(is_trial || is_pro) - .map(|this| { - if is_signed_in && is_free { - this.on_click(move |_, _window, cx| { - telemetry::event!("Start Trial Clicked", state = "post-sign-in"); - cx.open_url(&zed_urls::start_trial_url(cx)) - }) - } else { - this.on_click(move |_, _, cx| { - telemetry::event!("Welcome Zed Agent Sign In Clicked"); - let client = Client::global(cx); - cx.spawn(async move |cx| client.sign_in_with_optional_connect(true, cx).await) - .detach_and_log_err(cx); - }) - } - }) -} - -fn render_ai_section(user_store: &Entity, cx: &mut App) -> impl IntoElement { - let registry_agents = AgentRegistryStore::try_global(cx) - .map(|store| store.read(cx).agents().to_vec()) - .unwrap_or_default(); - - let installed_agents = cx - .global::() - .get::(None) - .clone(); - - let column_count = 1 + FEATURED_AGENT_IDS.len() as u16; - - let grid = FEATURED_AGENT_IDS.iter().fold( - div() - .w_full() - .mt_1p5() - .grid() - .grid_cols(column_count) - .gap_2() - .child(render_zed_agent_button(user_store, cx)), - |grid, agent_id| { - let Some(agent) = registry_agents - .iter() - .find(|a| a.id().as_ref() == *agent_id) - else { - return grid; - }; - let is_installed = installed_agents.contains_key(*agent_id); - grid.child(render_registry_agent_button(agent, is_installed, cx)) - }, - ); - +// PaddleBoard: replaces the upstream 5-card "Agent Setup" row with a single +// entry point into the AI Dock. The dock consolidates agents, skills, and +// MCP servers; onboarding stays terse and defers detailed browsing. +fn render_ai_section(_user_store: &Entity, _cx: &mut App) -> impl IntoElement { v_flex() .gap_0p5() - .child(Label::new("Agent Setup")) + .child(Label::new("AI Dock")) .child( - Label::new("Install your favorite agents and start your first thread.") + Label::new("Browse and install agents, skills, and MCP servers.") .color(Color::Muted), ) - .child(grid) + .child( + div().mt_1p5().w_full().child( + Button::new("welcome-open-ai-dock", "Open the AI Dock") + .full_width() + .style(ButtonStyle::Outlined) + .end_icon(Icon::new(IconName::ArrowUpRight)) + .on_click(|_, window, cx| { + telemetry::event!("Welcome Open AI Dock Clicked"); + window.dispatch_action( + paddleboard_actions::ai_dock::Open.boxed_clone(), + cx, + ); + }), + ), + ) } pub(crate) fn render_basics_page(user_store: &Entity, cx: &mut App) -> impl IntoElement { diff --git a/crates/paddleboard/Cargo.toml b/crates/paddleboard/Cargo.toml index 6a5e5387e3..e4fac8708e 100644 --- a/crates/paddleboard/Cargo.toml +++ b/crates/paddleboard/Cargo.toml @@ -227,6 +227,7 @@ paddleboard_env_vars.workspace = true paddleboard_sandbox_prereqs.workspace = true paddleboard_sandbox_prereqs_ui.workspace = true paddleboard_sandbox_settings.workspace = true +paddleboard_ai_dock.workspace = true tokio = { workspace = true, features = ["rt"] } zlog.workspace = true zlog_settings.workspace = true diff --git a/crates/paddleboard/src/main.rs b/crates/paddleboard/src/main.rs index 3dd82ff7b8..c0187962df 100644 --- a/crates/paddleboard/src/main.rs +++ b/crates/paddleboard/src/main.rs @@ -500,6 +500,7 @@ fn main() { gpui_tokio::init(cx); paddleboard_sandbox_settings::init(cx); paddleboard_sandbox_prereqs_ui::init(cx); + paddleboard_ai_dock::init(cx); if let Some(app_commit_sha) = app_commit_sha { AppCommitSha::set_global(app_commit_sha, cx); } diff --git a/crates/paddleboard_actions/src/lib.rs b/crates/paddleboard_actions/src/lib.rs index 402f015cc6..86f8b2b686 100644 --- a/crates/paddleboard_actions/src/lib.rs +++ b/crates/paddleboard_actions/src/lib.rs @@ -889,3 +889,17 @@ pub mod notebook { ] ); } + +pub mod ai_dock { + use gpui::actions; + + actions!( + ai_dock, + [ + /// Opens the PaddleBoard AI Dock on the Agents tab. The existing + /// `zed::McpServers` action opens it directly on the MCP tab, + /// so old keybindings keep working. + Open, + ] + ); +} diff --git a/crates/paddleboard_ai_dock/Cargo.toml b/crates/paddleboard_ai_dock/Cargo.toml new file mode 100644 index 0000000000..b42df37d13 --- /dev/null +++ b/crates/paddleboard_ai_dock/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "paddleboard_ai_dock" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/paddleboard_ai_dock.rs" + +[dependencies] +agent_settings.workspace = true +agent_ui.workspace = true +anyhow.workspace = true +client.workspace = true +collections.workspace = true +fs.workspace = true +gpui.workspace = true +language.workspace = true +log.workspace = true +menu.workspace = true +paddleboard_actions.workspace = true +project.workspace = true +serde.workspace = true +serde_json.workspace = true +settings.workspace = true +ui.workspace = true +util.workspace = true +which.workspace = true +workspace.workspace = true diff --git a/crates/paddleboard_ai_dock/src/ai_dock.rs b/crates/paddleboard_ai_dock/src/ai_dock.rs new file mode 100644 index 0000000000..791cfb1611 --- /dev/null +++ b/crates/paddleboard_ai_dock/src/ai_dock.rs @@ -0,0 +1,207 @@ +use std::sync::Arc; + +use gpui::{ + ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, + WeakEntity, +}; +use ui::{ + ToggleButtonGroup, ToggleButtonGroupSize, ToggleButtonGroupStyle, ToggleButtonSimple, Tooltip, + prelude::*, +}; +use workspace::{ModalView, Workspace}; + +use crate::catalog::{Catalog, CatalogGlobal}; + +mod agents_tab; +mod mcp_tab; +mod skills_tab; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AiDockTab { + Agents, + Skills, + Mcp, +} + +pub struct AiDock { + focus_handle: FocusHandle, + workspace: WeakEntity, + tab: AiDockTab, + catalog: Arc, + mcp_view: Option>, +} + +impl AiDock { + pub fn toggle( + workspace: &mut Workspace, + tab: AiDockTab, + window: &mut Window, + cx: &mut Context, + ) { + let weak_workspace = workspace.weak_handle(); + workspace.toggle_modal(window, cx, |_window, cx| AiDock { + focus_handle: cx.focus_handle(), + workspace: weak_workspace, + tab, + catalog: CatalogGlobal::get(cx), + mcp_view: None, + }); + } + + fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context) { + cx.emit(DismissEvent); + } + + fn switch_tab(&mut self, tab: AiDockTab, window: &mut Window, cx: &mut Context) { + if self.tab == tab { + return; + } + self.tab = tab; + if matches!(tab, AiDockTab::Mcp) { + self.ensure_mcp_view(window, cx); + } + cx.notify(); + } + + fn ensure_mcp_view(&mut self, window: &mut Window, cx: &mut Context) { + if self.mcp_view.is_some() { + return; + } + let Some(workspace) = self.workspace.upgrade() else { + return; + }; + let view = workspace.update(cx, |workspace, cx| { + agent_ui::McpServersView::new(workspace, window, cx) + }); + self.mcp_view = Some(view); + } + + fn render_tab_switcher(&self, cx: &mut Context) -> AnyElement { + let selected_index = match self.tab { + AiDockTab::Agents => 0, + AiDockTab::Skills => 1, + AiDockTab::Mcp => 2, + }; + + ToggleButtonGroup::single_row( + "ai-dock-tab-switcher", + [ + ToggleButtonSimple::new( + "Agents", + cx.listener(|this, _event, window, cx| { + this.switch_tab(AiDockTab::Agents, window, cx); + }), + ), + ToggleButtonSimple::new( + "Skills", + cx.listener(|this, _event, window, cx| { + this.switch_tab(AiDockTab::Skills, window, cx); + }), + ), + ToggleButtonSimple::new( + "MCP Servers", + cx.listener(|this, _event, window, cx| { + this.switch_tab(AiDockTab::Mcp, window, cx); + }), + ), + ], + ) + .style(ToggleButtonGroupStyle::Outlined) + .size(ToggleButtonGroupSize::Medium) + .selected_index(selected_index) + .into_any_element() + } + + fn render_header(&self, cx: &mut Context) -> AnyElement { + let counts = ( + self.catalog.agents.len(), + self.catalog.skills.len(), + self.catalog.mcp_servers.len(), + ); + h_flex() + .w_full() + .justify_between() + .gap_2() + .child( + v_flex() + .gap_0p5() + .child(Headline::new("AI Dock").size(HeadlineSize::Large)) + .child( + Label::new(format!( + "{} agents · {} skills · {} MCP servers", + counts.0, counts.1, counts.2 + )) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + .child( + IconButton::new("ai-dock-close", IconName::Close) + .tooltip(Tooltip::text("Close")) + .on_click(cx.listener(|_, _: &ClickEvent, _window, cx| { + cx.emit(DismissEvent); + })), + ) + .into_any_element() + } + + fn render_tab_body(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { + match self.tab { + AiDockTab::Agents => agents_tab::render(self, cx).into_any_element(), + AiDockTab::Skills => skills_tab::render(self, cx).into_any_element(), + AiDockTab::Mcp => mcp_tab::render(self, window, cx), + } + } +} + +impl EventEmitter for AiDock {} + +impl Focusable for AiDock { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl ModalView for AiDock {} + +impl Render for AiDock { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + // Make sure the MCP view exists if we opened directly to MCP. + if matches!(self.tab, AiDockTab::Mcp) && self.mcp_view.is_none() { + self.ensure_mcp_view(window, cx); + } + + let header = self.render_header(cx); + let tab_switcher = self.render_tab_switcher(cx); + let body = self.render_tab_body(window, cx); + + v_flex() + .id("ai-dock") + .key_context("AiDock") + .elevation_3(cx) + .w(rems(56.)) + .h(rems(36.)) + .track_focus(&self.focus_handle(cx)) + .on_action(cx.listener(Self::cancel)) + .on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, cx| { + this.focus_handle.focus(window, cx); + })) + .child( + v_flex() + .p_4() + .gap_3() + .border_b_1() + .border_color(cx.theme().colors().border_variant) + .child(header) + .child(tab_switcher), + ) + .child( + div() + .id("ai-dock-body") + .flex_1() + .min_h_0() + .overflow_hidden() + .child(body), + ) + } +} diff --git a/crates/paddleboard_ai_dock/src/ai_dock/agents_tab.rs b/crates/paddleboard_ai_dock/src/ai_dock/agents_tab.rs new file mode 100644 index 0000000000..ce0c26ebba --- /dev/null +++ b/crates/paddleboard_ai_dock/src/ai_dock/agents_tab.rs @@ -0,0 +1,173 @@ +use client::Client; +use collections::HashMap; +use fs::Fs; +use gpui::ClickEvent; +use project::AgentRegistryStore; +use project::agent_server_store::AllAgentServersSettings; +use settings::{CustomAgentServerSettings, SettingsStore, update_settings_file}; +use ui::prelude::*; + +use crate::catalog::AgentEntry; +use crate::ai_dock::AiDock; + +pub(super) fn render(modal: &AiDock, cx: &mut Context) -> impl IntoElement { + let catalog = modal.catalog.clone(); + let registry_agents = AgentRegistryStore::try_global(cx) + .map(|store| store.read(cx).agents().to_vec()) + .unwrap_or_default(); + let installed_agents = cx + .global::() + .get::(None) + .clone(); + + v_flex() + .id("ai-dock-agents-list") + .size_full() + .p_4() + .gap_2() + .overflow_y_scroll() + .children( + catalog + .agents + .iter() + .map(|entry| render_agent_row(entry, ®istry_agents, &installed_agents, cx)), + ) +} + +fn render_agent_row( + entry: &AgentEntry, + registry_agents: &[project::RegistryAgent], + installed_agents: &AllAgentServersSettings, + cx: &mut Context, +) -> AnyElement { + let installed = entry.builtin_zed || installed_agents.contains_key(&entry.id); + let registry_agent = registry_agents.iter().find(|a| a.id().as_ref() == entry.id); + + let icon = if entry.builtin_zed { + Icon::new(IconName::ZedAgent) + } else if let Some(reg) = registry_agent.as_ref() { + match reg.icon_path() { + Some(path) => Icon::from_external_svg(path.clone()), + None => Icon::new(IconName::Sparkle), + } + } else { + Icon::new(IconName::Sparkle) + } + .size(IconSize::Small) + .color(Color::Muted); + + let action_button: AnyElement = if entry.builtin_zed { + zed_agent_button(cx) + } else if installed { + Button::new(SharedString::from(format!("ai-dock-open-{}", entry.id)), "Configure") + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .into_any_element() + } else { + let agent_id = entry.id.clone(); + let fs = ::global(cx); + Button::new(SharedString::from(format!("ai-dock-install-{}", entry.id)), "Install") + .style(ButtonStyle::Filled) + .label_size(LabelSize::Small) + .on_click(move |_: &ClickEvent, _window, cx| { + let agent_id = agent_id.clone(); + update_settings_file(fs.clone(), cx, move |settings, _| { + let agent_servers = settings.agent_servers.get_or_insert_default(); + agent_servers.entry(agent_id).or_insert_with(|| { + CustomAgentServerSettings::Registry { + env: Default::default(), + default_mode: None, + default_model: None, + favorite_models: Vec::new(), + default_config_options: HashMap::default(), + favorite_config_option_values: HashMap::default(), + } + }); + }); + }) + .into_any_element() + }; + + let homepage_link: Option = entry.homepage.as_ref().map(|url| { + let url = url.clone(); + IconButton::new( + SharedString::from(format!("ai-dock-homepage-{}", entry.id)), + IconName::ArrowUpRight, + ) + .icon_size(IconSize::Small) + .tooltip(ui::Tooltip::text("Open homepage")) + .on_click(move |_: &ClickEvent, _window, cx| { + cx.open_url(&url); + }) + .into_any_element() + }); + + h_flex() + .w_full() + .p_3() + .gap_3() + .items_start() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border_variant) + .bg(cx.theme().colors().elevated_surface_background.opacity(0.5)) + .child(div().pt_0p5().child(icon)) + .child( + v_flex() + .flex_1() + .min_w_0() + .gap_0p5() + .child( + h_flex() + .gap_2() + .child(Label::new(SharedString::from(entry.name.clone()))) + .when(installed, |this| { + this.child( + Label::new("Installed") + .size(LabelSize::XSmall) + .color(Color::Success), + ) + }), + ) + .child( + Label::new(SharedString::from(entry.description.clone())) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + .child( + h_flex() + .gap_1() + .children(homepage_link) + .child(action_button), + ) + .into_any_element() +} + +fn zed_agent_button(cx: &mut Context) -> AnyElement { + let client = Client::global(cx); + let status = *client.status().borrow(); + let is_signed_out = status.is_signed_out() + || matches!( + status, + client::Status::AuthenticationError | client::Status::ConnectionError + ); + + if is_signed_out { + Button::new("ai-dock-zed-signin", "Sign In") + .style(ButtonStyle::Filled) + .label_size(LabelSize::Small) + .on_click(move |_: &ClickEvent, _window, cx| { + let client = Client::global(cx); + cx.spawn(async move |cx| client.sign_in_with_optional_connect(true, cx).await) + .detach_and_log_err(cx); + }) + .into_any_element() + } else { + Button::new("ai-dock-zed-configured", "Signed In") + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .disabled(true) + .into_any_element() + } +} diff --git a/crates/paddleboard_ai_dock/src/ai_dock/mcp_tab.rs b/crates/paddleboard_ai_dock/src/ai_dock/mcp_tab.rs new file mode 100644 index 0000000000..0bfc184b1b --- /dev/null +++ b/crates/paddleboard_ai_dock/src/ai_dock/mcp_tab.rs @@ -0,0 +1,33 @@ +use gpui::AnyElement; +use ui::prelude::*; + +use crate::ai_dock::AiDock; + +pub(super) fn render( + modal: &mut AiDock, + _window: &mut Window, + _cx: &mut Context, +) -> AnyElement { + // The MCP tab hosts an absorbed `agent_ui::McpServersView` — the same + // surface that lived as a standalone workspace pane item before the + // AI Dock consolidation. The view is created lazily by + // `AiDock::ensure_mcp_view` when the user first switches to this tab (or + // when the dock opens directly to MCP via the legacy + // `paddleboard_actions::McpServers` action). + match modal.mcp_view.as_ref() { + Some(view) => div() + .size_full() + .child(view.clone()) + .into_any_element(), + None => v_flex() + .size_full() + .items_center() + .justify_center() + .child( + Label::new("Loading MCP servers…") + .color(Color::Muted) + .size(LabelSize::Small), + ) + .into_any_element(), + } +} diff --git a/crates/paddleboard_ai_dock/src/ai_dock/skills_tab.rs b/crates/paddleboard_ai_dock/src/ai_dock/skills_tab.rs new file mode 100644 index 0000000000..26a01ef4e7 --- /dev/null +++ b/crates/paddleboard_ai_dock/src/ai_dock/skills_tab.rs @@ -0,0 +1,141 @@ +use std::path::PathBuf; + +use gpui::ClickEvent; +use ui::prelude::*; + +use crate::catalog::SkillEntry; +use crate::ai_dock::AiDock; + +pub(super) fn render(modal: &AiDock, cx: &mut Context) -> impl IntoElement { + let catalog = modal.catalog.clone(); + + // Detect installed skills by scanning the two well-known directories. + // Skills are markdown files named `.md` under `.claude/commands/` + // (project-scoped) or `~/.claude/commands/` (user-scoped). + let project_dir = project_skills_dir(modal, cx); + let user_dir = user_skills_dir(); + let scope_of = |id: &str| -> Option { + if let Some(dir) = project_dir.as_ref() { + if dir.join(format!("{id}.md")).exists() { + return Some(SkillScope::Project); + } + } + if let Some(dir) = user_dir.as_ref() { + if dir.join(format!("{id}.md")).exists() { + return Some(SkillScope::User); + } + } + None + }; + + v_flex() + .id("ai-dock-skills-list") + .size_full() + .p_4() + .gap_2() + .overflow_y_scroll() + .children( + catalog + .skills + .iter() + .map(|entry| render_skill_row(entry, scope_of(&entry.id), cx)), + ) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SkillScope { + Project, + User, +} + +impl SkillScope { + fn label(self) -> &'static str { + match self { + SkillScope::Project => "Project", + SkillScope::User => "User", + } + } +} + +fn project_skills_dir(_modal: &AiDock, _cx: &App) -> Option { + // For v1, use the current working directory's `.claude/commands/`. + // A future polish pass can read this from the active workspace's + // first worktree instead, since modals don't track which workspace + // they belong to as cleanly as panel items do. + let cwd = std::env::current_dir().ok()?; + Some(cwd.join(".claude").join("commands")) +} + +fn user_skills_dir() -> Option { + let home = std::env::var_os("HOME").map(PathBuf::from)?; + Some(home.join(".claude").join("commands")) +} + +fn render_skill_row( + entry: &SkillEntry, + scope: Option, + cx: &mut Context, +) -> AnyElement { + let icon = Icon::new(IconName::Sparkle) + .size(IconSize::Small) + .color(Color::Muted); + + let action_button: AnyElement = if let Some(scope) = scope { + Button::new( + SharedString::from(format!("ai-dock-skill-installed-{}", entry.id)), + format!("Installed ({})", scope.label()), + ) + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .disabled(true) + .into_any_element() + } else { + // No bundled content yet — direct to homepage if present, otherwise + // surface a disabled button so users see the skill exists. + match entry.homepage.clone() { + Some(url) => Button::new( + SharedString::from(format!("ai-dock-skill-info-{}", entry.id)), + "Learn More", + ) + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .on_click(move |_: &ClickEvent, _window, cx| { + cx.open_url(&url); + }) + .into_any_element(), + None => Button::new( + SharedString::from(format!("ai-dock-skill-na-{}", entry.id)), + "Not installed", + ) + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .disabled(true) + .into_any_element(), + } + }; + + h_flex() + .w_full() + .p_3() + .gap_3() + .items_start() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border_variant) + .bg(cx.theme().colors().elevated_surface_background.opacity(0.5)) + .child(div().pt_0p5().child(icon)) + .child( + v_flex() + .flex_1() + .min_w_0() + .gap_0p5() + .child(Label::new(SharedString::from(entry.name.clone()))) + .child( + Label::new(SharedString::from(entry.description.clone())) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + .child(action_button) + .into_any_element() +} diff --git a/crates/paddleboard_ai_dock/src/catalog.rs b/crates/paddleboard_ai_dock/src/catalog.rs new file mode 100644 index 0000000000..cd48aa8a76 --- /dev/null +++ b/crates/paddleboard_ai_dock/src/catalog.rs @@ -0,0 +1,105 @@ +use std::sync::Arc; + +use gpui::{App, Global}; +use serde::Deserialize; + +/// In-repo catalog of installable agents, skills, and MCP servers. Loaded +/// once at startup from `assets/ai_dock/catalog.json` (embedded via +/// `include_str!`), so adding an entry means opening a PR. +#[derive(Debug, Clone, Default, Deserialize)] +pub struct Catalog { + #[serde(default)] + pub agents: Vec, + #[serde(default)] + pub skills: Vec, + #[serde(default)] + pub mcp_servers: Vec, +} + +/// One agent entry. `id` should match the corresponding `RegistryAgent` id +/// (so the Store can cross-reference `project::AgentRegistryStore` to learn +/// the install status without duplicating that logic). +#[derive(Debug, Clone, Deserialize)] +pub struct AgentEntry { + pub id: String, + pub name: String, + pub description: String, + #[serde(default)] + pub homepage: Option, + /// Pinned to the small "Featured" strip on the Welcome screen. + #[serde(default)] + pub featured: bool, + /// Identifies the special-case Zed Agent. Renders the sign-in/plan flow + /// instead of the generic agent-server install path. + #[serde(default)] + pub builtin_zed: bool, +} + +/// One skill entry. Skills are markdown files dropped into `.claude/commands/` +/// (per-project) or `~/.claude/commands/` (per-user). Bundled skills carry +/// the markdown body inline so "Add" works without a network round-trip. +#[derive(Debug, Clone, Deserialize)] +pub struct SkillEntry { + pub id: String, + pub name: String, + pub description: String, + #[serde(default)] + pub homepage: Option, + #[serde(default)] + pub featured: bool, +} + +/// One MCP server entry. The AI Dock's MCP tab uses these to populate the +/// "Available" list; selecting one delegates to the absorbed +/// `McpServersView` (which knows how to actually register the server with +/// `context_server_store`). +#[derive(Debug, Clone, Deserialize)] +pub struct McpEntry { + pub id: String, + pub name: String, + pub description: String, + pub command: String, + #[serde(default)] + pub args: Vec, + #[serde(default)] + pub homepage: Option, + #[serde(default)] + pub featured: bool, +} + +impl Catalog { + /// Parse the bundled catalog. If parsing fails we fall back to an empty + /// catalog and log; that keeps the AI Dock rendering instead of crashing + /// the workspace on a malformed JSON edit. + pub fn load() -> Self { + const SOURCE: &str = include_str!("../../../assets/ai_dock/catalog.json"); + match serde_json::from_str::(SOURCE) { + Ok(catalog) => catalog, + Err(err) => { + log::error!("paddleboard_ai_dock: failed to parse catalog.json: {err:#}"); + Catalog::default() + } + } + } + + pub fn empty() -> Self { + Self::default() + } +} + +#[derive(Clone)] +pub(crate) struct CatalogGlobal(pub(crate) Arc); + +impl Global for CatalogGlobal {} + +impl CatalogGlobal { + pub(crate) fn get(cx: &App) -> Arc { + cx.try_global::() + .map(|g| g.0.clone()) + .unwrap_or_else(|| Arc::new(Catalog::empty())) + } +} + +pub fn catalog(cx: &App) -> Arc { + CatalogGlobal::get(cx) +} diff --git a/crates/paddleboard_ai_dock/src/paddleboard_ai_dock.rs b/crates/paddleboard_ai_dock/src/paddleboard_ai_dock.rs new file mode 100644 index 0000000000..4363a298bf --- /dev/null +++ b/crates/paddleboard_ai_dock/src/paddleboard_ai_dock.rs @@ -0,0 +1,39 @@ +//! PaddleBoard AI Dock: a single browse-surface for Agents, Skills, and MCP +//! servers. Replaces the hardcoded "Agent Setup" row on the Welcome screen +//! and absorbs the standalone MCP Servers page. +//! +//! Entry point: the `paddleboard_actions::ai_dock::Open` action toggles an +//! `AiDock` on the active workspace. The dock owns three tab views and a +//! shared `Catalog` loaded once at startup from the in-repo JSON. + +use std::sync::Arc; + +use gpui::App; +use workspace::Workspace; + +mod ai_dock; +pub mod catalog; + +pub use ai_dock::{AiDock, AiDockTab}; +pub use catalog::{AgentEntry, Catalog, McpEntry, SkillEntry}; + +/// Initialize the AI Dock: load the catalog into a global and wire the +/// `Open` action onto every workspace. +pub fn init(cx: &mut App) { + let catalog = Arc::new(Catalog::load()); + cx.set_global(catalog::CatalogGlobal(catalog)); + + cx.observe_new(|workspace: &mut Workspace, _window, _cx| { + workspace.register_action( + |workspace, _: &paddleboard_actions::ai_dock::Open, window, cx| { + AiDock::toggle(workspace, AiDockTab::Agents, window, cx); + }, + ); + workspace.register_action( + |workspace, _: &paddleboard_actions::McpServers, window, cx| { + AiDock::toggle(workspace, AiDockTab::Mcp, window, cx); + }, + ); + }) + .detach(); +} diff --git a/crates/workspace/src/tour.md b/crates/workspace/src/tour.md index 09afde4b9e..2abe8bbdcb 100644 --- a/crates/workspace/src/tour.md +++ b/crates/workspace/src/tour.md @@ -28,35 +28,42 @@ Long-lived processes (dev servers, demo apps, `adk web`) use the **Sandbox Servi ### 4. Sandboxed MCP Servers PaddleBoard runs your **MCP servers** inside the same Podman + gVisor sandbox as the Sandbox Tool. -- Configure them via `Cmd-Shift-P` → **`zed: Mcp Servers`** — filter by status (All / Running / Stopped / Error), add new servers, or inspect live state. +- Manage them in the AI Dock: `Cmd-Shift-P` → **`zed: Mcp Servers`** opens the dock on the MCP tab (filter All / Running / Stopped / Error, add servers, browse the catalog of common ones). - Or use `"source": "sandboxed_stdio"` in `settings.json` directly. - Forward only the host env vars you need by name — values stay out of the agent's context. - The worktree is mounted at `/workspace` so filesystem-touching servers (git, fs, etc.) still work. -### 5. Step-Through Mode +### 5. AI Dock +One place to browse and install everything the agent talks to — the marina where every external collaborator ties up. +- Open it: `Cmd-Shift-P` → **`ai_dock: Open`**, or hit **Open the AI Dock** on the Welcome screen. +- Three tabs: **Agents** (Zed, Claude, Codex, Copilot, Cursor), **Skills** (slash commands), **MCP Servers** (catalog + absorbed management UI). +- Installed items show a green badge; missing ones get a one-click **Install / Sign In / Learn More** that does the category-appropriate thing. +- The catalog is `assets/ai_dock/catalog.json` in-repo — adds are PRs, not fetches. + +### 6. Step-Through Mode Approve every tool call before the agent executes it. - Click the **⏭** icon in the agent thread toolbar to enable (it turns accent-colored). - Each tool call pauses with **Step** (run it) or **Skip** (return empty and move on). - Only the root thread is gated — subagents run without interruption. -### 6. Agent Orchestration Panel +### 7. Agent Orchestration Panel A live tree view of every active agent session, including subagents. - Open it: panel bar `ListTree` icon, or `Cmd-Shift-P` → **`orchestration_panel: Toggle Focus`**. - Subagents nest under the thread that spawned them. - Status dot shows generating vs. idle; click any row to jump to that thread. -### 7. LLM Provider Picker Panel +### 8. LLM Provider Picker Panel A dedicated panel for switching the active language model provider without opening settings. - Dock it wherever is convenient and change providers as you work. - **ChatGPT Subscription auth**: sign in with your ChatGPT Plus or Pro account via OAuth — no API key needed. The flow opens in the embedded browser panel; tokens persist in PB's credential store. -### 8. Multi-Workspace +### 9. Multi-Workspace Keep multiple projects in one window, each as its own workspace with its own pane tree and its own agent threads. - Open the worktree picker: `Cmd-Shift-P` → **`git: Worktree`**. - **Switch** between existing worktrees, **create** a new worktree-backed workspace (accept the auto-generated branch name like `dusty-pelican` or supply your own), or **open in new window**. - The orchestration panel shows agent threads from every workspace at once — perfect for parallel agent sessions against different projects. -### 9. Built-in Language Servers +### 10. Built-in Language Servers PaddleBoard ships built-in LSP support for four languages that Zed historically punts to extensions — **no extension installation required**. - **Java** via [jdtls](https://github.com/eclipse/eclipse.jdt.ls) - **Kotlin** via [kotlin-language-server](https://github.com/fwcd/kotlin-language-server) diff --git a/paddleboard-5.png b/paddleboard-5.png new file mode 100644 index 0000000000000000000000000000000000000000..20816bea1070987792ccf609fc97a51f296b8098 GIT binary patch literal 19947 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 s{6E&u=k literal 0 HcmV?d00001 From 9669d4b5874249895cbb35d48e5b4f8552f23499 Mon Sep 17 00:00:00 2001 From: "Jason \"Jay\" Smith" Date: Fri, 22 May 2026 10:12:24 -0700 Subject: [PATCH 2/4] command_palette: rename `zed:` to `paddleboard:` in palette display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `humanize_action_name` now strips a leading `zed::` and replaces it with `paddleboard::` before applying the existing humanization. Propagates to every caller of the helper — command palette, which_key, keymap_editor, and the paddleboard binary's menu builder — so the visible action prefix matches the rest of the fork's branding. Display-only. `Action::name()` still returns `zed::Foo`, so every binding in `assets/keymaps/` keeps resolving without edits. The alternative (rewriting 17 `#[action(namespace = zed)]` declarations plus 23 keymap references) would have been a maintenance trap on every upstream merge. Tagged with a `// PaddleBoard:` divergence comment and a regression-locking test asserting `humanize_action_name("zed::OpenOnboarding") == "paddleboard: open onboarding"`. Release Notes: - Improved command palette labels to read `paddleboard: ` instead of `zed: ` (cosmetic; existing keybindings unchanged). Co-Authored-By: Claude Opus 4.7 --- crates/command_palette/src/command_palette.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 6a08d9b758..8c658e310f 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -695,6 +695,18 @@ impl PickerDelegate for CommandPaletteDelegate { } pub fn humanize_action_name(name: &str) -> String { + // PaddleBoard: rename the `zed` action namespace to `paddleboard` for + // user-facing display (command palette, which-key, keymap editor, menus). + // The underlying action name returned by `Action::name()` is still + // `zed::...`, so every binding in `assets/keymaps/` keeps resolving — this + // is presentation-only, not a real namespace rename. + let renamed; + let name = if let Some(rest) = name.strip_prefix("zed::") { + renamed = format!("paddleboard::{rest}"); + renamed.as_str() + } else { + name + }; let capacity = name.len() + name.chars().filter(|c| c.is_uppercase()).count(); let mut result = String::with_capacity(capacity); for char in name.chars() { @@ -753,6 +765,11 @@ mod tests { humanize_action_name("go_to_line::Deploy"), "go to line: deploy" ); + // PaddleBoard: `zed::` namespace is remapped to `paddleboard:` for display. + assert_eq!( + humanize_action_name("zed::OpenOnboarding"), + "paddleboard: open onboarding" + ); } #[test] From a8fd95efe4b904e79d8fa83a95b886ebfbb05c67 Mon Sep 17 00:00:00 2001 From: "Jason \"Jay\" Smith" Date: Fri, 22 May 2026 10:13:42 -0700 Subject: [PATCH 3/4] paddleboard_ai_dock: install bundled skills, add Welcome featured strip, drop /simplify MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Followup polish on the 2026-05-21 AI Dock landing. What lands: - **Skills tab install path.** When a catalog skill has bundled markdown content AND is not yet installed, the row now renders two buttons — **Add to project** and **Add to user** — that write the file into `/.claude/commands/.md` or `~/.claude/commands/.md` and `cx.notify()` so the badge flips to "Installed". Errors surface via `Workspace::show_error`. Sync IO inside the click handler is intentional: a few-KB write completes in microseconds, and backgrounding would have required threading a `WeakEntity` for no real win. - **Bundled content** via `bundled_skill_content(id)` in `catalog.rs`, using `include_str!` directly against `.claude/commands/.md`. The slash command this repo uses and the install copy are physically the same bytes — zero drift, and renaming the source file would break the build. - **`project_skills_dir` fix.** Previously fell back to `std::env::current_dir()`, which is the launch directory, not the active workspace. Now reads `workspace.project().visible_worktrees(cx).next()` and joins `.claude/commands/`, with the CWD path only as a last-resort fallback. Both detection and install honor this — picking the wrong project root for a write would have been a silent data hazard. - **Welcome Featured strip.** Below the existing "Open the AI Dock" button, the onboarding screen now shows a small "Featured" label plus four outlined pills — **Claude**, **Codex**, **Copilot**, **Cursor**. Each pill dispatches the same `ai_dock::Open` action; the editorial value is the names being visible to first-run users, not a per-pill action. A parallel `WELCOME_FEATURED_AGENT_LABELS` constant lives next to the existing `FEATURED_AGENT_IDS` so the upstream-shaped `onboarding.rs:245` telemetry call site stays zero-touch. - **Catalog cleanup.** Dropped the phantom `/simplify` entry — its description was copy-pasted from `/review` and no real `/simplify` skill exists anywhere. Skills count: 6 → 5; the dock subtitle auto-updates. Also fixed `$schema_note` that still referenced the pre-rename "Store" naming and the deleted `paddleboard_store` crate path. - **Docs sync.** `WELCOME.md` and `crates/workspace/src/tour.md` now mention the Featured strip and the new Skills install buttons. Stale `zed: Mcp Servers` palette references in both files updated to `paddleboard: Mcp Servers` to match the namespace display rename. - **`.gitignore`** excludes `aidock-*.png` so future smoke-test screenshots stay local instead of cluttering `git status`. Tests added in `paddleboard_ai_dock::catalog::tests`: - `bundled_skill_content_returns_known_skills` - `bundled_skill_content_returns_none_for_unbundled` - `every_bundled_id_is_in_catalog` (structural — catches an orphan bundled id whose catalog entry was deleted, which would leave install buttons that never render) Verified: `cargo check -p paddleboard_ai_dock -p onboarding`, `./script/clippy -p paddleboard_ai_dock -p onboarding` (release, all targets, deny warnings), `cargo test -p paddleboard_ai_dock` (3/3 pass), `cargo build -p paddleboard`. UI smoke test confirmed the Featured strip renders and the modal header now reads "5 agents · 5 skills · 5 MCP servers" with `/simplify` gone from the Skills list. Open follow-ups (deferred): - `/verify`, `/review`, `/security-review` are still in the catalog without bundled content; render as "Not installed" with no install path. These are claude-code harness-bundled skills — decide whether to drop them from the catalog (harness owns discovery) or ship our own copies. - MCP tab still ignores the catalog: header counts 5, tab shows 0 installed and never lists the 5 entries (filesystem, fetch, git, github, puppeteer). The three tabs have asymmetric browse behavior — catalog-driven for Agents/Skills, installed-only for MCP. Release Notes: - Added install buttons (Add to project / Add to user) for bundled skills in the AI Dock and a Welcome-screen Featured strip surfacing Claude, Codex, Copilot, and Cursor. The `/build` and `/update-tour` skills now have install paths instead of routing to a homepage link. Co-Authored-By: Claude Opus 4.7 --- .gitignore | 3 + RECAPS.md | 40 +++++ WELCOME.md | 8 +- assets/ai_dock/catalog.json | 9 +- crates/onboarding/src/basics_page.rs | 50 ++++++- .../src/ai_dock/skills_tab.rs | 140 +++++++++++++++--- crates/paddleboard_ai_dock/src/catalog.rs | 56 +++++++ crates/workspace/src/tour.md | 6 +- 8 files changed, 278 insertions(+), 34 deletions(-) diff --git a/.gitignore b/.gitignore index b9365a3c40..6fbcc5ba86 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,6 @@ crates/docs_preprocessor/actions.json /december-2025-releases.md /docs/december-2025-documentation-gaps.md site/ + +# AI Dock smoke-test screenshots (verification evidence — local only) +aidock-*.png diff --git a/RECAPS.md b/RECAPS.md index eb2e0599f7..67d61ffdb7 100644 --- a/RECAPS.md +++ b/RECAPS.md @@ -4,6 +4,46 @@ Running log of completed work sessions, newest first. Each entry summarizes a co --- +## 2026-05-22 + +### AI Dock — smoke test (followup #1) +- Verified yesterday's AI Dock PR (`3d22299897`) end-to-end in the running app. Built a fresh debug binary (prior `target/debug/paddleboard` was timestamped May 21 17:50, ~30 minutes *before* the commit), launched it, drove it via the command palette with user-captured screenshots (`aidock-1.png` … `aidock-5.png`). All six claims from the smoke-test plan passed: Welcome button replaces the 5-card grid, modal opens on Agents with header counts matching the catalog (5 / 6 / 5), tab switcher works, filesystem detection correctly badges `/build` + `/update-tour` as **Installed (Project)**, absorbed `McpServersView` renders cleanly inside the 36rem modal body with no clipping, legacy `zed::McpServers` action routes to the MCP tab. +- **Screen Recording perms were blocked again.** Same blocker the previous session hit, with an added detail: parent terminal is **iTerm2**, not Terminal.app — granting iTerm2 the perm would have killed this Claude session on restart. Fell back to user-captured screenshots, which gave full visual evidence without the restart problem. Worth memorializing as the default verification path for any GUI work going forward. +- **Findings worth following up on (not blockers, deferred):** + - **MCP tab ignores the catalog.** Header subtitle says "5 MCP servers" but the MCP tab itself just embeds `McpServersView` (installed-only) and shows "No MCP servers installed yet." The 5 catalog entries (`filesystem`, `fetch`, `git`, `github`, `puppeteer`) are never rendered. The three tabs have asymmetric behavior: browse-then-install for Agents/Skills, installed-only for MCP. Either drop the MCP count from the header, render the catalog above the installed list, or pre-populate "+ Add Server" with the catalog. Adds to the AI Dock open-followup list. + - **`/simplify` is a phantom catalog entry.** Listed in `assets/ai_dock/catalog.json` with a description that looks copy-pasted from `/review` ("Review changed code for reuse…"). No corresponding skill exists in the harness's known skill list. Catalog accuracy issue; either drop or write the actual skill before the Skills tab becomes a real discovery surface. + - **Cosmetic, not acted on**: in `aidock-2.png` the welcome screen text is visible *below* the modal — scrim doesn't extend full-height, or modal floats inside the workspace pane rather than the window. + - **Terminology asymmetry**: Agents tab uses "Installed" badge + "Install" button; Skills tab uses "Installed (Project)" / "Not installed" badge only. Picking one pattern would help. + +### AI Dock — Welcome Featured strip (followup #3) + docs/tour sync +- Added a small **Featured** strip to the Welcome screen's AI Dock section: four compact outlined pills labeled `Claude` / `Codex` / `Copilot` / `Cursor`, rendered below the existing full-width "Open the AI Dock" button via a new `render_welcome_featured_strip()` helper in `crates/onboarding/src/basics_page.rs`. Each pill dispatches the same `paddleboard_actions::ai_dock::Open` action — they're editorial discoverability, not separate destinations. A future polish could pre-scroll the dock to the matching agent card; out of scope for v1 (would need a new action variant carrying the target id). +- **Why a parallel constant instead of upgrading `FEATURED_AGENT_IDS`**: the existing `FEATURED_AGENT_IDS: &[&str]` is referenced by `onboarding.rs:245` for telemetry (`.iter().filter().copied()`) and a tuple-ification would have required updating that call site too. Adding `WELCOME_FEATURED_AGENT_LABELS: &[(&str, &str)]` alongside it keeps the upstream-shaped telemetry path zero-touch. The two arrays must stay in id-sync; a future test could enforce that, but four entries felt below the threshold for now. +- **Telemetry**: each pill fires `Welcome Featured Agent Clicked` with the agent id, distinct from the existing `Welcome Open AI Dock Clicked`. Lets us measure whether the strip drives more dock opens than the main button alone. +- **Docs sync (per [[feedback-update-welcome-md]])**: edited `WELCOME.md` to (a) mention the Featured strip in the AI Dock bullet, (b) describe the new Skills install buttons (`Add to project` / `Add to user`) from followup #2 — that text was missing because the doc was last updated when Skills were detection-only, and (c) replace two stale `zed: Mcp Servers` palette references with `paddleboard: Mcp Servers` to match the namespace display-rename earlier in the session. +- **Tour sync (`crates/workspace/src/tour.md`)**: ran `/update-tour`. Two edits — section 4 (Sandboxed MCP Servers) now reads `paddleboard: Mcp Servers`; section 5 (AI Dock) gained a sentence about the Featured strip and a sentence about the bundled-skill install buttons. Section count and emoji budget unchanged; AI Dock section stayed inside the 4–7-line target (now 6 lines). +- **Sticky-file caveat** carried over: `workspace.rs:785` / `paddleboard/src/main.rs:1494` only write `PaddleBoard_Tour.md` when it doesn't exist. Existing users won't see the updated tour until they delete the file or the write-gate is changed to overwrite-on-update. Fresh installs pick it up. Worth fixing eventually so docs actually reach users, but it's the same trap noted in prior sessions — not new debt from this change. +- **Verified**: `cargo check -p onboarding` clean, `./script/clippy -p onboarding` (release, all targets, deny warnings) clean. No new tests — the strip is rendering-only with a static label list, and the click handler reuses an action that's already tested by the main button. + +### AI Dock — Skills tab install path (followup #2) +- Bundled markdown content for the two PaddleBoard-owned slash commands (`/build`, `/update-tour`) so the Skills tab can actually install them, not just link out. **No content duplication** — `crates/paddleboard_ai_dock/src/catalog.rs::bundled_skill_content` uses `include_str!` against `.claude/commands/.md` directly, so the slash command this repo uses and the bundled install copy are physically the same bytes. Renaming `.claude/commands/build.md` would break the `include_str!` at compile time, which is the right failure mode. +- **What renders now**: in `skills_tab.rs::render_skill_row`, when a skill is *not installed* AND bundled content exists, the row shows two outlined buttons — `Add to project` and `Add to user` — side by side. Click writes the file via `fs::create_dir_all` + `fs::write`, then `cx.notify()` triggers re-detection so the badge flips to `Installed (Project|User)`. Sync IO inside the click handler is intentional (a few KB markdown write completes in microseconds; a `cx.background_spawn` would have required threading a `WeakEntity` back to update state, more code for no real win). +- **Tightened `project_skills_dir`**: previously fell back to `std::env::current_dir()`, which is the launch directory, not the active workspace. Now reads `workspace.project().visible_worktrees(cx).next()` and joins `.claude/commands/`, with the CWD path only as a last-resort fallback for the no-workspace case. Detection AND install both honor this — picking the wrong project root for a write would have been a silent data hazard. +- **Per-scope button disabling**: if a scope's target dir can't be resolved (no workspace open → no project dir; no `$HOME` → no user dir), the corresponding button greys out instead of disappearing, so the user can see what *would* be possible. +- **Errors** route through `Workspace::show_error` (the same path other workspace errors use). Falls back to `log::error!` if the workspace entity is gone by the time the click fires. +- **Catalog cleanup**: dropped the phantom `/simplify` entry. Description was copy-pasted from `/review` and there's no real `/simplify` skill anywhere in the harness. Catalog drops 6 skills → 5; header subtitle in the dock auto-updates because counts are derived. Also fixed `$schema_note` in `catalog.json` that still referenced the old "Store" naming and the deleted `paddleboard_store/src/catalog.rs` path. +- **Tests added** to `catalog::tests`: `bundled_skill_content_returns_known_skills` (asserts the bundled markdown loads and contains the expected slash-command markers), `bundled_skill_content_returns_none_for_unbundled` (locks in the closed-list shape), and `every_bundled_id_is_in_catalog` — a structural test that would catch someone bundling a skill whose catalog entry was deleted, which would leave install buttons that never render. +- **Verified**: `cargo check -p paddleboard_ai_dock`, `./script/clippy -p paddleboard_ai_dock` (release, all targets, deny warnings), `cargo test -p paddleboard_ai_dock` (3/3 pass), `cargo build -p paddleboard`. No UI smoke test yet — in this repo both bundled skills are already installed, so the install buttons don't appear without a temporary `mv .claude/commands/build.md /tmp/`. User chose to confirm by header count vs. doing the install-flow dance. +- **Open followups**: `/verify`, `/review`, `/security-review` are still in the catalog without bundled content — they render `Not installed` and there's no install path. These are claude-code harness-bundled skills, so duplicating them in PB might shadow rather than help. Decide whether to drop them from the catalog (harness owns discovery) or actually ship our own copies. Also: the no-workspace fallback to `std::env::current_dir()` is rarely correct; a future polish might just disable "Add to project" entirely when there's no workspace, since "the launch dir" is a weak signal for "what project do I mean." + +### Command palette: `zed:` → `paddleboard:` (display-only rename) +- User noted that despite the AI Dock rename, the command palette still shows lots of `zed: ...` entries. Renamed at the display layer rather than at the action declaration site. +- **What changed**: `crates/command_palette/src/command_palette.rs::humanize_action_name` now strips a leading `zed::` and replaces it with `paddleboard::` before applying the existing humanization. Propagates to every caller — command palette, `which_key`, `keymap_editor` (3 sites), and the `paddleboard` binary's menu builder. +- **What was deliberately *not* touched**: action declarations and keymap files. `Action::name()` still returns `zed::OpenOnboarding`, so all 23 `"zed::*"` references in `assets/keymaps/default-macos.json` keep resolving without edits. This avoided rewriting 17 `#[action(namespace = zed)]` declarations across `paddleboard_actions/src/lib.rs`, `onboarding/src/onboarding.rs`, `gpui/src/action.rs`, and `component_preview/`, plus the keymap references — a maintenance trap that would have reappeared on every upstream merge. +- **Tagged with `// PaddleBoard:`** divergence comment, and a regression-locking test (`humanize_action_name("zed::OpenOnboarding") == "paddleboard: open onboarding"`). `./script/clippy -p command_palette` clean; the existing 3 humanize assertions plus the new one all pass; debug `paddleboard` binary rebuilt clean. +- **Tradeoff to remember**: cosmetic-only. Users who type `zed` in the palette no longer get matches — fine for the new branding intent, but if anyone has muscle memory or external docs referencing `zed: ...` palette commands, they'll need to learn `paddleboard: ...` instead. The actual action `name()` is unchanged, so docs that reference action names (not palette labels) keep working. + +--- + ## 2026-05-21 ### AI Dock — built, McpServersPage absorbed diff --git a/WELCOME.md b/WELCOME.md index e438919f93..d277745338 100644 --- a/WELCOME.md +++ b/WELCOME.md @@ -78,7 +78,7 @@ Most editors run **MCP (Model Context Protocol) servers** directly on your host. PaddleBoard adds a fourth context-server transport, `sandboxed_stdio`, that runs the MCP server inside a `podman run -i --rm --runtime=runsc` container. Stdin and stdout are proxied transparently, so the JSON-RPC framing keeps working without any change on the agent side. -**Manage servers in the AI Dock** — `zed: Mcp Servers` (or `ai_dock: Open` then the **MCP Servers** tab) opens the PaddleBoard AI Dock with the absorbed server view. You get the full add/filter (All / Running / Stopped / Error) / inspect surface plus a side-by-side **Available** catalog of well-known servers without hand-editing JSON. +**Manage servers in the AI Dock** — `paddleboard: Mcp Servers` (or `ai_dock: Open` then the **MCP Servers** tab) opens the PaddleBoard AI Dock with the absorbed server view. You get the full add/filter (All / Running / Stopped / Error) / inspect surface plus a side-by-side **Available** catalog of well-known servers without hand-editing JSON. You can still configure servers by hand in `settings.json` if you prefer: @@ -107,9 +107,9 @@ The original `stdio` transport (which runs the binary directly on your host) is One place to browse and install everything the agent talks to. Think of it as the marina where every external collaborator your PaddleBoard talks to ties up. -- Open it from the command palette (`ai_dock: Open`) or the **Open the AI Dock** button on the Welcome screen. +- Open it from the command palette (`ai_dock: Open`) or the **Open the AI Dock** button on the Welcome screen. The Welcome screen also shows a small **Featured** strip of well-known agents (Claude, Codex, Copilot, Cursor) — clicking any pill opens the Dock so first-run users have something concrete to recognize. - Three tabs: **Agents** (Zed, Claude, Codex, Copilot, Cursor, …), **Skills** (slash commands shipped with the project or installed in `~/.claude/commands/`), and **MCP Servers** (the absorbed management page plus a catalog of common servers). -- Installed items show a green badge; missing ones show an **Install / Sign In / Learn More** action that does the right thing for the category — agent installs are a one-click settings write, sign-in flows route to your existing identity, and MCP server adds delegate to the existing setup machinery. +- Installed items show a green badge; missing ones show an **Install / Sign In / Learn More** action that does the right thing for the category — agent installs are a one-click settings write, sign-in flows route to your existing identity, MCP server adds delegate to the existing setup machinery, and bundled skills (currently `/build` and `/update-tour`) install with **Add to project** / **Add to user** buttons that drop a markdown file into the right `.claude/commands/` directory. - The catalog itself is `assets/ai_dock/catalog.json` in this repo — adding an entry is a PR, not a network fetch, so what shows up in the Dock is exactly what the team has reviewed. The AI Dock replaces the old hardcoded 5-card "Agent Setup" row on the Welcome screen and the standalone MCP Servers pane — both routes now land here. @@ -198,7 +198,7 @@ Everything else: multi-buffer editor, LSP, DAP debugger, git panel, terminal, Vi | Enable step-through mode | Click the ⏭ icon in the agent thread toolbar | | See all agent threads | Click the list-tree icon in the panel bar | | Switch LLM provider | Open the LLM Picker panel from the panel bar | -| Configure MCP servers | `Cmd-Shift-P` → `zed: Mcp Servers` | +| Configure MCP servers | `Cmd-Shift-P` → `paddleboard: Mcp Servers` | | Switch / create a worktree | `Cmd-Shift-P` → `git: Worktree` | | Run code in a sandbox | Ask the agent to run a command — it uses the Sandbox Tool automatically | | Run a service in a sandbox | Ask the agent to start a server (e.g. `python3 -m http.server 8000`) — it uses the Sandbox Service Tool, and the URL appears in the Forwarded Ports row of the browser panel | diff --git a/assets/ai_dock/catalog.json b/assets/ai_dock/catalog.json index 1e9029ab61..4b601b2e6b 100644 --- a/assets/ai_dock/catalog.json +++ b/assets/ai_dock/catalog.json @@ -1,5 +1,5 @@ { - "$schema_note": "PaddleBoard Store catalog. Edit via PR. See crates/paddleboard_store/src/catalog.rs for the schema.", + "$schema_note": "PaddleBoard AI Dock catalog. Edit via PR. See crates/paddleboard_ai_dock/src/catalog.rs for the schema.", "agents": [ { "id": "zed-agent", @@ -60,13 +60,6 @@ "homepage": null, "featured": false }, - { - "id": "simplify", - "name": "/simplify", - "description": "Review changed code for reuse, quality, and efficiency, then fix any issues found.", - "homepage": null, - "featured": false - }, { "id": "review", "name": "/review", diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs index 828c99b0cd..c7f56a20ab 100644 --- a/crates/onboarding/src/basics_page.rs +++ b/crates/onboarding/src/basics_page.rs @@ -435,9 +435,23 @@ fn render_import_settings_section(tab_index: &mut isize, cx: &mut App) -> impl I pub(crate) const FEATURED_AGENT_IDS: &[&str] = &["claude-acp", "codex-acp", "github-copilot-cli", "cursor"]; +// PaddleBoard: parallel display labels for the welcome-screen featured strip. +// Kept separate from FEATURED_AGENT_IDS so the telemetry constant stays a +// plain `&[&str]` (its `.iter().filter()` usage in onboarding.rs doesn't need +// to learn about tuples). Pills all dispatch the same `ai_dock::Open` action; +// the editorial value is the names being visible on the Welcome screen, not +// a per-pill action. +const WELCOME_FEATURED_AGENT_LABELS: &[(&str, &str)] = &[ + ("claude-acp", "Claude"), + ("codex-acp", "Codex"), + ("github-copilot-cli", "Copilot"), + ("cursor", "Cursor"), +]; + // PaddleBoard: replaces the upstream 5-card "Agent Setup" row with a single -// entry point into the AI Dock. The dock consolidates agents, skills, and -// MCP servers; onboarding stays terse and defers detailed browsing. +// entry point into the AI Dock plus a small featured strip. The dock +// consolidates agents, skills, and MCP servers; the pills surface a few +// well-known names so first-run users have something concrete to recognize. fn render_ai_section(_user_store: &Entity, _cx: &mut App) -> impl IntoElement { v_flex() .gap_0p5() @@ -461,6 +475,38 @@ fn render_ai_section(_user_store: &Entity, _cx: &mut App) -> impl Int }), ), ) + .child(render_welcome_featured_strip()) +} + +fn render_welcome_featured_strip() -> impl IntoElement { + v_flex() + .mt_2() + .gap_1() + .child( + Label::new("Featured") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + h_flex() + .gap_1() + .children(WELCOME_FEATURED_AGENT_LABELS.iter().map(|(id, label)| { + let id = *id; + Button::new( + SharedString::from(format!("welcome-featured-{id}")), + SharedString::from(*label), + ) + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .on_click(move |_, window, cx| { + telemetry::event!("Welcome Featured Agent Clicked", agent = id); + window.dispatch_action( + paddleboard_actions::ai_dock::Open.boxed_clone(), + cx, + ); + }) + })), + ) } pub(crate) fn render_basics_page(user_store: &Entity, cx: &mut App) -> impl IntoElement { diff --git a/crates/paddleboard_ai_dock/src/ai_dock/skills_tab.rs b/crates/paddleboard_ai_dock/src/ai_dock/skills_tab.rs index 26a01ef4e7..62b81c2465 100644 --- a/crates/paddleboard_ai_dock/src/ai_dock/skills_tab.rs +++ b/crates/paddleboard_ai_dock/src/ai_dock/skills_tab.rs @@ -3,8 +3,8 @@ use std::path::PathBuf; use gpui::ClickEvent; use ui::prelude::*; -use crate::catalog::SkillEntry; use crate::ai_dock::AiDock; +use crate::catalog::{SkillEntry, bundled_skill_content}; pub(super) fn render(modal: &AiDock, cx: &mut Context) -> impl IntoElement { let catalog = modal.catalog.clone(); @@ -34,16 +34,19 @@ pub(super) fn render(modal: &AiDock, cx: &mut Context) -> impl IntoEleme .p_4() .gap_2() .overflow_y_scroll() - .children( - catalog - .skills - .iter() - .map(|entry| render_skill_row(entry, scope_of(&entry.id), cx)), - ) + .children(catalog.skills.iter().map(|entry| { + render_skill_row( + entry, + scope_of(&entry.id), + project_dir.is_some(), + user_dir.is_some(), + cx, + ) + })) } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum SkillScope { +pub(crate) enum SkillScope { Project, User, } @@ -55,13 +58,28 @@ impl SkillScope { SkillScope::User => "User", } } + + fn resolve_dir(self, modal: &AiDock, cx: &App) -> Option { + match self { + SkillScope::Project => project_skills_dir(modal, cx), + SkillScope::User => user_skills_dir(), + } + } } -fn project_skills_dir(_modal: &AiDock, _cx: &App) -> Option { - // For v1, use the current working directory's `.claude/commands/`. - // A future polish pass can read this from the active workspace's - // first worktree instead, since modals don't track which workspace - // they belong to as cleanly as panel items do. +fn project_skills_dir(modal: &AiDock, cx: &App) -> Option { + // Prefer the active workspace's first visible worktree — that's the + // "project" the user thinks they're in. Fall back to the process CWD + // when there's no workspace (e.g. an empty PaddleBoard window) so the + // detection still picks up `.claude/commands/` in the launch dir. + if let Some(workspace) = modal.workspace.upgrade() { + let workspace = workspace.read(cx); + let project = workspace.project().read(cx); + if let Some(worktree) = project.visible_worktrees(cx).next() { + let root = worktree.read(cx).abs_path(); + return Some(root.join(".claude").join("commands")); + } + } let cwd = std::env::current_dir().ok()?; Some(cwd.join(".claude").join("commands")) } @@ -74,13 +92,17 @@ fn user_skills_dir() -> Option { fn render_skill_row( entry: &SkillEntry, scope: Option, + has_project_dir: bool, + has_user_dir: bool, cx: &mut Context, ) -> AnyElement { let icon = Icon::new(IconName::Sparkle) .size(IconSize::Small) .color(Color::Muted); - let action_button: AnyElement = if let Some(scope) = scope { + let bundled = bundled_skill_content(&entry.id).is_some(); + + let action_area: AnyElement = if let Some(scope) = scope { Button::new( SharedString::from(format!("ai-dock-skill-installed-{}", entry.id)), format!("Installed ({})", scope.label()), @@ -89,9 +111,45 @@ fn render_skill_row( .label_size(LabelSize::Small) .disabled(true) .into_any_element() + } else if bundled { + // Install buttons — one per scope. Disable a button when its target + // directory can't be resolved (no workspace open for project; no + // $HOME for user) so the user still sees what *would* be possible. + let entry_id = entry.id.clone(); + let project_btn = { + let id = entry_id.clone(); + Button::new( + SharedString::from(format!("ai-dock-skill-add-project-{}", entry.id)), + "Add to project", + ) + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .disabled(!has_project_dir) + .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| { + install_skill(this, &id, SkillScope::Project, window, cx); + })) + }; + let user_btn = { + let id = entry_id; + Button::new( + SharedString::from(format!("ai-dock-skill-add-user-{}", entry.id)), + "Add to user", + ) + .style(ButtonStyle::Outlined) + .label_size(LabelSize::Small) + .disabled(!has_user_dir) + .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| { + install_skill(this, &id, SkillScope::User, window, cx); + })) + }; + h_flex() + .gap_1() + .child(project_btn) + .child(user_btn) + .into_any_element() } else { - // No bundled content yet — direct to homepage if present, otherwise - // surface a disabled button so users see the skill exists. + // No bundled content — direct to homepage if present, else surface a + // disabled "Not installed" pill so users see the skill exists. match entry.homepage.clone() { Some(url) => Button::new( SharedString::from(format!("ai-dock-skill-info-{}", entry.id)), @@ -136,6 +194,54 @@ fn render_skill_row( .color(Color::Muted), ), ) - .child(action_button) + .child(action_area) .into_any_element() } + +fn install_skill( + modal: &mut AiDock, + id: &str, + scope: SkillScope, + _window: &mut Window, + cx: &mut Context, +) { + let Some(content) = bundled_skill_content(id) else { + log::error!("paddleboard_ai_dock: install_skill called for unbundled id `{id}`"); + return; + }; + let Some(dir) = scope.resolve_dir(modal, cx) else { + report_install_error( + modal, + format!("Could not resolve the {} skills directory.", scope.label()), + cx, + ); + return; + }; + + if let Err(err) = write_skill_file(&dir, id, content) { + log::error!("paddleboard_ai_dock: failed to install skill `{id}`: {err}"); + report_install_error( + modal, + format!("Failed to install /{id}: {err}"), + cx, + ); + return; + } + + cx.notify(); +} + +fn write_skill_file(dir: &PathBuf, id: &str, content: &str) -> std::io::Result<()> { + std::fs::create_dir_all(dir)?; + std::fs::write(dir.join(format!("{id}.md")), content) +} + +fn report_install_error(modal: &AiDock, message: String, cx: &mut Context) { + if let Some(workspace) = modal.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + workspace.show_error(&anyhow::anyhow!(message), cx); + }); + } else { + log::error!("paddleboard_ai_dock: install error (no workspace to notify): {message}"); + } +} diff --git a/crates/paddleboard_ai_dock/src/catalog.rs b/crates/paddleboard_ai_dock/src/catalog.rs index cd48aa8a76..67c04a0717 100644 --- a/crates/paddleboard_ai_dock/src/catalog.rs +++ b/crates/paddleboard_ai_dock/src/catalog.rs @@ -103,3 +103,59 @@ impl CatalogGlobal { pub fn catalog(cx: &App) -> Arc { CatalogGlobal::get(cx) } + +/// Bundled markdown body for catalog skills we ship in-tree. Returning `Some` +/// turns on the Skills tab's "Add to project" / "Add to user" buttons for +/// that entry; returning `None` falls back to the homepage / disabled state. +/// +/// The `include_str!` paths point at the canonical files under +/// `.claude/commands/`, not a duplicated copy under `assets/ai_dock/skills/`, +/// so the slash command used in this repo and the bundled install copy can +/// never drift. +pub fn bundled_skill_content(id: &str) -> Option<&'static str> { + match id { + "build" => Some(include_str!("../../../.claude/commands/build.md")), + "update-tour" => Some(include_str!("../../../.claude/commands/update-tour.md")), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bundled_skill_content_returns_known_skills() { + let build = bundled_skill_content("build").expect("build is bundled"); + assert!( + build.contains("/build"), + "bundled `build.md` should mention `/build`: got {build:?}" + ); + + let update_tour = bundled_skill_content("update-tour").expect("update-tour is bundled"); + assert!( + update_tour.contains("WELCOME.md") || update_tour.contains("tour"), + "bundled `update-tour.md` should reference WELCOME.md or `tour`: got {update_tour:?}" + ); + } + + #[test] + fn bundled_skill_content_returns_none_for_unbundled() { + assert!(bundled_skill_content("review").is_none()); + assert!(bundled_skill_content("verify").is_none()); + assert!(bundled_skill_content("nonexistent").is_none()); + } + + #[test] + fn every_bundled_id_is_in_catalog() { + let catalog = Catalog::load(); + let bundled_ids = ["build", "update-tour"]; + for id in bundled_ids { + assert!( + catalog.skills.iter().any(|s| s.id == id), + "bundled skill `{id}` must have a matching catalog entry; \ + otherwise the install buttons never render" + ); + } + } +} diff --git a/crates/workspace/src/tour.md b/crates/workspace/src/tour.md index 2abe8bbdcb..5155352dc8 100644 --- a/crates/workspace/src/tour.md +++ b/crates/workspace/src/tour.md @@ -28,16 +28,16 @@ Long-lived processes (dev servers, demo apps, `adk web`) use the **Sandbox Servi ### 4. Sandboxed MCP Servers PaddleBoard runs your **MCP servers** inside the same Podman + gVisor sandbox as the Sandbox Tool. -- Manage them in the AI Dock: `Cmd-Shift-P` → **`zed: Mcp Servers`** opens the dock on the MCP tab (filter All / Running / Stopped / Error, add servers, browse the catalog of common ones). +- Manage them in the AI Dock: `Cmd-Shift-P` → **`paddleboard: Mcp Servers`** opens the dock on the MCP tab (filter All / Running / Stopped / Error, add servers, browse the catalog of common ones). - Or use `"source": "sandboxed_stdio"` in `settings.json` directly. - Forward only the host env vars you need by name — values stay out of the agent's context. - The worktree is mounted at `/workspace` so filesystem-touching servers (git, fs, etc.) still work. ### 5. AI Dock One place to browse and install everything the agent talks to — the marina where every external collaborator ties up. -- Open it: `Cmd-Shift-P` → **`ai_dock: Open`**, or hit **Open the AI Dock** on the Welcome screen. +- Open it: `Cmd-Shift-P` → **`ai_dock: Open`**, or hit **Open the AI Dock** on the Welcome screen. The Welcome screen also surfaces a **Featured** strip (Claude / Codex / Copilot / Cursor pills) so first-run users have recognizable names to click. - Three tabs: **Agents** (Zed, Claude, Codex, Copilot, Cursor), **Skills** (slash commands), **MCP Servers** (catalog + absorbed management UI). -- Installed items show a green badge; missing ones get a one-click **Install / Sign In / Learn More** that does the category-appropriate thing. +- Installed items show a green badge; missing ones get a one-click **Install / Sign In / Learn More** that does the category-appropriate thing. Bundled skills (currently `/build` and `/update-tour`) install with **Add to project** / **Add to user** buttons that drop a markdown file into the right `.claude/commands/` directory. - The catalog is `assets/ai_dock/catalog.json` in-repo — adds are PRs, not fetches. ### 6. Step-Through Mode From 41e1a3703184c26b6f2fe2f6b69e3a099335da72 Mon Sep 17 00:00:00 2001 From: "Jason \"Jay\" Smith" Date: Fri, 22 May 2026 10:16:30 -0700 Subject: [PATCH 4/4] RECAPS: AI Dock followups session Today's session: AI Dock smoke test (no code), Skills tab install path, Welcome Featured strip + docs/tour sync, and a command-palette display rename from "zed:" to "paddleboard:". Notes on the commit split, the gitignore entry for smoke-test PNGs, and the open follow-ups left for next session. Release Notes: - N/A Co-Authored-By: Claude Opus 4.7 --- RECAPS.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/RECAPS.md b/RECAPS.md index 67d61ffdb7..22985e9079 100644 --- a/RECAPS.md +++ b/RECAPS.md @@ -15,6 +15,13 @@ Running log of completed work sessions, newest first. Each entry summarizes a co - **Cosmetic, not acted on**: in `aidock-2.png` the welcome screen text is visible *below* the modal — scrim doesn't extend full-height, or modal floats inside the workspace pane rather than the window. - **Terminology asymmetry**: Agents tab uses "Installed" badge + "Install" button; Skills tab uses "Installed (Project)" / "Not installed" badge only. Picking one pattern would help. +### Session commits + `.gitignore` for smoke-test artifacts +- Today's work shipped as **two commits**, not one, so the workspace-wide cosmetic rename stays orthogonal to the AI Dock feature work — either can be reverted without touching the other: + - `9669d4b587 command_palette: rename `zed:` to `paddleboard:` in palette display` (1 file, +17) + - `a8fd95efe4 paddleboard_ai_dock: install bundled skills, add Welcome featured strip, drop /simplify` (8 files, +278/-34) +- The smoke-test artifacts (`aidock-1.png` … `aidock-7.png`) were *not* committed. Instead `.gitignore` got a new entry `aidock-*.png` so the screenshots stay locally as verification evidence but `git status` stops listing them. Pattern is reusable for any future per-feature smoke runs. +- Followup #1 (the AI Dock smoke test) intentionally had **no code change** — it surfaced findings (MCP catalog gap, `/simplify` phantom, terminology asymmetry) that fed into followup #2, but produced no diff of its own. RECAPS captures the findings; nothing to commit for #1 in isolation. + ### AI Dock — Welcome Featured strip (followup #3) + docs/tour sync - Added a small **Featured** strip to the Welcome screen's AI Dock section: four compact outlined pills labeled `Claude` / `Codex` / `Copilot` / `Cursor`, rendered below the existing full-width "Open the AI Dock" button via a new `render_welcome_featured_strip()` helper in `crates/onboarding/src/basics_page.rs`. Each pill dispatches the same `paddleboard_actions::ai_dock::Open` action — they're editorial discoverability, not separate destinations. A future polish could pre-scroll the dock to the matching agent card; out of scope for v1 (would need a new action variant carrying the target id). - **Why a parallel constant instead of upgrading `FEATURED_AGENT_IDS`**: the existing `FEATURED_AGENT_IDS: &[&str]` is referenced by `onboarding.rs:245` for telemetry (`.iter().filter().copied()`) and a tuple-ification would have required updating that call site too. Adding `WELCOME_FEATURED_AGENT_LABELS: &[(&str, &str)]` alongside it keeps the upstream-shaped telemetry path zero-touch. The two arrays must stay in id-sync; a future test could enforce that, but four entries felt below the threshold for now.