mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add workspace skills to agent chat slash menu (#25600)
> This Pull Request was updated by Mux working on behalf of Mike. Adds workspace skills to the agent chat slash menu, sourced entirely from the chat's pinned context resources (the single-chat GET response the page already fetches), the same inventory `read_skill` resolves from. No new API endpoint is introduced. Personal entries insert `/name`, or `/personal/name` when the name collides with a workspace skill or the chat's pinned context has not resolved yet; workspace entries insert `/workspace/name`. Qualified aliases stay searchable even when the displayed trigger is bare. Before a chat binds a workspace (new chat form, or a selected but unbound workspace), the menu lists personal skills only. Sending a message invalidates the chat detail query, and chatd broadcasts a context watch event when a first-turn bind pins the chat, so the menu picks up newly pinned context without a reload. Makes `UpdateChatWorkspaceBinding` a no-op when the requested workspace/build/agent binding is unchanged, preserving `updated_at` so chat list ordering and watch events stay stable. Includes regression coverage for the no-op binding guard, pinned-context skill mapping, collision qualification, and skills menu behavior. Refs [CODAGT-474](https://linear.app/codercom/issue/CODAGT-474/ux-improvements-for-coder-agents) (skills autocompleting in the editor).
This commit is contained in:
@@ -1264,65 +1264,86 @@ SELECT *
|
||||
FROM chats_expanded;
|
||||
|
||||
-- name: UpdateChatWorkspaceBinding :one
|
||||
WITH updated_chat AS (
|
||||
UPDATE chats SET
|
||||
workspace_id = sqlc.narg('workspace_id')::uuid,
|
||||
build_id = sqlc.narg('build_id')::uuid,
|
||||
agent_id = sqlc.narg('agent_id')::uuid,
|
||||
updated_at = NOW()
|
||||
WHERE id = @id::uuid
|
||||
RETURNING *
|
||||
WITH current_chat AS (
|
||||
SELECT *
|
||||
FROM chats
|
||||
WHERE id = @id::uuid
|
||||
),
|
||||
binding_changed AS (
|
||||
SELECT
|
||||
workspace_id IS DISTINCT FROM sqlc.narg('workspace_id')::uuid
|
||||
OR build_id IS DISTINCT FROM sqlc.narg('build_id')::uuid
|
||||
OR agent_id IS DISTINCT FROM sqlc.narg('agent_id')::uuid AS changed
|
||||
FROM current_chat
|
||||
),
|
||||
changed_chat AS (
|
||||
UPDATE chats SET
|
||||
workspace_id = sqlc.narg('workspace_id')::uuid,
|
||||
build_id = sqlc.narg('build_id')::uuid,
|
||||
agent_id = sqlc.narg('agent_id')::uuid,
|
||||
updated_at = NOW()
|
||||
WHERE id = @id::uuid
|
||||
AND (SELECT changed FROM binding_changed)
|
||||
RETURNING *
|
||||
),
|
||||
result_chat AS (
|
||||
SELECT *
|
||||
FROM changed_chat
|
||||
UNION ALL
|
||||
SELECT *
|
||||
FROM current_chat
|
||||
WHERE NOT (SELECT changed FROM binding_changed)
|
||||
),
|
||||
chats_expanded AS (
|
||||
SELECT
|
||||
updated_chat.id,
|
||||
updated_chat.owner_id,
|
||||
updated_chat.workspace_id,
|
||||
updated_chat.title,
|
||||
updated_chat.status,
|
||||
updated_chat.worker_id,
|
||||
updated_chat.started_at,
|
||||
updated_chat.heartbeat_at,
|
||||
updated_chat.created_at,
|
||||
updated_chat.updated_at,
|
||||
updated_chat.parent_chat_id,
|
||||
updated_chat.root_chat_id,
|
||||
updated_chat.last_model_config_id,
|
||||
updated_chat.last_reasoning_effort,
|
||||
updated_chat.archived,
|
||||
updated_chat.last_error,
|
||||
updated_chat.mode,
|
||||
updated_chat.mcp_server_ids,
|
||||
updated_chat.labels,
|
||||
updated_chat.build_id,
|
||||
updated_chat.agent_id,
|
||||
updated_chat.pin_order,
|
||||
updated_chat.last_read_message_id,
|
||||
updated_chat.dynamic_tools,
|
||||
updated_chat.organization_id,
|
||||
updated_chat.plan_mode,
|
||||
updated_chat.client_type,
|
||||
updated_chat.last_turn_summary,
|
||||
updated_chat.snapshot_version,
|
||||
updated_chat.history_version,
|
||||
updated_chat.queue_version,
|
||||
updated_chat.generation_attempt,
|
||||
updated_chat.retry_state,
|
||||
updated_chat.retry_state_version,
|
||||
updated_chat.runner_id,
|
||||
updated_chat.requires_action_deadline_at,
|
||||
COALESCE(root.user_acl, updated_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, updated_chat.group_acl) AS group_acl,
|
||||
result_chat.id,
|
||||
result_chat.owner_id,
|
||||
result_chat.workspace_id,
|
||||
result_chat.title,
|
||||
result_chat.status,
|
||||
result_chat.worker_id,
|
||||
result_chat.started_at,
|
||||
result_chat.heartbeat_at,
|
||||
result_chat.created_at,
|
||||
result_chat.updated_at,
|
||||
result_chat.parent_chat_id,
|
||||
result_chat.root_chat_id,
|
||||
result_chat.last_model_config_id,
|
||||
result_chat.last_reasoning_effort,
|
||||
result_chat.archived,
|
||||
result_chat.last_error,
|
||||
result_chat.mode,
|
||||
result_chat.mcp_server_ids,
|
||||
result_chat.labels,
|
||||
result_chat.build_id,
|
||||
result_chat.agent_id,
|
||||
result_chat.pin_order,
|
||||
result_chat.last_read_message_id,
|
||||
result_chat.dynamic_tools,
|
||||
result_chat.organization_id,
|
||||
result_chat.plan_mode,
|
||||
result_chat.client_type,
|
||||
result_chat.last_turn_summary,
|
||||
result_chat.snapshot_version,
|
||||
result_chat.history_version,
|
||||
result_chat.queue_version,
|
||||
result_chat.generation_attempt,
|
||||
result_chat.retry_state,
|
||||
result_chat.retry_state_version,
|
||||
result_chat.runner_id,
|
||||
result_chat.requires_action_deadline_at,
|
||||
COALESCE(root.user_acl, result_chat.user_acl) AS user_acl,
|
||||
COALESCE(root.group_acl, result_chat.group_acl) AS group_acl,
|
||||
owner.username AS owner_username,
|
||||
owner.name AS owner_name,
|
||||
updated_chat.context_aggregate_hash,
|
||||
updated_chat.context_dirty_since,
|
||||
updated_chat.context_dirty_resources,
|
||||
updated_chat.context_error
|
||||
result_chat.context_aggregate_hash,
|
||||
result_chat.context_dirty_since,
|
||||
result_chat.context_dirty_resources,
|
||||
result_chat.context_error
|
||||
FROM
|
||||
updated_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(updated_chat.root_chat_id, updated_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = updated_chat.owner_id
|
||||
result_chat
|
||||
LEFT JOIN chats root ON root.id = COALESCE(result_chat.root_chat_id, result_chat.parent_chat_id)
|
||||
JOIN visible_users owner ON owner.id = result_chat.owner_id
|
||||
)
|
||||
SELECT *
|
||||
FROM chats_expanded;
|
||||
@@ -1485,17 +1506,19 @@ SET
|
||||
context_dirty_since = NULL
|
||||
WHERE id = @id::uuid;
|
||||
|
||||
-- name: HydrateAgentChatsContext :exec
|
||||
-- name: HydrateAgentChatsContext :many
|
||||
-- Stamps the pinned hash and error on every not-yet-hydrated chat for
|
||||
-- an agent (context_aggregate_hash IS NULL) and copies the agent's
|
||||
-- current context resources onto those chats in the same statement, so
|
||||
-- a chat's pinned hash and pinned bodies are always written together.
|
||||
-- Runs as a side effect of an agent push and of chat-create hydration,
|
||||
-- so chats created before the agent was ready pick up the snapshot
|
||||
-- without a dirty event. The ON CONFLICT upsert is defensive: a
|
||||
-- without a dirty marker. The ON CONFLICT upsert is defensive: a
|
||||
-- not-yet-hydrated chat has no pinned rows, so it normally inserts.
|
||||
-- Does not bump chats.updated_at; the resource upsert's ON CONFLICT branch
|
||||
-- sets chat_context_resources.updated_at on the rows it rewrites.
|
||||
-- Returns the hydrated chat IDs so callers can notify watchers of every
|
||||
-- chat the statement pinned.
|
||||
WITH hydrated AS (
|
||||
UPDATE chats
|
||||
SET
|
||||
@@ -1505,25 +1528,28 @@ WITH hydrated AS (
|
||||
AND archived = false
|
||||
AND context_aggregate_hash IS NULL
|
||||
RETURNING id
|
||||
),
|
||||
copied AS (
|
||||
INSERT INTO chat_context_resources (
|
||||
chat_id, source, body_kind, body, content_hash, size_bytes, status, error, source_path
|
||||
)
|
||||
SELECT
|
||||
hydrated.id, r.source, r.body_kind, r.body, r.content_hash,
|
||||
r.size_bytes, r.status, r.error, r.source_path
|
||||
FROM hydrated
|
||||
CROSS JOIN workspace_agent_context_resources r
|
||||
WHERE r.workspace_agent_id = @agent_id::uuid
|
||||
ON CONFLICT (chat_id, source) DO UPDATE SET
|
||||
body_kind = EXCLUDED.body_kind,
|
||||
body = EXCLUDED.body,
|
||||
content_hash = EXCLUDED.content_hash,
|
||||
size_bytes = EXCLUDED.size_bytes,
|
||||
status = EXCLUDED.status,
|
||||
error = EXCLUDED.error,
|
||||
source_path = EXCLUDED.source_path,
|
||||
updated_at = now()
|
||||
)
|
||||
INSERT INTO chat_context_resources (
|
||||
chat_id, source, body_kind, body, content_hash, size_bytes, status, error, source_path
|
||||
)
|
||||
SELECT
|
||||
hydrated.id, r.source, r.body_kind, r.body, r.content_hash,
|
||||
r.size_bytes, r.status, r.error, r.source_path
|
||||
FROM hydrated
|
||||
CROSS JOIN workspace_agent_context_resources r
|
||||
WHERE r.workspace_agent_id = @agent_id::uuid
|
||||
ON CONFLICT (chat_id, source) DO UPDATE SET
|
||||
body_kind = EXCLUDED.body_kind,
|
||||
body = EXCLUDED.body,
|
||||
content_hash = EXCLUDED.content_hash,
|
||||
size_bytes = EXCLUDED.size_bytes,
|
||||
status = EXCLUDED.status,
|
||||
error = EXCLUDED.error,
|
||||
source_path = EXCLUDED.source_path,
|
||||
updated_at = now();
|
||||
SELECT id FROM hydrated;
|
||||
|
||||
-- name: MarkChatsContextDirtyByAgent :many
|
||||
-- Flips active, already-hydrated chats for an agent to dirty when the
|
||||
|
||||
Reference in New Issue
Block a user