+ @if (isSlashCommandMenuOpenFor('chat')) {
role="option"
[class.active]="i === slashHighlightIndex()"
[attr.aria-selected]="i === slashHighlightIndex()"
- (mousedown)="pickSlashCommand(command, $event)"
+ (mousedown)="pickSlashCommand(command, $event, 'chat')"
>
{{ command.name }}
@@ -1349,10 +1332,11 @@
i18n-placeholder="@@featureChat-typeMessagePlaceholder"
[value]="chatMessage()"
(input)="onChatMessageInput($any($event.target).value)"
+ (focus)="onChatMessageFocus()"
(keydown)="onChatInputKeydown($event)"
- [attr.aria-autocomplete]="slashCommandMenuOpen() ? 'list' : null"
- [attr.aria-controls]="slashCommandMenuOpen() ? 'chatCommandTypeaheadListbox' : null"
- [attr.aria-expanded]="slashCommandMenuOpen()"
+ [attr.aria-autocomplete]="isSlashCommandMenuOpenFor('chat') ? 'list' : null"
+ [attr.aria-controls]="isSlashCommandMenuOpenFor('chat') ? 'chatCommandTypeaheadListbox' : null"
+ [attr.aria-expanded]="isSlashCommandMenuOpenFor('chat')"
[disabled]="
!(socketConnected$ | async) ||
(socketReconnecting$ | async) ||
@@ -1760,13 +1744,50 @@
>
}
+ @if (isSlashCommandMenuOpenFor('planRefine')) {
+
+
+ @for (command of filteredSlashCommands(); track command.name; let i = $index) {
+
+ }
+
+
+ }
diff --git a/libs/domains/agenstra/frontend/feature-agent-console/src/lib/chat/chat.component.scss b/libs/domains/agenstra/frontend/feature-agent-console/src/lib/chat/chat.component.scss
index fc1a51509..5be372b36 100644
--- a/libs/domains/agenstra/frontend/feature-agent-console/src/lib/chat/chat.component.scss
+++ b/libs/domains/agenstra/frontend/feature-agent-console/src/lib/chat/chat.component.scss
@@ -169,20 +169,6 @@
position: relative;
}
-.chat-command-mark {
- display: flex;
- align-items: center;
- padding: 0.5rem 0.75rem 0;
-}
-
-.chat-command-mark__badge {
- max-width: 100%;
-}
-
-.chat-command-mark__clear {
- font-size: 0.65rem;
-}
-
.chat-command-typeahead {
position: absolute;
left: 0;
@@ -218,10 +204,6 @@
padding-left: 0;
}
-.chat-textarea-container--command-marked {
- padding-top: 0;
-}
-
// Textarea body
.chat-textarea {
min-width: 0; // Allow input to shrink
@@ -242,6 +224,7 @@
}
.chat-plan-refine-composer {
+ position: relative;
display: flex;
flex-direction: column;
border: 1px solid var(--bs-border-color);
diff --git a/libs/domains/agenstra/frontend/feature-agent-console/src/lib/chat/chat.component.ts b/libs/domains/agenstra/frontend/feature-agent-console/src/lib/chat/chat.component.ts
index 9a1191231..79c6266a2 100644
--- a/libs/domains/agenstra/frontend/feature-agent-console/src/lib/chat/chat.component.ts
+++ b/libs/domains/agenstra/frontend/feature-agent-console/src/lib/chat/chat.component.ts
@@ -171,8 +171,8 @@ import {
} from './agent-chat-response-markdown';
import { accumulateStreamingTurnFromEvents } from './agent-chat-streaming-aggregate';
import { AgentChatTodosExpandCoordinator } from './agent-chat-todos-expand.coordinator';
-import { sanitizeAndTrustMarkdownHtml } from './chat-markdown-html';
import { mergeTicketAutomationChatCardPayload } from './chat-automation-card-merge';
+import { sanitizeAndTrustMarkdownHtml } from './chat-markdown-html';
import {
chatPlanStateBadgeColor as chatPlanStateBadgeColorFn,
chatPlanStateLabel as chatPlanStateLabelFn,
@@ -208,6 +208,8 @@ type ChatMessageWithFilter = {
} | null;
};
+type SlashCommandInputTarget = 'chat' | 'planRefine';
+
/** Segment colors of the stacked workspace provisioning bar (cycled per environment). */
const WORKSPACE_PROGRESS_SEGMENT_VARIANTS: FpcProgressVariant[] = [
'primary',
@@ -858,20 +860,20 @@ export class AgentConsoleChatComponent implements OnInit, AfterViewChecked, OnDe
/** Last reply error keyed by question id. */
readonly questionReplyErrors = signal>(new Map());
selectedChatModel = signal(null);
- /** Marked slash command (OpenCode-style); prepended on send. */
- selectedCommand = signal(null);
/** Dismiss typeahead without clearing the `/` fragment (Escape). */
private readonly slashMenuDismissed = signal(false);
+ private readonly slashCommandTarget = signal('chat');
slashHighlightIndex = signal(0);
selectedAgentId = signal(null);
/** Token after leading `/` while composing a slash command (`null` = menu inactive). */
readonly slashCommandQuery = computed(() => {
- if (this.selectedCommand() || this.slashMenuDismissed()) {
+ if (this.slashMenuDismissed()) {
return null;
}
- const match = this.chatMessage().match(/^\/([^\s\n]*)$/);
+ const value = this.slashCommandTarget() === 'planRefine' ? this.planRefineMessage() : this.chatMessage();
+ const match = value.match(/^\/([^\s\n]*)$/);
return match ? match[1] : null;
});
@@ -3037,13 +3039,7 @@ export class AgentConsoleChatComponent implements OnInit, AfterViewChecked, OnDe
}
onSendMessage(): void {
- let message = this.chatMessage().trim();
- // Append selected command to message if one is selected
- const selectedCmd = this.selectedCommand();
-
- if (selectedCmd) {
- message = message ? `${selectedCmd}\n${message}` : selectedCmd;
- }
+ const message = this.chatMessage().trim();
if (!message) {
return;
@@ -3071,8 +3067,6 @@ export class AgentConsoleChatComponent implements OnInit, AfterViewChecked, OnDe
this.lastUserMessageTimestamp.set(Date.now());
this.chatMessage.set('');
- // Clear selected command after sending
- this.selectedCommand.set(null);
// Trigger scroll after sending message
this.stickToBottom.set(true);
this.shouldScrollToBottom = true;
@@ -3083,7 +3077,7 @@ export class AgentConsoleChatComponent implements OnInit, AfterViewChecked, OnDe
}
onEnhanceMessage(): void {
- this.enhanceText(this.chatMessage(), 'chat', true);
+ this.enhanceText(this.chatMessage(), 'chat');
}
clearPlanRefineEnhanceError(): void {
@@ -3094,13 +3088,8 @@ export class AgentConsoleChatComponent implements OnInit, AfterViewChecked, OnDe
this.enhanceText(this.planRefineMessage(), 'planRefine');
}
- private enhanceText(rawMessage: string, target: 'chat' | 'planRefine', includeSlashCommand = false): void {
- let message = rawMessage.trim();
- const selectedCmd = this.selectedCommand();
-
- if (includeSlashCommand && selectedCmd) {
- message = message ? `${selectedCmd}\n${message}` : selectedCmd;
- }
+ private enhanceText(rawMessage: string, target: 'chat' | 'planRefine'): void {
+ const message = rawMessage.trim();
if (!message) {
return;
@@ -3128,12 +3117,7 @@ export class AgentConsoleChatComponent implements OnInit, AfterViewChecked, OnDe
}
onPlanMessage(): void {
- let message = this.chatMessage().trim();
- const selectedCmd = this.selectedCommand();
-
- if (selectedCmd) {
- message = message ? `${selectedCmd}\n${message}` : selectedCmd;
- }
+ const message = this.chatMessage().trim();
if (!message) {
return;
@@ -3248,21 +3232,38 @@ export class AgentConsoleChatComponent implements OnInit, AfterViewChecked, OnDe
}
onChatInputKeydown(event: KeyboardEvent): void {
- if (this.slashCommandMenuOpen()) {
+ if (this.handleSlashCommandKeydown(event, 'chat')) {
+ return;
+ }
+
+ if (event.key === 'Enter' && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
+ event.preventDefault();
+ this.onSendMessage();
+ }
+ }
+
+ onPlanRefineInputKeydown(event: KeyboardEvent): void {
+ this.handleSlashCommandKeydown(event, 'planRefine');
+ }
+
+ private handleSlashCommandKeydown(event: KeyboardEvent, target: SlashCommandInputTarget): boolean {
+ this.slashCommandTarget.set(target);
+
+ if (this.isSlashCommandMenuOpenFor(target)) {
const filtered = this.filteredSlashCommands();
if (event.key === 'ArrowDown') {
event.preventDefault();
this.slashHighlightIndex.update((index) => Math.min(index + 1, filtered.length - 1));
- return;
+ return true;
}
if (event.key === 'ArrowUp') {
event.preventDefault();
this.slashHighlightIndex.update((index) => Math.max(index - 1, 0));
- return;
+ return true;
}
if (event.key === 'Enter' || event.key === 'Tab') {
@@ -3270,45 +3271,60 @@ export class AgentConsoleChatComponent implements OnInit, AfterViewChecked, OnDe
const command = filtered[this.slashHighlightIndex()] ?? filtered[0];
if (command) {
- this.pickSlashCommand(command);
+ this.pickSlashCommand(command, undefined, target);
}
- return;
+ return true;
}
if (event.key === 'Escape') {
event.preventDefault();
this.slashMenuDismissed.set(true);
- return;
+ return true;
}
}
- if (event.key === 'Enter' && !event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey) {
- event.preventDefault();
- this.onSendMessage();
- }
+ return false;
}
onChatMessageInput(value: string): void {
this.chatMessage.set(value);
+ this.slashCommandTarget.set('chat');
+ this.slashMenuDismissed.set(false);
+ }
+
+ onChatMessageFocus(): void {
+ this.slashCommandTarget.set('chat');
+ }
+
+ onPlanRefineMessageInput(value: string): void {
+ this.planRefineMessage.set(value);
+ this.slashCommandTarget.set('planRefine');
this.slashMenuDismissed.set(false);
}
- pickSlashCommand(command: AgentSlashCommand | string, event?: Event): void {
+ onPlanRefineMessageFocus(): void {
+ this.slashCommandTarget.set('planRefine');
+ }
+
+ isSlashCommandMenuOpenFor(target: SlashCommandInputTarget): boolean {
+ return this.slashCommandTarget() === target && this.slashCommandMenuOpen();
+ }
+
+ pickSlashCommand(command: AgentSlashCommand | string, event?: Event, target = this.slashCommandTarget()): void {
event?.preventDefault();
const name = typeof command === 'string' ? command : command.name;
- this.selectedCommand.set(name);
- this.chatMessage.set(
- this.chatMessage()
- .replace(/^\/[^\s\n]*/, '')
- .replace(/^\n/, ''),
- );
- this.slashMenuDismissed.set(true);
- }
+ const insertCommand = (value: string) => value.replace(/^\/[^\s\n]*/, `${name} `);
+
+ if (target === 'planRefine') {
+ this.planRefineMessage.update(insertCommand);
+ } else {
+ this.chatMessage.update(insertCommand);
+ }
- clearSelectedCommand(): void {
- this.selectedCommand.set(null);
+ this.slashCommandTarget.set(target);
+ this.slashMenuDismissed.set(true);
}
onChatModelChange(value: string): void {
@@ -5184,7 +5200,7 @@ export class AgentConsoleChatComponent implements OnInit, AfterViewChecked, OnDe
}
formatTimestamp(timestamp: number): string {
- return new Date(timestamp).toLocaleTimeString();
+ return new Date(timestamp).toLocaleString();
}
openTicketAutomationRunFromChat(payload: TicketAutomationRunChatEventPayload): void {