Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
495 changes: 351 additions & 144 deletions crates/agent-gateway/internal/proto/v2/gateway.pb.go

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/agent-gateway/internal/protocol/pbws/guard.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func vetAgentRequest(sm session.AgentView, env *gatewayv2.GatewayEnvelope) error
*gatewayv2.GatewayEnvelope_HistoryDelete,
*gatewayv2.GatewayEnvelope_HistoryPrefix,
*gatewayv2.GatewayEnvelope_HistoryPin,
*gatewayv2.GatewayEnvelope_HistorySkills,
*gatewayv2.GatewayEnvelope_HistoryShareGet,
*gatewayv2.GatewayEnvelope_HistoryShareSet,
*gatewayv2.GatewayEnvelope_HistoryWorkdirs,
Expand Down
16 changes: 16 additions & 0 deletions crates/agent-gateway/internal/protocol/pbws/guard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,19 @@ func TestVetAgentRequestAllowsProviderUsage(t *testing.T) {
t.Fatalf("vetAgentRequest() error = %v", err)
}
}

func TestVetAgentRequestAllowsHistorySkills(t *testing.T) {
env := &gatewayv2.GatewayEnvelope{
Payload: &gatewayv2.GatewayEnvelope_HistorySkills{
HistorySkills: &gatewayv2.HistorySkillsRequest{
ConversationId: "conversation-1",
SkillPresetId: "focused-review",
SkillsDisabled: true,
},
},
}

if err := vetAgentRequest(session.AgentView{}, env); err != nil {
t.Fatalf("vetAgentRequest() error = %v", err)
}
}
18 changes: 18 additions & 0 deletions crates/agent-gateway/proto/v2/gateway.proto
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ message GatewayEnvelope {
ManagedProcessRequest managed_process_request = 91;
HistoryBranchRequest history_branch = 92;
ProviderUsageRequest provider_usage = 93;
HistorySkillsRequest history_skills = 94;
}

// Legacy tunnel control/frame payloads (pre-rewrite protocol) and the
Expand Down Expand Up @@ -132,6 +133,7 @@ message AgentEnvelope {
ChatIngressBatch chat_ingress_batch = 95;
ChatIngressResume chat_ingress_resume = 96;
ChatIngressFragment chat_ingress_fragment = 97;
HistorySkillsResponse history_skills_resp = 98;
ErrorResponse error = 99;
}

Expand Down Expand Up @@ -612,6 +614,8 @@ message ChatRequest {
string client_request_id = 8;
ChatRuntimeControls runtime_controls = 9;
string queue_policy = 10;
string skill_preset_id = 11;
optional bool skills_disabled = 12;
}

message ChatMessageRef {
Expand Down Expand Up @@ -776,6 +780,8 @@ message HistoryGetResponse {
int32 returned_message_count = 4;
bool has_more = 5;
ConversationSummary conversation = 6;
string skill_preset_id = 7;
bool skills_disabled = 8;
}

message HistoryPrefixRequest {
Expand All @@ -791,6 +797,8 @@ message HistoryPrefixResponse {
int32 returned_message_count = 4;
bool has_more = 5;
ConversationSummary conversation = 6;
string skill_preset_id = 7;
bool skills_disabled = 8;
}

message HistoryRenameRequest {
Expand Down Expand Up @@ -1255,3 +1263,13 @@ message ChatIngressAck {
REJECTED = 4;
}
}

message HistorySkillsRequest {
string conversation_id = 1;
string skill_preset_id = 2;
bool skills_disabled = 3;
}

message HistorySkillsResponse {
ConversationSummary conversation = 1;
}
133 changes: 124 additions & 9 deletions crates/agent-gateway/web/src/app/GatewayApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ import { LocaleContext, t as translate } from "@/i18n";
import { registerAskUserQuestionAnswerHandler } from "@/lib/chat/askUserQuestionBridge";
import type { ChatHistorySummary } from "@/lib/chat/chatHistory";
import { buildModelOptions } from "@/lib/chat/chatPageHelpers";
import {
applyConversationSkillsOverride,
applyPersistedConversationSkills,
rekeyConversationSkills,
} from "@/lib/chat/conversationSkillsState";
import type { HistoryMessageRef } from "@/lib/chat/conversationState";
import {
adoptHistoryWindowState,
Expand Down Expand Up @@ -95,7 +100,9 @@ import {
type RightDockFileTreeStatePatch,
type RightDockProjectState,
removeRightDockProjectState,
resolveEffectiveSkillNames,
resolveEffectiveTheme,
resolveSkillPreset,
resolveWorkspaceProjects,
type SelectedModel,
setSelectedModel,
Expand All @@ -105,14 +112,14 @@ import {
updateRightDockFileTreeState,
updateRightDockProjectState,
updateRightDockWidth,
updateSkillPreset,
updateSkills,
updateSshProjectHostIds,
updateSystem,
type WorkspaceProject,
workspaceProjectPathKey,
} from "@/lib/settings";
import { createUuid } from "@/lib/shared/id";
import { mergeAlwaysEnabledSkillNames } from "@/lib/skills";
import { terminalSessionBelongsToProject } from "@/lib/terminal/sessionStore";
import type { TerminalSession } from "@/lib/terminal/types";
import { createGatewayWorkspaceActivityClient } from "@/lib/workspace-activity/gatewayWorkspaceActivityClient";
Expand Down Expand Up @@ -278,6 +285,11 @@ export default function GatewayApp() {
const [conversationModelOverrides, setConversationModelOverrides] = useState<
ReadonlyMap<string, SelectedModel>
>(new Map());
const [conversationSkills, setConversationSkills] = useState<
ReadonlyMap<string, { skillPresetId: string; skillsDisabled: boolean }>
>(new Map());
const conversationSkillsDirtyIdsRef = useRef<ReadonlySet<string>>(new Set());
const conversationSkillsPersistenceRef = useRef(new Map<string, Promise<void>>());
const [chatError, setChatError] = useState<string | null>(null);
// Top-right toast stack for upload/attachment feedback — mirrors the GUI's
// NotifyToast usage so upload failures never render as conversation output.
Expand All @@ -296,6 +308,23 @@ export default function GatewayApp() {
const [, setChatQueueRevision] = useState(0);
const [selectedHistoryId, setSelectedHistoryId] = useState("");
const [selectedHistory, setSelectedHistory] = useState<HistoryDetail | null>(null);
useEffect(() => {
const detail = selectedHistory;
const id = detail?.conversation_id?.trim();
if (!detail || !id) return;
setConversationSkills((current) => {
const next = applyPersistedConversationSkills(
{ selections: current, dirtyIds: conversationSkillsDirtyIdsRef.current },
id,
{
skillPresetId: detail.skill_preset_id?.trim() || "default",
skillsDisabled: detail.skills_disabled === true,
},
);
conversationSkillsDirtyIdsRef.current = next.dirtyIds;
return next.selections;
});
}, [selectedHistory]);
// Two-phase conversation open (openController): "opening" gates the
// composer/transcript loading affordances; showOverlay drives the switch
// overlay (appears only after ~150ms of still-loading).
Expand Down Expand Up @@ -1080,6 +1109,15 @@ export default function GatewayApp() {
if (!next.has(nextId)) next.set(nextId, override);
return next;
});
setConversationSkills((current) => {
const next = rekeyConversationSkills(
{ selections: current, dirtyIds: conversationSkillsDirtyIdsRef.current },
previousId,
nextId,
);
conversationSkillsDirtyIdsRef.current = next.dirtyIds;
return next.selections;
});
},
[moveConversationUploads, sidebarStore, transcriptStoreRegistry],
);
Expand Down Expand Up @@ -2297,6 +2335,8 @@ export default function GatewayApp() {
runtimeControls,
baseMessageRef: options?.editMessageRef,
queuePolicy: options?.queuePolicy ?? "auto",
skillPresetId: conversationSkills.get(activeConversationId)?.skillPresetId ?? "default",
skillsDisabled: conversationSkills.get(activeConversationId)?.skillsDisabled ?? false,
};

const outcome = await chatCommandPipeline.submit({
Expand Down Expand Up @@ -2462,6 +2502,8 @@ export default function GatewayApp() {
clientRequestId: createUuid(),
runtimeControls: chatRuntimeControlsForCurrentProvider,
queuePolicy,
skillPresetId: conversationSkills.get(conversationIdValue)?.skillPresetId ?? "default",
skillsDisabled: conversationSkills.get(conversationIdValue)?.skillsDisabled ?? false,
});
refreshChatQueueSnapshot(conversationIdValue);
return true;
Expand Down Expand Up @@ -3669,6 +3711,8 @@ export default function GatewayApp() {
queuedChatEditSessionRef.current = null;
setQueuedChatTurns([]);
setChatQueueRevision(0);
setConversationSkills(new Map());
conversationSkillsDirtyIdsRef.current = new Set();
resetProjectToolsRuntimeRef.current();
setSelectedHistoryId("");
setSelectedHistory(null);
Expand Down Expand Up @@ -3733,6 +3777,8 @@ export default function GatewayApp() {
setSelectedHistoryId("");
setSelectedHistory(null);
setConversationModelOverrides(new Map());
setConversationSkills(new Map());
conversationSkillsDirtyIdsRef.current = new Set();
setFullHistoryLoading(false);
setQueuedChatTurns([]);
setChatQueueRevision(0);
Expand Down Expand Up @@ -3888,11 +3934,72 @@ export default function GatewayApp() {
[displayedConversationId, setSettings],
);

const skillsEnabled = settings.skills.enabled && isAgentMode;
const selectedSkillNames = useMemo(
() => (skillsEnabled ? mergeAlwaysEnabledSkillNames(settings.skills.selected) : []),
[skillsEnabled, settings.skills.selected],
const handleConversationSkillsChange = useCallback(
(presetId: string, disabled: boolean) => {
const targetConversationId = displayedConversationId.trim();
if (!targetConversationId) return;
const selection = {
skillPresetId: presetId.trim() || "default",
skillsDisabled: disabled,
};
setConversationSkills((current) => {
const next = applyConversationSkillsOverride(
{ selections: current, dirtyIds: conversationSkillsDirtyIdsRef.current },
targetConversationId,
selection,
);
conversationSkillsDirtyIdsRef.current = next.dirtyIds;
return next.selections;
});
if (!api || isLocalDraftConversationId(targetConversationId)) return;
const previous = conversationSkillsPersistenceRef.current.get(targetConversationId);
const persist = (previous ?? Promise.resolve())
.catch(() => undefined)
.then(async () => {
await api.setHistorySkills(
targetConversationId,
selection.skillPresetId,
selection.skillsDisabled,
);
setConversationSkills((current) => {
const next = applyPersistedConversationSkills(
{ selections: current, dirtyIds: conversationSkillsDirtyIdsRef.current },
targetConversationId,
selection,
);
conversationSkillsDirtyIdsRef.current = next.dirtyIds;
return next.selections;
});
})
.catch((error) => {
addNotify("error", asErrorMessage(error, "保存 Skill 方案失败"));
});
conversationSkillsPersistenceRef.current.set(targetConversationId, persist);
void persist.finally(() => {
if (conversationSkillsPersistenceRef.current.get(targetConversationId) === persist) {
conversationSkillsPersistenceRef.current.delete(targetConversationId);
}
});
},
[addNotify, api, displayedConversationId],
);

const activeConversationSkills = conversationSkills.get(displayedConversationId) ?? {
skillPresetId: "default",
skillsDisabled: false,
};
const effectiveSkillsSelection = useMemo(
() =>
resolveEffectiveSkillNames({
settings: settings.skills,
presetId: activeConversationSkills.skillPresetId,
skillsDisabled: activeConversationSkills.skillsDisabled,
executionMode: settings.system.executionMode,
}),
[activeConversationSkills, settings.skills, settings.system.executionMode],
);
const skillsEnabled = effectiveSkillsSelection.enabled;
const selectedSkillNames = effectiveSkillsSelection.skillNames;
const { availableSkills, skillsRootDir } = useChatSkills({
skillsEnabled,
selectedSkillNames,
Expand Down Expand Up @@ -4197,9 +4304,12 @@ export default function GatewayApp() {
const composer = composerRef.current;
if (!composer || !codeReviewSkill) return;
setSettings((prev) => {
const selected = mergeAlwaysEnabledSkillNames(prev.skills.selected);
if (selected.includes(codeReviewSkill.name)) return prev;
return updateSkills(prev, { selected: [...selected, codeReviewSkill.name] });
const preset = resolveSkillPreset(prev.skills, effectiveSkillsSelection.presetId);
if (preset.skillNames.includes(codeReviewSkill.name)) return prev;
const skills = updateSkillPreset(prev.skills, preset.id, {
skillNames: [...preset.skillNames, codeReviewSkill.name],
});
return updateSkills(prev, { presets: skills.presets });
});
const alreadyInserted = composer
.getDraft()
Expand All @@ -4208,7 +4318,7 @@ export default function GatewayApp() {
composer.insertSkillMention(codeReviewSkill);
}
composer.focus();
}, [codeReviewSkill, setSettings]);
}, [codeReviewSkill, effectiveSkillsSelection.presetId, setSettings]);
const handleRightDockInsertCommitMention = useCallback((commit: GitCommitContextPayload) => {
composerRef.current?.insertCommitMention(commit);
composerRef.current?.focus();
Expand Down Expand Up @@ -4841,6 +4951,11 @@ export default function GatewayApp() {
workdir={displayedConversationWorkdir}
enabledSkills={enabledComposerSkills}
isAgentMode={isAgentMode}
skillPresets={settings.skills.presets}
skillPresetId={effectiveSkillsSelection.presetId}
skillsDisabled={activeConversationSkills.skillsDisabled}
skillsGloballyEnabled={settings.skills.enabled}
onSkillsChange={handleConversationSkillsChange}
chatRuntimeControls={chatRuntimeControlsForCurrentProvider}
reasoningOptions={chatRuntimeReasoningOptions}
thinkingAlwaysOn={chatRuntimeThinkingAlwaysOn}
Expand Down
29 changes: 29 additions & 0 deletions crates/agent-gateway/web/src/i18n/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ export const translations: Record<Locale, Record<string, string>> = {
"chat.runtime.webSearchOn": "联网搜索已开启",
"chat.runtime.webSearchOff": "联网搜索已关闭",
"chat.runtime.webSearchTooltip": "联网搜索",
"chat.skills.presetTooltip": "选择 Skills 方案",
"chat.skills.disabledForConversation": "本会话关闭",
"chat.runtime.tunnelToolAvailable": "内网穿透工具已可用",
"chat.runtime.tunnelToolUnavailable": "内网穿透工具不可用",
"chat.runtime.tunnelAgentModeRequired": "Agent 模式下可使用内网穿透工具",
Expand Down Expand Up @@ -1704,6 +1706,8 @@ export const translations: Record<Locale, Record<string, string>> = {
"settings.cronTypePromptHint": "通过 Prompt 驱动 AI 自动执行任务",
"settings.cronPromptLabel": "Prompt",
"settings.cronPromptModelLabel": "执行模型",
"settings.cronSkillPresetLabel": "Skills 方案",
"settings.cronSkillsDisabled": "关闭 Skills",
"settings.cronPromptModelPlaceholder": "选择当前已配置的模型",
"settings.cronWorkdirLabel": "运行工作空间",
"settings.cronWorkdirFollowActive": "跟随当前工作空间(默认)",
Expand Down Expand Up @@ -1946,6 +1950,16 @@ export const translations: Record<Locale, Record<string, string>> = {
/* ── Settings Skills ── */
"settings.skillsDesc": "为 AI 扩展可调用的技能模块",
"settings.skillsHubTitle": "Skills Hub",
"settings.skillsPresetLabel": "Skills 方案",
"settings.skillsPresetCreate": "新建方案",
"settings.skillsPresetDuplicate": "复制方案",
"settings.skillsPresetRename": "重命名方案",
"settings.skillsPresetDelete": "删除方案",
"settings.skillsPresetUntitled": "新方案",
"settings.skillsPresetCopySuffix": "副本",
"settings.skillsPresetRenamePrompt": "输入新的方案名称",
"settings.skillsPresetNameExists": "方案名称已存在",
"settings.skillsPresetDeleteConfirm": "删除此方案?引用它的会话和 Cron 将回退到 Default。",
"settings.skillsHubSubtitle": "浏览、安装并管理对话可用的技能模块",
"settings.skillsHubEnabled": "Skills 已启用",
"settings.skillsHubEnabledBadge": "已启用",
Expand Down Expand Up @@ -2313,6 +2327,8 @@ export const translations: Record<Locale, Record<string, string>> = {
"chat.runtime.webSearchOn": "Web search enabled",
"chat.runtime.webSearchOff": "Web search disabled",
"chat.runtime.webSearchTooltip": "Toggle web search",
"chat.skills.presetTooltip": "Select Skills preset",
"chat.skills.disabledForConversation": "Off for this chat",
"chat.runtime.tunnelToolAvailable": "Tunnel tool available",
"chat.runtime.tunnelToolUnavailable": "Tunnel tool unavailable",
"chat.runtime.tunnelAgentModeRequired": "Tunnel tool requires Agent mode",
Expand Down Expand Up @@ -3929,6 +3945,8 @@ export const translations: Record<Locale, Record<string, string>> = {
"settings.cronTypePromptHint": "Drive AI to automatically execute tasks via prompt",
"settings.cronPromptLabel": "Prompt",
"settings.cronPromptModelLabel": "Model",
"settings.cronSkillPresetLabel": "Skills preset",
"settings.cronSkillsDisabled": "Disable Skills",
"settings.cronPromptModelPlaceholder": "Select a configured model",
"settings.cronWorkdirLabel": "Workspace",
"settings.cronWorkdirFollowActive": "Follow the active workspace (default)",
Expand Down Expand Up @@ -4178,6 +4196,17 @@ export const translations: Record<Locale, Record<string, string>> = {
/* ── Settings Skills ── */
"settings.skillsDesc": "Extend the AI with callable skill modules",
"settings.skillsHubTitle": "Skills Hub",
"settings.skillsPresetLabel": "Skills preset",
"settings.skillsPresetCreate": "Create preset",
"settings.skillsPresetDuplicate": "Duplicate preset",
"settings.skillsPresetRename": "Rename preset",
"settings.skillsPresetDelete": "Delete preset",
"settings.skillsPresetUntitled": "New preset",
"settings.skillsPresetCopySuffix": "Copy",
"settings.skillsPresetRenamePrompt": "Enter a new preset name",
"settings.skillsPresetNameExists": "A preset with this name already exists",
"settings.skillsPresetDeleteConfirm":
"Delete this preset? Conversations and Cron tasks using it will fall back to Default.",
"settings.skillsHubSubtitle": "Browse, install, and manage skills for your conversations",
"settings.skillsHubEnabled": "Skills enabled",
"settings.skillsHubEnabledBadge": "Enabled",
Expand Down
Loading
Loading