Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
2232863
feat(config): add framework-update fields to agent normalize
jaylfc Apr 19, 2026
9be7398
feat(frameworks): manifest update metadata + validator
jaylfc Apr 19, 2026
f54624e
fix(frameworks): remove hallucinated framework entries (ironclaw/molt…
jaylfc Apr 19, 2026
e0b78c6
feat(app): validate framework manifests at startup
jaylfc Apr 19, 2026
e55c05d
feat(releases): parser + fetch helper for GitHub Releases
jaylfc Apr 19, 2026
1373700
feat(auto_update): hourly framework releases poll
jaylfc Apr 19, 2026
c44d565
feat(containers): snapshot_list and snapshot_delete helpers
jaylfc Apr 19, 2026
3f71a69
feat(framework-update): prune old pre-update snapshots
jaylfc Apr 19, 2026
2d6431f
feat(framework-update): bootstrap-ping wait with 500ms polling
jaylfc Apr 19, 2026
859f2f0
feat(framework-update): start_update orchestration
jaylfc Apr 19, 2026
9ad098a
feat(openclaw): bootstrap handler bumps bootstrap_last_seen_at
jaylfc Apr 19, 2026
dc0c932
feat(framework): GET /api/agents/{slug}/framework endpoint
jaylfc Apr 19, 2026
ec21e44
feat(framework): POST /api/agents/{slug}/framework/update
jaylfc Apr 19, 2026
6a8449d
feat(framework): GET /api/frameworks/latest with refresh
jaylfc Apr 19, 2026
656d332
feat(agent-image): bake taos-framework-update.sh into base image
jaylfc Apr 19, 2026
664d0ed
feat(app): probe installed framework version on startup
jaylfc Apr 19, 2026
fbbcf1c
feat(desktop): framework-api client
jaylfc Apr 19, 2026
aee9423
feat(agent-settings): Framework tab with installed/latest + update bu…
jaylfc Apr 19, 2026
52d562f
feat(agents): sidebar dot on out-of-date agents
jaylfc Apr 19, 2026
8c892cc
feat(store): affected-agent pill on framework cards
jaylfc Apr 19, 2026
5ac3d5f
test(e2e): framework tab skeletons
jaylfc Apr 19, 2026
d72df0d
test(e2e): store pill + sidebar dot skeletons
jaylfc Apr 19, 2026
3ccd512
build: rebuild desktop bundle for framework-update feature
jaylfc Apr 19, 2026
4f682ce
fix(frameworks): restore picoclaw/nanoclaw and other real frameworks …
jaylfc Apr 19, 2026
a668127
fix(framework-tab): pin update request to confirmed tag, handle null …
jaylfc Apr 19, 2026
aed638b
feat(scripts): per-framework install scripts with embedded SSE bridges
jaylfc Apr 19, 2026
264d98e
fix(scripts): hermes uses 'gateway run' + api_key in config.yaml; ope…
jaylfc Apr 19, 2026
2dbd284
fix(chat): group-chat replies — bridges thread channel_id; bridge_ses…
jaylfc Apr 19, 2026
f49d5bd
fix(scripts): hermes — use os.environ for MODEL/LLM_KEY in python her…
jaylfc Apr 19, 2026
05ffa16
fix(chat): route fork-side replies via trace_id when channel_id missing
jaylfc Apr 19, 2026
ad6c13d
feat(frameworks): promote hermes alpha → beta
jaylfc Apr 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions desktop/src/apps/AgentsApp.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { Bot, Plus, Trash2, ScrollText, Play, Server, X, ChevronRight, ChevronLeft, Check, Wrench, MessageSquare, PauseCircle, RotateCcw, Archive, HardDrive } from "lucide-react";
import { Bot, Box, Plus, Trash2, ScrollText, Play, Server, X, ChevronRight, ChevronLeft, Check, Wrench, MessageSquare, PauseCircle, RotateCcw, Archive, HardDrive } from "lucide-react";
import { fetchLatestFrameworks, LatestVersion } from "@/lib/framework-api";
import { AgentSkillsPanel } from "./AgentSkillsPanel";
import { AgentMessagesPanel } from "./AgentMessagesPanel";
import { PersonaTab } from "@/components/agent-settings/PersonaTab";
import { MemoryTab } from "@/components/agent-settings/MemoryTab";
import { FrameworkTab } from "@/components/agent-settings/FrameworkTab";
import {
fetchClusterWorkers,
workersToAggregated,
Expand Down Expand Up @@ -53,6 +55,7 @@ interface Agent {
agent_md?: string;
source_persona_id?: string | null;
migrated_to_v2_personas?: boolean;
framework_version_sha?: string | null;
}

interface DiskState {
Expand Down Expand Up @@ -115,6 +118,7 @@ const STATUS_STYLES: Record<string, string> = {
function AgentRow({
agent,
diskState,
latestByFramework,
onViewLogs,
onViewSkills,
onViewMessages,
Expand All @@ -123,13 +127,19 @@ function AgentRow({
}: {
agent: Agent;
diskState?: DiskState | null;
latestByFramework: Record<string, LatestVersion>;
onViewLogs: (name: string) => void;
onViewSkills: (name: string) => void;
onViewMessages: (name: string) => void;
onDelete: (name: string) => void;
onResume: (name: string) => void;
}) {
const emoji = resolveAgentEmoji(agent.emoji, agent.framework);
const latestForAgent = agent.framework ? latestByFramework[agent.framework] : undefined;
const updateAvailable =
agent.framework_version_sha &&
latestForAgent &&
latestForAgent.sha !== agent.framework_version_sha;
return (
<Card className="flex items-center gap-4 px-4 py-3 hover:bg-shell-surface/50 transition-colors">
<div className="flex items-center gap-2.5 flex-1 min-w-0">
Expand All @@ -145,6 +155,13 @@ function AgentRow({
{emoji}
</span>
<span className="font-medium text-sm truncate">{agent.display_name || agent.name}</span>
{updateAvailable && (
<span
aria-label="framework update available"
title="framework update available"
className="inline-block w-2 h-2 bg-yellow-400 rounded-full ml-1 shrink-0"
/>
)}
{agent.paused && (
<span
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[10px] font-medium bg-amber-500/20 text-amber-400 border border-amber-500/20"
Expand Down Expand Up @@ -248,7 +265,7 @@ function AgentRow({
/* AgentDetailPanel (Logs + Skills tabs) */
/* ------------------------------------------------------------------ */

type DetailTab = "logs" | "persona" | "memory" | "skills" | "messages";
type DetailTab = "logs" | "persona" | "memory" | "framework" | "skills" | "messages";

function AgentDetailPanel({
agent,
Expand Down Expand Up @@ -346,6 +363,10 @@ function AgentDetailPanel({
<Archive size={13} className="mr-1.5" />
Memory
</TabsTrigger>
<TabsTrigger value="framework">
<Box size={13} className="mr-1.5" />
Framework
</TabsTrigger>
<TabsTrigger value="skills">
<Wrench size={13} className="mr-1.5" />
Skills
Expand Down Expand Up @@ -381,6 +402,9 @@ function AgentDetailPanel({
<TabsContent value="memory" className="h-full mt-0">
<MemoryTab agent={agent} onUpdated={onAgentUpdated} />
</TabsContent>
<TabsContent value="framework" className="h-full mt-0">
<FrameworkTab agent={agent} onUpdated={onAgentUpdated} />
</TabsContent>
<TabsContent value="skills" className="h-full mt-0">
<AgentSkillsPanel
agentId={agent.name}
Expand Down Expand Up @@ -1529,6 +1553,7 @@ export function AgentsApp({ windowId: _windowId }: { windowId: string }) {
const [detail, setDetail] = useState<{ name: string; tab: DetailTab } | null>(null);
const [diskStates, setDiskStates] = useState<Record<string, DiskState>>({});
const [quotaErrors, setQuotaErrors] = useState<Record<string, string>>({});
const [latestByFramework, setLatestByFramework] = useState<Record<string, LatestVersion>>({});

const fetchAgents = useCallback(async () => {
try {
Expand All @@ -1554,6 +1579,7 @@ export function AgentsApp({ windowId: _windowId }: { windowId: string }) {
kv_cache_quant_k: a.kv_cache_quant_k ? String(a.kv_cache_quant_k) : (a.kv_cache_quant ? String(a.kv_cache_quant) : "fp16"),
kv_cache_quant_v: a.kv_cache_quant_v ? String(a.kv_cache_quant_v) : (a.kv_cache_quant ? String(a.kv_cache_quant) : "fp16"),
kv_cache_quant_boundary_layers: typeof a.kv_cache_quant_boundary_layers === "number" ? a.kv_cache_quant_boundary_layers : 0,
framework_version_sha: a.framework_version_sha != null ? String(a.framework_version_sha) : null,
}))
);
setLoading(false);
Expand Down Expand Up @@ -1629,6 +1655,10 @@ export function AgentsApp({ windowId: _windowId }: { windowId: string }) {
return () => window.removeEventListener("taos:agent-resumed", handler);
}, [fetchAgents]);

useEffect(() => {
fetchLatestFrameworks().then(setLatestByFramework).catch(() => {});
}, []);

async function handleResume(name: string) {
try {
const res = await fetch(`/api/agents/${encodeURIComponent(name)}/resume`, {
Expand Down Expand Up @@ -1881,6 +1911,7 @@ export function AgentsApp({ windowId: _windowId }: { windowId: string }) {
key={agent.name}
agent={agent}
diskState={diskStates[agent.name] ?? null}
latestByFramework={latestByFramework}
onViewLogs={(name) => setDetail({ name, tab: "logs" })}
onViewSkills={(name) => setDetail({ name, tab: "skills" })}
onViewMessages={(name) => setDetail({ name, tab: "messages" })}
Expand Down
38 changes: 33 additions & 5 deletions desktop/src/apps/StoreApp.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useState, useEffect, useCallback } from "react";
import { ShoppingBag, Search, Download, Trash2, Check, Package, Loader2, Bot, Brain, Server, Plug, Wrench, Image, Music, Video, Globe, Home, Cpu } from "lucide-react";
import { Button, Card, CardContent, CardFooter, CardHeader, Input } from "@/components/ui";
import { fetchLatestFrameworks, LatestVersion } from "@/lib/framework-api";

/* ------------------------------------------------------------------ */
/* Types */
Expand Down Expand Up @@ -392,7 +393,7 @@ function resolveIconUrl(appId: string): string | null {
/* AppCard */
/* ------------------------------------------------------------------ */

function AppCard({ app, onInstall, onUninstall }: { app: CatalogApp; onInstall: (id: string) => void; onUninstall: (id: string) => void }) {
function AppCard({ app, affected, onInstall, onUninstall }: { app: CatalogApp; affected: number; onInstall: (id: string) => void; onUninstall: (id: string) => void }) {
const [busy, setBusy] = useState(false);
const [iconFailed, setIconFailed] = useState(false);

Expand Down Expand Up @@ -448,9 +449,14 @@ function AppCard({ app, onInstall, onUninstall }: { app: CatalogApp; onInstall:
)}
</div>
<div className="min-w-0">
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 flex-wrap">
<span className="font-medium text-white/90 truncate text-sm">{app.name}</span>
{app.installed && <Check className="w-3.5 h-3.5 text-emerald-400 shrink-0" />}
{affected > 0 && (
<span className="bg-yellow-700/30 text-yellow-200 text-xs px-2 py-0.5 rounded ml-2 shrink-0">
Update available · {affected} {affected === 1 ? "agent" : "agents"}
</span>
)}
</div>
<span className="text-[11px] text-white/30">v{app.version}</span>
</div>
Expand Down Expand Up @@ -497,6 +503,8 @@ export function StoreApp({ windowId: _windowId }: { windowId: string }) {
const [search, setSearch] = useState("");
const [activeCategory, setActiveCategory] = useState("all");
const [loading, setLoading] = useState(true);
const [latest, setLatest] = useState<Record<string, LatestVersion>>({});
const [agentList, setAgentList] = useState<any[]>([]);

const fetchCatalog = useCallback(async () => {
try {
Expand Down Expand Up @@ -540,6 +548,14 @@ export function StoreApp({ windowId: _windowId }: { windowId: string }) {
if (cat) setActiveCategory(cat);
}, []);

useEffect(() => {
fetchLatestFrameworks().then(setLatest).catch(() => {});
fetch("/api/agents")
.then((r) => r.ok ? r.json() : [])
.then((j) => setAgentList(Array.isArray(j) ? j : (j?.agents ?? [])))
.catch(() => {});
}, []);

const activeCat = CATEGORIES.find((c) => c.id === activeCategory);

const filtered = apps.filter((app) => {
Expand Down Expand Up @@ -660,9 +676,21 @@ export function StoreApp({ windowId: _windowId }: { windowId: string }) {
</div>
) : (
<div className="grid grid-cols-[repeat(auto-fill,minmax(250px,1fr))] gap-4">
{filtered.map((app) => (
<AppCard key={app.id} app={app} onInstall={handleInstall} onUninstall={handleUninstall} />
))}
{filtered.map((app) => {
const latestForApp = latest[app.id];
const affected = app.type === "agent-framework"
? agentList.filter(
(a: any) =>
a.framework === app.id &&
a.framework_version_sha &&
latestForApp &&
latestForApp.sha !== a.framework_version_sha
).length
: 0;
return (
<AppCard key={app.id} app={app} affected={affected} onInstall={handleInstall} onUninstall={handleUninstall} />
);
})}
</div>
)}
</div>
Expand Down
114 changes: 114 additions & 0 deletions desktop/src/components/agent-settings/FrameworkTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { useEffect, useState } from "react";
import { fetchFrameworkState, FrameworkState, startFrameworkUpdate } from "@/lib/framework-api";

export function FrameworkTab({ agent, onUpdated }: { agent: { name: string }; onUpdated: () => void }) {
const [state, setState] = useState<FrameworkState | null>(null);
const [err, setErr] = useState<string | null>(null);
const [confirming, setConfirming] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [elapsed, setElapsed] = useState(0);

async function load() {
try { setState(await fetchFrameworkState(agent.name)); setErr(null); }
catch (e: any) { setErr(String(e)); }
}

useEffect(() => { load(); }, [agent.name]);

useEffect(() => {
if (state?.update_status !== "updating") return;
const id = setInterval(() => { load(); }, 2000);
return () => clearInterval(id);
}, [state?.update_status]);
Comment on lines +11 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd desktop/src/components/agent-settings && wc -l FrameworkTab.tsx

Repository: jaylfc/tinyagentos

Length of output: 82


🏁 Script executed:

cd desktop/src/components/agent-settings && head -50 FrameworkTab.tsx

Repository: jaylfc/tinyagentos

Length of output: 2184


🏁 Script executed:

cd desktop/src/components/agent-settings && tail -60 FrameworkTab.tsx

Repository: jaylfc/tinyagentos

Length of output: 2734


🏁 Script executed:

cd desktop/src/components/agent-settings && sed -n '40,106p' FrameworkTab.tsx

Repository: jaylfc/tinyagentos

Length of output: 3063


Add agent.name to the polling effect dependency array to prevent stale requests from previous agents.

The polling effect (lines 18-22) depends only on state?.update_status and will continue running even when the agent changes. If the user switches agents while update_status === "updating", the old agent's polling interval persists and competes with the new agent's load request. Whichever response finishes last will overwrite the current state, potentially showing stale data from the previous agent.

Add agent.name to the dependency array so the interval cleans up when the agent changes:

}, [state?.update_status, agent.name]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@desktop/src/components/agent-settings/FrameworkTab.tsx` around lines 11 - 22,
The polling useEffect that starts an interval when state?.update_status ===
"updating" can keep polling the previous agent because its dependency array only
includes state?.update_status; update the effect to also depend on agent.name so
the interval is torn down when the agent changes (i.e., modify the dependency
array of the useEffect that uses setInterval and clearInterval to include
agent.name), ensuring the existing cleanup runs and load() will reflect the
current agent.name instead of allowing stale responses to overwrite state.


useEffect(() => {
if (state?.update_status !== "updating" || !state.update_started_at) { setElapsed(0); return; }
const tick = () => setElapsed(Math.floor(Date.now() / 1000) - (state.update_started_at ?? 0));
tick();
const id = setInterval(tick, 1000);
return () => clearInterval(id);
}, [state?.update_status, state?.update_started_at]);

async function doUpdate() {
setSubmitting(true);
try {
// Pin the request to the exact tag the user just confirmed so the
// backend can't drift to a newer release if its cache advances mid-click.
await startFrameworkUpdate(agent.name, state?.latest?.tag);
// Optimistically flip to "updating" so the polling effect arms even
// if a racing load() reads an idle status before the backend writes.
setState((prev) => prev ? { ...prev, update_status: "updating", update_started_at: Math.floor(Date.now() / 1000) } : prev);
await load();
onUpdated();
} catch (e: any) { setErr(String(e)); }
finally { setSubmitting(false); setConfirming(false); }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

if (err) return <div className="p-4 text-sm text-red-400">Error: {err}</div>;
if (!state) return <div className="p-4 text-sm opacity-60">Loading…</div>;

return (
<div className="flex flex-col gap-4 p-4">
<div className="text-sm">This agent runs <b>{state.framework}</b></div>
<dl className="grid grid-cols-[120px_1fr] gap-y-1 text-sm">
<dt className="opacity-60">Installed</dt>
<dd><code>{state.installed.tag ?? "(unknown)"}</code> · <code>{state.installed.sha ?? "—"}</code></dd>
<dt className="opacity-60">Latest</dt>
<dd>
{state.latest
? <><code>{state.latest.tag}</code> · <code>{state.latest.sha}</code>
{state.latest.published_at && <span className="opacity-60 ml-2">published {state.latest.published_at}</span>}</>
: <span className="opacity-60">(not available)</span>}
</dd>
</dl>

{state.update_available && state.update_status === "idle" && (
<div className="flex items-center gap-2">
<span className="bg-yellow-700/30 text-yellow-200 px-2 py-0.5 rounded text-xs">Update available</span>
<button onClick={() => setConfirming(true)} disabled={submitting}
className="bg-blue-600 px-3 py-1.5 rounded text-sm">
Update Framework
</button>
</div>
)}

{!state.update_available && state.update_status === "idle" && state.latest && (
<div className="text-sm text-green-400">✓ You're on the latest version</div>
)}

{state.update_status === "updating" && (
<div className="bg-white/5 border border-white/10 rounded px-3 py-2 text-sm">
Updating {state.framework}… started {elapsed}s ago.
</div>
)}

{state.update_status === "failed" && (
<div className="bg-red-950/40 border border-red-800 rounded px-3 py-2 text-sm">
<div>Update failed: {state.last_error}</div>
{state.last_snapshot && (
<div className="opacity-70 mt-1">Snapshot retained: <code>{state.last_snapshot}</code></div>
)}
</div>
)}

{confirming && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div className="bg-shell-bg border border-white/10 rounded p-4 max-w-sm">
<p className="text-sm mb-3">
Update {agent.name}'s {state.framework} to <code>{state.latest?.tag ?? "latest"}</code>?
The agent will go offline for up to 2 minutes. Messages will queue.
</p>
<div className="flex justify-end gap-2">
<button onClick={() => setConfirming(false)} className="opacity-60 text-sm">Cancel</button>
<button onClick={doUpdate} disabled={submitting} className="bg-blue-600 px-3 py-1.5 rounded text-sm">
{submitting ? "Starting…" : "Update"}
</button>
</div>
</div>
</div>
)}

<div className="mt-auto pt-4 text-xs opacity-50">Switch framework — coming soon</div>
</div>
);
}
37 changes: 37 additions & 0 deletions desktop/src/lib/framework-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export type FrameworkVersion = { tag: string | null; sha: string | null };
export type LatestVersion = { tag: string; sha: string; published_at?: string };

export interface FrameworkState {
framework: string;
installed: FrameworkVersion;
latest: LatestVersion | null;
update_available: boolean;
update_status: "idle" | "updating" | "failed";
update_started_at: number | null;
last_error: string | null;
last_snapshot: string | null;
}

export async function fetchFrameworkState(slug: string): Promise<FrameworkState> {
const r = await fetch(`/api/agents/${encodeURIComponent(slug)}/framework`);
if (!r.ok) throw new Error(`framework fetch ${r.status}`);
return r.json();
}

export async function startFrameworkUpdate(slug: string, targetVersion?: string): Promise<void> {
const r = await fetch(`/api/agents/${encodeURIComponent(slug)}/framework/update`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(targetVersion ? { target_version: targetVersion } : {}),
});
if (!r.ok) {
const body = await r.json().catch(() => ({}));
throw new Error(body.error || `update start ${r.status}`);
}
}

export async function fetchLatestFrameworks(refresh = false): Promise<Record<string, LatestVersion>> {
const r = await fetch(`/api/frameworks/latest${refresh ? "?refresh=true" : ""}`);
if (!r.ok) throw new Error(`latest frameworks ${r.status}`);
return r.json();
}
Loading
Loading