diff --git a/frontend/ai.client/src/app/components/model-settings/model-settings.html b/frontend/ai.client/src/app/components/model-settings/model-settings.html index df537d4b..66f54c53 100644 --- a/frontend/ai.client/src/app/components/model-settings/model-settings.html +++ b/frontend/ai.client/src/app/components/model-settings/model-settings.html @@ -383,7 +383,7 @@

- @for (skill of skillService.skills(); track skill.skillId) { + @for (skill of skillService.visibleSkills(); track skill.skillId) {
} @else {
- @for (tool of toolService.tools(); track tool.toolId) { + @for (tool of toolService.visibleTools(); track tool.toolId) {
diff --git a/frontend/ai.client/src/app/services/skill/skill.service.spec.ts b/frontend/ai.client/src/app/services/skill/skill.service.spec.ts index 9464dbed..bd2acf43 100644 --- a/frontend/ai.client/src/app/services/skill/skill.service.spec.ts +++ b/frontend/ai.client/src/app/services/skill/skill.service.spec.ts @@ -87,12 +87,19 @@ describe('SkillService', () => { expect(service.enabledSkillIds()).toEqual(['web_research']); }); + it('filters visibleSkills to only the bound skills while locked', () => { + expect(service.visibleSkills().map(s => s.skillId).sort()).toEqual(['pdf_workflows', 'web_research']); + service.lockToAgentSkills(['web_research']); + expect(service.visibleSkills().map(s => s.skillId)).toEqual(['web_research']); + }); + it('restores the user set when cleared', () => { service.lockToAgentSkills(['web_research']); service.clearAgentLock(); expect(service.agentLocked()).toBe(false); // Back to per-skill state: only pdf_workflows is user-enabled. expect(service.enabledSkillIds()).toEqual(['pdf_workflows']); + expect(service.visibleSkills().length).toBe(2); }); }); diff --git a/frontend/ai.client/src/app/services/skill/skill.service.ts b/frontend/ai.client/src/app/services/skill/skill.service.ts index e5118b7f..39e7f540 100644 --- a/frontend/ai.client/src/app/services/skill/skill.service.ts +++ b/frontend/ai.client/src/app/services/skill/skill.service.ts @@ -107,6 +107,19 @@ export class SkillService { readonly hasSkills = computed(() => this._skills().length > 0); + /** + * The skills the picker should render. Agent-locked → only the bound skills + * (the agent dictates a fixed set, so hide the rest); otherwise every + * accessible skill. + */ + readonly visibleSkills = computed(() => { + const locked = this._agentLockedSkillIds(); + if (locked !== null) { + return this._skills().filter(s => locked.includes(s.skillId)); + } + return this._skills(); + }); + /** * Whether a skill row should render as ON. Agent-locked → membership in the * bound set; otherwise the user's own enabled state. diff --git a/frontend/ai.client/src/app/services/tool/tool.service.spec.ts b/frontend/ai.client/src/app/services/tool/tool.service.spec.ts index 51ee29ab..611b6898 100644 --- a/frontend/ai.client/src/app/services/tool/tool.service.spec.ts +++ b/frontend/ai.client/src/app/services/tool/tool.service.spec.ts @@ -96,12 +96,21 @@ describe('ToolService', () => { expect(service.enabledToolIds()).toEqual(['code-interp']); }); + it('filters visibleTools to only the bound tools while locked', () => { + // Unlocked: every accessible tool is visible. + expect(service.visibleTools().map(t => t.toolId).sort()).toEqual(['code-interp', 'search-web']); + service.lockToAgentTools(['code-interp']); + // Locked: only the bound tool is shown (the rest are hidden, not greyed). + expect(service.visibleTools().map(t => t.toolId)).toEqual(['code-interp']); + }); + it('restores the user set when cleared', () => { service.lockToAgentTools(['code-interp']); service.clearAgentLock(); expect(service.agentLocked()).toBe(false); // Back to the per-tool computation (both mock tools are enabled). expect(service.enabledToolIds().sort()).toEqual(['code-interp', 'search-web']); + expect(service.visibleTools().length).toBe(2); }); it('locks to an empty set (agent with no tools ≠ free-select)', () => { diff --git a/frontend/ai.client/src/app/services/tool/tool.service.ts b/frontend/ai.client/src/app/services/tool/tool.service.ts index f36aec61..fc0b5597 100644 --- a/frontend/ai.client/src/app/services/tool/tool.service.ts +++ b/frontend/ai.client/src/app/services/tool/tool.service.ts @@ -168,6 +168,19 @@ export class ToolService { return this.enabledTools().length; }); + /** + * The tools the picker should render. Agent-locked → only the bound tools + * (the agent dictates a fixed set, so hide the rest rather than show a long + * greyed list); otherwise every accessible tool. + */ + readonly visibleTools = computed(() => { + const locked = this._agentLockedToolIds(); + if (locked !== null) { + return this._tools().filter(t => locked.includes(t.toolId)); + } + return this._tools(); + }); + /** * Whether a tool row should render as ON. Agent-locked → membership in the * bound set (so greyed toggles honestly show the Agent's toolset, not the diff --git a/frontend/ai.client/src/app/session/session.page.ts b/frontend/ai.client/src/app/session/session.page.ts index 892d3ff6..0d4fabbb 100644 --- a/frontend/ai.client/src/app/session/session.page.ts +++ b/frontend/ai.client/src/app/session/session.page.ts @@ -340,13 +340,18 @@ export class ConversationPage implements OnDestroy { return; } - // No assistant in the URL — clear any stale state from a prior load. + // No assistant in the URL — clear any stale local state from a prior load. if (loadedAssistant || this.assistantError() || this.agent()) { this.assistant.set(null); this.assistantError.set(null); this.agent.set(null); - this.clearAgentBindingLocks(); } + // Always release the picker locks. They live in root singleton services + // that OUTLIVE this component, so a freshly-created "new chat" component + // (whose own assistant()/agent() signals start null, making the guard + // above false) must still release locks the previous conversation left + // behind. Idempotent — a no-op when nothing is locked. + this.clearAgentBindingLocks(); }); // Self-heal effect: when the user lands on `/s/:id` without an