Merge branch 'feat/2.6.0-beta4' of github.com:dataelement/bisheng into feat/2.6.0-beta4

This commit is contained in:
GuoQing Zhang
2026-06-18 15:31:35 +08:00
12 changed files with 201 additions and 82 deletions
@@ -330,7 +330,8 @@ export default function AiChatMessages({
hideShare={hideShare}
conversation={{ title: headerTitleText, flowId: "", conversationId, flowType: 15 }}
onOpenWorkspace={onOpenWorkspace}
hasWorkspaceFiles={hasWorkspaceFiles && !workspaceOpen}
hasWorkspaceFiles={hasWorkspaceFiles}
workspaceOpen={workspaceOpen}
/>
)}
<div
@@ -364,6 +364,33 @@ const ChatView = ({ id = '', index = 0, shareToken = '' }: { id?: string, index?
// auto-expand (the entry icon appearing is enough).
const { getLinsight, updateLinsight } = useLinsightManager();
const taskArtifacts = useWorkspacePanel();
// F035: enter/exit animation for the fullscreen workspace overlay. The overlay
// is a separate instance from the docked panel (PreviewBody isn't cached, so we
// never mount both at once). `fsMounted` keeps it in the DOM across the collapse
// transition; `fsExpanded` drives the scale/opacity. Entering: mount collapsed,
// then expand on the next frame so the transition runs. Exiting: collapse, then
// unmount on transitionEnd.
const [fsMounted, setFsMounted] = useState(false);
const [fsExpanded, setFsExpanded] = useState(false);
useEffect(() => {
if (taskArtifacts.fullscreen) {
setFsMounted(true);
// Double rAF: let the collapsed state paint before flipping to expanded,
// otherwise React can batch both and the enter transition is skipped.
let r2 = 0;
const r1 = requestAnimationFrame(() => {
r2 = requestAnimationFrame(() => setFsExpanded(true));
});
return () => {
cancelAnimationFrame(r1);
cancelAnimationFrame(r2);
};
}
setFsExpanded(false);
return undefined;
}, [taskArtifacts.fullscreen]);
const taskLinsight = latestTaskVersionId ? getLinsight(latestTaskVersionId) : null;
const taskWorkspaceFiles = useMemo(() => {
const uploaded = toUploadedArtifacts(taskLinsight?.files as any[]);
@@ -454,7 +481,7 @@ const ChatView = ({ id = '', index = 0, shareToken = '' }: { id?: string, index?
isStreaming={isStreaming}
shareToken={shareToken}
knowledgeChatLayout
contentWidthClassName="w-full max-w-[800px] mx-auto px-4 touch-mobile:max-w-full"
contentWidthClassName="w-full max-w-[800px] mx-auto px-3 touch-mobile:max-w-full"
onRegenerate={regenerate}
onOpenCitationPanel={onOpenCitationPanel}
activeCitationMessageId={activeCitationMessageId}
@@ -531,19 +558,36 @@ const ChatView = ({ id = '', index = 0, shareToken = '' }: { id?: string, index?
{/* F035: inline workspace panel docked to the right of the chat
main for the latest task turn. Opens from the header entry
button; the file preview renders in place. Fullscreen is a
separate overlay (below) covering the whole route viewport. */}
{latestTaskVersionId && taskArtifacts.open && !taskArtifacts.fullscreen && (
<div className="min-h-0 shrink-0 p-1 w-[46%] min-w-[440px] max-w-[720px]">
<WorkspacePanel
files={taskWorkspaceFiles}
versionId={latestTaskVersionId}
previewFile={taskArtifacts.previewFile}
fullscreen={false}
onPreview={taskArtifacts.openPreview}
onBack={taskArtifacts.backToList}
onClose={taskArtifacts.closeWorkspace}
onToggleFullscreen={taskArtifacts.toggleFullscreen}
/>
separate overlay (below) covering the whole route viewport.
Kept mounted whenever a task turn exists (gated only on
!fullscreen) so open/close animates the wrapper's width +
opacity instead of hard mount/unmount. Width uses an inline
clamp() (not min/max-w classes) so it interpolates smoothly
0px → 46% — min-width doesn't transition and would otherwise
snap to 440px on the first frame. The inner box keeps a
min-width so the panel content slides/clips rather than
reflowing while it collapses. */}
{latestTaskVersionId && !taskArtifacts.fullscreen && (
<div
className={cn(
'min-h-0 shrink-0 overflow-hidden transition-[width,opacity,padding] duration-300 ease-[cubic-bezier(0.32,0.72,0,1)]',
taskArtifacts.open ? 'p-1 opacity-100' : 'pointer-events-none p-0 opacity-0',
)}
style={{ width: taskArtifacts.open ? 'clamp(440px, 46%, 720px)' : '0px' }}
>
<div className="h-full min-w-[420px]">
<WorkspacePanel
files={taskWorkspaceFiles}
versionId={latestTaskVersionId}
previewFile={taskArtifacts.previewFile}
fullscreen={false}
onPreview={taskArtifacts.openPreview}
onBack={taskArtifacts.backToList}
onClose={taskArtifacts.closeWorkspace}
onToggleFullscreen={taskArtifacts.toggleFullscreen}
/>
</div>
</div>
)}
@@ -628,9 +672,25 @@ const ChatView = ({ id = '', index = 0, shareToken = '' }: { id?: string, index?
viewport (chatContainerRef is relative + un-clipped), flush to the
edges with no padding. Covers the chat (incl. its header) but not the
browser, so the global nav stays. Separate instance from the inline
panel above (toggling fullscreen remounts the preview). */}
{latestTaskVersionId && taskArtifacts.open && taskArtifacts.fullscreen && (
<div className="absolute inset-0 z-50 bg-white">
panel above (only one is ever mounted, so the preview never double
fetches). Scale + opacity transition from the right (where the docked
panel sits) so expanding/collapsing fullscreen animates smoothly
instead of hard mount/unmount; unmounts on transitionEnd after the
collapse so the exit plays too. */}
{latestTaskVersionId && fsMounted && (
<div
className={cn(
'absolute inset-0 z-50 origin-right bg-white transition-[transform,opacity] duration-300 ease-[cubic-bezier(0.32,0.72,0,1)]',
fsExpanded ? 'scale-100 opacity-100' : 'scale-[0.96] opacity-0',
)}
onTransitionEnd={(e) => {
// Unmount only after the collapse finishes (ignore the expand end
// and bubbled child transitions).
if (e.target === e.currentTarget && e.propertyName === 'transform' && !taskArtifacts.fullscreen) {
setFsMounted(false);
}
}}
>
<WorkspacePanel
files={taskWorkspaceFiles}
versionId={latestTaskVersionId}
@@ -1,6 +1,7 @@
import { PanelRight } from 'lucide-react';
import { useLocation } from 'react-router-dom';
import { useLocalize, useMediaQuery, usePrefersMobileLayout } from '~/hooks';
import { cn } from '~/utils';
import ShareChat from '../Share/ShareChat';
const types = {
@@ -10,12 +11,15 @@ const types = {
15: 'workbench_chat'
} as const;
export default function HeaderTitle({ conversation, readOnly, hideShare = false, onOpenWorkspace, hasWorkspaceFiles = false }: {
export default function HeaderTitle({ conversation, readOnly, hideShare = false, onOpenWorkspace, hasWorkspaceFiles = false, workspaceOpen = false }: {
conversation?: any;
readOnly?: boolean;
hideShare?: boolean;
onOpenWorkspace?: () => void;
hasWorkspaceFiles?: boolean;
/** Workspace panel open state — drives the entry button's fade so it stays
in sync with the panel's open/close animation instead of hard-toggling. */
workspaceOpen?: boolean;
}) {
const localize = useLocalize();
const { pathname } = useLocation();
@@ -32,7 +36,11 @@ export default function HeaderTitle({ conversation, readOnly, hideShare = false,
}
return (
<div className="sticky top-0 z-10 flex h-[56px] w-full items-center justify-between bg-white px-4 text-[#212121]">
<div
className={cn(
'sticky top-0 z-10 flex h-[56px] w-full items-center justify-between bg-white pl-4 pr-4 text-[#212121]',
)}
>
{/* Left placeholder to balance the center layout */}
<div className="flex-1"></div>
@@ -53,16 +61,26 @@ export default function HeaderTitle({ conversation, readOnly, hideShare = false,
/>
)}
{/* F035: task-mode workspace entry — opens the drawer of uploaded sources +
generated deliverables for the latest task turn (ChatView owns the drawer). */}
generated deliverables for the latest task turn (ChatView owns the drawer).
Kept mounted and faded by `workspaceOpen` (not mount/unmounted) so it
tracks the panel's open/close animation. On open it fades out instantly
(the panel covers it); on close it waits `delay-150` so it reappears only
after the panel has mostly collapsed — avoids the button flashing in
mid-animation. */}
{hasWorkspaceFiles && onOpenWorkspace && (
<button
type="button"
onClick={onOpenWorkspace}
title={localize('com_linsight_workspace')}
aria-label="workspace"
className="flex h-7 w-7 items-center justify-center rounded-lg text-gray-600 hover:bg-gray-100"
className={cn(
'flex h-7 shrink-0 items-center justify-center overflow-hidden rounded-lg text-gray-600 transition-[width,opacity] duration-200 hover:bg-gray-100',
// Collapse width to 0 (not just opacity) when open so it reserves no
// slot in the header's right cell; expand back with a short delay on close.
workspaceOpen ? 'pointer-events-none w-0 opacity-0' : 'w-7 opacity-100 delay-150',
)}
>
<PanelRight size={16} />
<PanelRight size={16} className="shrink-0" />
</button>
)}
</div>
@@ -43,7 +43,7 @@ export function ResultSection({ answer, files, versionId, onPreview }: ResultSec
{/* answer summary, markdown rendered */}
{answer && (
<div className="bs-mkdown rounded-2xl border border-gray-100 bg-white p-4 text-sm leading-6 text-gray-800">
<div className="bs-mkdown rounded-2xl border border-gray-100 bg-white p-4 text-sm leading-6 text-gray-800 [&_p:last-child]:mb-0">
<Markdown content={answer} isLatestMessage={true} webContent={false} />
</div>
)}
@@ -28,7 +28,7 @@ interface WorkspacePanelProps {
}
const iconBtn =
'flex h-7 w-7 items-center justify-center rounded-md text-[#8C8C8C] transition-colors hover:bg-gray-100 hover:text-[#335CFF]';
'flex h-7 w-7 items-center justify-center rounded-lg text-[#8C8C8C] transition-colors hover:bg-gray-100';
export function WorkspacePanel({
files,
@@ -47,7 +47,7 @@ export function WorkspacePanel({
key={file.file_id || file.file_url}
role="button"
tabIndex={0}
className="group flex cursor-pointer items-center gap-2.5 rounded-lg py-2 hover:bg-[#F7F7F7]"
className="group flex cursor-pointer items-center gap-2.5 rounded-lg px-1 py-2 hover:bg-[#F7F7F7]"
onClick={() => onPreview(file)}
onKeyDown={(e) => e.key === 'Enter' && onPreview(file)}
>
@@ -81,7 +81,7 @@ export function WorkspacePanel({
{previewFile ? (
<>
{/* preview toolbar */}
<div className="flex h-[52px] shrink-0 items-center gap-2 px-3">
<div className="flex h-12 shrink-0 items-center gap-2 px-3">
<button type="button" aria-label={localize('com_linsight_back_to_workspace')} className={iconBtn} onClick={onBack}>
<Outlined.ArrowLeft className="size-4" />
</button>
@@ -115,18 +115,18 @@ export function WorkspacePanel({
) : (
<>
{/* list header */}
<div className="flex h-[52px] shrink-0 items-center justify-between px-4">
<div className="flex h-12 shrink-0 items-center justify-between px-4">
<span className="text-sm font-medium text-[#212121]">{localize('com_linsight_workspace')}</span>
<button type="button" aria-label={localize('com_ui_close')} className={iconBtn} onClick={onClose}>
<Outlined.Close className="size-4" />
</button>
</div>
{/* list body — overall padding 16px, 16px between rows */}
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-os p-4">
{/* list body — 4px top, 16px bottom, 12px left/right; 16px between rows */}
<div className="min-h-0 flex-1 overflow-y-auto scrollbar-os px-3 pt-1 pb-4">
{files.length ? (
<div className="flex flex-col gap-4">{files.map(renderRow)}</div>
) : (
<div className="py-10 text-center text-sm text-gray-400">
<div className="flex h-full items-center justify-center text-center text-sm font-normal text-gray-400">
{localize('com_linsight_workspace_empty')}
</div>
)}
@@ -131,7 +131,7 @@ export function ClarifyCard({ data, disabled = false, onSubmit }: ClarifyCardPro
return (
<div
className="my-3 w-full rounded-2xl border border-[#EEF2F6] bg-white p-6 shadow-[0_4px_20px_rgba(0,0,0,0.03)]"
className="my-3 w-full rounded-2xl border border-[#EEF2F6] bg-white p-4 shadow-[0_4px_20px_rgba(0,0,0,0.03)]"
style={{
backgroundImage: 'url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'5\' height=\'5\'%3E%3Ccircle cx=\'0.5\' cy=\'0.5\' r=\'0.5\' fill=\'%23EAEEFF\'/%3E%3C/svg%3E")',
backgroundSize: '5px 5px',
@@ -145,7 +145,7 @@ export function ClarifyCard({ data, disabled = false, onSubmit }: ClarifyCardPro
<button
type="button"
onClick={handleClose}
className="shrink-0 rounded-full p-1 text-[#8C8C8C] hover:bg-gray-100 transition-colors"
className="shrink-0 rounded-md p-1 text-[#8C8C8C] hover:bg-gray-100 transition-colors"
aria-label="close"
>
<X size={16} />
@@ -154,7 +154,7 @@ export function ClarifyCard({ data, disabled = false, onSubmit }: ClarifyCardPro
{/* Body: Current question */}
{q ? (
<div className="mt-4">
<div className="mt-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-[14px] font-bold text-[#1A1A1A]">{q.question}</span>
@@ -174,7 +174,7 @@ export function ClarifyCard({ data, disabled = false, onSubmit }: ClarifyCardPro
type="button"
disabled={page === 0}
onClick={() => setPage(page - 1)}
className="rounded-full p-1 hover:bg-gray-100/80 disabled:opacity-30 transition-colors"
className="rounded-md p-1 hover:bg-gray-100/80 disabled:opacity-30 transition-colors"
>
<ChevronLeft size={16} />
</button>
@@ -185,14 +185,14 @@ export function ClarifyCard({ data, disabled = false, onSubmit }: ClarifyCardPro
type="button"
disabled={page === questions.length - 1}
onClick={() => setPage(page + 1)}
className="rounded-full p-1 hover:bg-gray-100/80 disabled:opacity-30 transition-colors"
className="rounded-md p-1 hover:bg-gray-100/80 disabled:opacity-30 transition-colors"
>
<ChevronRight size={16} />
</button>
</div>
)}
</div>
<ul className="mt-4 space-y-3">
<ul className="mt-4 space-y-2">
{q.options.map((option, i) => {
const active = selected.includes(option);
const { title: optTitle, desc: optDesc } = parseOption(option);
@@ -203,9 +203,9 @@ export function ClarifyCard({ data, disabled = false, onSubmit }: ClarifyCardPro
disabled={disabled || submitted}
onClick={() => handleSelect(q, option)}
className={cn(
'flex w-full items-start gap-2 rounded-xl px-4 py-2.5 text-left text-sm transition-all duration-200 select-none border-0',
'flex h-9 w-full items-center gap-2 rounded-lg px-4 text-left text-sm transition-all duration-200 select-none border-0',
active
? 'bg-[#EDF2FF] text-[#335CFF] font-medium shadow-[0_2px_8px_rgba(51,92,255,0.08)]'
? 'bg-[#EEE] text-[#212121] font-medium'
: 'text-[#1A1A1A] hover:bg-gray-50/80',
)}
>
@@ -213,12 +213,12 @@ export function ClarifyCard({ data, disabled = false, onSubmit }: ClarifyCardPro
<div className="flex-1 min-w-0">
<span className={cn(active ? 'text-[#335CFF]' : 'text-[#1A1A1A]')}>{optTitle}</span>
{optDesc && (
<span className={cn('ml-1 text-[13px] font-normal', active ? 'text-[#335CFF]/80' : 'text-[#8C8C8C]')}>
<span className="ml-1 text-[13px] font-normal text-[#8C8C8C]">
{optDesc}
</span>
)}
</div>
{active && <Check size={16} className="shrink-0 text-[#335CFF] self-center" />}
{active && <Check size={16} className="shrink-0 text-[#212121] self-center" />}
</button>
</li>
);
@@ -226,7 +226,12 @@ export function ClarifyCard({ data, disabled = false, onSubmit }: ClarifyCardPro
{/* Trailing "type your own" entry: inline input */}
<li>
<div
className="flex items-center gap-2 rounded-xl bg-[#F5F7FA] px-4 py-2.5 transition-all duration-200"
className={cn(
'flex h-9 items-center gap-2 rounded-lg px-4 transition-all duration-200',
// No box by default (matches the other options); the
// input-box background only appears once it's active.
customSelected ? 'bg-[#EEE]' : 'hover:bg-gray-50/80',
)}
>
<span className="shrink-0 text-sm font-medium text-[#8C8C8C]">
{q.options.length + 1}.
@@ -227,14 +227,8 @@ export function ExecutionFlow({ versionId, conversationId, isSharePage = false,
</div>
</div>
{/* ── footer: waiting hint + task panel + unified input ─────────── */}
{/* ── footer: task panel + unified input ────────────────────────── */}
<div className="mx-auto w-full max-w-[800px] shrink-0 px-4 pb-4">
{pendingInput && (
<div className="mb-2 flex items-center gap-2 rounded-lg px-3 py-1.5 text-xs text-gray-800">
<span className="size-1.5 animate-pulse-scale rounded-full bg-gray-700" />
{localize('com_linsight_waiting_your_input')}
</div>
)}
{/* Design (Figma 12221-40080/40081): card inset 24px each side
relative to the input, 12px gap above it. */}
<div className="px-6 pb-3">
@@ -14,16 +14,16 @@ export function RunningSpinner() {
}
/** Map a step/tool name to its leading icon (completed state). */
export function stepTypeIcon(name?: string) {
export function stepTypeIcon(name?: string, size = 16) {
const n = (name || '').toLowerCase();
const cls = 'text-[#333]';
if (/agent|subagent/.test(n)) return <Outlined.PeopleRound size={16} className={cls} />;
if (/knowledge|knowledge_base|space|retrieval|recall|检索|知识/.test(n)) return <Outlined.BookOpenText size={16} className={cls} />;
if (/think|reason|思考/.test(n)) return <Outlined.Bulb size={16} className={cls} />;
if (/research|调研/.test(n)) return <Outlined.Dashboard size={16} className={cls} />;
if (/web[_\s-]?search|websearch|联网|网页|网络搜索/.test(n)) return <Outlined.Earth size={16} className={cls} />;
if (/write|edit|撰写|编写|写入/.test(n)) return <Outlined.Write size={16} className={cls} />;
return <Wrench size={16} className={cls} />; // default
if (/agent|subagent/.test(n)) return <Outlined.PeopleRound size={size} className={cls} />;
if (/knowledge|knowledge_base|space|retrieval|recall|检索|知识/.test(n)) return <Outlined.BookOpenText size={size} className={cls} />;
if (/think|reason|思考/.test(n)) return <Outlined.Bulb size={size} className={cls} />;
if (/research|调研/.test(n)) return <Outlined.Dashboard size={size} className={cls} />;
if (/web[_\s-]?search|websearch|联网|网页|网络搜索/.test(n)) return <Outlined.Earth size={size} className={cls} />;
if (/write|edit|撰写|编写|写入/.test(n)) return <Outlined.Write size={size} className={cls} />;
return <Wrench size={size} className={cls} />; // default
}
/** Shared style for expanded detail text blocks. */
@@ -5,11 +5,28 @@
* called inside that agent's namespace; a finished card shows "called N tools".
*/
import { Outlined } from 'bisheng-icons';
import { Check, Recycle } from 'lucide-react';
import { Check } from 'lucide-react';
import type { CSSProperties } from 'react';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';
import { StepRow } from './StepRow';
import type { MergedStep, SubagentGroup } from './stepUtils';
import { StepRow, stepTypeIcon } from './StepRow';
import type { SubagentGroup } from './stepUtils';
/** Dotted base texture, reused verbatim from the clarification card
* (ClarifyCard): a 5px-tiled SVG with a faint #EAEEFF dot. */
const DOT_BG: CSSProperties = {
backgroundColor: '#FFFFFF',
backgroundImage:
'url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'5\' height=\'5\'%3E%3Ccircle cx=\'0.5\' cy=\'0.5\' r=\'0.5\' fill=\'%23EAEEFF\'/%3E%3C/svg%3E")',
backgroundSize: '5px 5px',
};
/** Diagonal glint overlaid on the dotted base (Figma 12221:40064). A soft grey
* streak on its own layer; the `animate-sheen-sweep` keyframe slides it across
* the card (clipped by the card's overflow-hidden) so the gradient flows. A
* plain white streak is invisible on the near-white card, so it carries a light
* grey cast to read as motion. */
const SHEEN =
'linear-gradient(120deg, transparent 0%, rgba(140,140,140,0.025) 28%, rgba(140,140,140,0.09) 50%, rgba(140,140,140,0.025) 72%, transparent 100%)';
function SubagentCard({ agent }: { agent: SubagentGroup['agents'][number] }) {
const localize = useLocalize();
@@ -18,16 +35,41 @@ function SubagentCard({ agent }: { agent: SubagentGroup['agents'][number] }) {
const calledCount = children.length;
const currentTool = [...children].reverse().find((c) => c.running) || children[children.length - 1];
// the dotted texture stays in both states; the sweeping sheen is the
// "in-progress" treatment only and stops once the agent finishes.
const running = step.running;
return (
<div className="min-w-40 max-w-56 rounded-xl border border-gray-200 bg-white p-2.5 shadow-sm">
<div className="flex items-center gap-1.5 text-xs font-medium text-gray-700">
<Recycle size={12} className="shrink-0 text-blue-500" />
<span className="truncate">{step.name}</span>
</div>
<div className="mt-1.5 flex items-center gap-1.5 text-xs text-gray-500">
<div
className="relative flex min-w-40 max-w-56 flex-col items-start gap-1 overflow-hidden rounded-lg border-[0.5px] border-[#ECECEC] px-4 py-2 shadow-[0px_4px_6px_0px_rgba(167,186,224,0.05)]"
style={DOT_BG}
>
{/* diagonal glint sweeping above the dots; semi-transparent so the
dot texture still reads through. The static mask fades the streak
near the card's L/R edges, so the overflow-hidden clip lands in an
already-transparent zone instead of cutting the band into a hard
vertical line. Only while the agent is running. */}
{running && (
<div
aria-hidden
className="pointer-events-none absolute inset-0"
style={{
WebkitMaskImage: 'linear-gradient(to right, transparent 0%, #000 18%, #000 82%, transparent 100%)',
maskImage: 'linear-gradient(to right, transparent 0%, #000 18%, #000 82%, transparent 100%)',
}}
>
<div className="absolute inset-0 animate-sheen-sweep" style={{ backgroundImage: SHEEN }} />
</div>
)}
{/* circular white badge carrying the agent's type icon */}
<span className="relative flex items-center rounded-full bg-white p-[5px]">
{stepTypeIcon(step.name, 14)}
</span>
<span className="relative max-w-full truncate text-xs leading-5 text-[#1D2129]">{step.name}</span>
<div className="relative flex max-w-full items-center gap-2 text-xs leading-5 text-[#999]">
{step.running ? (
<>
<span className="size-1.5 shrink-0 animate-pulse rounded-full bg-blue-500" />
<span className="size-2 shrink-0 animate-pulse rounded-full bg-[#212121]" />
<span className="truncate">{currentTool?.name || localize('com_linsight_subagent_running')}</span>
</>
) : (
@@ -204,13 +204,6 @@ export function TaskTurnPanel({ versionId, conversationId, answer, readOnly = fa
</div>
)}
{/* waiting-for-input hint */}
{pendingInput && (
<div className="mb-2 mt-2 flex items-center gap-2 rounded-lg px-3 py-1.5 text-xs text-gray-800">
<span className="size-1.5 animate-pulse-scale rounded-full bg-gray-700" />
{localize('com_linsight_waiting_your_input')}
</div>
)}
{/* task checklist progress is rendered by <PinnedTaskPanel> pinned
above the input (ChatView) — not inline in the message stream. */}
@@ -221,8 +214,8 @@ export function TaskTurnPanel({ versionId, conversationId, answer, readOnly = fa
synthesizes get_final_result_file / the report), so the user does
not mistake an in-progress task for a finished one. */}
{running && !queueing && !planning && !pendingInput && (
<div className="mb-2 mt-2 flex items-center gap-2 rounded-lg px-3 py-1.5 text-xs text-gray-800">
<span className="size-1.5 animate-pulse-scale rounded-full bg-gray-700" />
<div className="mb-2 mt-2 flex items-center gap-2 rounded-lg py-1.5 text-xs text-gray-800">
<span className="inline-block size-3 animate-pulse-scale rounded-full bg-black" />
{localize('com_linsight_generating')}
</div>
)}
@@ -200,9 +200,6 @@ export function useFileManager({ activeSpace, initialFolderId, enabled = true }:
setTotal(files.length + (hasMore ? 1 : 0));
}, [files.length, hasMore]);
// Track which initialFolderId has been consumed (value, not boolean)
// so re-navigation to a different folder deep link works correctly.
const consumedFolderIdRef = useRef<string | undefined>(undefined);
// Bumped on space switch to guarantee the filter effect fires even when
// search state was already empty (no dep change otherwise).
const [reloadToken, setReloadToken] = useState(0);
@@ -215,9 +212,12 @@ export function useFileManager({ activeSpace, initialFolderId, enabled = true }:
setSearchTagIds([]);
setStatusFilter([]);
// If there's an unconsumed initial folder from URL, navigate there
if (initialFolderId && consumedFolderIdRef.current !== initialFolderId) {
consumedFolderIdRef.current = initialFolderId;
// The URL folder id is the single source of truth — sync the content
// pane to it on every change. A previous "consumed" guard skipped
// re-entering an already-visited folder (its id was still marked
// consumed), which left the URL on /folder/<id> while the pane reset
// to the space root — folders became un-enterable from the sidebar tree.
if (initialFolderId) {
setCurrentFolderId(initialFolderId);
// Fetch the breadcrumb path for the URL folder. The API now
// includes the folder itself as the leaf (with its real name),
+6
View File
@@ -40,10 +40,16 @@ module.exports = {
'0%': { transform: 'translateX(-100%)' },
'100%': { transform: 'translateX(400%)' },
},
// diagonal glint sweeping continuously across the subagent card
'sheen-sweep': {
'0%': { transform: 'translateX(-130%)' },
'100%': { transform: 'translateX(130%)' },
},
},
animation: {
'fade-in': 'fadeIn 0.5s ease-out forwards',
'crawl-slide': 'crawl-slide 1.4s linear infinite',
'sheen-sweep': 'sheen-sweep 2s linear infinite',
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out',
'pulse-scale': 'pulse-scale 1s ease-in-out infinite',