diff --git a/ui/src/pages/chat/components/chat-attachments.ts b/ui/src/pages/chat/components/chat-attachments.ts
index a74ccd531181..53eb74807f9e 100644
--- a/ui/src/pages/chat/components/chat-attachments.ts
+++ b/ui/src/pages/chat/components/chat-attachments.ts
@@ -63,7 +63,7 @@ export class ChatAttachmentReadLifecycle {
}
}
-export function isFileDrag(dataTransfer: DataTransfer | null): boolean {
+function isFileDrag(dataTransfer: DataTransfer | null): boolean {
return Array.from(dataTransfer?.types ?? []).includes("Files");
}
@@ -81,7 +81,7 @@ const TEXT_ENTRY_INPUT_TYPES = new Set([
// actually accept it; anywhere else (disabled/readonly inputs, non-text
// controls like checkbox/range) an uncancelled URL drop navigates the app
// away and discards unsent drafts.
-export function isEditableDropTarget(event: DragEvent): boolean {
+function isEditableDropTarget(event: DragEvent): boolean {
const target = event.target;
if (!(target instanceof Element)) {
return false;
@@ -100,7 +100,7 @@ function currentAttachments(props: ChatAttachmentControlsProps): ChatAttachment[
return props.getAttachments?.() ?? props.attachments ?? [];
}
-export function clickComposerInput(target: HTMLElement, selector: string) {
+function clickComposerInput(target: HTMLElement, selector: string) {
target.closest("details")?.removeAttribute("open");
target
.closest(".agent-chat__composer-shell, .new-session-page__composer")
@@ -363,7 +363,7 @@ function handleChatAttachmentFileSelect(e: Event, props: ChatAttachmentControlsP
void appendAttachmentFiles(files, props);
}
-export function handleChatAttachmentDrop(e: DragEvent, props: ChatAttachmentControlsProps) {
+function handleChatAttachmentDrop(e: DragEvent, props: ChatAttachmentControlsProps) {
e.preventDefault();
void appendAttachmentFiles([...(e.dataTransfer?.files ?? [])], props);
}
@@ -372,6 +372,8 @@ type ChatAttachmentDropProps = ChatAttachmentControlsProps & {
canCompose: boolean;
};
+// Both composers share balanced nested drag state and cancel non-editable
+// text/URL drops so disabled surfaces cannot navigate away from a draft.
export function createChatAttachmentDropHandlers(props: ChatAttachmentDropProps) {
let depth = 0;
const setActive = (event: DragEvent, active: boolean) => {
@@ -432,42 +434,74 @@ export function createChatAttachmentDropHandlers(props: ChatAttachmentDropProps)
export function renderChatAttachmentInputs(props: ChatAttachmentControlsProps) {
return html`
- {
- if (!props.disabled) {
- handleChatAttachmentFileSelect(event, props);
+ ${(["file", "photo", "camera"] as const).map(
+ (kind) => html`
+ {
+ if (!props.disabled) {
+ handleChatAttachmentFileSelect(event, props);
+ }
+ }}
+ />
+ `,
+ )}
+ `;
+}
+
+export function handleChatAttachmentMenuSelection(
+ event: CustomEvent<{ item: { value?: string } }>,
+): boolean {
+ const value = event.detail.item.value;
+ if (value !== "camera" && value !== "photo" && value !== "file") {
+ return false;
+ }
+ clickComposerInput(event.currentTarget as HTMLElement, `.agent-chat__${value}-input`);
+ return true;
+}
+
+export function renderChatAttachmentMenuTrigger(disabled: boolean | undefined) {
+ return html`
+
+ `;
+}
+
+export function renderChatAttachmentMenuOptions(fileIcon = icons.folder) {
+ return html`
+
+
+
`;
}
@@ -477,51 +511,9 @@ export function renderChatAttachmentMenu(props: ChatAttachmentControlsProps) {
class="agent-chat__attach-menu"
placement="top-start"
aria-label=${t("chat.composer.addAttachment")}
- @wa-select=${(event: CustomEvent<{ item: { value?: string } }>) => {
- const menu = event.currentTarget as HTMLElement;
- const selector =
- event.detail.item.value === "camera"
- ? ".agent-chat__camera-input"
- : event.detail.item.value === "photo"
- ? ".agent-chat__photo-input"
- : event.detail.item.value === "file"
- ? ".agent-chat__file-input"
- : null;
- if (selector) {
- clickComposerInput(menu, selector);
- }
- }}
+ @wa-select=${handleChatAttachmentMenuSelection}
>
-
-
-
-
+ ${renderChatAttachmentMenuTrigger(props.disabled)} ${renderChatAttachmentMenuOptions()}
`;
}
diff --git a/ui/src/pages/chat/components/chat-composer-plus-menu.ts b/ui/src/pages/chat/components/chat-composer-plus-menu.ts
index d44760e3886b..bdebe30969c3 100644
--- a/ui/src/pages/chat/components/chat-composer-plus-menu.ts
+++ b/ui/src/pages/chat/components/chat-composer-plus-menu.ts
@@ -16,7 +16,12 @@ import {
readOwnEntry,
resolveToolOverrideState,
} from "../../../lib/sessions/tool-overrides.ts";
-import { clickComposerInput, type ChatAttachmentControlsProps } from "./chat-attachments.ts";
+import {
+ handleChatAttachmentMenuSelection,
+ renderChatAttachmentMenuOptions,
+ renderChatAttachmentMenuTrigger,
+ type ChatAttachmentControlsProps,
+} from "./chat-attachments.ts";
export type ChatComposerPlusMenuView = "root" | "skills" | "connectors" | `tools:${string}`;
@@ -86,6 +91,38 @@ function renderBackRow() {
`;
}
+function renderCapabilityToggleRow(options: {
+ value: string;
+ label: string;
+ checked: boolean;
+ disabled: boolean;
+ title: string | null | undefined;
+ note?: TemplateResult | typeof nothing;
+}) {
+ return html`
+
+ `;
+}
+
function renderRootView(props: ChatComposerPlusMenuProps) {
const connectorCount = props.mcpServers.filter((server) =>
resolveToolOverrideState(
@@ -99,20 +136,7 @@ function renderRootView(props: ChatComposerPlusMenuProps) {
props.webSearchBaseEnabled,
props.toolOverrides?.webSearch,
);
- const attachments = html`
-
-
-
- `;
+ const attachments = renderChatAttachmentMenuOptions(icons.paperclip);
if (!props.showCapabilities) {
return attachments;
}
@@ -183,42 +207,22 @@ function renderSkillView(props: ChatComposerPlusMenuProps) {
${t("chat.composer.menu.noSkills")}
`
: props.skills.map((skill, index) => {
- const rowDisabled = skill.missingDeps || skill.blocked || disabledReason !== null;
const title = skill.missingDeps
? t("chat.composer.menu.depsMissing")
: skill.blocked
? t("chat.composer.menu.skillBlocked")
: disabledReason;
- return html`
-
- `;
+ return renderCapabilityToggleRow({
+ value: `skill:${index}`,
+ label: skill.name,
+ checked: skill.enabled,
+ disabled: skill.missingDeps || skill.blocked || disabledReason !== null,
+ title,
+ note:
+ skill.missingDeps || skill.blocked
+ ? html``
+ : nothing,
+ });
});
return html`
${renderBackRow()} ${rows} ${menuDivider()}
@@ -239,33 +243,21 @@ function renderConnectorView(props: ChatComposerPlusMenuProps) {
const override = readOwnEntry(props.toolOverrides?.mcpServers, server.name);
const enabled = resolveToolOverrideState(server.enabled, override);
return html`
-
+ ${renderCapabilityToggleRow({
+ value: `connector:${index}`,
+ label: server.name,
+ checked: enabled,
+ disabled: disabledReason !== null,
+ title: disabledReason,
+ note: html``,
+ })}
${props.onOpenToolAccess
? html`