feat(web): prominent agent launcher (FAB, Ctrl/Cmd+J, sidebar entry, quick goals)

- Bottom-right FAB (56px, prism ring) with active-run count, pulse and latest
  status summary; turns into "collapse" beside the open drawer; hidden on the
  Agent page and while the mini dock shows; lifts above bottom floating bars
- Ctrl/Cmd+J toggles the drawer, Esc closes; input is focused on open
- Emphasized Agent entry at the top of the sidebar (expanded and collapsed)
  with active-run count; header button kept only below xl
- Drawer first screen offers 3 page-aware quick goals that fill the input
- Respects reduced motion; AgentShortcutsHelp exported for the settings page

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-28 02:10:28 +08:00
1 parent 16ab2a6c84
commit 39a001c099
7 files changed
+433 -8

No files matched your search

@@ -1,7 +1,7 @@
import { useQueryClient } from "@tanstack/react-query";
import { EventType } from "@umt/contracts";
import { FlaskConical, Play, RefreshCw, WifiOff } from "lucide-react";
import { useState } from "react";
import { FlaskConical, Play, RefreshCw, Sparkles, WifiOff } from "lucide-react";
import { useRef, useState } from "react";
import { toast } from "sonner";
import {
type AgentMode,
@@ -78,6 +78,7 @@ export function GoalComposer({
prefix,
defaultGoal = "",
compact = false,
suggestions,
}: {
onSubmit: (goal: string, mode: AgentMode) => void;
pending: boolean;
@@ -85,11 +86,23 @@ export function GoalComposer({
prefix?: string;
defaultGoal?: string;
compact?: boolean;
/** 快捷目标:点一下填入输入框(不直接提交),光标停在末尾 */
suggestions?: string[];
}) {
const [goal, setGoal] = useState(defaultGoal);
const [mode, setMode] = useState<AgentMode>("execute");
const box = useRef<HTMLDivElement>(null);
const fill = (text: string) => {
setGoal(text);
requestAnimationFrame(() => {
const ta = box.current?.querySelector("textarea");
if (!ta) return;
ta.focus();
ta.setSelectionRange(text.length, text.length);
});
};
return (
<div className="space-y-1.5">
<div ref={box} className="space-y-1.5">
<PromptInput
value={goal}
onValueChange={setGoal}
@@ -116,6 +129,27 @@ export function GoalComposer({
<p className="px-1 text-meta text-muted-foreground" aria-live="polite">
{MODE_HELP[mode].help} Enter 发送,Shift+Enter 换行。
</p>
{suggestions?.length ? (
<div className="pt-2">
<p className="mb-1.5 flex items-center gap-1.5 px-1 text-meta text-muted-foreground">
<Sparkles aria-hidden className="size-3.5" />
试试这些
</p>
<ul aria-label="快捷目标" className="flex flex-col gap-1.5">
{suggestions.map((s) => (
<li key={s}>
<button
type="button"
onClick={() => fill(s)}
className="w-full cursor-pointer rounded-xl border border-border/70 px-3 py-2 text-left text-caption text-foreground/90 outline-none transition-colors duration-micro hover:bg-foreground/[0.04] hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
>
{s.endsWith(" ") || s.endsWith(":") ? `${s}…` : s}
</button>
</li>
))}
</ul>
</div>
) : null}
</div>
);
}