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:
@@ -27,6 +27,12 @@ import {
|
||||
Layers,
|
||||
Shield,
|
||||
Activity,
|
||||
Square,
|
||||
Clock,
|
||||
Sparkles,
|
||||
StopCircle,
|
||||
Bot,
|
||||
User,
|
||||
} from "lucide-solid";
|
||||
import type { InstanceMessageStore } from "@/stores/message-v2/instance-store";
|
||||
import type { Task } from "@/types/session";
|
||||
@@ -105,9 +111,21 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
|
||||
return {
|
||||
used: usage?.actualUsageTokens ?? 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 task = selectedTask();
|
||||
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 (
|
||||
<main class="h-full flex flex-col bg-[#0a0a0b] text-zinc-300 font-sans selection:bg-indigo-500/30">
|
||||
{/* Header */}
|
||||
@@ -274,6 +311,27 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<Command size={18} strokeWidth={2} />
|
||||
</button>
|
||||
@@ -336,7 +394,9 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
|
||||
</Show>
|
||||
|
||||
{/* 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
|
||||
ref={scrollContainer}
|
||||
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-white/5 border-white/10 text-zinc-500"
|
||||
}`}
|
||||
title="APEX - Autonomous Programming EXecution mode"
|
||||
>
|
||||
Auto
|
||||
APEX
|
||||
</button>
|
||||
<button
|
||||
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-white/5 border-white/10 text-zinc-500"
|
||||
}`}
|
||||
title="SHIELD - Auto-approval mode"
|
||||
>
|
||||
Shield
|
||||
SHIELD
|
||||
</button>
|
||||
<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">
|
||||
@@ -459,9 +521,24 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
|
||||
</Show>
|
||||
<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">
|
||||
<Loader2 size={10} class="text-indigo-400 animate-spin" />
|
||||
<span class="text-[9px] font-bold text-indigo-400">{isAgentThinking() ? "Thinking" : "Sending"}</span>
|
||||
<div class="flex space-x-0.5">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -479,7 +556,51 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
|
||||
|
||||
{/* Footer Row */}
|
||||
<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">
|
||||
<Hash size={14} />
|
||||
</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>
|
||||
<span class="text-[9px]">to send</span>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -511,6 +633,36 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
|
||||
</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 >
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user