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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,19 @@ Agent Teams Chat — cross-developer agent communication via Slack threads. Enab
- Search threads by topic or content
- Configurable message format using handlebars-style templates

### [@arvoretech/agent-teams-ui](./packages/agent-teams-ui)

Agent Teams UI — TUI dashboard for monitoring agent teams in real time. Watches `.agent-teams/` and auto-refreshes.

**Features:**

- Team view with per-teammate task counts and current activity
- Kanban board with task selection and detail view
- WhatsApp-style message view with date separators and sender grouping
- Chat view showing agent stdout as conversation, filterable by agent
- Live timer, progress bar, and new-activity indicators per tab
- Auto-refresh via filesystem watching (chokidar)

### [@arvoretech/metabase-mcp](./packages/metabase)

Interact with Metabase BI platform directly from your AI assistant.
Expand Down
42 changes: 41 additions & 1 deletion packages/agent-teams-lead/src/spawner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { writeFile, mkdir, readFile, unlink, appendFile } from "node:fs/promises
import { existsSync } from "node:fs";
import { createRequire } from "node:module";
import { join, resolve } from "node:path";
import type { Teammate } from "./types.js";
import type { Teammate, Message } from "./types.js";

interface SpawnedProcess {
teammateId: string;
Expand Down Expand Up @@ -135,6 +135,7 @@ export class TeammateSpawner {

proc.on("exit", (code) => {
this.log(teammate.name, `Exited with code ${code}`);
this.deliverPendingMessages(teammate.id, teammate.name);
this.processes.delete(teammate.id);
this.cleanupAgentConfig(configPath);
});
Expand Down Expand Up @@ -371,4 +372,43 @@ export class TeammateSpawner {
// noop
}
}

private async deliverPendingMessages(
teammateId: string,
teammateName: string
): Promise<void> {
try {
const messagesPath = join(
this.workspacePath,
".agent-teams",
"messages.json"
);
if (!existsSync(messagesPath)) return;

const raw = await readFile(messagesPath, "utf-8");
const messages = JSON.parse(raw) as Message[];

const pending = messages.filter(
(m) =>
(m.to === teammateId || m.to === "broadcast") &&
!m.read_by.includes(teammateId)
);

if (pending.length === 0) return;

await this.log(
teammateName,
`[pending] ${pending.length} unread message(s) on exit:`
);

for (const msg of pending) {
await this.log(
teammateName,
` [${msg.kind}] from ${msg.from_name}: ${msg.subject} — ${msg.body}`
);
}
} catch {
// noop
}
}
}
12 changes: 12 additions & 0 deletions packages/agent-teams-lead/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,18 @@ export class TeamStore {
return this.artifacts.find((a) => a.id === artifactId);
}

async ackMessages(readerId: string, messageIds: string[]): Promise<void> {
return withFileLock(this.messagesPath(), async () => {
this.messages = await this.readJson<Message[]>(this.messagesPath(), []);
for (const msg of this.messages) {
if (messageIds.includes(msg.id) && !msg.read_by.includes(readerId)) {
msg.read_by.push(readerId);
}
}
await this.writeJson(this.messagesPath(), this.messages);
});
}

getArtifacts(): Artifact[] {
return [...this.artifacts];
}
Expand Down
34 changes: 34 additions & 0 deletions packages/agent-teams-lead/src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,15 @@ export class LeadTools {
(inProgress.length === 0 && pending.length === 0) ||
allTeammatesDone
) {
const leadMessages = this.store.getMessages({
to: "lead",
unread_by: "lead",
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (leadMessages.length > 0) {
await this.store.ackMessages("lead", leadMessages.map((m) => m.id));
}

return this.ok({
done: true,
reason:
Expand All @@ -227,13 +236,30 @@ export class LeadTools {
title: t.title,
notes: t.notes,
})),
pending_messages: leadMessages.map((m) => ({
id: m.id,
from_name: m.from_name,
kind: m.kind,
subject: m.subject,
body: m.body,
created_at: m.created_at,
})),
});
}

await new Promise((resolve) => setTimeout(resolve, pollInterval));
}

const tasks = this.store.getTasks();
const leadMessagesOnTimeout = this.store.getMessages({
to: "lead",
unread_by: "lead",
});

if (leadMessagesOnTimeout.length > 0) {
await this.store.ackMessages("lead", leadMessagesOnTimeout.map((m) => m.id));
}

return this.ok({
done: false,
timed_out: true,
Expand All @@ -254,6 +280,14 @@ export class LeadTools {
blocked: tasks
.filter((t) => t.status === "blocked")
.map((t) => ({ id: t.id, title: t.title })),
pending_messages: leadMessagesOnTimeout.map((m) => ({
id: m.id,
from_name: m.from_name,
kind: m.kind,
subject: m.subject,
body: m.body,
created_at: m.created_at,
})),
});
} catch (error) {
return this.errorResult(error);
Expand Down
70 changes: 70 additions & 0 deletions packages/agent-teams-ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# @arvoretech/agent-teams-ui

TUI dashboard for monitoring agent teams in real time. Built with [Ink](https://github.com/vadimdemedes/ink) (React for the terminal).

## Usage

```bash
npx @arvoretech/agent-teams-ui [workspace-path]
```

If no path is provided, uses the current directory. The TUI watches `.agent-teams/` for changes and auto-refreshes.

## Tabs

### [1] Team

Teammates with status, task counts `[done/total]`, and current activity with elapsed time.

### [2] Board

Kanban columns: Pending | In Progress | Blocked | Done.

- `j/k` — select task
- `Enter` — open detail view (description, criteria, summary, touched files, notes)
- `Esc` — back to board

### [3] Messages

Inter-agent messages displayed as a group chat with date separators, sender grouping, and kind tags (`info`, `question`, `blocker`, `decision`, `answer`).

### [4] Chat

Agent stdout/stderr rendered as a conversation. Each agent gets a distinct color.

- `f` — cycle filter by agent (or show all)
- `j/k` — scroll up/down
- `g/G` — jump to top/bottom

## Header

- Live elapsed timer since team creation
- Progress bar `[done/total tasks X%]`
- `*` indicator on tabs with new activity

## Keyboard

| Key | Action |
|-----|--------|
| `1-4` | Switch tab |
| `Tab` / `Arrow` | Navigate tabs |
| `r` | Manual refresh |
| `q` / `Ctrl+C` | Quit |

## Development

```bash
pnpm install
pnpm dev # tsx watch mode
pnpm build # tsc
```

## How it works

Reads JSON files from `.agent-teams/` (same files used by `agent-teams-lead` and `agent-teams-teammate`):

- `team.json` — team config and teammates
- `tasks.json` — task list with status
- `messages.json` — inter-agent messages
- `artifacts.json` — published artifacts
- `team.log` — stdout/stderr from teammate processes
32 changes: 32 additions & 0 deletions packages/agent-teams-ui/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@arvoretech/agent-teams-ui",
"version": "0.1.0",
"description": "TUI dashboard for agent teams — view teams, tasks, messages, and chats",
"main": "dist/index.js",
"type": "module",
"bin": {
"agent-teams-ui": "./dist/index.js"
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"scripts": {
"build": "tsc && echo '#!/usr/bin/env node' | cat - dist/index.js > dist/index.js.tmp && mv dist/index.js.tmp dist/index.js",
"dev": "tsx src/index.tsx",
"start": "node dist/index.js"
},
"dependencies": {
"ink": "^6.8.0",
"ink-spinner": "^5.0.0",
"react": "^19.2.4",
"chokidar": "^4.0.0"
},
"devDependencies": {
"@types/node": "^20.10.0",
"@types/react": "^19.2.14",
"tsx": "^4.6.0",
"typescript": "^5.3.0"
},
"author": "Arvore",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
}
Loading
Loading