diff --git a/desktop/assets/octo-idle-sprite.png b/desktop/assets/octo-idle-sprite.png new file mode 100644 index 00000000..b49210ed Binary files /dev/null and b/desktop/assets/octo-idle-sprite.png differ diff --git a/desktop/assets/octo-thinking-sprite.png b/desktop/assets/octo-thinking-sprite.png new file mode 100644 index 00000000..61966803 Binary files /dev/null and b/desktop/assets/octo-thinking-sprite.png differ diff --git a/desktop/src/renderer/src/components/DashboardScreen.tsx b/desktop/src/renderer/src/components/DashboardScreen.tsx index 33d0a9bc..4bf5093d 100644 --- a/desktop/src/renderer/src/components/DashboardScreen.tsx +++ b/desktop/src/renderer/src/components/DashboardScreen.tsx @@ -1,7 +1,10 @@ import { AlertTriangle, Clock, Download, ExternalLink, Eye, FileJson, GitBranch, ListChecks, Pencil, Play, Plus, RotateCw, Square, Trash2, Wrench, X } from "lucide-react"; import { motion } from "framer-motion"; import { useCallback, useEffect, useMemo, useState } from "react"; +import type { ReactNode } from "react"; +import octoIdleSprite from "../../../../assets/octo-idle-sprite.png"; +import octoThinkingSprite from "../../../../assets/octo-thinking-sprite.png"; import octoImage from "../../../../assets/octo.png"; import type { CopyFn } from "../lib/appTypes"; import { Button } from "./Button"; @@ -128,6 +131,24 @@ function statusTextClass(status?: string): string { return "worker-detail-status"; } +function isIdleOctoState(status?: string): boolean { + return String(status ?? "").toLowerCase() === "idle"; +} + +function isThinkingOctoState(status?: string): boolean { + return String(status ?? "").toLowerCase() === "thinking"; +} + +function animatedOctoForState(status?: string): { className: string; sprite: string } | null { + if (isIdleOctoState(status)) { + return { className: "dashboard-octo-idle", sprite: octoIdleSprite }; + } + if (isThinkingOctoState(status)) { + return { className: "dashboard-octo-thinking", sprite: octoThinkingSprite }; + } + return null; +} + function formatTime(value?: string | number): string { if (!value) { return "-"; @@ -467,6 +488,7 @@ export function DashboardScreen({ const selectedWorkerTemplate = selectedWorker?.template_id ? templates.find((template) => template.id === selectedWorker.template_id) ?? null : null; + const animatedOcto = animatedOctoForState(displayOctoState); function startCreateTemplate(): void { setEditingTemplateId(""); @@ -545,29 +567,68 @@ export function DashboardScreen({ void window.octopalDesktop.openOctopalLogs(installDir); } - function renderControl() { + function renderDashboardHeader({ + title, + titleRaw = title, + detail, + detailRaw = detail, + latest, + latestRaw = latest, + actions, + }: { + title: string; + titleRaw?: string; + detail: string; + detailRaw?: string; + latest?: string; + latestRaw?: string; + actions?: ReactNode; + }) { return ( -
-
-
+
+
+ {animatedOcto ? ( + + ) : ( Octopal mascot - {displayOctoState} -
-
-

{octoHeadline}

-

{octoDetail}

-

- {copy("latestAction")}: {latestAction} + )} + {displayOctoState} +

+
+

{title}

+

{detail}

+ {latest ? ( +

+ {copy("latestAction")}: {latest}

-
- {updateAvailable || desktopUpdateAvailable ? ( -
- -
) : null}
+ {actions ?
{actions}
: null} +
+ ); + } + + function renderControl() { + return ( +
+ {renderDashboardHeader({ + title: octoHeadline, + titleRaw: octoHeadlineRaw, + detail: octoDetail, + detailRaw: octoDetailRaw, + latest: latestAction, + latestRaw: latestActionRaw, + actions: updateAvailable || desktopUpdateAvailable ? ( + + ) : null, + })} {attention || octoNeedsAttention || dashboardError ? (
@@ -672,13 +733,12 @@ export function DashboardScreen({ function renderWorkers() { return (
-
- Octopal mascot -
-

{copy("workerTemplates")}

-

{copy("workerTemplatesBody")}

-
-
+ {renderDashboardHeader({ + title: copy("workerTemplates"), + detail: copy("workerTemplatesBody"), + latest: latestAction, + latestRaw: latestActionRaw, + })} {templateError ?

{templateError}

: null} {templateNotice ?

{templateNotice}

: null}
@@ -723,13 +783,12 @@ export function DashboardScreen({ function renderSystem() { return (
-
- Octopal mascot -
-

{systemTitle}

-

{systemDetail}

-
-
+ {renderDashboardHeader({ + title: systemTitle, + detail: systemDetail, + latest: latestAction, + latestRaw: latestActionRaw, + })} {attention || dashboardError ? (
diff --git a/desktop/src/renderer/src/styles.css b/desktop/src/renderer/src/styles.css index 27fcae48..c668aa09 100644 --- a/desktop/src/renderer/src/styles.css +++ b/desktop/src/renderer/src/styles.css @@ -2093,29 +2093,31 @@ p { padding-right: 210px; } -.dashboard-assistant-head-compact { - grid-template-columns: 76px minmax(0, 1fr); - padding-right: 220px; -} - .dashboard-octo { width: 92px; height: 92px; object-fit: contain; } +.dashboard-octo-idle, +.dashboard-octo-thinking { + display: block; + background-repeat: no-repeat; + background-position: 0 0; + background-size: 1500% 100%; + animation: octo-sprite-frames 4.8s steps(15) infinite; +} + +.dashboard-octo-thinking { + animation-duration: 9.6s; +} + .dashboard-octo-stack { display: grid; justify-items: center; gap: 6px; } -.dashboard-octo-small { - width: 68px; - height: 68px; - object-fit: contain; -} - .dashboard-bubble { position: relative; width: fit-content; @@ -3263,6 +3265,12 @@ p { } } +@keyframes octo-sprite-frames { + to { + background-position-x: 107.142857%; + } +} + @media (max-width: 860px) { .titlebar { height: 52px; @@ -3470,15 +3478,13 @@ p { flex: 1 0 auto; } - .dashboard-assistant-head, - .dashboard-assistant-head-compact { + .dashboard-assistant-head { grid-template-columns: 76px minmax(0, 1fr); gap: 14px; padding-right: 0; } - .dashboard-octo, - .dashboard-octo-small { + .dashboard-octo { width: 68px; height: 68px; } @@ -3563,4 +3569,9 @@ p { scroll-behavior: auto !important; transition-duration: 1ms !important; } + + .dashboard-octo-idle, + .dashboard-octo-thinking { + animation: none !important; + } }