| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react"; | 1 | import { MonitorIcon, MoonIcon, SunIcon, TerminalIcon } from "lucide-react"; |
| 2 | import { useCallback, useEffect, useMemo, useState } from "react"; | 2 | import { useCallback, useEffect, useMemo, useState } from "react"; |
| 3 | import { isElectron } from "../env"; | 3 | import { isElectron } from "../env"; |
| 4 | import { useTheme } from "../hooks/useTheme"; | 4 | import { useTheme } from "../hooks/useTheme"; |
| ... | @@ -32,12 +32,18 @@ function inferProjectName(cwd: string): string { | ... | @@ -32,12 +32,18 @@ function inferProjectName(cwd: string): string { |
| 32 | } | 32 | } |
| 33 | | 33 | |
| 34 | interface ThreadStatusPill { | 34 | interface 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 | } |
| 40 | | 40 | |
| | 41 | interface TerminalStatusIndicator { |
| | 42 | label: "Terminal process running"; |
| | 43 | colorClass: string; |
| | 44 | pulse: boolean; |
| | 45 | } |
| | 46 | |
| 41 | function hasUnseenCompletion(thread: Thread): boolean { | 47 | function 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 | } |
| 91 | | 97 | |
| 92 | function terminalStatusPill(thread: Thread): ThreadStatusPill | null { | 98 | function 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 | <button | 411 | <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 | <span | 435 | <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 | })} |