Skip to content
Open
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
8 changes: 5 additions & 3 deletions packages/daemon/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,21 @@ export class StreamServer {

constructor(options: StreamServerOptions = {}) {
this.port = options.port ?? DEFAULT_PORT;
const host = process.env.HOST ?? "0.0.0.0";

// Use in-memory storage during development (no dataDir = in-memory)
this.server = new DurableStreamTestServer({
port: this.port,
host: "127.0.0.1",
host,
});

this.streamUrl = `http://127.0.0.1:${this.port}${SESSIONS_STREAM_PATH}`;
this.streamUrl = `http://${host}:${this.port}${SESSIONS_STREAM_PATH}`;
}

async start(): Promise<void> {
await this.server.start();
log("Server", `Durable Streams server running on http://127.0.0.1:${this.port}`);
const host = process.env.HOST ?? "0.0.0.0";
log("Server", `Durable Streams server running on http://${host}:${this.port}`);

// Create or connect to the sessions stream
try {
Expand Down
44 changes: 44 additions & 0 deletions packages/ui/src/components/RepoDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Select } from "@radix-ui/themes";

interface RepoOption {
repoId: string;
repoUrl: string | null;
sessionCount: number;
statuses: Set<string>; // 'working', 'waiting', 'idle', 'needs-approval'
}

interface RepoDropdownProps {
repos: RepoOption[];
selectedRepo: string;
onSelectRepo: (repoId: string) => void;
}

function getStatusEmojis(statuses: Set<string>): string {
const emojis: string[] = [];
if (statuses.has("working")) emojis.push("🟒");
if (statuses.has("needs-approval")) emojis.push("🟠");
if (statuses.has("waiting")) emojis.push("🟑");
if (statuses.has("idle") && emojis.length === 0) emojis.push("βšͺ");
return emojis.join("");
}

export function RepoDropdown({ repos, selectedRepo, onSelectRepo }: RepoDropdownProps) {
return (
<Select.Root value={selectedRepo} onValueChange={onSelectRepo}>
<Select.Trigger
className="mobile-dropdown-trigger"
style={{ width: "100%" }}
/>
<Select.Content>
<Select.Item value="all">All Repos</Select.Item>
{repos.map((repo) => (
<Select.Item key={repo.repoId} value={repo.repoId}>
{getStatusEmojis(repo.statuses)} {repo.repoId} ({repo.sessionCount})
</Select.Item>
))}
</Select.Content>
</Select.Root>
);
}

export type { RepoOption };
133 changes: 133 additions & 0 deletions packages/ui/src/components/SessionCardCompact.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { Card, Flex, Text, Code, Badge } from "@radix-ui/themes";
import type { Session } from "../data/schema";

interface SessionCardCompactProps {
session: Session;
onClick?: () => void;
}

function getStatusEmoji(session: Session): string {
if (session.status === "working") return "🟒";
if (session.status === "waiting" && session.hasPendingToolUse) return "🟠";
if (session.status === "waiting") return "🟑";
return "βšͺ";
}

function formatTimeAgo(isoString: string): string {
const now = Date.now();
const then = new Date(isoString).getTime();
const diff = now - then;

const seconds = Math.floor(diff / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);

if (days > 0) return `${days}d ago`;
if (hours > 0) return `${hours}h ago`;
if (minutes > 0) return `${minutes}m ago`;
return `${seconds}s ago`;
}

function getCIStatusColor(status: string): "green" | "red" | "yellow" | "gray" {
switch (status) {
case "success":
return "green";
case "failure":
return "red";
case "running":
case "pending":
return "yellow";
default:
return "gray";
}
}

function getCIStatusIcon(status: string): string {
switch (status) {
case "success":
return "βœ“";
case "failure":
return "βœ—";
case "running":
case "pending":
return "β—Ž";
default:
return "?";
}
}

export function SessionCardCompact({ session, onClick }: SessionCardCompactProps) {
const repoName = session.gitRepoId || "Other";
const branch = session.gitBranch || "no branch";

return (
<Card
size="2"
className="session-card-compact"
style={{ cursor: onClick ? "pointer" : "default" }}
onClick={onClick}
>
<Flex direction="column" gap="2">
{/* Header: status + repo/branch + time */}
<Flex justify="between" align="center">
<Flex align="center" gap="2">
<Text size="2">{getStatusEmoji(session)}</Text>
<Text size="1" color="gray" weight="medium">
{repoName} β€’ {branch.length > 20 ? branch.slice(0, 17) + "..." : branch}
</Text>
</Flex>
<Text size="1" color="gray">
{formatTimeAgo(session.lastActivityAt)}
</Text>
</Flex>

{/* Goal/prompt */}
<Text size="2" weight="medium" highContrast>
{session.goal || session.originalPrompt.slice(0, 60)}
</Text>

{/* Summary or pending tool */}
<Text size="1" color="gray" style={{ lineHeight: 1.4 }}>
{session.hasPendingToolUse && session.pendingTool ? (
<>
⚠️ {session.pendingTool.tool}: {session.pendingTool.target.slice(0, 40)}
</>
) : (
session.summary
)}
</Text>

{/* Footer: PR/branch + message count */}
<Flex justify="between" align="center">
<Flex align="center" gap="2">
{session.pr ? (
<a
href={session.pr.url}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
style={{ textDecoration: "none" }}
>
<Badge
color={getCIStatusColor(session.pr.ciStatus)}
variant="soft"
size="1"
>
{getCIStatusIcon(session.pr.ciStatus)} PR #{session.pr.number}
</Badge>
</a>
) : session.gitBranch ? (
<Code size="1" variant="soft" color="gray">
[{session.gitBranch.slice(0, 20)}]
</Code>
) : null}
</Flex>
<Text size="1" color="gray">
{session.messageCount} msgs
</Text>
</Flex>
</Flex>
</Card>
);
}
Loading