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,
|
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 >
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user