feat(webui): polish sidebar and session transitions (#5393)
This commit is contained in:
@@ -83,7 +83,6 @@ import { useClipboardAndDrop } from "@/hooks/useClipboardAndDrop";
|
||||
import { useLogoFallback } from "@/hooks/useLogoFallback";
|
||||
import { useMediaQuery } from "@/hooks/useMediaQuery";
|
||||
import type { SendAttachment, SendOptions } from "@/hooks/useNanobotStream";
|
||||
import { usePageVisibility } from "@/hooks/usePageVisibility";
|
||||
import { useVoiceRecorder, type VoiceRecorderErrorKey } from "@/hooks/useVoiceRecorder";
|
||||
import type {
|
||||
CliAppInfo,
|
||||
@@ -207,8 +206,6 @@ interface ThreadComposerProps {
|
||||
onStop?: () => void;
|
||||
surfaceRef?: Ref<HTMLDivElement>;
|
||||
onTranscribeAudio?: (dataUrl: string, options?: { durationMs?: number }) => Promise<string>;
|
||||
/** Unix seconds from server; turn elapsed timer above input while set. */
|
||||
runStartedAt?: number | null;
|
||||
/** Sustained objective for this chat (WebSocket ``goal_state``). */
|
||||
goalState?: GoalStateWsPayload;
|
||||
workspaceScope?: WorkspaceScopePayload | null;
|
||||
@@ -695,63 +692,38 @@ function mcpPresetMentionPayload(preset: McpPresetInfo): OutboundMcpPresetMentio
|
||||
};
|
||||
}
|
||||
|
||||
function RunPulseIcon() {
|
||||
return (
|
||||
<span className="run-pulse-icon relative flex h-4 w-4 shrink-0 items-center justify-center" aria-hidden>
|
||||
<span className="run-pulse-icon__ring" />
|
||||
<span className="run-pulse-icon__dot" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function RunElapsedStrip({
|
||||
startedAt,
|
||||
function GoalStateStrip({
|
||||
goalState,
|
||||
}: {
|
||||
startedAt: number | null;
|
||||
goalState?: GoalStateWsPayload;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const pageVisible = usePageVisibility();
|
||||
const [goalPanelOpen, setGoalPanelOpen] = useState(false);
|
||||
const showTimer = startedAt != null;
|
||||
const stripLabel = goalStateStripPreview(goalState, t);
|
||||
const showGoal = !!stripLabel?.trim();
|
||||
const active = showTimer || showGoal;
|
||||
const active = !!stripLabel?.trim();
|
||||
const [, setTick] = useState(0);
|
||||
const stripWrapperRef = useRef<HTMLDivElement>(null);
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const expandToggleRef = useRef<HTMLButtonElement>(null);
|
||||
const stripSnapshotRef = useRef<{
|
||||
startedAt: number | null;
|
||||
goalState?: GoalStateWsPayload;
|
||||
stripLabel: string | null;
|
||||
} | null>(null);
|
||||
const [panelMaxPx, setPanelMaxPx] = useState(280);
|
||||
|
||||
if (active) {
|
||||
stripSnapshotRef.current = { startedAt, goalState, stripLabel };
|
||||
stripSnapshotRef.current = { goalState, stripLabel };
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!active) setGoalPanelOpen(false);
|
||||
}, [active]);
|
||||
|
||||
useEffect(() => {
|
||||
if (startedAt == null || !pageVisible) return;
|
||||
setTick((n) => n + 1);
|
||||
const id = window.setInterval(() => setTick((n) => n + 1), 1000);
|
||||
return () => window.clearInterval(id);
|
||||
}, [pageVisible, startedAt]);
|
||||
|
||||
const display = active
|
||||
? { startedAt, goalState, stripLabel }
|
||||
? { goalState, stripLabel }
|
||||
: stripSnapshotRef.current;
|
||||
const displayStartedAt = display?.startedAt ?? null;
|
||||
const displayGoalState = display?.goalState;
|
||||
const displayStripLabel = display?.stripLabel ?? null;
|
||||
const displayShowTimer = displayStartedAt != null;
|
||||
const displayShowGoal = !!displayStripLabel?.trim();
|
||||
|
||||
const objectiveFull = displayGoalState?.objective?.trim() ?? "";
|
||||
const summaryFull = displayGoalState?.ui_summary?.trim() ?? "";
|
||||
@@ -819,17 +791,11 @@ function RunElapsedStrip({
|
||||
};
|
||||
}, [goalPanelOpen]);
|
||||
|
||||
const elapsed =
|
||||
displayStartedAt != null ? Math.max(0, Math.floor(Date.now() / 1000 - displayStartedAt)) : 0;
|
||||
const m = Math.floor(elapsed / 60);
|
||||
const sec = elapsed % 60;
|
||||
const shortElapsed = m > 0 ? `${m}:${sec.toString().padStart(2, "0")}` : `${sec}s`;
|
||||
const timerTitle = displayShowTimer
|
||||
? t("thread.composer.runRuntimeTitle", { elapsed: shortElapsed })
|
||||
: null;
|
||||
if (!display) return null;
|
||||
|
||||
const ariaParts = [timerTitle, displayShowGoal ? displayStripLabel : null].filter(Boolean);
|
||||
const ariaLabel = ariaParts.join(" · ");
|
||||
const ariaLabel = displayStripLabel
|
||||
? t("thread.composer.goalStateStrip", { label: displayStripLabel })
|
||||
: t("thread.composer.goalStateFallback");
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -838,6 +804,11 @@ function RunElapsedStrip({
|
||||
data-composer-status-drawer=""
|
||||
data-state={active ? "open" : "closed"}
|
||||
aria-hidden={active ? undefined : true}
|
||||
onTransitionEnd={(event) => {
|
||||
if (active || event.target !== event.currentTarget) return;
|
||||
stripSnapshotRef.current = null;
|
||||
setTick((n) => n + 1);
|
||||
}}
|
||||
>
|
||||
{goalPanelOpen && canExpandGoal && markdownBody ? (
|
||||
<div
|
||||
@@ -891,19 +862,9 @@ function RunElapsedStrip({
|
||||
role="status"
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{displayShowTimer ? (
|
||||
<RunPulseIcon />
|
||||
) : (
|
||||
<Target className="h-4 w-4 shrink-0 text-primary/75" aria-hidden />
|
||||
)}
|
||||
<Target className="h-4 w-4 shrink-0 text-primary/75" aria-hidden />
|
||||
<span className="flex min-w-0 flex-1 items-center gap-1.5 text-[12px] font-medium text-foreground/75">
|
||||
{timerTitle ? <span className="shrink-0">{timerTitle}</span> : null}
|
||||
{timerTitle && displayShowGoal ? (
|
||||
<span className="shrink-0 text-muted-foreground/45" aria-hidden>
|
||||
·
|
||||
</span>
|
||||
) : null}
|
||||
{displayShowGoal ? (
|
||||
{displayStripLabel ? (
|
||||
<span className="truncate">
|
||||
{t("thread.composer.goalStateStrip", { label: displayStripLabel })}
|
||||
</span>
|
||||
@@ -963,7 +924,6 @@ export function ThreadComposer({
|
||||
onStop,
|
||||
surfaceRef,
|
||||
onTranscribeAudio,
|
||||
runStartedAt = null,
|
||||
goalState,
|
||||
workspaceScope = null,
|
||||
workspaceControlsHidden = false,
|
||||
@@ -2370,7 +2330,7 @@ export function ThreadComposer({
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<RunElapsedStrip startedAt={runStartedAt} goalState={goalState} />
|
||||
<GoalStateStrip goalState={goalState} />
|
||||
<div className="relative">
|
||||
{hasMentionDecorations ? (
|
||||
<ComposerCliMentionOverlay
|
||||
|
||||
Reference in New Issue
Block a user