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;
@@ -153,7 +171,7 @@ export default function MultiTaskChat(props: MultiTaskChatProps) {
createEffect(() => { createEffect(() => {
const ids = filteredMessageIds(); const ids = filteredMessageIds();
const thinking = isAgentThinking(); const thinking = isAgentThinking();
// Scroll when message count changes or when thinking starts // Scroll when message count changes or when thinking starts
if (ids.length > 0 || thinking) { if (ids.length > 0 || thinking) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
@@ -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,181 +394,275 @@ 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">
<div {/* Main chat area */}
ref={scrollContainer} <div class="flex-1 min-h-0 flex flex-col overflow-hidden">
class="flex-1 min-h-0 overflow-y-auto overflow-x-hidden custom-scrollbar" <div
> ref={scrollContainer}
<Show when={!selectedTaskId()} fallback={ class="flex-1 min-h-0 overflow-y-auto overflow-x-hidden custom-scrollbar"
<div class="p-3 pb-4 overflow-x-hidden"> >
<MessageBlockList <Show when={!selectedTaskId()} fallback={
instanceId={props.instanceId} <div class="p-3 pb-4 overflow-x-hidden">
sessionId={activeTaskSessionId()} <MessageBlockList
store={messageStore} instanceId={props.instanceId}
messageIds={filteredMessageIds} sessionId={activeTaskSessionId()}
lastAssistantIndex={lastAssistantIndex} store={messageStore}
showThinking={() => true} messageIds={filteredMessageIds}
thinkingDefaultExpanded={() => true} lastAssistantIndex={lastAssistantIndex}
showUsageMetrics={() => true} showThinking={() => true}
scrollContainer={() => scrollContainer} thinkingDefaultExpanded={() => true}
setBottomSentinel={setBottomSentinel} showUsageMetrics={() => true}
/> scrollContainer={() => scrollContainer}
</div> setBottomSentinel={setBottomSentinel}
}> />
{/* Pipeline View */}
<div class="p-4 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div class="space-y-2">
<h2 class="text-2xl font-black text-white tracking-tight leading-none">Pipeline</h2>
<p class="text-xs font-medium text-zinc-500 uppercase tracking-[0.2em]">Agentic Orchestration</p>
</div> </div>
}>
<div class="space-y-4"> {/* Pipeline View */}
<div class="flex items-center justify-between"> <div class="p-4 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500">
<span class="text-[10px] font-bold text-zinc-600 uppercase tracking-widest">Active Threads</span> <div class="space-y-2">
<div class="h-px flex-1 bg-white/5 mx-4" /> <h2 class="text-2xl font-black text-white tracking-tight leading-none">Pipeline</h2>
<span class="text-[10px] font-black text-indigo-400 bg-indigo-500/10 px-2 py-0.5 rounded border border-indigo-500/20"> <p class="text-xs font-medium text-zinc-500 uppercase tracking-[0.2em]">Agentic Orchestration</p>
{tasks().length}
</span>
</div> </div>
<div class="grid gap-3"> <div class="space-y-4">
<For each={tasks()} fallback={ <div class="flex items-center justify-between">
<div class="group relative p-8 rounded-3xl border border-dashed border-white/5 bg-zinc-900/20 flex flex-col items-center justify-center text-center space-y-4 transition-all hover:bg-zinc-900/40 hover:border-white/10"> <span class="text-[10px] font-bold text-zinc-600 uppercase tracking-widest">Active Threads</span>
<div class="w-12 h-12 rounded-2xl bg-white/5 flex items-center justify-center text-zinc-600 group-hover:text-indigo-400 group-hover:scale-110 transition-all duration-500"> <div class="h-px flex-1 bg-white/5 mx-4" />
<Plus size={24} strokeWidth={1.5} /> <span class="text-[10px] font-black text-indigo-400 bg-indigo-500/10 px-2 py-0.5 rounded border border-indigo-500/20">
</div> {tasks().length}
<div class="space-y-1"> </span>
<p class="text-sm font-bold text-zinc-400">No active tasks</p> </div>
<p class="text-[11px] text-zinc-600">Send a message below to start a new thread</p>
</div> <div class="grid gap-3">
</div> <For each={tasks()} fallback={
}> <div class="group relative p-8 rounded-3xl border border-dashed border-white/5 bg-zinc-900/20 flex flex-col items-center justify-center text-center space-y-4 transition-all hover:bg-zinc-900/40 hover:border-white/10">
{(task) => ( <div class="w-12 h-12 rounded-2xl bg-white/5 flex items-center justify-center text-zinc-600 group-hover:text-indigo-400 group-hover:scale-110 transition-all duration-500">
<button <Plus size={24} strokeWidth={1.5} />
onClick={() => setSelectedTaskId(task.id)}
class="group relative p-4 rounded-2xl border border-white/5 bg-zinc-900/40 hover:bg-zinc-800/60 hover:border-indigo-500/30 transition-all duration-300 text-left flex items-start space-x-4 active:scale-[0.98]"
>
<div class={`mt-1 w-2 h-2 rounded-full shadow-[0_0_10px_rgba(var(--color),0.5)] ${task.status === "completed" ? "bg-emerald-500 shadow-emerald-500/40" :
task.status === "in-progress" ? "bg-indigo-500 shadow-indigo-500/40 animate-pulse" :
"bg-zinc-600 shadow-zinc-600/20"
}`} />
<div class="flex-1 min-w-0 space-y-1">
<p class="text-sm font-bold text-zinc-100 truncate group-hover:text-white transition-colors">
{task.title}
</p>
<div class="flex items-center space-x-3 text-[10px] font-bold text-zinc-500 uppercase tracking-tight">
<span>{new Date(task.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
<span class="w-1 h-1 rounded-full bg-zinc-800" />
<span>{task.messageIds?.length || 0} messages</span>
</div>
</div> </div>
<ChevronRight size={16} class="text-zinc-700 group-hover:text-indigo-400 group-hover:translate-x-1 transition-all" /> <div class="space-y-1">
</button> <p class="text-sm font-bold text-zinc-400">No active tasks</p>
)} <p class="text-[11px] text-zinc-600">Send a message below to start a new thread</p>
</For> </div>
</div> </div>
</div> }>
</div> {(task) => (
</Show> <button
</div> onClick={() => setSelectedTaskId(task.id)}
class="group relative p-4 rounded-2xl border border-white/5 bg-zinc-900/40 hover:bg-zinc-800/60 hover:border-indigo-500/30 transition-all duration-300 text-left flex items-start space-x-4 active:scale-[0.98]"
{/* Chat Input Area - Fixed at bottom */} >
<div class="p-3 bg-[#0a0a0b] border-t border-white/5 shrink-0"> <div class={`mt-1 w-2 h-2 rounded-full shadow-[0_0_10px_rgba(var(--color),0.5)] ${task.status === "completed" ? "bg-emerald-500 shadow-emerald-500/40" :
<div class="w-full bg-zinc-900/80 border border-white/10 rounded-2xl shadow-lg p-3"> task.status === "in-progress" ? "bg-indigo-500 shadow-indigo-500/40 animate-pulse" :
{/* Header Row */} "bg-zinc-600 shadow-zinc-600/20"
<div class="flex items-center justify-between mb-2"> }`} />
<div class="flex items-center space-x-2"> <div class="flex-1 min-w-0 space-y-1">
<div class="w-5 h-5 rounded-lg bg-gradient-to-br from-indigo-500 to-violet-600 flex items-center justify-center"> <p class="text-sm font-bold text-zinc-100 truncate group-hover:text-white transition-colors">
<AtSign size={10} class="text-white" strokeWidth={3} /> {task.title}
</div> </p>
<div class="flex flex-col"> <div class="flex items-center space-x-3 text-[10px] font-bold text-zinc-500 uppercase tracking-tight">
<span class="text-[10px] font-bold text-zinc-100 uppercase tracking-wide"> <span>{new Date(task.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
{selectedTaskId() ? "Task Context" : "Global Pipeline"} <span class="w-1 h-1 rounded-full bg-zinc-800" />
</span> <span>{task.messageIds?.length || 0} messages</span>
<span class="text-[9px] text-zinc-500 uppercase"> </div>
{selectedTaskId() ? "MultiX Threaded" : "Auto-Task"} </div>
</span> <ChevronRight size={16} class="text-zinc-700 group-hover:text-indigo-400 group-hover:translate-x-1 transition-all" />
</div> </button>
</div> )}
</For>
<div class="flex items-center space-x-2">
<button
onClick={() => toggleAutonomous(props.instanceId)}
class={`px-2 py-0.5 rounded text-[9px] font-bold uppercase border ${solo().isAutonomous
? "bg-indigo-500/20 border-indigo-500/40 text-indigo-400"
: "bg-white/5 border-white/10 text-zinc-500"
}`}
>
Auto
</button>
<button
onClick={() => toggleAutoApproval(props.instanceId)}
class={`px-2 py-0.5 rounded text-[9px] font-bold uppercase border ${solo().autoApproval
? "bg-emerald-500/20 border-emerald-500/40 text-emerald-400"
: "bg-white/5 border-white/10 text-zinc-500"
}`}
>
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">
{formatTokenTotal(tokenStats().used)}
</div> </div>
</Show> </div>
<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>
</Show>
</div> </div>
</div> </Show>
</div>
{/* Text Input */} {/* Chat Input Area - Fixed at bottom */}
<textarea <div class="p-3 bg-[#0a0a0b] border-t border-white/5 shrink-0">
value={chatInput()} <div class="w-full bg-zinc-900/80 border border-white/10 rounded-2xl shadow-lg p-3">
onInput={(e) => setChatInput(e.currentTarget.value)} {/* Header Row */}
placeholder={selectedTaskId() ? "Send instruction to this task..." : "Type to create a new task and begin..."} <div class="flex items-center justify-between mb-2">
class="w-full bg-transparent border-none focus:ring-0 focus:outline-none text-[13px] text-zinc-100 placeholder-zinc-600 resize-none min-h-[40px] max-h-32 leading-relaxed disabled:opacity-50" <div class="flex items-center space-x-2">
onKeyDown={handleKeyDown} <div class="w-5 h-5 rounded-lg bg-gradient-to-br from-indigo-500 to-violet-600 flex items-center justify-center">
disabled={isSending()} <AtSign size={10} class="text-white" strokeWidth={3} />
rows={1} </div>
/> <div class="flex flex-col">
<span class="text-[10px] font-bold text-zinc-100 uppercase tracking-wide">
{selectedTaskId() ? "Task Context" : "Global Pipeline"}
</span>
<span class="text-[9px] text-zinc-500 uppercase">
{selectedTaskId() ? "MultiX Threaded" : "Auto-Task"}
</span>
</div>
</div>
{/* Footer Row */} <div class="flex items-center space-x-2">
<div class="flex items-center justify-between pt-2 border-t border-white/5 mt-2"> <button
<div class="flex items-center space-x-3"> onClick={() => toggleAutonomous(props.instanceId)}
<button class="text-zinc-600 hover:text-zinc-400 transition-colors p-1"> class={`px-2 py-0.5 rounded text-[9px] font-bold uppercase border ${solo().isAutonomous
<Hash size={14} /> ? "bg-indigo-500/20 border-indigo-500/40 text-indigo-400"
</button> : "bg-white/5 border-white/10 text-zinc-500"
<button class="text-zinc-600 hover:text-zinc-400 transition-colors p-1"> }`}
<Mic size={14} /> title="APEX - Autonomous Programming EXecution mode"
</button> >
<div class="w-px h-3 bg-zinc-800" /> APEX
<div class="flex items-center space-x-1 text-zinc-600"> </button>
<kbd class="px-1.5 py-0.5 bg-zinc-800 rounded text-[9px] font-bold border border-white/5">ENTER</kbd> <button
<span class="text-[9px]">to send</span> onClick={() => toggleAutoApproval(props.instanceId)}
class={`px-2 py-0.5 rounded text-[9px] font-bold uppercase border ${solo().autoApproval
? "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
</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">
{formatTokenTotal(tokenStats().used)}
</div>
</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">
<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>
</div> </div>
<button {/* Text Input */}
onClick={handleSendMessage} <textarea
disabled={!chatInput().trim() || isSending()} value={chatInput()}
class="px-4 py-1.5 bg-indigo-500 hover:bg-indigo-400 text-white rounded-lg text-[11px] font-bold uppercase tracking-wide transition-all disabled:opacity-30 disabled:cursor-not-allowed flex items-center space-x-1.5" onInput={(e) => setChatInput(e.currentTarget.value)}
> placeholder={selectedTaskId() ? "Send instruction to this task..." : "Type to create a new task and begin..."}
<Show when={isSending()} fallback={ class="w-full bg-transparent border-none focus:ring-0 focus:outline-none text-[13px] text-zinc-100 placeholder-zinc-600 resize-none min-h-[40px] max-h-32 leading-relaxed disabled:opacity-50"
<> onKeyDown={handleKeyDown}
<span>{selectedTaskId() ? "Update Task" : "Launch Task"}</span> disabled={isSending()}
<ArrowUp size={12} strokeWidth={3} /> rows={1}
</> />
}>
<Loader2 size={12} class="animate-spin" /> {/* Footer Row */}
</Show> <div class="flex items-center justify-between pt-2 border-t border-white/5 mt-2">
</button> <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>
<button class="text-zinc-600 hover:text-zinc-400 transition-colors p-1">
<Mic size={14} />
</button>
<div class="w-px h-3 bg-zinc-800" />
<div class="flex items-center space-x-1 text-zinc-600">
<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
onClick={handleSendMessage}
disabled={!chatInput().trim() || isSending()}
class="px-4 py-1.5 bg-indigo-500 hover:bg-indigo-400 text-white rounded-lg text-[11px] font-bold uppercase tracking-wide transition-all disabled:opacity-30 disabled:cursor-not-allowed flex items-center space-x-1.5"
>
<Show when={isSending()} fallback={
<>
<span>{selectedTaskId() ? "Update Task" : "Launch Task"}</span>
<ArrowUp size={12} strokeWidth={3} />
</>
}>
<Loader2 size={12} class="animate-spin" />
</Show>
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</main>
{/* 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 >
); );
} }