diff --git a/src/apps/officer-web/Screens/Dashboard/CapabilityPage.tsx b/src/apps/officer-web/Screens/Dashboard/CapabilityPage.tsx index 73772dfa..953313d6 100644 --- a/src/apps/officer-web/Screens/Dashboard/CapabilityPage.tsx +++ b/src/apps/officer-web/Screens/Dashboard/CapabilityPage.tsx @@ -1,3 +1,4 @@ +import type { FormEvent } from 'react'; import { useState, useEffect, useRef } from 'react'; import { Link, useNavigate, useParams, useSearchParams } from 'react-router'; import { useQuery, useQueryClient } from '@tanstack/react-query'; @@ -344,7 +345,8 @@ export const CapabilityList = ({ queryFn: () => client.get(endpoint), }); - const handleCreate = async () => { + const handleCreate = async (ev?: FormEvent) => { + ev?.preventDefault(); const name = newName.trim(); if (!name) return; try { @@ -382,16 +384,15 @@ export const CapabilityList = ({ )} {creating && ( -
+
setNewName(ev.target.value)} onKeyDown={(ev) => { - if (ev.key === 'Enter') { - ev.preventDefault(); - handleCreate(); - } if (ev.key === 'Escape') { setCreating(false); setNewName(''); @@ -401,13 +402,14 @@ export const CapabilityList = ({ className="flex-1 min-w-0 rounded border border-duck-dark/20 bg-background px-2 py-1 text-base md:text-sm text-duck-dark placeholder:text-duck-dark/30 focus:outline-none focus:ring-1 focus:ring-duck-teal/30" /> -
+ )} {externalSearch === undefined && (
diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApiKeys.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApiKeys.tsx index e9a43ed3..95534cb9 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApiKeys.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApiKeys.tsx @@ -1,3 +1,4 @@ +import type { FormEvent } from 'react'; import { useState } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; @@ -61,7 +62,8 @@ export const ApiKeys = () => { const refresh = () => queryClient.invalidateQueries({ queryKey: API_KEYS_QUERY_KEY }); - const create = async () => { + const create = async (ev?: FormEvent) => { + ev?.preventDefault(); const trimmed = name.trim(); if (!trimmed || isCreating) return; setIsCreating(true); @@ -130,22 +132,21 @@ export const ApiKeys = () => {
)} -
+
- -
+ {keys.length === 0 ? (
diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApifyConfig.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApifyConfig.tsx index ad4d3eb7..fb82ebb4 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApifyConfig.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/ApifyConfig.tsx @@ -1,3 +1,4 @@ +import type { FormEvent } from 'react'; import { useState, useEffect } from 'react'; import { toast } from 'sonner'; import { Input } from '@/components/ui/input'; @@ -31,7 +32,8 @@ export const ApifyConfig = () => { .finally(() => setIsLoading(false)); }, []); - const handleSave = async () => { + const handleSave = async (ev?: FormEvent) => { + ev?.preventDefault(); if (isSaving) return; setIsSaving(true); try { @@ -60,7 +62,7 @@ export const ApifyConfig = () => {
)} -
+
-
+ ); }; diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/DavAppPasswords.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/DavAppPasswords.tsx index edbb83fc..98ccd0c5 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/DavAppPasswords.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/DavAppPasswords.tsx @@ -1,3 +1,4 @@ +import type { FormEvent } from 'react'; import { useState, useEffect, useCallback } from 'react'; import { toast } from 'sonner'; import { Copy, Trash2, Ban, Plus, Smartphone } from 'lucide-react'; @@ -59,7 +60,8 @@ export const DavAppPasswords = () => { useEffect(load, [load]); - const create = async () => { + const create = async (ev?: FormEvent) => { + ev?.preventDefault(); const trimmed = label.trim(); if (!trimmed || isCreating) return; setIsCreating(true); @@ -142,22 +144,21 @@ export const DavAppPasswords = () => { )} -
+
- -
+ {passwords.length === 0 ? (
diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/GoogleOAuthConfig.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/GoogleOAuthConfig.tsx index 96452d52..f6beaa96 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/GoogleOAuthConfig.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/IntegrationsSettings/GoogleOAuthConfig.tsx @@ -1,3 +1,4 @@ +import type { FormEvent } from 'react'; import { useState, useEffect } from 'react'; import { toast } from 'sonner'; import { ChevronDown } from 'lucide-react'; @@ -266,7 +267,8 @@ export const GoogleOAuthConfig = () => { if (!isLoading && clientId && clientSecret) verify(); }, [isLoading]); - const handleSave = async () => { + const handleSave = async (ev?: FormEvent) => { + ev?.preventDefault(); if (isSaving) return; setIsSaving(true); try { @@ -290,7 +292,7 @@ export const GoogleOAuthConfig = () => { -
+
-
+
); }; diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx index d155b151..586bec8b 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/AIHarnessesSection.tsx @@ -226,6 +226,23 @@ export const AIHarnessesSection = () => { } }; + const submitLocalUrl = (ev?: React.FormEvent) => { + ev?.preventDefault(); + if (probeState.step === 'url' && localUrl.trim()) handleProbe(); + }; + + const submitAuth = (ev?: React.FormEvent) => { + ev?.preventDefault(); + if (probeState.step !== 'auth') return; + const ready = probeState.probe.authType === 'basic' ? authUsername && authPassword : authApiKey; + if (ready) handleAuthSubmit(probeState.probe); + }; + + const submitApiKey = (ev: React.FormEvent, providerId: string) => { + ev.preventDefault(); + if (keyInputs[providerId]) saveApiKey(providerId); + }; + const removeLocalProvider = async (id: string) => { await client.delete(`/server-settings/chat-providers/local-providers/${id}`); queryClient.invalidateQueries({ queryKey: ['CHAT_PROVIDERS_LOCAL_PROVIDERS'] }); @@ -291,57 +308,60 @@ export const AIHarnessesSection = () => { {addingLocal ? (
{/* Step 1: Name + URL inputs stacked */} - setLocalName(ev.target.value)} - onKeyDown={(ev) => { - if (ev.key === 'Escape') resetLocalForm(); - }} - disabled={probeState.step !== 'url'} - autoFocus - /> - setLocalUrl(ev.target.value)} - onKeyDown={(ev) => { - if (ev.key === 'Escape') resetLocalForm(); - if (ev.key === 'Enter' && localUrl.trim() && probeState.step === 'url') handleProbe(); - }} - disabled={probeState.step !== 'url'} - /> -
- {probeState.step === 'url' && ( - + )} + {(probeState.step === 'probing' || probeState.step === 'saving') && ( + + )} + - )} - {(probeState.step === 'probing' || probeState.step === 'saving') && ( - - )} - -
+ Cancel + +
+ {/* Step 2: Auth form (if needed) */} {probeState.step === 'auth' && ( -
+
{probeState.probe.name} requires authentication @@ -361,17 +381,12 @@ export const AIHarnessesSection = () => { placeholder="Password" value={authPassword} onChange={(ev) => setAuthPassword(ev.target.value)} - onKeyDown={(ev) => { - if (ev.key === 'Enter' && authUsername && authPassword) - handleAuthSubmit(probeState.probe); - }} /> @@ -384,23 +399,19 @@ export const AIHarnessesSection = () => { placeholder="API Key" value={authApiKey} onChange={(ev) => setAuthApiKey(ev.target.value)} - onKeyDown={(ev) => { - if (ev.key === 'Enter' && authApiKey) handleAuthSubmit(probeState.probe); - }} autoFocus /> )} -
+ )} ) : ( @@ -460,7 +471,10 @@ export const AIHarnessesSection = () => { {editingProvider === provider.key && ( -
+
submitApiKey(ev, provider.providerId)} + className="flex items-center gap-2 pl-4" + > { setKeyInputs((prev) => ({ ...prev, [provider.providerId]: ev.target.value })) } onKeyDown={(ev) => { - if (ev.key === 'Enter' && keyInputs[provider.providerId]) saveApiKey(provider.providerId); if (ev.key === 'Escape') setEditingProvider(null); }} autoFocus /> -
+ )} ))} @@ -501,7 +513,11 @@ export const AIHarnessesSection = () => { if (!provider || connectedProviders.some((cp) => cp.providerId === provider.providerId)) return null; return ( -
+
submitApiKey(ev, provider.providerId)} + className="flex items-center gap-2" + > @@ -514,15 +530,13 @@ export const AIHarnessesSection = () => { setKeyInputs((prev) => ({ ...prev, [provider.providerId]: ev.target.value })) } onKeyDown={(ev) => { - if (ev.key === 'Enter' && keyInputs[provider.providerId]) saveApiKey(provider.providerId); if (ev.key === 'Escape') setEditingProvider(null); }} autoFocus /> -
+ ); })()} {unconnectedProviders.length > 0 && ( diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/OCRSection.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/OCRSection.tsx index aa0360ed..440f61a6 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/OCRSection.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/OCRSection.tsx @@ -1,3 +1,4 @@ +import type { FormEvent } from 'react'; import { useState, useEffect } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; @@ -51,7 +52,8 @@ export const OCRSection = () => { fetchModels(config.url); }, [config]); - const handleSave = async () => { + const handleSave = async (ev?: FormEvent) => { + ev?.preventDefault(); if (isSaving) return; setIsSaving(true); try { @@ -92,7 +94,7 @@ export const OCRSection = () => { if (isLoading) return

Loading...

; return ( -
+
+ ); }; diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/SMTPSection.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/SMTPSection.tsx index 97648d43..881b8cdd 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/SMTPSection.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/SMTPSection.tsx @@ -1,3 +1,4 @@ +import type { FormEvent } from 'react'; import { useState, useEffect } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; @@ -84,7 +85,8 @@ export const SMTPSection = () => { ...(provider === 'resend' ? { apiKey } : { host, port: parseInt(port) || 587, username, password, secure }), }); - const handleSave = async () => { + const handleSave = async (ev?: FormEvent) => { + ev?.preventDefault(); if (isSaving) return; setIsSaving(true); try { @@ -118,7 +120,8 @@ export const SMTPSection = () => { } }; - const handleTest = async () => { + const handleTest = async (ev?: FormEvent) => { + ev?.preventDefault(); if (isTesting || !testEmail) return; setIsTesting(true); try { @@ -151,150 +154,147 @@ export const SMTPSection = () => { return (
- - - {provider === 'resend' && ( +
- )} - {showSmtpFields && ( - <> -
- - -
- {provider === 'smtp' && ( - <> -
- - -
-
); diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/STTSection.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/STTSection.tsx index 68ef5a60..1333ad49 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/STTSection.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/STTSection.tsx @@ -1,3 +1,4 @@ +import type { FormEvent } from 'react'; import { useState, useEffect } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; @@ -28,7 +29,8 @@ export const STTSection = () => { setUrl(config.url); }, [config]); - const handleSave = async () => { + const handleSave = async (ev?: FormEvent) => { + ev?.preventDefault(); if (isSaving) return; setIsSaving(true); try { @@ -69,7 +71,7 @@ export const STTSection = () => { if (isLoading) return

Loading...

; return ( -
+
+ ); }; diff --git a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/TTSSection.tsx b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/TTSSection.tsx index 3e5f9f07..39d0514e 100644 --- a/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/TTSSection.tsx +++ b/src/apps/officer-web/Screens/Dashboard/Settings/ServerSettings/TTSSection.tsx @@ -1,3 +1,4 @@ +import type { FormEvent } from 'react'; import { useState, useEffect, useRef } from 'react'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; @@ -102,7 +103,8 @@ export const TTSSection = () => { ...(apiKey ? { apiKey } : {}), }); - const handleSave = async () => { + const handleSave = async (ev?: FormEvent) => { + ev?.preventDefault(); if (isSaving) return; setIsSaving(true); try { @@ -152,7 +154,7 @@ export const TTSSection = () => { if (isLoading) return

Loading...

; return ( -
+
+ ) : ( 0 : body.trim().length > 0; - const submit = () => { + const submit = (ev?: FormEvent) => { + ev?.preventDefault(); if (!canSubmit) return; const spec: BackgroundJobSpec = { mode, @@ -123,7 +125,7 @@ export const BackgroundJobDialog = ({ -
+
-
- - - - + + + + + ); diff --git a/src/workspaces/officerdev/src/apps/Chat/components/QuestionActivity.tsx b/src/workspaces/officerdev/src/apps/Chat/components/QuestionActivity.tsx index 38a57a45..f17def64 100644 --- a/src/workspaces/officerdev/src/apps/Chat/components/QuestionActivity.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/components/QuestionActivity.tsx @@ -163,31 +163,29 @@ export const QuestionActivity = ({ message, onAnswer }: QuestionActivityProps) = {/* "Other" — provided by the client on purpose. Both schemas say the model must NOT offer one as an option, precisely because the escape hatch is the UI's job. */} {!done && ( -
+
{ + ev.preventDefault(); + const text = (otherText[qi] ?? '').trim(); + if (text) record(qi, text); + }} + > setOtherText((prev) => ({ ...prev, [qi]: ev.target.value }))} - onKeyDown={(ev) => { - if (ev.key !== 'Enter') return; - ev.preventDefault(); - const text = (otherText[qi] ?? '').trim(); - if (text) record(qi, text); - }} placeholder="Other..." className="flex-1 px-3 py-1.5 rounded-lg border border-border text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:border-duck-teal/40" /> -
+ )} {multi && !done && ( diff --git a/src/workspaces/officerdev/src/apps/Chat/components/WebpageDialog.tsx b/src/workspaces/officerdev/src/apps/Chat/components/WebpageDialog.tsx index ce2bbfc7..9eccfd11 100644 --- a/src/workspaces/officerdev/src/apps/Chat/components/WebpageDialog.tsx +++ b/src/workspaces/officerdev/src/apps/Chat/components/WebpageDialog.tsx @@ -1,3 +1,4 @@ +import type { FormEvent } from 'react'; import { useState } from 'react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; @@ -11,7 +12,8 @@ type WebpageDialogProps = { export function WebpageDialog({ open, onOpenChange, onSubmit }: WebpageDialogProps) { const [urlInput, setUrlInput] = useState(''); - const handleSubmit = () => { + const handleSubmit = (ev?: FormEvent) => { + ev?.preventDefault(); const url = urlInput.trim(); if (!url) return; onSubmit(url); @@ -26,29 +28,23 @@ export function WebpageDialog({ open, onOpenChange, onSubmit }: WebpageDialogPro Attach Webpage Enter a URL to scrape and attach as context. -
+
setUrlInput(ev.target.value)} - onKeyDown={(ev) => { - if (ev.key === 'Enter') { - ev.preventDefault(); - handleSubmit(); - } - }} placeholder="https://example.com" className="flex-1 rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-duck-teal/30 focus:border-duck-teal/50" autoFocus /> -
+ ); diff --git a/src/workspaces/officerdev/src/apps/ChatHistory/DirPickerModal.tsx b/src/workspaces/officerdev/src/apps/ChatHistory/DirPickerModal.tsx index 01b92e60..18004c52 100644 --- a/src/workspaces/officerdev/src/apps/ChatHistory/DirPickerModal.tsx +++ b/src/workspaces/officerdev/src/apps/ChatHistory/DirPickerModal.tsx @@ -40,7 +40,8 @@ export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps) const goto = (idx: number) => setPath(idx < 0 ? '/' : `/${segments.slice(0, idx + 1).join('/')}`); const enter = (name: string) => setPath(path === '/' ? `/${name}` : `${path}/${name}`); - const createFolder = async () => { + const createFolder = async (ev?: React.FormEvent) => { + ev?.preventDefault(); const n = newName.trim(); if (!n) return; await api.createDir(path === '/' ? `/${n}` : `${path}/${n}`); @@ -104,25 +105,24 @@ export const DirPickerModal = ({ open, onClose, onSelect }: DirPickerModalProps) {/* Footer */}
{creating ? ( -
+
setNewName(ev.target.value)} onKeyDown={(ev) => { - if (ev.key === 'Enter') createFolder(); if (ev.key === 'Escape') setCreating(false); }} placeholder="New folder name" className="min-w-0 flex-1 rounded border border-border bg-transparent px-2 py-1 text-xs outline-none" /> -
+ ) : (
-
+ ); } diff --git a/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqCategories.tsx b/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqCategories.tsx index 93285ded..7e98d3e5 100644 --- a/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqCategories.tsx +++ b/src/workspaces/officerdev/src/apps/CommandHQ/CommandHqCategories.tsx @@ -38,7 +38,8 @@ export const CommandHqCategories = () => { const [adding, setAdding] = useState(false); const [name, setName] = useState(''); - const submitNew = async () => { + const submitNew = async (ev?: React.FormEvent) => { + ev?.preventDefault(); const trimmed = name.trim(); if (!trimmed) { setAdding(false); @@ -92,21 +93,23 @@ export const CommandHqCategories = () => { /> ))} {adding && ( - setName(ev.target.value)} - onBlur={submitNew} - onKeyDown={(ev) => { - if (ev.key === 'Enter') submitNew(); - if (ev.key === 'Escape') { - setName(''); - setAdding(false); - } - }} - placeholder="Category name" - className="mx-1 rounded-lg border border-border bg-background px-2 py-1.5 text-sm outline-none focus:border-primary" - /> + // No submit button — a form with exactly one text field still submits on Enter. +
+ setName(ev.target.value)} + onBlur={() => void submitNew()} + onKeyDown={(ev) => { + if (ev.key === 'Escape') { + setName(''); + setAdding(false); + } + }} + placeholder="Category name" + className="mx-1 rounded-lg border border-border bg-background px-2 py-1.5 text-sm outline-none focus:border-primary" + /> +
)} {!adding && categories.length === 0 && (
No categories yet
diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileItem.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileItem.tsx index 80572b80..d51e5195 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileItem.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileItem.tsx @@ -503,22 +503,31 @@ const InlineRenameInput = ({ }; return ( - setValue(ev.target.value)} - onBlur={() => { - // Ignore blur before the input is ready (context menu closing steals focus) - if (!ready.current) return; + // A real
so Enter commits the rename — this was a bare input relying on a manual keydown + // check. `contents` keeps the extra element invisible to the surrounding flex/grid layout. + { + ev.preventDefault(); commit(); }} - onKeyDown={(ev) => { - if (ev.key === 'Enter') commit(); - if (ev.key === 'Escape') onCancel(); - }} - onClick={(ev) => ev.stopPropagation()} - className="text-sm font-medium text-duck-dark bg-background border border-duck-teal/50 rounded px-1 py-0.5 outline-none max-w-full text-left" - /> + className="contents" + > + setValue(ev.target.value)} + onBlur={() => { + // Ignore blur before the input is ready (context menu closing steals focus) + if (!ready.current) return; + commit(); + }} + onKeyDown={(ev) => { + if (ev.key === 'Escape') onCancel(); + }} + onClick={(ev) => ev.stopPropagation()} + className="text-sm font-medium text-duck-dark bg-background border border-duck-teal/50 rounded px-1 py-0.5 outline-none max-w-full text-left" + /> +
); }; diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/NewEntryDialog.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/NewEntryDialog.tsx index f6b0b14f..973307bd 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/NewEntryDialog.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/NewEntryDialog.tsx @@ -104,7 +104,8 @@ export const NewEntryDialog = ({ kind, onClose, onSubmit, existingNames }: NewEn const problem = kind === 'clone' ? urlProblem(name) : nameProblem(name, existingNames); const canSubmit = name.trim().length > 0 && problem === null; - const commit = () => { + const commit = (ev?: React.FormEvent) => { + ev?.preventDefault(); if (!canSubmit) return; onSubmit(name.trim()); onClose(); @@ -113,42 +114,41 @@ export const NewEntryDialog = ({ kind, onClose, onSubmit, existingNames }: NewEn return ( !open && onClose()}> - - - - {title} - - {description} - + {/* A real
so Enter submits — this was a div with a manual keydown check for Enter, the + button was the only way in. `grid gap-4` replaces DialogContent's own gap now that the form + is the single child spanning header/fields/footer. */} + + + + + {title} + + {description} + -
- - setName(ev.target.value)} - onKeyDown={(ev) => { - if (ev.key === 'Enter') { - ev.preventDefault(); - commit(); - } - }} - /> - {/* Reserves its own line whether or not there is a problem, so the dialog does not jump as you - type past a colliding name. */} -

{problem ?? ''}

-
+
+ + setName(ev.target.value)} + /> + {/* Reserves its own line whether or not there is a problem, so the dialog does not jump as you + type past a colliding name. */} +

{problem ?? ''}

+
- - - - + + + + +
); diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx index 05c38c84..b0089d6e 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/TaskRunnerModal.tsx @@ -157,7 +157,8 @@ const AgenticTaskRunner = ({ wasGenerating.current = chat.isGenerating; }, [chat.isGenerating]); - const handleRun = () => { + const handleRun = (ev?: React.FormEvent) => { + ev?.preventDefault(); // Build prompt from task body + input values, or fall back to generic prompt let prompt = defaultInput; if (taskBody && inputDefs) { @@ -182,24 +183,29 @@ const AgenticTaskRunner = ({ if (phase === 'ready') { return ( <> - {hasConfigurableInputs && ( - - )} -
- -
+ {/* A real
so Enter runs the task — this was a div with no submit path. `contents` keeps + the wrapper invisible to the flex layout; the ModelSelector stays a sibling, outside the + form, since it renders its own untyped buttons that must not become submit buttons. */} + + {hasConfigurableInputs && ( + + )} +
+ +
+
+ {/* type="button": this form's toggles must never submit it, since they now live inside a + real
(Agentic/Pipeline ready phases). */}
-
+ ); } diff --git a/src/workspaces/officerdev/src/apps/Preview/PreviewHeader.tsx b/src/workspaces/officerdev/src/apps/Preview/PreviewHeader.tsx index 82f4958a..79a51faf 100644 --- a/src/workspaces/officerdev/src/apps/Preview/PreviewHeader.tsx +++ b/src/workspaces/officerdev/src/apps/Preview/PreviewHeader.tsx @@ -26,7 +26,8 @@ export const PreviewHeader = ({ panelId }: { panelId?: string }) => { if (!editing) setDraft(display); }, [display, editing]); - const commit = () => { + const commit = (ev?: React.FormEvent) => { + ev?.preventDefault(); const next = normalizeUrl(draft); if (next) setUrl(next); setEditing(false); @@ -34,25 +35,29 @@ export const PreviewHeader = ({ panelId }: { panelId?: string }) => { if (editing) { return ( - <> +
setDraft(ev.target.value)} onKeyDown={(ev) => { - if (ev.key === 'Enter') commit(); if (ev.key === 'Escape') setEditing(false); }} className="min-w-0 flex-1 rounded border border-duck-dark/20 bg-transparent px-1.5 py-0.5 text-xs" /> - - - + ); } diff --git a/src/workspaces/widgets/DailyGoals/index.tsx b/src/workspaces/widgets/DailyGoals/index.tsx index 747b96dd..65dec99f 100644 --- a/src/workspaces/widgets/DailyGoals/index.tsx +++ b/src/workspaces/widgets/DailyGoals/index.tsx @@ -85,7 +85,8 @@ export const DailyGoals = () => { const [newGoal, setNewGoal] = useState(''); const inputRef = useRef(null); - const addGoal = () => { + const addGoal = (ev?: React.FormEvent) => { + ev?.preventDefault(); const text = newGoal.trim(); if (!text) return; nextGoalId++; @@ -131,13 +132,22 @@ export const DailyGoals = () => { @@ -162,29 +172,22 @@ export const DailyGoals = () => { )} -
+
setNewGoal(ev.target.value)} - onKeyDown={(ev) => { - if (ev.key === 'Enter') { - ev.preventDefault(); - addGoal(); - } - }} className="flex-1 rounded-md border border-border bg-background px-2 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" /> -
+ ); diff --git a/src/workspaces/widgets/Pomodoro/index.tsx b/src/workspaces/widgets/Pomodoro/index.tsx index 0966c9e8..812aa857 100644 --- a/src/workspaces/widgets/Pomodoro/index.tsx +++ b/src/workspaces/widgets/Pomodoro/index.tsx @@ -79,14 +79,14 @@ function usePomodoroState() { type Phase = 'focus' | 'break' | 'long-break'; const phaseLabel: Record = { - 'focus': 'Focus', - 'break': 'Break', + focus: 'Focus', + break: 'Break', 'long-break': 'Long Break', }; const phaseColor: Record = { - 'focus': 'text-destructive', - 'break': 'text-success', + focus: 'text-destructive', + break: 'text-success', 'long-break': 'text-accent', }; @@ -116,7 +116,8 @@ const SoundPicker = ({ label, value, onChange }: SoundPickerProps) => { const audioRef = useRef(null); const filtered = useMemo(() => { - const source = activeCategory === 'all' ? allSounds : (categories.find((c) => c.id === activeCategory)?.sounds ?? []); + const source = + activeCategory === 'all' ? allSounds : (categories.find((c) => c.id === activeCategory)?.sounds ?? []); if (!search) return source; const q = search.toLowerCase(); return source.filter((s) => s.name.toLowerCase().includes(q)); @@ -165,7 +166,10 @@ const SoundPicker = ({ label, value, onChange }: SoundPickerProps) => { @@ -175,6 +179,11 @@ const SoundPicker = ({ label, value, onChange }: SoundPickerProps) => { placeholder="Search sounds..." value={search} onChange={(ev) => setSearch(ev.target.value)} + // This is a filter, not a submit — but it now lives inside SettingsPanel's
, so without + // this it would submit the settings on Enter instead of just narrowing the list. + onKeyDown={(ev) => { + if (ev.key === 'Enter') ev.preventDefault(); + }} className="w-full rounded border border-border bg-background px-2 py-1 text-xs outline-none focus:ring-1 focus:ring-ring" />
@@ -199,23 +208,31 @@ const SoundPicker = ({ label, value, onChange }: SoundPickerProps) => { className={`flex items-center gap-1.5 rounded px-2 py-1 text-left text-xs cursor-pointer transition-colors ${ value === null ? 'bg-primary/10 text-primary' : 'hover:bg-accent' }`} - onClick={() => { stopPreview(); onChange(null); }} + onClick={() => { + stopPreview(); + onChange(null); + }} > None (silent) {filtered.map((s) => ( -
+
@@ -229,15 +246,23 @@ const SoundPicker = ({ label, value, onChange }: SoundPickerProps) => { const SettingsPanel = ({ settings, onSave }: { settings: PomodoroSettings; onSave: (s: PomodoroSettings) => void }) => { const [draft, setDraft] = useState(settings); - const numericFields: { key: 'focusMinutes' | 'breakMinutes' | 'longBreakMinutes' | 'sessionsBeforeLong'; label: string }[] = [ + const numericFields: { + key: 'focusMinutes' | 'breakMinutes' | 'longBreakMinutes' | 'sessionsBeforeLong'; + label: string; + }[] = [ { key: 'focusMinutes', label: 'Focus (min)' }, { key: 'breakMinutes', label: 'Break (min)' }, { key: 'longBreakMinutes', label: 'Long break (min)' }, { key: 'sessionsBeforeLong', label: 'Sessions before long' }, ]; + const save = (ev: React.FormEvent) => { + ev.preventDefault(); + onSave(draft); + }; + return ( -
+ {numericFields.map(({ key, label }) => (
{label} @@ -262,14 +287,13 @@ const SettingsPanel = ({ settings, onSave }: { settings: PomodoroSettings; onSav onChange={(name) => setDraft((d) => ({ ...d, breakEndSound: name }))} /> -
+ ); }; @@ -363,7 +387,15 @@ export const Pomodoro = () => {
- + {
- {completedToday} session{completedToday !== 1 ? 's' : ''} today + + {completedToday} session{completedToday !== 1 ? 's' : ''} today +