feat: add chat and website updates
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import type { Command } from "commander";
|
||||
import type { SingleResult } from "../../output/index.js";
|
||||
import {
|
||||
attachAgentNamesToMessages,
|
||||
connectChatClient,
|
||||
resolveChatAuthorAgentId,
|
||||
toChatCommandError,
|
||||
type ChatCommandOptions,
|
||||
} from "./shared.js";
|
||||
@@ -22,11 +24,13 @@ export async function runPostCommand(
|
||||
const payload = await client.postChatMessage({
|
||||
room,
|
||||
body,
|
||||
authorAgentId: resolveChatAuthorAgentId(),
|
||||
replyToMessageId: options.replyTo,
|
||||
});
|
||||
const [message] = await attachAgentNamesToMessages(client, [toChatMessageRow(payload.message!)]);
|
||||
return {
|
||||
type: "single",
|
||||
data: toChatMessageRow(payload.message!),
|
||||
data: message!,
|
||||
schema: chatMessageSchema,
|
||||
};
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Command } from "commander";
|
||||
import type { ListResult } from "../../output/index.js";
|
||||
import {
|
||||
attachAgentNamesToMessages,
|
||||
connectChatClient,
|
||||
parseSinceValue,
|
||||
toChatCommandError,
|
||||
@@ -42,9 +43,13 @@ export async function runReadCommand(
|
||||
since: parseSinceValue(options.since),
|
||||
authorAgentId: options.agent,
|
||||
});
|
||||
const messages = await attachAgentNamesToMessages(
|
||||
client,
|
||||
payload.messages.map(toChatMessageRow),
|
||||
);
|
||||
return {
|
||||
type: "list",
|
||||
data: payload.messages.map(toChatMessageRow),
|
||||
data: messages,
|
||||
schema: chatMessageSchema,
|
||||
};
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { OutputSchema } from "../../output/index.js";
|
||||
import type { AnyCommandResult, OutputOptions } from "../../output/index.js";
|
||||
|
||||
export interface ChatRoomRow {
|
||||
name: string;
|
||||
@@ -11,9 +12,11 @@ export interface ChatRoomRow {
|
||||
export interface ChatMessageRow {
|
||||
id: string;
|
||||
author: string;
|
||||
authorName: string | null;
|
||||
createdAt: string;
|
||||
replyTo: string;
|
||||
mentions: string;
|
||||
mentionAgentIds: string[];
|
||||
mentionLabels: string[];
|
||||
body: string;
|
||||
}
|
||||
|
||||
@@ -33,13 +36,59 @@ export const chatMessageSchema: OutputSchema<ChatMessageRow> = {
|
||||
columns: [
|
||||
{ header: "ID", field: "id", width: 36 },
|
||||
{ header: "AUTHOR", field: "author", width: 16 },
|
||||
{ header: "AUTHOR NAME", field: (row) => row.authorName ?? "-", width: 20 },
|
||||
{ header: "CREATED", field: "createdAt", width: 24 },
|
||||
{ header: "REPLY TO", field: "replyTo", width: 36 },
|
||||
{ header: "MENTIONS", field: "mentions", width: 24 },
|
||||
{ header: "MENTIONS", field: (row) => row.mentionLabels.join(", ") || "-", width: 24 },
|
||||
{ header: "MESSAGE", field: "body", width: 60 },
|
||||
],
|
||||
renderHuman: renderChatTranscript,
|
||||
};
|
||||
|
||||
function renderChatTranscript(
|
||||
result: AnyCommandResult<ChatMessageRow>,
|
||||
_options: OutputOptions,
|
||||
): string {
|
||||
const data = result.type === "list" ? result.data : [result.data];
|
||||
if (data.length === 0) {
|
||||
return "";
|
||||
}
|
||||
return data.map(renderChatMessageBlock).join("\n\n");
|
||||
}
|
||||
|
||||
function renderChatMessageBlock(message: ChatMessageRow): string {
|
||||
const authorLabel = message.authorName
|
||||
? `${message.authorName} (${message.author})`
|
||||
: message.author;
|
||||
const lines = [`┌─ ${authorLabel} ── ${formatTimestamp(message.createdAt)} ── [msg ${message.id}]`];
|
||||
|
||||
if (message.replyTo !== "-") {
|
||||
lines.push(`│ reply-to: msg ${message.replyTo}`);
|
||||
}
|
||||
if (message.mentionLabels.length > 0) {
|
||||
lines.push(`│ mentions: ${message.mentionLabels.join(", ")}`);
|
||||
}
|
||||
|
||||
lines.push("│");
|
||||
|
||||
const bodyLines = message.body.split(/\r?\n/);
|
||||
for (const line of bodyLines) {
|
||||
lines.push(`│ ${line}`);
|
||||
}
|
||||
|
||||
lines.push("│");
|
||||
lines.push("└─");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function formatTimestamp(input: string): string {
|
||||
const date = new Date(input);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return input;
|
||||
}
|
||||
return date.toISOString().replace("T", " ").replace(".000Z", "Z");
|
||||
}
|
||||
|
||||
export function toChatRoomRow(room: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -67,9 +116,11 @@ export function toChatMessageRow(message: {
|
||||
return {
|
||||
id: message.id,
|
||||
author: message.authorAgentId,
|
||||
authorName: null,
|
||||
createdAt: message.createdAt,
|
||||
replyTo: message.replyToMessageId ?? "-",
|
||||
mentions: message.mentionAgentIds.length > 0 ? message.mentionAgentIds.join(",") : "-",
|
||||
mentionAgentIds: message.mentionAgentIds,
|
||||
mentionLabels: message.mentionAgentIds,
|
||||
body: message.body,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
||||
import { parseDuration } from "../../utils/duration.js";
|
||||
import type { CommandError, CommandOptions } from "../../output/index.js";
|
||||
import type { ChatMessageRow } from "./schema.js";
|
||||
|
||||
export interface ChatCommandOptions extends CommandOptions {
|
||||
host?: string;
|
||||
@@ -22,6 +23,43 @@ export async function connectChatClient(host?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function attachAgentNamesToMessages(
|
||||
client: Awaited<ReturnType<typeof connectToDaemon>>,
|
||||
messages: ChatMessageRow[],
|
||||
): Promise<ChatMessageRow[]> {
|
||||
const agentIds = new Set<string>();
|
||||
for (const message of messages) {
|
||||
agentIds.add(message.author);
|
||||
for (const mentionId of message.mentionAgentIds) {
|
||||
agentIds.add(mentionId);
|
||||
}
|
||||
}
|
||||
|
||||
if (agentIds.size === 0) {
|
||||
return messages;
|
||||
}
|
||||
|
||||
const payload = await client.fetchAgents({
|
||||
filter: { includeArchived: true },
|
||||
});
|
||||
const agentNames = new Map<string, string>();
|
||||
for (const entry of payload.entries) {
|
||||
const title = entry.agent.title?.trim();
|
||||
if (title) {
|
||||
agentNames.set(entry.agent.id, title);
|
||||
}
|
||||
}
|
||||
|
||||
return messages.map((message) => ({
|
||||
...message,
|
||||
authorName: agentNames.get(message.author) ?? null,
|
||||
mentionLabels: message.mentionAgentIds.map((agentId) => {
|
||||
const name = agentNames.get(agentId);
|
||||
return name ? `${name} (${agentId})` : agentId;
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
export function toChatCommandError(code: string, action: string, err: unknown): CommandError {
|
||||
if (err && typeof err === "object" && "code" in err && "message" in err) {
|
||||
return err as CommandError;
|
||||
@@ -82,3 +120,8 @@ export function parseTimeoutMs(input?: string): number | undefined {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveChatAuthorAgentId(): string {
|
||||
const agentId = process.env.PASEO_AGENT_ID?.trim();
|
||||
return agentId && agentId.length > 0 ? agentId : "manual";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Command } from "commander";
|
||||
import type { ListResult } from "../../output/index.js";
|
||||
import {
|
||||
attachAgentNamesToMessages,
|
||||
connectChatClient,
|
||||
parseTimeoutMs,
|
||||
toChatCommandError,
|
||||
@@ -29,9 +30,13 @@ export async function runWaitCommand(
|
||||
afterMessageId,
|
||||
timeoutMs: parseTimeoutMs(options.timeout),
|
||||
});
|
||||
const messages = await attachAgentNamesToMessages(
|
||||
client,
|
||||
payload.messages.map(toChatMessageRow),
|
||||
);
|
||||
return {
|
||||
type: "list",
|
||||
data: payload.messages.map(toChatMessageRow),
|
||||
data: messages,
|
||||
schema: chatMessageSchema,
|
||||
};
|
||||
} catch (err) {
|
||||
|
||||
@@ -40,6 +40,9 @@ export function render<T>(
|
||||
return renderYaml(result, opts);
|
||||
case "table":
|
||||
default:
|
||||
if (result.schema.renderHuman) {
|
||||
return result.schema.renderHuman(result, opts);
|
||||
}
|
||||
return renderTable(result, opts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ export interface OutputSchema<T> {
|
||||
idField: keyof T | ((item: T) => string);
|
||||
/** Column definitions for table output */
|
||||
columns: ColumnDef<T>[];
|
||||
/** Optional: custom renderer for human/table output */
|
||||
renderHuman?: (result: AnyCommandResult<T>, options: OutputOptions) => string;
|
||||
/** Optional: transform data before JSON/YAML output */
|
||||
serialize?: (data: T) => unknown;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,17 @@ interface Agent {
|
||||
provider: string;
|
||||
}
|
||||
|
||||
interface ChatMessage {
|
||||
id: string;
|
||||
author: string;
|
||||
authorName: string | null;
|
||||
createdAt: string;
|
||||
replyTo: string;
|
||||
mentionAgentIds: string[];
|
||||
mentionLabels: string[];
|
||||
body: string;
|
||||
}
|
||||
|
||||
// Schema for agents
|
||||
const agentSchema: OutputSchema<Agent> = {
|
||||
idField: "id",
|
||||
@@ -211,6 +222,36 @@ test("render uses table format by default", () => {
|
||||
assert.ok(output.includes("ID"), "Should use table format with headers");
|
||||
});
|
||||
|
||||
test("render uses custom human renderer when provided", () => {
|
||||
const chatSchema: OutputSchema<ChatMessage> = {
|
||||
idField: "id",
|
||||
columns: [{ header: "ID", field: "id" }],
|
||||
renderHuman: (result) => {
|
||||
const data = result.type === "list" ? result.data : [result.data];
|
||||
return data.map((message) => `msg ${message.id}: ${message.body}`).join("\n");
|
||||
},
|
||||
};
|
||||
const chatResult: ListResult<ChatMessage> = {
|
||||
type: "list",
|
||||
data: [
|
||||
{
|
||||
id: "m1",
|
||||
author: "agent-1",
|
||||
authorName: "Planner",
|
||||
createdAt: "2026-03-29T10:00:00Z",
|
||||
replyTo: "-",
|
||||
mentionAgentIds: [],
|
||||
mentionLabels: [],
|
||||
body: "hello",
|
||||
},
|
||||
],
|
||||
schema: chatSchema,
|
||||
};
|
||||
|
||||
const output = render(chatResult, { noColor: true });
|
||||
assert.strictEqual(output, "msg m1: hello", "Should use custom human renderer");
|
||||
});
|
||||
|
||||
test("render uses json format when specified", () => {
|
||||
const output = render(listResult, { format: "json" });
|
||||
const parsed = JSON.parse(output);
|
||||
|
||||
@@ -32,13 +32,22 @@ try {
|
||||
"post",
|
||||
"coord-room",
|
||||
"first message for @agent-1",
|
||||
]);
|
||||
], {
|
||||
env: { PASEO_AGENT_ID: "00000000-0000-4000-8000-000000000111" },
|
||||
});
|
||||
assert.strictEqual(posted.exitCode, 0, posted.stderr);
|
||||
assert(posted.stdout.includes("first message"), posted.stdout);
|
||||
assert(posted.stdout.includes("00000000-0000-4000-8000-000000000111"), posted.stdout);
|
||||
|
||||
const read = await ctx.paseo(["chat", "read", "coord-room", "--limit", "10"]);
|
||||
assert.strictEqual(read.exitCode, 0, read.stderr);
|
||||
assert(read.stdout.includes("first message"), read.stdout);
|
||||
assert(read.stdout.includes("00000000-0000-4000-8000-000000000111"), read.stdout);
|
||||
|
||||
const readJson = await ctx.paseo(["chat", "read", "coord-room", "--limit", "10", "--json"]);
|
||||
assert.strictEqual(readJson.exitCode, 0, readJson.stderr);
|
||||
const readPayload = JSON.parse(readJson.stdout);
|
||||
assert.strictEqual(readPayload[0]?.author, "00000000-0000-4000-8000-000000000111");
|
||||
|
||||
const waitPromise = ctx.paseo(["chat", "wait", "coord-room", "--timeout", "5s"]);
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
|
||||
@@ -333,6 +333,7 @@ export async function runPaseoCli(
|
||||
options?: {
|
||||
timeout?: number;
|
||||
cwd?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
},
|
||||
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
|
||||
const timeout = options?.timeout ?? 60000;
|
||||
@@ -348,6 +349,7 @@ export async function runPaseoCli(
|
||||
...TEST_DAEMON_ENV_DEFAULTS,
|
||||
PASEO_HOST: `${TEST_DAEMON_HOST}:${ctx.port}`,
|
||||
PASEO_HOME: ctx.paseoHome,
|
||||
...options?.env,
|
||||
},
|
||||
cwd,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
@@ -399,7 +401,7 @@ export async function createE2ETestContext(options?: { timeout?: number }): Prom
|
||||
/** Run a paseo CLI command against this daemon */
|
||||
paseo: (
|
||||
args: string[],
|
||||
opts?: { timeout?: number; cwd?: string },
|
||||
opts?: { timeout?: number; cwd?: string; env?: NodeJS.ProcessEnv },
|
||||
) => Promise<{
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
@@ -409,7 +411,7 @@ export async function createE2ETestContext(options?: { timeout?: number }): Prom
|
||||
> {
|
||||
const ctx = await startTestDaemon({ timeout: options?.timeout });
|
||||
|
||||
const paseo = (args: string[], opts?: { timeout?: number; cwd?: string }) =>
|
||||
const paseo = (args: string[], opts?: { timeout?: number; cwd?: string; env?: NodeJS.ProcessEnv }) =>
|
||||
runPaseoCli(ctx, args, opts);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user