Skip to content

Commit 42e5986

Browse files
committed
feat(ai-chat): add mode selection for Coder and Architect agents
Introduce mode selection UI for AI chat agents, allowing users to switch between different prompt variants (e.g., Edit Mode, Agent Mode) directly from the chat input. Modes map to prompt variant IDs and override the settings-configured default when selected. Changes: - Add `isDefault` property to `ChatMode` interface for bidirectional sync - Create `AbstractModeAwareChatAgent` base class with shared mode logic - Update `CoderAgent` with Edit, Agent, and Agent Next modes - Update `ArchitectAgent` with Default, Simple, and Plan modes - Initialize mode selector from agent's settings-configured default - Export architect prompt ID constants for mode mapping
1 parent fc6845b commit 42e5986

File tree

6 files changed

+156
-8
lines changed

6 files changed

+156
-8
lines changed

packages/ai-chat-ui/src/browser/chat-input-widget.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,9 +364,12 @@ export class AIChatInputWidget extends ReactWidget {
364364
// Only update and re-render when the agent changes
365365
if (agent && agentId !== previousAgentId) {
366366
const modes = agent.modes ?? [];
367+
const defaultMode = modes.find(m => m.isDefault);
368+
const initialModeId = defaultMode?.id;
367369
this.receivingAgent = {
368370
agentId: agentId,
369-
modes
371+
modes,
372+
currentModeId: initialModeId
370373
};
371374
this.chatInputHasModesKey.set(modes.length > 1);
372375
this.update();
@@ -494,6 +497,7 @@ export class AIChatInputWidget extends ReactWidget {
494497
this.editorRef = editor;
495498
this.setupEditorEventListeners();
496499
this.editorReady.resolve();
500+
this.scheduleUpdateReceivingAgent();
497501
}}
498502
showContext={this.configuration?.showContext}
499503
showPinnedAgent={this.configuration?.showPinnedAgent}

packages/ai-chat/src/common/chat-agents.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ export namespace ChatAgentLocation {
143143
export interface ChatMode {
144144
readonly id: string;
145145
readonly name: string;
146+
readonly isDefault?: boolean;
146147
}
147148

148149
export const ChatAgent = Symbol('ChatAgent');

packages/ai-ide/src/browser/architect-agent.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,21 @@
1313
//
1414
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
1515
// *****************************************************************************
16-
import { AbstractStreamParsingChatAgent, ChatRequestModel, ChatService, ChatSession, MutableChatModel, MutableChatRequestModel } from '@theia/ai-chat/lib/common';
16+
import {
17+
ChatMode, ChatRequestModel, ChatService, ChatSession,
18+
MutableChatModel, MutableChatRequestModel
19+
} from '@theia/ai-chat/lib/common';
1720
import { TaskContextStorageService } from '@theia/ai-chat/lib/browser/task-context-service';
1821
import { LanguageModelRequirement } from '@theia/ai-core';
1922
import { inject, injectable } from '@theia/core/shared/inversify';
20-
import { architectSystemVariants, ARCHITECT_PLANNING_PROMPT_ID } from '../common/architect-prompt-template';
23+
import { architectSystemVariants, ARCHITECT_DEFAULT_PROMPT_ID, ARCHITECT_PLANNING_PROMPT_ID, ARCHITECT_SIMPLE_PROMPT_ID } from '../common/architect-prompt-template';
2124
import { nls } from '@theia/core';
2225
import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
2326
import { AI_SUMMARIZE_SESSION_AS_TASK_FOR_CODER, AI_UPDATE_TASK_CONTEXT_COMMAND, AI_EXECUTE_PLAN_WITH_CODER } from '../common/summarize-session-commands';
27+
import { AbstractModeAwareChatAgent } from './mode-aware-chat-agent';
2428

2529
@injectable()
26-
export class ArchitectAgent extends AbstractStreamParsingChatAgent {
30+
export class ArchitectAgent extends AbstractModeAwareChatAgent {
2731
@inject(ChatService) protected readonly chatService: ChatService;
2832
@inject(TaskContextStorageService) protected readonly taskContextStorageService: TaskContextStorageService;
2933

@@ -39,6 +43,22 @@ export class ArchitectAgent extends AbstractStreamParsingChatAgent {
3943
'An AI assistant integrated into Theia IDE, designed to assist software developers. This agent can access the users workspace, it can get a list of all available files \
4044
and folders and retrieve their content. It cannot modify files. It can therefore answer questions about the current project, project files and source code in the \
4145
workspace, such as how to build the project, where to put source code, where to find specific code or configurations, etc.');
46+
47+
protected readonly modeDefinitions: Omit<ChatMode, 'isDefault'>[] = [
48+
{
49+
id: ARCHITECT_DEFAULT_PROMPT_ID,
50+
name: nls.localize('theia/ai/ide/architectAgent/mode/default', 'Default Mode')
51+
},
52+
{
53+
id: ARCHITECT_SIMPLE_PROMPT_ID,
54+
name: nls.localize('theia/ai/ide/architectAgent/mode/simple', 'Simple Mode')
55+
},
56+
{
57+
id: ARCHITECT_PLANNING_PROMPT_ID,
58+
name: nls.localize('theia/ai/ide/architectAgent/mode/plan', 'Plan Mode')
59+
},
60+
];
61+
4262
override prompts = [architectSystemVariants];
4363
protected override systemPromptId: string | undefined = architectSystemVariants.id;
4464

packages/ai-ide/src/browser/coder-agent.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,16 @@
1313
//
1414
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
1515
// *****************************************************************************
16-
import { AbstractStreamParsingChatAgent, ChatRequestModel, ChatService, ChatSession, MutableChatModel, MutableChatRequestModel } from '@theia/ai-chat/lib/common';
16+
import {
17+
ChatMode, ChatRequestModel, ChatService, ChatSession,
18+
MutableChatModel, MutableChatRequestModel
19+
} from '@theia/ai-chat/lib/common';
1720
import { inject, injectable } from '@theia/core/shared/inversify';
1821
import {
1922
CODER_SYSTEM_PROMPT_ID,
23+
CODER_EDIT_TEMPLATE_ID,
24+
CODER_AGENT_MODE_TEMPLATE_ID,
25+
CODER_AGENT_MODE_NEXT_TEMPLATE_ID,
2026
getCoderAgentModePromptTemplate,
2127
getCoderAgentModeNextPromptTemplate,
2228
getCoderPromptTemplateEdit,
@@ -27,9 +33,10 @@ import { LanguageModelRequirement, PromptVariantSet } from '@theia/ai-core';
2733
import { nls } from '@theia/core';
2834
import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering';
2935
import { AI_CHAT_NEW_CHAT_WINDOW_COMMAND, ChatCommands } from '@theia/ai-chat-ui/lib/browser/chat-view-commands';
36+
import { AbstractModeAwareChatAgent } from './mode-aware-chat-agent';
3037

3138
@injectable()
32-
export class CoderAgent extends AbstractStreamParsingChatAgent {
39+
export class CoderAgent extends AbstractModeAwareChatAgent {
3340
@inject(ChatService) protected readonly chatService: ChatService;
3441
id: string = 'Coder';
3542
name = 'Coder';
@@ -43,6 +50,22 @@ export class CoderAgent extends AbstractStreamParsingChatAgent {
4350
'An AI assistant integrated into Theia IDE, designed to assist software developers. This agent can access the users workspace, it can get a list of all available files \
4451
and folders and retrieve their content. Furthermore, it can suggest modifications of files to the user. It can therefore assist the user with coding tasks or other \
4552
tasks involving file changes.');
53+
54+
protected readonly modeDefinitions: Omit<ChatMode, 'isDefault'>[] = [
55+
{
56+
id: CODER_EDIT_TEMPLATE_ID,
57+
name: nls.localize('theia/ai/ide/coderAgent/mode/edit', 'Edit Mode')
58+
},
59+
{
60+
id: CODER_AGENT_MODE_TEMPLATE_ID,
61+
name: nls.localizeByDefault('Agent Mode')
62+
},
63+
{
64+
id: CODER_AGENT_MODE_NEXT_TEMPLATE_ID,
65+
name: nls.localize('theia/ai/ide/coderAgent/mode/agentNext', 'Agent Mode (Next)')
66+
},
67+
];
68+
4669
override prompts: PromptVariantSet[] = [{
4770
id: CODER_SYSTEM_PROMPT_ID,
4871
defaultVariant: getCoderPromptTemplateEdit(),
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2026 EclipseSource GmbH.
3+
//
4+
// This program and the accompanying materials are made available under the
5+
// terms of the Eclipse Public License v. 2.0 which is available at
6+
// http://www.eclipse.org/legal/epl-2.0.
7+
//
8+
// This Source Code may also be made available under the following Secondary
9+
// Licenses when the conditions for such availability set forth in the Eclipse
10+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
// with the GNU Classpath Exception which is available at
12+
// https://www.gnu.org/software/classpath/license.html.
13+
//
14+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15+
// *****************************************************************************
16+
import {
17+
AbstractStreamParsingChatAgent, ChatMode, ChatSessionContext, SystemMessageDescription
18+
} from '@theia/ai-chat/lib/common';
19+
import { AIVariableContext } from '@theia/ai-core';
20+
import { injectable } from '@theia/core/shared/inversify';
21+
22+
/**
23+
* An abstract chat agent that supports mode selection for selecting prompt variants.
24+
*
25+
* Agents extending this class define their available modes via `modeDefinitions`.
26+
* The `modes` getter dynamically computes which mode is the default based on the
27+
* current prompt variant settings. When a request is made with a specific `modeId`,
28+
* that mode's prompt variant is used instead of the settings-configured default.
29+
*/
30+
@injectable()
31+
export abstract class AbstractModeAwareChatAgent extends AbstractStreamParsingChatAgent {
32+
/**
33+
* Mode definitions without the `isDefault` property.
34+
* Subclasses must provide their specific mode definitions.
35+
* Each mode's `id` should correspond to a prompt variant ID.
36+
*/
37+
protected abstract readonly modeDefinitions: Omit<ChatMode, 'isDefault'>[];
38+
39+
/**
40+
* The ID of the prompt variant set used for mode selection.
41+
* Defaults to `systemPromptId`. Override if a different variant set should be used.
42+
*/
43+
protected get promptVariantSetId(): string | undefined {
44+
return this.systemPromptId;
45+
}
46+
47+
/**
48+
* Returns the available modes with `isDefault` computed based on current settings.
49+
*/
50+
get modes(): ChatMode[] {
51+
const variantSetId = this.promptVariantSetId;
52+
if (!variantSetId) {
53+
return this.modeDefinitions.map(mode => ({ ...mode, isDefault: false }));
54+
}
55+
const effectiveVariantId = this.promptService.getEffectiveVariantId(variantSetId);
56+
return this.modeDefinitions.map(mode => ({
57+
...mode,
58+
isDefault: mode.id === effectiveVariantId
59+
}));
60+
}
61+
62+
protected override async getSystemMessageDescription(context: AIVariableContext): Promise<SystemMessageDescription | undefined> {
63+
if (this.systemPromptId === undefined) {
64+
return undefined;
65+
}
66+
67+
// Check for mode-based override from request
68+
const modeId = ChatSessionContext.is(context) ? context.request?.request.modeId : undefined;
69+
const effectiveVariantId = this.getEffectiveVariantIdWithMode(modeId);
70+
71+
if (!effectiveVariantId) {
72+
return undefined;
73+
}
74+
75+
const isEdited = this.isPromptVariantCustomized(effectiveVariantId);
76+
const resolvedPrompt = await this.promptService.getResolvedPromptFragment(effectiveVariantId, undefined, context);
77+
return resolvedPrompt ? SystemMessageDescription.fromResolvedPromptFragment(resolvedPrompt, effectiveVariantId, isEdited) : undefined;
78+
}
79+
80+
/**
81+
* Determines the effective variant ID, considering mode override.
82+
* If modeId is provided and is a valid variant for the prompt set, it takes precedence.
83+
* Otherwise falls back to settings-based selection.
84+
*/
85+
protected getEffectiveVariantIdWithMode(modeId?: string): string | undefined {
86+
const variantSetId = this.promptVariantSetId;
87+
if (!variantSetId) {
88+
return undefined;
89+
}
90+
if (modeId) {
91+
const variantIds = this.promptService.getVariantIds(variantSetId);
92+
if (variantIds.includes(modeId)) {
93+
return modeId;
94+
}
95+
}
96+
return this.promptService.getEffectiveVariantId(variantSetId);
97+
}
98+
}

packages/ai-ide/src/common/architect-prompt-template.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ import {
2929
} from './task-context-function-ids';
3030

3131
export const ARCHITECT_PLANNING_PROMPT_ID = 'architect-system-planning-next';
32+
export const ARCHITECT_SIMPLE_PROMPT_ID = 'architect-system-simple';
33+
export const ARCHITECT_DEFAULT_PROMPT_ID = 'architect-system-default';
3234

3335
export const architectSystemVariants = <PromptVariantSet>{
3436
id: 'architect-system',
3537
defaultVariant: {
36-
id: 'architect-system-default',
38+
id: ARCHITECT_DEFAULT_PROMPT_ID,
3739
template: `{{!-- This prompt is licensed under the MIT License (https://opensource.org/license/mit).
3840
Made improvements or adaptations to this prompt template? We'd love for you to share it with the community! Contribute back here:
3941
https://github.com/eclipse-theia/theia/discussions/new?category=prompt-template-contribution --}}
@@ -72,7 +74,7 @@ Always look at the relevant files to understand your task using the function ~{$
7274
},
7375
variants: [
7476
{
75-
id: 'architect-system-simple',
77+
id: ARCHITECT_SIMPLE_PROMPT_ID,
7678
template: `{{!-- This prompt is licensed under the MIT License (https://opensource.org/license/mit).
7779
Made improvements or adaptations to this prompt template? We'd love for you to share it with the community! Contribute back here:
7880
https://github.com/eclipse-theia/theia/discussions/new?category=prompt-template-contribution --}}

0 commit comments

Comments
 (0)