+
{tasks.map((t) => (
))}
@@ -1343,14 +1755,15 @@ function Column({ col, tasks, onAction, onViewDetail }) {
- No tasks
+ Clear
)}
@@ -5690,8 +6103,62 @@ export default function App() {
: tasks;
const runningCount = tasks.filter((t) => t.status === "running").length;
- const scheduledCount = tasks.filter((t) => t.status === "scheduled").length;
+ const queueCount = tasks.filter((t) =>
+ ["pending", "scheduled", "blocked"].includes(t.status),
+ ).length;
+ const doneCount = tasks.filter((t) =>
+ ["completed", "failed", "cancelled"].includes(t.status),
+ ).length;
const enabledHeartbeatCount = heartbeats.filter((h) => h.enabled).length;
+ const pausedHeartbeatCount = Math.max(heartbeats.length - enabledHeartbeatCount, 0);
+ const heartbeatIssueCount = heartbeats.filter((h) => h.last_error).length;
+ const enabledSkillCount = skills.filter((s) => s.enabled).length;
+ const pausedSkillCount = Math.max(skills.length - enabledSkillCount, 0);
+ const skillPatternCount = (skillData.patterns || []).filter(
+ (p) => p.recurrence_count >= 2,
+ ).length;
+ const activeSummary = {
+ tasks: {
+ label: `${runningCount} running / ${queueCount} queued`,
+ tone: runningCount > 0 ? theme.blue : theme.green,
+ background: runningCount > 0 ? theme.blueBg : theme.greenBg,
+ metrics: [
+ { label: "Total", value: tasks.length },
+ { label: "Queue", value: queueCount, tone: theme.orange },
+ { label: "Running", value: runningCount, tone: theme.blue },
+ { label: "Done", value: doneCount, tone: theme.green },
+ ],
+ },
+ heartbeats: {
+ label:
+ heartbeatIssueCount > 0
+ ? `${enabledHeartbeatCount} enabled / ${heartbeatIssueCount} issues`
+ : `${enabledHeartbeatCount} enabled / ${pausedHeartbeatCount} paused`,
+ tone: heartbeatIssueCount > 0 ? theme.orange : theme.cyan,
+ background: heartbeatIssueCount > 0 ? theme.orangeBg : theme.cyanBg,
+ metrics: [
+ { label: "Total", value: heartbeats.length },
+ { label: "Enabled", value: enabledHeartbeatCount, tone: theme.green },
+ { label: "Paused", value: pausedHeartbeatCount, tone: theme.textMuted },
+ {
+ label: "Issues",
+ value: heartbeatIssueCount,
+ tone: heartbeatIssueCount ? theme.orange : theme.green,
+ },
+ ],
+ },
+ skills: {
+ label: `${enabledSkillCount} enabled / ${skillPatternCount} patterns`,
+ tone: theme.accent,
+ background: theme.accentGlow,
+ metrics: [
+ { label: "Installed", value: skills.length },
+ { label: "Enabled", value: enabledSkillCount, tone: theme.green },
+ { label: "Paused", value: pausedSkillCount, tone: theme.textMuted },
+ { label: "Patterns", value: skillPatternCount, tone: theme.accent },
+ ],
+ },
+ }[activeView];
if (backendError) {
return (
@@ -5753,11 +6220,13 @@ export default function App() {
style={{
minHeight: "100vh",
background: theme.bg,
+ backgroundImage: theme.boardBg,
+ backgroundSize: "100% 100%, 100% 100%, 100% 100%",
color: theme.text,
- fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
+ fontFamily: APP_FONT_STACK,
}}
>
-
+
{/* API error toast */}
{apiError && (
-
+
+
+
+
+
+ AgentForge
+
+
+ Agent orchestration board
+
+
+
+
- ⌘
+
-
+
+
- AgentForge
-
-
- {connected ? (
- ● Connected
- ) : (
- ● Disconnected — run `bun taskboard.ts`
- )}
- {connected &&
- ` · ${runningCount} running · ${scheduledCount} scheduled · ${enabledHeartbeatCount} heartbeats`}
+ {[
+ { key: "tasks", label: "Tasks", icon: KanbanSquare },
+ { key: "heartbeats", label: "Heartbeats", icon: HeartPulse },
+ { key: "skills", label: "Skills", icon: Sparkles },
+ ].map((tab) => (
+
+ ))}
-
-
-
-
- {[
- { key: "tasks", label: "Tasks" },
- { key: "heartbeats", label: "Heartbeats" },
- { key: "skills", label: "Skills" },
- ].map((tab) => (
+ {activeView !== "skills" && (
+
+
+ setFilter(e.target.value)}
+ style={{
+ border: "none",
+ background: "transparent",
+ color: theme.text,
+ fontSize: 12,
+ outline: "none",
+ width: 164,
+ fontFamily: APP_FONT_STACK,
+ }}
+ />
+
+ )}
+
+ {(() => {
+ const cycle = { system: "light", light: "dark", dark: "system" };
+ const icons = { system: MonitorCog, light: Sun, dark: Moon };
+ const labels = { system: "System theme", light: "Light mode", dark: "Dark mode" };
+ const ThemeIcon = icons[colorMode];
+ return (
+
setColorMode(cycle[colorMode])}
+ active={colorMode !== "system"}
+ >
+
+
+ );
+ })()}
+
+
setShowSettings(true)}>
+
+
+
+ {activeView !== "skills" && (
- ))}
+ )}
-
setFilter(e.target.value)}
- style={{
- padding: "8px 14px",
- borderRadius: 8,
- border: `1px solid ${theme.border}`,
- background: theme.surface,
- color: theme.text,
- fontSize: 12,
- outline: "none",
- width: 180,
- }}
- />
- {/* Color mode toggle */}
- {(() => {
- const cycle = { system: "light", light: "dark", dark: "system" };
- const icons = { system: "⊙", light: "☀", dark: "☾" };
- const labels = { system: "System theme", light: "Light mode", dark: "Dark mode" };
- return (
-
-
-
- );
- })()}
-
-
-
- {activeView !== "skills" && (
-
- )}
+
+
+
+ {activeSummary.metrics.map((metric) => (
+
+ ))}
{activeView === "tasks" ? (
- {COLUMNS.map((col) => (
-
col.statuses.includes(t.status))}
- onAction={handleAction}
- onViewDetail={setDetail}
- />
- ))}
+
+ {COLUMNS.map((col) => (
+ col.statuses.includes(t.status))}
+ onAction={handleAction}
+ onViewDetail={setDetail}
+ />
+ ))}
+
) : activeView === "heartbeats" ? (
-
+
No heartbeats yet
@@ -6207,6 +6703,40 @@ export default function App() {
// CSS动画定义
const styles = `
+ html, body, #root {
+ min-height: 100%;
+ margin: 0;
+ }
+
+ body {
+ overflow-x: hidden;
+ }
+
+ button, input, textarea, select {
+ font: inherit;
+ }
+
+ ::selection {
+ background: ${theme.accentGlow};
+ color: ${theme.text};
+ }
+
+ ::-webkit-scrollbar {
+ width: 10px;
+ height: 10px;
+ }
+
+ ::-webkit-scrollbar-track {
+ background: transparent;
+ }
+
+ ::-webkit-scrollbar-thumb {
+ background: ${theme.borderActive};
+ border: 3px solid transparent;
+ border-radius: 8px;
+ background-clip: padding-box;
+ }
+
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }