feat: add enhanced MULTIX UI features

Added all missing MULTIX enhancements matching the original screenshot:

1. STREAMING indicator:
   - Animated purple badge with sparkles icon
   - Shows live token count during streaming
   - Pulsing animation effect

2. Status badges:
   - PENDING/RUNNING/DONE badges for tasks
   - Color-coded based on status

3. APEX/SHIELD renamed:
   - 'Auto' → 'APEX' with tooltip
   - 'Shield' → 'SHIELD' with tooltip

4. THINKING indicator:
   - Bouncing dots animation (3 dots)
   - Shows THINKING or SENDING status

5. STOP button:
   - Red stop button appears during agent work
   - Calls cancel endpoint to interrupt

6. Detailed token stats bar:
   - INPUT/OUTPUT tokens
   - REASONING tokens (amber)
   - CACHE READ (emerald)
   - CACHE WRITE (cyan)
   - COST (violet)
   - MODEL (indigo)

7. Message navigation sidebar:
   - YOU/ASST labels for each message
   - Click to scroll to message
   - Appears on right side when viewing task
This commit is contained in:
Gemini AI
2025-12-23 13:42:49 +04:00
Unverified
parent 9c6d92efcd
commit 00bee04867

View File

@@ -27,6 +27,12 @@ import {
Layers, Layers,
Shield, Shield,
Activity, Activity,
Square,
Clock,
Sparkles,
StopCircle,
Bot,
User,
} from "lucide-solid"; } from "lucide-solid";
import type { InstanceMessageStore } from "@/stores/message-v2/instance-store"; import type { InstanceMessageStore } from "@/stores/message-v2/instance-store";
import type { Task } from "@/types/session"; import type { Task } from "@/types/session";
@@ -105,9 +111,21 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
return { return {
used: usage?.actualUsageTokens ?? 0, used: usage?.actualUsageTokens ?? 0,
total: usage?.totalCost ?? 0, total: usage?.totalCost ?? 0,
input: usage?.inputTokens ?? 0,
output: usage?.outputTokens ?? 0,
reasoning: usage?.reasoningTokens ?? 0,
cacheRead: usage?.cacheReadTokens ?? 0,
cacheWrite: usage?.cacheWriteTokens ?? 0,
cost: usage?.totalCost ?? 0,
}; };
}); });
// Get current model from instance
const currentModel = createMemo(() => {
const instance = instances().get(props.instanceId);
return instance?.modelId || "unknown";
});
const activeTaskSessionId = createMemo(() => { const activeTaskSessionId = createMemo(() => {
const task = selectedTask(); const task = selectedTask();
return task?.taskSessionId || props.sessionId; return task?.taskSessionId || props.sessionId;
@@ -242,6 +260,25 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
} }
}; };
// Stop/cancel the current agent operation
const handleStopAgent = async () => {
const task = selectedTask();
if (!task) return;
log.info("Stopping agent for task:", task.id);
// Send interrupt signal via the session API
try {
const targetSessionId = task.taskSessionId || props.sessionId;
// Use the cancel endpoint or interrupt mechanism
await fetch(`/api/workspaces/${props.instanceId}/sessions/${targetSessionId}/cancel`, {
method: "POST",
});
log.info("Agent stopped successfully");
} catch (error) {
log.error("Failed to stop agent:", error);
}
};
return ( return (
<main class="h-full flex flex-col bg-[#0a0a0b] text-zinc-300 font-sans selection:bg-indigo-500/30"> <main class="h-full flex flex-col bg-[#0a0a0b] text-zinc-300 font-sans selection:bg-indigo-500/30">
{/* Header */} {/* Header */}
@@ -274,6 +311,27 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
</div> </div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
{/* STREAMING indicator */}
<Show when={isAgentThinking()}>
<div class="flex items-center space-x-2 px-3 py-1.5 bg-violet-500/15 border border-violet-500/30 rounded-lg animate-pulse shadow-[0_0_20px_rgba(139,92,246,0.2)]">
<Sparkles size={12} class="text-violet-400 animate-spin" style={{ "animation-duration": "3s" }} />
<span class="text-[10px] font-black text-violet-400 uppercase tracking-tight">Streaming</span>
<span class="text-[10px] font-bold text-violet-300">{formatTokenTotal(tokenStats().used)}</span>
</div>
</Show>
{/* Task status badge */}
<Show when={selectedTask()}>
<div class={`px-2 py-1 rounded text-[9px] font-black uppercase tracking-tight border ${selectedTask()?.status === "completed"
? "bg-emerald-500/15 border-emerald-500/30 text-emerald-400"
: selectedTask()?.status === "in-progress"
? "bg-indigo-500/15 border-indigo-500/30 text-indigo-400"
: "bg-amber-500/15 border-amber-500/30 text-amber-400"
}`}>
{selectedTask()?.status === "completed" ? "DONE" : selectedTask()?.status === "in-progress" ? "RUNNING" : "PENDING"}
</div>
</Show>
<button class="p-2 text-zinc-500 hover:text-white transition-all hover:bg-white/5 rounded-xl active:scale-90"> <button class="p-2 text-zinc-500 hover:text-white transition-all hover:bg-white/5 rounded-xl active:scale-90">
<Command size={18} strokeWidth={2} /> <Command size={18} strokeWidth={2} />
</button> </button>
@@ -336,7 +394,9 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
</Show> </Show>
{/* Main Content Area - min-h-0 is critical for flex containers with overflow */} {/* Main Content Area - min-h-0 is critical for flex containers with overflow */}
<div class="flex-1 min-h-0 relative overflow-hidden flex flex-col"> <div class="flex-1 min-h-0 relative overflow-hidden flex">
{/* Main chat area */}
<div class="flex-1 min-h-0 flex flex-col overflow-hidden">
<div <div
ref={scrollContainer} ref={scrollContainer}
class="flex-1 min-h-0 overflow-y-auto overflow-x-hidden custom-scrollbar" class="flex-1 min-h-0 overflow-y-auto overflow-x-hidden custom-scrollbar"
@@ -440,8 +500,9 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
? "bg-indigo-500/20 border-indigo-500/40 text-indigo-400" ? "bg-indigo-500/20 border-indigo-500/40 text-indigo-400"
: "bg-white/5 border-white/10 text-zinc-500" : "bg-white/5 border-white/10 text-zinc-500"
}`} }`}
title="APEX - Autonomous Programming EXecution mode"
> >
Auto APEX
</button> </button>
<button <button
onClick={() => toggleAutoApproval(props.instanceId)} onClick={() => toggleAutoApproval(props.instanceId)}
@@ -449,8 +510,9 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
? "bg-emerald-500/20 border-emerald-500/40 text-emerald-400" ? "bg-emerald-500/20 border-emerald-500/40 text-emerald-400"
: "bg-white/5 border-white/10 text-zinc-500" : "bg-white/5 border-white/10 text-zinc-500"
}`} }`}
title="SHIELD - Auto-approval mode"
> >
Shield SHIELD
</button> </button>
<Show when={tokenStats().used > 0}> <Show when={tokenStats().used > 0}>
<div class="px-2 py-0.5 bg-emerald-500/10 rounded border border-emerald-500/20 text-[9px] font-bold text-emerald-400"> <div class="px-2 py-0.5 bg-emerald-500/10 rounded border border-emerald-500/20 text-[9px] font-bold text-emerald-400">
@@ -459,9 +521,24 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
</Show> </Show>
<Show when={isSending() || isAgentThinking()}> <Show when={isSending() || isAgentThinking()}>
<div class="flex items-center space-x-1 px-2 py-0.5 bg-indigo-500/10 rounded border border-indigo-500/20"> <div class="flex items-center space-x-1 px-2 py-0.5 bg-indigo-500/10 rounded border border-indigo-500/20">
<Loader2 size={10} class="text-indigo-400 animate-spin" /> <div class="flex space-x-0.5">
<span class="text-[9px] font-bold text-indigo-400">{isAgentThinking() ? "Thinking" : "Sending"}</span> <div class="w-1 h-1 bg-indigo-400 rounded-full animate-bounce" style={{ "animation-delay": "0ms" }} />
<div class="w-1 h-1 bg-indigo-400 rounded-full animate-bounce" style={{ "animation-delay": "150ms" }} />
<div class="w-1 h-1 bg-indigo-400 rounded-full animate-bounce" style={{ "animation-delay": "300ms" }} />
</div> </div>
<span class="text-[9px] font-bold text-indigo-400">{isAgentThinking() ? "THINKING" : "SENDING"}</span>
</div>
</Show>
{/* STOP button */}
<Show when={isAgentThinking()}>
<button
onClick={handleStopAgent}
class="flex items-center space-x-1 px-2 py-0.5 bg-rose-500/20 hover:bg-rose-500/30 rounded border border-rose-500/40 text-[9px] font-bold text-rose-400 transition-all"
title="Stop agent"
>
<StopCircle size={10} />
<span>STOP</span>
</button>
</Show> </Show>
</div> </div>
</div> </div>
@@ -479,7 +556,51 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
{/* Footer Row */} {/* Footer Row */}
<div class="flex items-center justify-between pt-2 border-t border-white/5 mt-2"> <div class="flex items-center justify-between pt-2 border-t border-white/5 mt-2">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-2 flex-wrap gap-y-1">
{/* Detailed token stats */}
<Show when={tokenStats().input > 0 || tokenStats().output > 0}>
<div class="flex items-center space-x-1.5">
<span class="text-[8px] font-bold text-zinc-600 uppercase">INPUT</span>
<span class="text-[9px] font-bold text-zinc-400">{tokenStats().input.toLocaleString()}</span>
</div>
<div class="w-px h-3 bg-zinc-800" />
<div class="flex items-center space-x-1.5">
<span class="text-[8px] font-bold text-zinc-600 uppercase">OUTPUT</span>
<span class="text-[9px] font-bold text-zinc-400">{tokenStats().output.toLocaleString()}</span>
</div>
<Show when={tokenStats().reasoning > 0}>
<div class="w-px h-3 bg-zinc-800" />
<div class="flex items-center space-x-1.5">
<span class="text-[8px] font-bold text-zinc-600 uppercase">REASONING</span>
<span class="text-[9px] font-bold text-amber-400">{tokenStats().reasoning.toLocaleString()}</span>
</div>
</Show>
<Show when={tokenStats().cacheRead > 0}>
<div class="w-px h-3 bg-zinc-800" />
<div class="flex items-center space-x-1.5">
<span class="text-[8px] font-bold text-zinc-600 uppercase">CACHE READ</span>
<span class="text-[9px] font-bold text-emerald-400">{tokenStats().cacheRead.toLocaleString()}</span>
</div>
</Show>
<Show when={tokenStats().cacheWrite > 0}>
<div class="w-px h-3 bg-zinc-800" />
<div class="flex items-center space-x-1.5">
<span class="text-[8px] font-bold text-zinc-600 uppercase">CACHE WRITE</span>
<span class="text-[9px] font-bold text-cyan-400">{tokenStats().cacheWrite.toLocaleString()}</span>
</div>
</Show>
<div class="w-px h-3 bg-zinc-800" />
<div class="flex items-center space-x-1.5">
<span class="text-[8px] font-bold text-zinc-600 uppercase">COST</span>
<span class="text-[9px] font-bold text-violet-400">${tokenStats().cost.toFixed(4)}</span>
</div>
<div class="w-px h-3 bg-zinc-800" />
<div class="flex items-center space-x-1.5">
<span class="text-[8px] font-bold text-zinc-600 uppercase">MODEL</span>
<span class="text-[9px] font-bold text-indigo-400">{currentModel()}</span>
</div>
</Show>
<Show when={!(tokenStats().input > 0 || tokenStats().output > 0)}>
<button class="text-zinc-600 hover:text-zinc-400 transition-colors p-1"> <button class="text-zinc-600 hover:text-zinc-400 transition-colors p-1">
<Hash size={14} /> <Hash size={14} />
</button> </button>
@@ -491,6 +612,7 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
<kbd class="px-1.5 py-0.5 bg-zinc-800 rounded text-[9px] font-bold border border-white/5">ENTER</kbd> <kbd class="px-1.5 py-0.5 bg-zinc-800 rounded text-[9px] font-bold border border-white/5">ENTER</kbd>
<span class="text-[9px]">to send</span> <span class="text-[9px]">to send</span>
</div> </div>
</Show>
</div> </div>
<button <button
@@ -511,6 +633,36 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
</div> </div>
</div> </div>
</div> </div>
</div>
{/* Message Navigation Sidebar - YOU/ASST labels */}
<Show when={selectedTaskId() && filteredMessageIds().length > 0}>
<div class="w-12 shrink-0 bg-zinc-900/40 border-l border-white/5 overflow-y-auto py-2 px-1 flex flex-col items-center gap-1">
<For each={filteredMessageIds()}>
{(messageId, index) => {
const msg = () => messageStore().getMessage(messageId);
const isUser = () => msg()?.role === "user";
return (
<button
onClick={() => {
// Scroll to message
const element = document.getElementById(`msg-${messageId}`);
element?.scrollIntoView({ behavior: "smooth", block: "center" });
}}
class={`w-9 py-1 rounded text-[8px] font-black uppercase transition-all ${isUser()
? "bg-indigo-500/20 border border-indigo-500/40 text-indigo-400 hover:bg-indigo-500/30"
: "bg-emerald-500/20 border border-emerald-500/40 text-emerald-400 hover:bg-emerald-500/30"
}`}
title={`${isUser() ? "User" : "Assistant"} message ${index() + 1}`}
>
{isUser() ? "YOU" : "ASST"}
</button>
);
}}
</For>
</div>
</Show>
</div>
</main > </main >
); );
} }