Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions agentos/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest run --passWithNoTests",
"typecheck": "tsc -b --noEmit"
},
"dependencies": {
"react": "^18.3.1",
Expand All @@ -20,6 +22,7 @@
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"vite": "^6.0.5"
"vite": "^6.0.5",
"vitest": "^2.0.0"
}
}
40 changes: 30 additions & 10 deletions agentos/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { LogsTab } from "./components/LogsTab.tsx";
import { WorkspaceTab } from "./components/WorkspaceTab.tsx";
import { SchedulesTab } from "./components/SchedulesTab.tsx";
import { HomePage } from "./components/HomePage.tsx";
import { RegisterAgentForm } from "./components/RegisterAgentForm.tsx";
import { SourceBadge } from "./components/SourceBadge.tsx";

type Tab = "chat" | "schedules" | "logs";
type View = "home" | "dashboard";
Expand Down Expand Up @@ -41,13 +43,16 @@ function typeLogo(harness: string): string | null {
function TypeBadge({ agent, className = "" }: { agent: Agent; className?: string }) {
const logo = typeLogo(agent.harness);
return (
<span className={`inline-flex items-center gap-1.5 text-[10px] rounded bg-accent/20 text-accent-soft pl-1 pr-2 py-0.5 ${className}`}>
<span
title={agent.label}
className={`inline-flex items-center gap-1 text-[10px] rounded bg-accent/20 text-accent-soft pl-1 pr-1.5 py-0.5 shrink-0 whitespace-nowrap max-w-[7.5rem] ${className}`}
>
{logo && (
<span className="h-4 w-4 grid place-items-center rounded bg-white shrink-0">
<span className="h-3.5 w-3.5 grid place-items-center rounded bg-white shrink-0">
<img src={logo} alt="" className="h-2.5 w-2.5 object-contain" />
</span>
)}
{agent.label}
<span className="truncate">{agent.label}</span>
</span>
);
}
Expand Down Expand Up @@ -81,7 +86,7 @@ export default function App() {
return (
<div className="flex h-full">
{/* Left rail */}
<aside className="w-72 shrink-0 border-r border-ink-600 bg-ink-800 flex flex-col">
<aside className="w-80 shrink-0 border-r border-ink-600 bg-ink-800 flex flex-col">
<div className="px-5 py-4 border-b border-ink-600">
<div className="flex items-center gap-2.5">
<img src="/logos/agentos.png" alt="ComputerAgent" className="h-8 w-8 rounded-md object-contain" />
Expand Down Expand Up @@ -125,19 +130,34 @@ export default function App() {
view === "dashboard" && selected === a.name ? "bg-ink-600 ring-1 ring-accent/40" : "hover:bg-ink-700"
}`}
>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 min-w-0">
<span className={`h-2 w-2 rounded-full shrink-0 ${a.activeSandboxes > 0 ? "bg-emerald-400" : "bg-gray-600"}`} />
<span className="font-medium text-sm truncate">{agentNameFromSource(a.source)}</span>
<TypeBadge agent={a} className="ml-auto" />
<span className="font-medium text-sm truncate min-w-0 flex-1" title={agentNameFromSource(a.sourceUrl ?? "")}>
{agentNameFromSource(a.sourceUrl ?? "")}
</span>
{a.origin === "registry" && (
<span
className="text-[9px] uppercase tracking-wider text-accent-soft bg-accent/10 rounded px-1.5 py-0.5 shrink-0"
title={`Registered via the SDK telemetry hook${a.registeredBy ? ` by ${a.registeredBy}` : ""}`}
>
lib
</span>
)}
<TypeBadge agent={a} />
</div>
<div className="mt-1.5">
<SourceBadge agent={a} />
</div>
<div className="mt-1 text-[11px] text-gray-500 truncate">{a.source}</div>
<div className="mt-1.5 flex gap-3 text-[10px] text-gray-500">
<span>{a.sessionCount} sessions</span>
<span>{a.logCount} logs</span>
<span>{timeAgo(a.lastActivity)}</span>
<span>{timeAgo(a.lastActivity ?? a.lastSeen ?? null)}</span>
</div>
</button>
))}
<div className="px-2 pt-3 pb-2">
<RegisterAgentForm onRegistered={() => api.agents().then(setAgents).catch(() => {})} />
</div>
</div>
)}
<div className="px-4 py-3 border-t border-ink-600 text-[10px] text-gray-600">agentos.clawagent.sh</div>
Expand All @@ -152,7 +172,7 @@ export default function App() {
<header className="px-6 py-4 border-b border-ink-600 flex items-center gap-4">
<div>
<div className="flex items-center gap-2">
<span className="text-base font-semibold">{agentNameFromSource(agent.source)}</span>
<span className="text-base font-semibold">{agentNameFromSource(agent.sourceUrl ?? "")}</span>
<TypeBadge agent={agent} />
</div>
<div className="text-xs text-gray-500">
Expand Down
197 changes: 197 additions & 0 deletions agentos/src/api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/**
* Pure-function unit tests for `displaySource`. No DOM, no fetch, no React.
*
* displaySource is the load-bearing piece behind <SourceBadge> in the agent
* rail: it turns the variable-shaped `agent.source` (git/local/inline object
* OR a legacy bare string) into a render-ready `{kind, primary, secondary,
* href?}` triple. Wrong output here = the dashboard renders the wrong title
* or links to the wrong repo, so we exercise every recognized shape + every
* fallback branch.
*/
import { describe, expect, it } from "vitest";
import { displaySource } from "./api.js";

describe("displaySource", () => {
describe("null / undefined input", () => {
it("returns the (no source) unknown sentinel for undefined", () => {
expect(displaySource(undefined)).toEqual({
kind: "unknown",
primary: "(no source)",
secondary: "",
});
});
it("returns the (no source) unknown sentinel for null", () => {
expect(displaySource(null)).toEqual({
kind: "unknown",
primary: "(no source)",
secondary: "",
});
});
it("returns the (no source) unknown sentinel for empty string", () => {
expect(displaySource("")).toEqual({
kind: "unknown",
primary: "(no source)",
secondary: "",
});
});
});

describe("structured local source", () => {
it("uses the last two path segments as the primary label", () => {
expect(displaySource({ type: "local", path: "/Users/zeus/repos/devsupport-agent" })).toEqual({
kind: "local",
primary: "repos/devsupport-agent",
secondary: "/Users/zeus/repos/devsupport-agent",
});
});
it("falls back to the full path when there's only one segment", () => {
expect(displaySource({ type: "local", path: "/agent" })).toEqual({
kind: "local",
primary: "agent",
secondary: "/agent",
});
});
it("handles a trailing slash without producing an empty primary", () => {
const out = displaySource({ type: "local", path: "/Users/zeus/x/" });
expect(out.kind).toBe("local");
expect(out.primary.length).toBeGreaterThan(0);
});
});

describe("structured inline source", () => {
it("uses manifest.name when present", () => {
expect(
displaySource({ type: "inline", manifest: { name: "ad-hoc-bot" } }),
).toEqual({ kind: "inline", primary: "ad-hoc-bot", secondary: "inline manifest" });
});
it("falls back to 'inline' when manifest has no name", () => {
expect(displaySource({ type: "inline", manifest: { spec_version: "0.1.0" } })).toEqual({
kind: "inline",
primary: "inline",
secondary: "inline manifest",
});
});
it("falls back to 'inline' when manifest.name is not a string", () => {
expect(
displaySource({
type: "inline",
manifest: { name: 42 as unknown as string },
}),
).toEqual({ kind: "inline", primary: "inline", secondary: "inline manifest" });
});
});

describe("structured git source", () => {
it("parses an https URL with .git suffix into owner/repo + host + href", () => {
expect(
displaySource({
type: "git",
url: "https://github.com/open-gitagent/ComputerAgent.git",
}),
).toEqual({
kind: "git",
primary: "open-gitagent/ComputerAgent",
secondary: "github.com",
href: "https://github.com/open-gitagent/ComputerAgent",
});
});
it("parses an ssh git@ URL into owner/repo + host + https href", () => {
expect(
displaySource({ type: "git", url: "git@github.com:open-gitagent/opengap.git" }),
).toEqual({
kind: "git",
primary: "open-gitagent/opengap",
secondary: "github.com",
href: "https://github.com/open-gitagent/opengap",
});
});
it("appends /tree/<ref> to the href when ref is provided", () => {
expect(
displaySource({
type: "git",
url: "https://github.com/open-gitagent/ComputerAgent",
ref: "main",
}),
).toEqual({
kind: "git",
primary: "open-gitagent/ComputerAgent",
secondary: "github.com",
href: "https://github.com/open-gitagent/ComputerAgent/tree/main",
});
});
it("url-encodes a ref containing a slash", () => {
const out = displaySource({
type: "git",
url: "https://github.com/o/r",
ref: "feat/abc",
});
expect(out.href).toBe("https://github.com/o/r/tree/feat%2Fabc");
});
it("recognizes gitlab.com hosts", () => {
expect(
displaySource({ type: "git", url: "https://gitlab.com/my-org/my-repo" }),
).toEqual({
kind: "git",
primary: "my-org/my-repo",
secondary: "gitlab.com",
href: "https://gitlab.com/my-org/my-repo",
});
});
it("recognizes bitbucket.org hosts", () => {
expect(
displaySource({ type: "git", url: "https://bitbucket.org/team/proj.git" }),
).toEqual({
kind: "git",
primary: "team/proj",
secondary: "bitbucket.org",
href: "https://bitbucket.org/team/proj",
});
});
it("parses a scheme-less host/owner/repo", () => {
expect(displaySource({ type: "git", url: "github.com/o/r" })).toEqual({
kind: "git",
primary: "o/r",
secondary: "github.com",
href: "https://github.com/o/r",
});
});
it("treats a bare owner/repo (no host) as github by default", () => {
expect(displaySource({ type: "git", url: "open-gitagent/opengap" })).toEqual({
kind: "git",
primary: "open-gitagent/opengap",
secondary: "github.com",
href: "https://github.com/open-gitagent/opengap",
});
});
});

describe("legacy string source", () => {
it("treats a full https URL the same as the structured form", () => {
const expected = {
kind: "git" as const,
primary: "open-gitagent/ComputerAgent",
secondary: "github.com",
href: "https://github.com/open-gitagent/ComputerAgent",
};
expect(displaySource("https://github.com/open-gitagent/ComputerAgent")).toEqual(expected);
});
it("treats a bare owner/repo string as github", () => {
expect(displaySource("open-gitagent/opengap")).toEqual({
kind: "git",
primary: "open-gitagent/opengap",
secondary: "github.com",
href: "https://github.com/open-gitagent/opengap",
});
});
});

describe("unrecognized shapes fall back cleanly", () => {
it("returns kind=unknown with raw primary when there's only one path segment", () => {
expect(displaySource("just-a-name")).toEqual({
kind: "unknown",
primary: "just-a-name",
secondary: "",
});
});
});
});
Loading