mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Make chat mentions inline
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Command } from "commander";
|
||||
import { withOutput } from "../../output/index.js";
|
||||
import { addJsonAndDaemonHostOptions, collectMultiple } from "../../utils/command-options.js";
|
||||
import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
|
||||
import { runCreateCommand } from "./create.js";
|
||||
import { runLsCommand } from "./ls.js";
|
||||
import { runInspectCommand } from "./inspect.js";
|
||||
@@ -44,13 +44,7 @@ export function createChatCommand(): Command {
|
||||
.description("Post a chat message")
|
||||
.argument("<name-or-id>", "Room name or ID")
|
||||
.argument("<message>", "Message body")
|
||||
.option("--reply-to <msg-id>", "Reply to a specific message ID")
|
||||
.option(
|
||||
"--mention <agent-id>",
|
||||
"Mention an agent ID (repeatable)",
|
||||
collectMultiple,
|
||||
[],
|
||||
),
|
||||
.option("--reply-to <msg-id>", "Reply to a specific message ID"),
|
||||
).action(withOutput(runPostCommand));
|
||||
|
||||
addJsonAndDaemonHostOptions(
|
||||
|
||||
@@ -9,7 +9,6 @@ import { chatMessageSchema, type ChatMessageRow, toChatMessageRow } from "./sche
|
||||
|
||||
export interface ChatPostOptions extends ChatCommandOptions {
|
||||
replyTo?: string;
|
||||
mention?: string[];
|
||||
}
|
||||
|
||||
export async function runPostCommand(
|
||||
@@ -24,7 +23,6 @@ export async function runPostCommand(
|
||||
room,
|
||||
body,
|
||||
replyToMessageId: options.replyTo,
|
||||
mentionAgentIds: options.mention ?? [],
|
||||
});
|
||||
return {
|
||||
type: "single",
|
||||
|
||||
@@ -31,9 +31,7 @@ try {
|
||||
"chat",
|
||||
"post",
|
||||
"coord-room",
|
||||
"first message",
|
||||
"--mention",
|
||||
"agent-1",
|
||||
"first message for @agent-1",
|
||||
]);
|
||||
assert.strictEqual(posted.exitCode, 0, posted.stderr);
|
||||
assert(posted.stdout.includes("first message"), posted.stdout);
|
||||
|
||||
@@ -4,7 +4,7 @@ log.initialize({ spyRendererConsole: true });
|
||||
import path from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { existsSync } from "node:fs";
|
||||
import { app, BrowserWindow, nativeImage, net, protocol } from "electron";
|
||||
import { app, BrowserWindow, nativeImage, nativeTheme, net, protocol } from "electron";
|
||||
import { registerDaemonManager } from "./daemon/daemon-manager.js";
|
||||
import {
|
||||
parseCliPassthroughArgsFromArgv,
|
||||
@@ -32,6 +32,17 @@ protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: APP_SCHEME, privileges: { standard: true, secure: true, supportFetchAPI: true } },
|
||||
]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Window chrome
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getTitleBarOverlayColors(): Electron.TitleBarOverlayOptions {
|
||||
if (nativeTheme.shouldUseDarkColors) {
|
||||
return { color: "#18181c", symbolColor: "#e4e4e7", height: 48 };
|
||||
}
|
||||
return { color: "#ffffff", symbolColor: "#09090b", height: 48 };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Window creation
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -96,11 +107,7 @@ async function createMainWindow(): Promise<void> {
|
||||
...(isMac
|
||||
? { trafficLightPosition: { x: 16, y: 14 } }
|
||||
: {
|
||||
titleBarOverlay: {
|
||||
color: "#18181c",
|
||||
symbolColor: "#e4e4e7",
|
||||
height: 48,
|
||||
},
|
||||
titleBarOverlay: getTitleBarOverlayColors(),
|
||||
autoHideMenuBar: true,
|
||||
}),
|
||||
webPreferences: {
|
||||
@@ -113,6 +120,12 @@ async function createMainWindow(): Promise<void> {
|
||||
setupWindowResizeEvents(mainWindow);
|
||||
setupDragDropPrevention(mainWindow);
|
||||
|
||||
if (!isMac) {
|
||||
nativeTheme.on("updated", () => {
|
||||
mainWindow.setTitleBarOverlay(getTitleBarOverlayColors());
|
||||
});
|
||||
}
|
||||
|
||||
mainWindow.once("ready-to-show", () => {
|
||||
mainWindow.show();
|
||||
});
|
||||
|
||||
@@ -370,7 +370,6 @@ export type PostChatMessageOptions = {
|
||||
room: string;
|
||||
body: string;
|
||||
replyToMessageId?: string | null;
|
||||
mentionAgentIds?: string[];
|
||||
requestId?: string;
|
||||
};
|
||||
export type ReadChatMessagesOptions = {
|
||||
@@ -2913,9 +2912,6 @@ export class DaemonClient {
|
||||
room: options.room,
|
||||
body: options.body,
|
||||
...(options.replyToMessageId ? { replyToMessageId: options.replyToMessageId } : {}),
|
||||
...(options.mentionAgentIds && options.mentionAgentIds.length > 0
|
||||
? { mentionAgentIds: options.mentionAgentIds }
|
||||
: {}),
|
||||
},
|
||||
responseType: "chat/post/response",
|
||||
timeout: 10000,
|
||||
|
||||
@@ -31,7 +31,6 @@ export const ChatPostRequestSchema = z.object({
|
||||
room: z.string(),
|
||||
body: z.string(),
|
||||
replyToMessageId: z.string().optional(),
|
||||
mentionAgentIds: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export const ChatReadRequestSchema = z.object({
|
||||
|
||||
@@ -3,7 +3,7 @@ import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import pino from "pino";
|
||||
import { ChatServiceError, FileBackedChatService } from "./chat-service.js";
|
||||
import { ChatServiceError, FileBackedChatService, parseMentionAgentIds } from "./chat-service.js";
|
||||
|
||||
describe("FileBackedChatService", () => {
|
||||
let paseoHome: string;
|
||||
@@ -48,8 +48,7 @@ describe("FileBackedChatService", () => {
|
||||
const first = await service.postMessage({
|
||||
room: room.name,
|
||||
authorAgentId: "agent-a",
|
||||
body: "first message",
|
||||
mentionAgentIds: ["agent-b", "agent-b", "agent-c"],
|
||||
body: "first message for @agent-b and @agent-c and again @agent-b",
|
||||
});
|
||||
await service.postMessage({
|
||||
room: room.id,
|
||||
@@ -141,4 +140,11 @@ describe("FileBackedChatService", () => {
|
||||
code: "chat_room_not_found",
|
||||
});
|
||||
});
|
||||
|
||||
test("extracts inline mentions from chat bodies", () => {
|
||||
expect(
|
||||
parseMentionAgentIds("Checking with @agent-a, (@agent_b), and duplicate @agent-a again."),
|
||||
).toEqual(["agent-a", "agent_b"]);
|
||||
expect(parseMentionAgentIds("email@example.com is not a mention")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,6 +31,19 @@ function trimToNull(value: string | null | undefined): string | null {
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
const CHAT_MENTION_PATTERN = /(?:^|[\s(])@([A-Za-z0-9][A-Za-z0-9._-]*)/g;
|
||||
|
||||
export function parseMentionAgentIds(body: string): string[] {
|
||||
const mentionAgentIds = new Set<string>();
|
||||
for (const match of body.matchAll(CHAT_MENTION_PATTERN)) {
|
||||
const agentId = match[1]?.trim();
|
||||
if (agentId) {
|
||||
mentionAgentIds.add(agentId);
|
||||
}
|
||||
}
|
||||
return Array.from(mentionAgentIds).sort();
|
||||
}
|
||||
|
||||
export class ChatServiceError extends Error {
|
||||
readonly code: string;
|
||||
|
||||
@@ -67,7 +80,6 @@ export interface PostChatMessageInput {
|
||||
authorAgentId: string;
|
||||
body: string;
|
||||
replyToMessageId?: string | null;
|
||||
mentionAgentIds?: string[];
|
||||
}
|
||||
|
||||
export interface ReadChatMessagesInput {
|
||||
@@ -195,9 +207,7 @@ export class FileBackedChatService {
|
||||
authorAgentId,
|
||||
body,
|
||||
replyToMessageId,
|
||||
mentionAgentIds: [...new Set((input.mentionAgentIds ?? []).map((value) => value.trim()))]
|
||||
.filter((value) => value.length > 0)
|
||||
.sort(),
|
||||
mentionAgentIds: parseMentionAgentIds(body),
|
||||
createdAt,
|
||||
});
|
||||
|
||||
|
||||
@@ -7730,7 +7730,6 @@ export class Session {
|
||||
authorAgentId: this.clientId,
|
||||
body: request.body,
|
||||
replyToMessageId: request.replyToMessageId,
|
||||
mentionAgentIds: request.mentionAgentIds,
|
||||
});
|
||||
this.emit({
|
||||
type: "chat/post/response",
|
||||
@@ -7740,11 +7739,79 @@ export class Session {
|
||||
error: null,
|
||||
},
|
||||
});
|
||||
void this.notifyMentionedAgents({
|
||||
room: request.room,
|
||||
authorAgentId: this.clientId,
|
||||
body: request.body,
|
||||
mentionAgentIds: message.mentionAgentIds,
|
||||
});
|
||||
} catch (error) {
|
||||
this.emitChatRpcError(request, error);
|
||||
}
|
||||
}
|
||||
|
||||
private async notifyMentionedAgents(input: {
|
||||
room: string;
|
||||
authorAgentId: string;
|
||||
body: string;
|
||||
mentionAgentIds: string[];
|
||||
}): Promise<void> {
|
||||
const mentionAgentIds = input.mentionAgentIds.filter(
|
||||
(agentId) => agentId !== input.authorAgentId,
|
||||
);
|
||||
if (mentionAgentIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const notification = this.buildChatMentionNotification({
|
||||
...input,
|
||||
mentionAgentIds,
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
mentionAgentIds.map(async (mentionedAgentId) => {
|
||||
const resolved = await this.resolveAgentIdentifier(mentionedAgentId);
|
||||
if (!resolved.ok) {
|
||||
this.sessionLogger.warn(
|
||||
{ mentionedAgentId, room: input.room, error: resolved.error },
|
||||
"Failed to resolve chat mention target",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.handleSendAgentMessage(resolved.agentId, notification);
|
||||
} catch (error) {
|
||||
this.sessionLogger.warn(
|
||||
{ err: error, mentionedAgentId: resolved.agentId, room: input.room },
|
||||
"Failed to notify mentioned agent about chat message",
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private buildChatMentionNotification(input: {
|
||||
room: string;
|
||||
authorAgentId: string;
|
||||
body: string;
|
||||
mentionAgentIds: string[];
|
||||
}): string {
|
||||
const mentioned = input.mentionAgentIds.map((agentId) => `@${agentId}`).join(", ");
|
||||
const bodyWithoutMentions = input.body
|
||||
.replace(/(^|\s)@[A-Za-z0-9][A-Za-z0-9._-]*/g, "$1")
|
||||
.trim();
|
||||
const body = bodyWithoutMentions.length > 0 ? bodyWithoutMentions : input.body;
|
||||
|
||||
return [
|
||||
`Chat mention from ${input.authorAgentId} in room "${input.room}".`,
|
||||
`Mentioned agents: ${mentioned}.`,
|
||||
"Message:",
|
||||
body,
|
||||
`Read the room with: paseo chat read ${input.room} --limit 20`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
private async handleChatReadRequest(
|
||||
request: Extract<SessionInboundMessage, { type: "chat/read" }>,
|
||||
): Promise<void> {
|
||||
|
||||
@@ -27,10 +27,8 @@ When using chat:
|
||||
- check chat often while working
|
||||
|
||||
Mentions are active:
|
||||
- use `--mention <agent-id>` on your post to notify a specific agent immediately
|
||||
- if you use `--reply-to`, the author of the replied-to message is notified
|
||||
- notifications are sent via `paseo send --no-wait` under the hood
|
||||
- mentions **interrupt** the target agent, so only mention when they need to act now
|
||||
- write mentions inline in the message body as `@<agent-id>` to notify a specific agent immediately
|
||||
- notifications are sent to the target agent without blocking the chat post
|
||||
- if a normal post is enough and no one needs to act right now, skip the mention
|
||||
|
||||
## Command Surface
|
||||
@@ -68,7 +66,7 @@ paseo chat post issue-456 "I can take that next." --reply-to msg-001
|
||||
With a direct mention:
|
||||
|
||||
```bash
|
||||
paseo chat post issue-456 "Can you verify the relay path next?" --mention <agent-id>
|
||||
paseo chat post issue-456 "@<agent-id> Can you verify the relay path next?"
|
||||
```
|
||||
|
||||
### Read recent messages
|
||||
@@ -101,7 +99,7 @@ When using a room:
|
||||
- read only a bounded window before acting
|
||||
- post updates when they would help another agent or your future self
|
||||
- use `--reply-to` when responding to a specific message
|
||||
- use `--mention` when you want to get a specific agent's attention — but know that mentions **interrupt** the target agent
|
||||
- use inline `@<agent-id>` mentions when you want to get a specific agent's attention
|
||||
- check chat frequently enough that shared coordination actually works
|
||||
- your own agent ID is available via `$PASEO_AGENT_ID`
|
||||
|
||||
@@ -119,4 +117,4 @@ Typical things to post:
|
||||
3. Read the room with bounded history
|
||||
4. Post clearly
|
||||
5. Use `--reply-to` when replying to a specific message
|
||||
6. Use `--mention` when you want to notify someone directly
|
||||
6. Use inline `@<agent-id>` mentions when you want to notify someone directly
|
||||
|
||||
@@ -97,11 +97,10 @@ paseo chat post <room> "Focus on implementing the API layer. Acceptance criteria
|
||||
- all new endpoints have tests
|
||||
- typecheck passes
|
||||
|
||||
Post your progress here. @mention me ($PASEO_AGENT_ID) when done." \
|
||||
--mention <agent-id>
|
||||
Post your progress here. @$PASEO_AGENT_ID when done, and start on this now @<agent-id>."
|
||||
```
|
||||
|
||||
The agent gets interrupted with the message and starts working. When done, it @mentions you back.
|
||||
The agent gets notified with the message and starts working. When done, it mentions you back in chat.
|
||||
|
||||
### Role-based provider selection
|
||||
|
||||
@@ -148,13 +147,13 @@ paseo chat read <room> --limit 10
|
||||
### Directing work
|
||||
|
||||
```bash
|
||||
paseo chat post <room> "The API is done. Now focus on the frontend integration." --mention <agent-id>
|
||||
paseo chat post <room> "@<agent-id> The API is done. Now focus on the frontend integration."
|
||||
```
|
||||
|
||||
### Course-correcting
|
||||
|
||||
```bash
|
||||
paseo chat post <room> "The tests you wrote are asserting the mock, not the real implementation. Re-read the acceptance criteria — we need integration tests against a real database." --mention <agent-id>
|
||||
paseo chat post <room> "@<agent-id> The tests you wrote are asserting the mock, not the real implementation. Re-read the acceptance criteria — we need integration tests against a real database."
|
||||
```
|
||||
|
||||
### Challenging agents
|
||||
|
||||
@@ -133,7 +133,7 @@ paseo chat inspect <name-or-id>
|
||||
# Post a message
|
||||
paseo chat post <room> "<message>"
|
||||
paseo chat post <room> "<message>" --reply-to <msg-id>
|
||||
paseo chat post <room> "<message>" --mention <agent-id> # Repeatable
|
||||
paseo chat post <room> "@<agent-id> <message>"
|
||||
|
||||
# Read messages
|
||||
paseo chat read <room>
|
||||
|
||||
Reference in New Issue
Block a user