authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-12 22:21:36-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-12 22:24:03-08:00
log5da67c40ebab884911f3fb1d51121d42b0fd1fba
tree935beb0b848f1d06047a27dc5e2cc76575e93fed
parent804572c8ff6c19a4e0355ff673b4e76cfe2069a2

Show pulsing terminal icon for active thread processes

- Replace terminal text badge with a right-aligned `TerminalIcon` - Add accessible label/title: "Terminal process running" - Keep pulse animation and terminal-active color styling

1 files changed, 28 insertions(+), 21 deletions(-)

apps/web/src/components/Sidebar.tsx+28-21
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";1import { MonitorIcon, MoonIcon, SunIcon, TerminalIcon } from "lucide-react";
2import { useCallback, useEffect, useMemo, useState } from "react";2import { useCallback, useEffect, useMemo, useState } from "react";
3import { isElectron } from "../env";3import { isElectron } from "../env";
4import { useTheme } from "../hooks/useTheme";4import { useTheme } from "../hooks/useTheme";
...@@ -32,12 +32,18 @@ function inferProjectName(cwd: string): string {...@@ -32,12 +32,18 @@ function inferProjectName(cwd: string): string {
32}32}
3333
34interface ThreadStatusPill {34interface ThreadStatusPill {
35 label: "Working" | "Connecting" | "Completed" | "Awaiting response" | "Terminal";35 label: "Working" | "Connecting" | "Completed" | "Awaiting response";
36 colorClass: string;36 colorClass: string;
37 dotClass: string;37 dotClass: string;
38 pulse: boolean;38 pulse: boolean;
39}39}
4040
41interface TerminalStatusIndicator {
42 label: "Terminal process running";
43 colorClass: string;
44 pulse: boolean;
45}
46
41function hasUnseenCompletion(thread: Thread): boolean {47function hasUnseenCompletion(thread: Thread): boolean {
42 if (!thread.latestTurnCompletedAt) return false;48 if (!thread.latestTurnCompletedAt) return false;
43 const completedAt = Date.parse(thread.latestTurnCompletedAt);49 const completedAt = Date.parse(thread.latestTurnCompletedAt);
...@@ -89,14 +95,13 @@ function threadStatusPill(thread: Thread, hasPendingApprovals: boolean): ThreadS...@@ -89,14 +95,13 @@ function threadStatusPill(thread: Thread, hasPendingApprovals: boolean): ThreadS
89 return null;95 return null;
90}96}
9197
92function terminalStatusPill(thread: Thread): ThreadStatusPill | null {98function terminalStatusIndicator(thread: Thread): TerminalStatusIndicator | null {
93 if (thread.runningTerminalIds.length === 0) {99 if (thread.runningTerminalIds.length === 0) {
94 return null;100 return null;
95 }101 }
96 return {102 return {
97 label: "Terminal",103 label: "Terminal process running",
98 colorClass: "text-teal-600 dark:text-teal-300/90",104 colorClass: "text-teal-600 dark:text-teal-300/90",
99 dotClass: "bg-teal-500 dark:bg-teal-300/90",
100 pulse: true,105 pulse: true,
101 };106 };
102}107}
...@@ -401,7 +406,7 @@ export default function Sidebar() {...@@ -401,7 +406,7 @@ export default function Sidebar() {
401 thread,406 thread,
402 pendingApprovalByThreadId.get(thread.id) === true,407 pendingApprovalByThreadId.get(thread.id) === true,
403 );408 );
404 const terminalStatus = terminalStatusPill(thread);409 const terminalStatus = terminalStatusIndicator(thread);
405 return (410 return (
406 <button411 <button
407 key={thread.id}412 key={thread.id}
...@@ -426,18 +431,6 @@ export default function Sidebar() {...@@ -426,18 +431,6 @@ export default function Sidebar() {
426 }}431 }}
427 >432 >
428 <div className="flex min-w-0 flex-1 items-center gap-1.5">433 <div className="flex min-w-0 flex-1 items-center gap-1.5">
429 {terminalStatus && (
430 <span
431 className={`inline-flex items-center gap-1 text-[10px] ${terminalStatus.colorClass}`}
432 >
433 <span
434 className={`h-1.5 w-1.5 rounded-full ${terminalStatus.dotClass} ${
435 terminalStatus.pulse ? "animate-pulse" : ""
436 }`}
437 />
438 <span className="hidden md:inline">{terminalStatus.label}</span>
439 </span>
440 )}
441 {threadStatus && (434 {threadStatus && (
442 <span435 <span
443 className={`inline-flex items-center gap-1 text-[10px] ${threadStatus.colorClass}`}436 className={`inline-flex items-center gap-1 text-[10px] ${threadStatus.colorClass}`}
...@@ -452,9 +445,23 @@ export default function Sidebar() {...@@ -452,9 +445,23 @@ export default function Sidebar() {
452 )}445 )}
453 <span className="min-w-0 flex-1 truncate text-xs">{thread.title}</span>446 <span className="min-w-0 flex-1 truncate text-xs">{thread.title}</span>
454 </div>447 </div>
455 <span className="shrink-0 text-[10px] text-muted-foreground/40">448 <div className="ml-2 flex shrink-0 items-center gap-1.5">
456 {formatRelativeTime(thread.createdAt)}449 {terminalStatus && (
457 </span>450 <span
451 role="img"
452 aria-label={terminalStatus.label}
453 title={terminalStatus.label}
454 className={`inline-flex items-center justify-center ${terminalStatus.colorClass}`}
455 >
456 <TerminalIcon
457 className={`size-3 ${terminalStatus.pulse ? "animate-pulse" : ""}`}
458 />
459 </span>
460 )}
461 <span className="text-[10px] text-muted-foreground/40">
462 {formatRelativeTime(thread.createdAt)}
463 </span>
464 </div>
458 </button>465 </button>
459 );466 );
460 })}467 })}