diff --git a/v2/src/lib/adapters/btmsg-bridge.test.ts b/v2/src/lib/adapters/btmsg-bridge.test.ts index e92bc11..72c7ba7 100644 --- a/v2/src/lib/adapters/btmsg-bridge.test.ts +++ b/v2/src/lib/adapters/btmsg-bridge.test.ts @@ -29,6 +29,7 @@ import { type BtmsgChannel, type BtmsgChannelMessage, } from './btmsg-bridge'; +import { GroupId, AgentId } from '../types/ids'; beforeEach(() => { vi.clearAllMocks(); @@ -42,10 +43,10 @@ describe('btmsg-bridge', () => { describe('BtmsgAgent camelCase fields', () => { it('receives camelCase fields from Rust backend', async () => { const agent: BtmsgAgent = { - id: 'a1', + id: AgentId('a1'), name: 'Coder', role: 'developer', - groupId: 'g1', // was: group_id + groupId: GroupId('g1'), // was: group_id tier: 1, model: 'claude-4', status: 'active', @@ -53,7 +54,7 @@ describe('btmsg-bridge', () => { }; mockInvoke.mockResolvedValue([agent]); - const result = await getGroupAgents('g1'); + const result = await getGroupAgents(GroupId('g1')); expect(result).toHaveLength(1); expect(result[0].groupId).toBe('g1'); @@ -65,7 +66,7 @@ describe('btmsg-bridge', () => { it('invokes btmsg_get_agents with groupId', async () => { mockInvoke.mockResolvedValue([]); - await getGroupAgents('g1'); + await getGroupAgents(GroupId('g1')); expect(mockInvoke).toHaveBeenCalledWith('btmsg_get_agents', { groupId: 'g1' }); }); }); @@ -74,8 +75,8 @@ describe('btmsg-bridge', () => { it('receives camelCase fields from Rust backend', async () => { const msg: BtmsgMessage = { id: 'm1', - fromAgent: 'a1', // was: from_agent - toAgent: 'a2', // was: to_agent + fromAgent: AgentId('a1'), // was: from_agent + toAgent: AgentId('a2'), // was: to_agent content: 'hello', read: false, replyTo: null, // was: reply_to @@ -85,7 +86,7 @@ describe('btmsg-bridge', () => { }; mockInvoke.mockResolvedValue([msg]); - const result = await getUnreadMessages('a2'); + const result = await getUnreadMessages(AgentId('a2')); expect(result[0].fromAgent).toBe('a1'); expect(result[0].toAgent).toBe('a2'); @@ -100,8 +101,8 @@ describe('btmsg-bridge', () => { it('receives camelCase fields including recipient info', async () => { const feed: BtmsgFeedMessage = { id: 'm1', - fromAgent: 'a1', - toAgent: 'a2', + fromAgent: AgentId('a1'), + toAgent: AgentId('a2'), content: 'review this', createdAt: '2026-01-01', replyTo: null, @@ -112,7 +113,7 @@ describe('btmsg-bridge', () => { }; mockInvoke.mockResolvedValue([feed]); - const result = await getAllFeed('g1'); + const result = await getAllFeed(GroupId('g1')); expect(result[0].senderName).toBe('Coder'); expect(result[0].recipientName).toBe('Reviewer'); @@ -125,14 +126,14 @@ describe('btmsg-bridge', () => { const channel: BtmsgChannel = { id: 'ch1', name: 'general', - groupId: 'g1', // was: group_id - createdBy: 'admin', // was: created_by + groupId: GroupId('g1'), // was: group_id + createdBy: AgentId('admin'), // was: created_by memberCount: 5, // was: member_count createdAt: '2026-01-01', }; mockInvoke.mockResolvedValue([channel]); - const result = await getChannels('g1'); + const result = await getChannels(GroupId('g1')); expect(result[0].groupId).toBe('g1'); expect(result[0].createdBy).toBe('admin'); @@ -145,7 +146,7 @@ describe('btmsg-bridge', () => { const msg: BtmsgChannelMessage = { id: 'cm1', channelId: 'ch1', // was: channel_id - fromAgent: 'a1', + fromAgent: AgentId('a1'), content: 'hello', createdAt: '2026-01-01', senderName: 'Coder', @@ -166,65 +167,65 @@ describe('btmsg-bridge', () => { describe('IPC commands', () => { it('getUnreadCount invokes btmsg_unread_count', async () => { mockInvoke.mockResolvedValue(5); - const result = await getUnreadCount('a1'); + const result = await getUnreadCount(AgentId('a1')); expect(result).toBe(5); expect(mockInvoke).toHaveBeenCalledWith('btmsg_unread_count', { agentId: 'a1' }); }); it('getHistory invokes btmsg_history', async () => { mockInvoke.mockResolvedValue([]); - await getHistory('a1', 'a2', 50); + await getHistory(AgentId('a1'), AgentId('a2'), 50); expect(mockInvoke).toHaveBeenCalledWith('btmsg_history', { agentId: 'a1', otherId: 'a2', limit: 50 }); }); it('getHistory defaults limit to 20', async () => { mockInvoke.mockResolvedValue([]); - await getHistory('a1', 'a2'); + await getHistory(AgentId('a1'), AgentId('a2')); expect(mockInvoke).toHaveBeenCalledWith('btmsg_history', { agentId: 'a1', otherId: 'a2', limit: 20 }); }); it('sendMessage invokes btmsg_send', async () => { mockInvoke.mockResolvedValue('msg-id'); - const result = await sendMessage('a1', 'a2', 'hello'); + const result = await sendMessage(AgentId('a1'), AgentId('a2'), 'hello'); expect(result).toBe('msg-id'); expect(mockInvoke).toHaveBeenCalledWith('btmsg_send', { fromAgent: 'a1', toAgent: 'a2', content: 'hello' }); }); it('setAgentStatus invokes btmsg_set_status', async () => { mockInvoke.mockResolvedValue(undefined); - await setAgentStatus('a1', 'active'); + await setAgentStatus(AgentId('a1'), 'active'); expect(mockInvoke).toHaveBeenCalledWith('btmsg_set_status', { agentId: 'a1', status: 'active' }); }); it('ensureAdmin invokes btmsg_ensure_admin', async () => { mockInvoke.mockResolvedValue(undefined); - await ensureAdmin('g1'); + await ensureAdmin(GroupId('g1')); expect(mockInvoke).toHaveBeenCalledWith('btmsg_ensure_admin', { groupId: 'g1' }); }); it('markRead invokes btmsg_mark_read', async () => { mockInvoke.mockResolvedValue(undefined); - await markRead('a2', 'a1'); + await markRead(AgentId('a2'), AgentId('a1')); expect(mockInvoke).toHaveBeenCalledWith('btmsg_mark_read', { readerId: 'a2', senderId: 'a1' }); }); it('sendChannelMessage invokes btmsg_channel_send', async () => { mockInvoke.mockResolvedValue('cm-id'); - const result = await sendChannelMessage('ch1', 'a1', 'hello channel'); + const result = await sendChannelMessage('ch1', AgentId('a1'), 'hello channel'); expect(result).toBe('cm-id'); expect(mockInvoke).toHaveBeenCalledWith('btmsg_channel_send', { channelId: 'ch1', fromAgent: 'a1', content: 'hello channel' }); }); it('createChannel invokes btmsg_create_channel', async () => { mockInvoke.mockResolvedValue('ch-id'); - const result = await createChannel('general', 'g1', 'admin'); + const result = await createChannel('general', GroupId('g1'), AgentId('admin')); expect(result).toBe('ch-id'); expect(mockInvoke).toHaveBeenCalledWith('btmsg_create_channel', { name: 'general', groupId: 'g1', createdBy: 'admin' }); }); it('addChannelMember invokes btmsg_add_channel_member', async () => { mockInvoke.mockResolvedValue(undefined); - await addChannelMember('ch1', 'a1'); + await addChannelMember('ch1', AgentId('a1')); expect(mockInvoke).toHaveBeenCalledWith('btmsg_add_channel_member', { channelId: 'ch1', agentId: 'a1' }); }); }); @@ -232,7 +233,7 @@ describe('btmsg-bridge', () => { describe('error propagation', () => { it('propagates invoke errors', async () => { mockInvoke.mockRejectedValue(new Error('btmsg database not found')); - await expect(getGroupAgents('g1')).rejects.toThrow('btmsg database not found'); + await expect(getGroupAgents(GroupId('g1'))).rejects.toThrow('btmsg database not found'); }); }); }); diff --git a/v2/src/lib/adapters/btmsg-bridge.ts b/v2/src/lib/adapters/btmsg-bridge.ts index 682bf26..b78fdee 100644 --- a/v2/src/lib/adapters/btmsg-bridge.ts +++ b/v2/src/lib/adapters/btmsg-bridge.ts @@ -5,12 +5,13 @@ */ import { invoke } from '@tauri-apps/api/core'; +import type { GroupId, AgentId } from '../types/ids'; export interface BtmsgAgent { - id: string; + id: AgentId; name: string; role: string; - groupId: string; + groupId: GroupId; tier: number; model: string | null; status: string; @@ -19,8 +20,8 @@ export interface BtmsgAgent { export interface BtmsgMessage { id: string; - fromAgent: string; - toAgent: string; + fromAgent: AgentId; + toAgent: AgentId; content: string; read: boolean; replyTo: string | null; @@ -31,8 +32,8 @@ export interface BtmsgMessage { export interface BtmsgFeedMessage { id: string; - fromAgent: string; - toAgent: string; + fromAgent: AgentId; + toAgent: AgentId; content: string; createdAt: string; replyTo: string | null; @@ -45,8 +46,8 @@ export interface BtmsgFeedMessage { export interface BtmsgChannel { id: string; name: string; - groupId: string; - createdBy: string; + groupId: GroupId; + createdBy: AgentId; memberCount: number; createdAt: string; } @@ -54,7 +55,7 @@ export interface BtmsgChannel { export interface BtmsgChannelMessage { id: string; channelId: string; - fromAgent: string; + fromAgent: AgentId; content: string; createdAt: string; senderName: string; @@ -64,70 +65,70 @@ export interface BtmsgChannelMessage { /** * Get all agents in a group with their unread counts. */ -export async function getGroupAgents(groupId: string): Promise { +export async function getGroupAgents(groupId: GroupId): Promise { return invoke('btmsg_get_agents', { groupId }); } /** * Get unread message count for an agent. */ -export async function getUnreadCount(agentId: string): Promise { +export async function getUnreadCount(agentId: AgentId): Promise { return invoke('btmsg_unread_count', { agentId }); } /** * Get unread messages for an agent. */ -export async function getUnreadMessages(agentId: string): Promise { +export async function getUnreadMessages(agentId: AgentId): Promise { return invoke('btmsg_unread_messages', { agentId }); } /** * Get conversation history between two agents. */ -export async function getHistory(agentId: string, otherId: string, limit: number = 20): Promise { +export async function getHistory(agentId: AgentId, otherId: AgentId, limit: number = 20): Promise { return invoke('btmsg_history', { agentId, otherId, limit }); } /** * Send a message from one agent to another. */ -export async function sendMessage(fromAgent: string, toAgent: string, content: string): Promise { +export async function sendMessage(fromAgent: AgentId, toAgent: AgentId, content: string): Promise { return invoke('btmsg_send', { fromAgent, toAgent, content }); } /** * Update agent status (active/sleeping/stopped). */ -export async function setAgentStatus(agentId: string, status: string): Promise { +export async function setAgentStatus(agentId: AgentId, status: string): Promise { return invoke('btmsg_set_status', { agentId, status }); } /** * Ensure admin agent exists with contacts to all agents. */ -export async function ensureAdmin(groupId: string): Promise { +export async function ensureAdmin(groupId: GroupId): Promise { return invoke('btmsg_ensure_admin', { groupId }); } /** * Get all messages in group (admin global feed). */ -export async function getAllFeed(groupId: string, limit: number = 100): Promise { +export async function getAllFeed(groupId: GroupId, limit: number = 100): Promise { return invoke('btmsg_all_feed', { groupId, limit }); } /** * Mark all messages from sender to reader as read. */ -export async function markRead(readerId: string, senderId: string): Promise { +export async function markRead(readerId: AgentId, senderId: AgentId): Promise { return invoke('btmsg_mark_read', { readerId, senderId }); } /** * Get channels in a group. */ -export async function getChannels(groupId: string): Promise { +export async function getChannels(groupId: GroupId): Promise { return invoke('btmsg_get_channels', { groupId }); } @@ -141,20 +142,20 @@ export async function getChannelMessages(channelId: string, limit: number = 100) /** * Send a message to a channel. */ -export async function sendChannelMessage(channelId: string, fromAgent: string, content: string): Promise { +export async function sendChannelMessage(channelId: string, fromAgent: AgentId, content: string): Promise { return invoke('btmsg_channel_send', { channelId, fromAgent, content }); } /** * Create a new channel. */ -export async function createChannel(name: string, groupId: string, createdBy: string): Promise { +export async function createChannel(name: string, groupId: GroupId, createdBy: AgentId): Promise { return invoke('btmsg_create_channel', { name, groupId, createdBy }); } /** * Add a member to a channel. */ -export async function addChannelMember(channelId: string, agentId: string): Promise { +export async function addChannelMember(channelId: string, agentId: AgentId): Promise { return invoke('btmsg_add_channel_member', { channelId, agentId }); } diff --git a/v2/src/lib/adapters/bttask-bridge.test.ts b/v2/src/lib/adapters/bttask-bridge.test.ts index 779d127..9003ad6 100644 --- a/v2/src/lib/adapters/bttask-bridge.test.ts +++ b/v2/src/lib/adapters/bttask-bridge.test.ts @@ -18,6 +18,7 @@ import { type Task, type TaskComment, } from './bttask-bridge'; +import { GroupId, AgentId } from '../types/ids'; beforeEach(() => { vi.clearAllMocks(); @@ -34,9 +35,9 @@ describe('bttask-bridge', () => { description: 'Critical fix', status: 'progress', priority: 'high', - assignedTo: 'a1', // was: assigned_to - createdBy: 'admin', // was: created_by - groupId: 'g1', // was: group_id + assignedTo: AgentId('a1'), // was: assigned_to + createdBy: AgentId('admin'), // was: created_by + groupId: GroupId('g1'), // was: group_id parentTaskId: null, // was: parent_task_id sortOrder: 1, // was: sort_order createdAt: '2026-01-01', // was: created_at @@ -44,7 +45,7 @@ describe('bttask-bridge', () => { }; mockInvoke.mockResolvedValue([task]); - const result = await listTasks('g1'); + const result = await listTasks(GroupId('g1')); expect(result).toHaveLength(1); expect(result[0].assignedTo).toBe('a1'); @@ -64,7 +65,7 @@ describe('bttask-bridge', () => { const comment: TaskComment = { id: 'c1', taskId: 't1', // was: task_id - agentId: 'a1', // was: agent_id + agentId: AgentId('a1'), // was: agent_id content: 'Working on it', createdAt: '2026-01-01', }; @@ -84,7 +85,7 @@ describe('bttask-bridge', () => { describe('IPC commands', () => { it('listTasks invokes bttask_list', async () => { mockInvoke.mockResolvedValue([]); - await listTasks('g1'); + await listTasks(GroupId('g1')); expect(mockInvoke).toHaveBeenCalledWith('bttask_list', { groupId: 'g1' }); }); @@ -102,14 +103,14 @@ describe('bttask-bridge', () => { it('addTaskComment invokes bttask_add_comment', async () => { mockInvoke.mockResolvedValue('c-id'); - const result = await addTaskComment('t1', 'a1', 'Done!'); + const result = await addTaskComment('t1', AgentId('a1'), 'Done!'); expect(result).toBe('c-id'); expect(mockInvoke).toHaveBeenCalledWith('bttask_add_comment', { taskId: 't1', agentId: 'a1', content: 'Done!' }); }); it('createTask invokes bttask_create with all fields', async () => { mockInvoke.mockResolvedValue('t-id'); - const result = await createTask('Fix bug', 'desc', 'high', 'g1', 'admin', 'a1'); + const result = await createTask('Fix bug', 'desc', 'high', GroupId('g1'), AgentId('admin'), AgentId('a1')); expect(result).toBe('t-id'); expect(mockInvoke).toHaveBeenCalledWith('bttask_create', { title: 'Fix bug', @@ -123,7 +124,7 @@ describe('bttask-bridge', () => { it('createTask invokes bttask_create without assignedTo', async () => { mockInvoke.mockResolvedValue('t-id'); - await createTask('Add tests', '', 'medium', 'g1', 'a1'); + await createTask('Add tests', '', 'medium', GroupId('g1'), AgentId('a1')); expect(mockInvoke).toHaveBeenCalledWith('bttask_create', { title: 'Add tests', description: '', @@ -144,7 +145,7 @@ describe('bttask-bridge', () => { describe('error propagation', () => { it('propagates invoke errors', async () => { mockInvoke.mockRejectedValue(new Error('btmsg database not found')); - await expect(listTasks('g1')).rejects.toThrow('btmsg database not found'); + await expect(listTasks(GroupId('g1'))).rejects.toThrow('btmsg database not found'); }); }); }); diff --git a/v2/src/lib/adapters/bttask-bridge.ts b/v2/src/lib/adapters/bttask-bridge.ts index 7146b8f..d0ce775 100644 --- a/v2/src/lib/adapters/bttask-bridge.ts +++ b/v2/src/lib/adapters/bttask-bridge.ts @@ -1,6 +1,7 @@ // bttask Bridge — Tauri IPC adapter for task board import { invoke } from '@tauri-apps/api/core'; +import type { GroupId, AgentId } from '../types/ids'; export interface Task { id: string; @@ -8,9 +9,9 @@ export interface Task { description: string; status: 'todo' | 'progress' | 'review' | 'done' | 'blocked'; priority: 'low' | 'medium' | 'high' | 'critical'; - assignedTo: string | null; - createdBy: string; - groupId: string; + assignedTo: AgentId | null; + createdBy: AgentId; + groupId: GroupId; parentTaskId: string | null; sortOrder: number; createdAt: string; @@ -20,12 +21,12 @@ export interface Task { export interface TaskComment { id: string; taskId: string; - agentId: string; + agentId: AgentId; content: string; createdAt: string; } -export async function listTasks(groupId: string): Promise { +export async function listTasks(groupId: GroupId): Promise { return invoke('bttask_list', { groupId }); } @@ -37,7 +38,7 @@ export async function updateTaskStatus(taskId: string, status: string): Promise< return invoke('bttask_update_status', { taskId, status }); } -export async function addTaskComment(taskId: string, agentId: string, content: string): Promise { +export async function addTaskComment(taskId: string, agentId: AgentId, content: string): Promise { return invoke('bttask_add_comment', { taskId, agentId, content }); } @@ -45,9 +46,9 @@ export async function createTask( title: string, description: string, priority: string, - groupId: string, - createdBy: string, - assignedTo?: string, + groupId: GroupId, + createdBy: AgentId, + assignedTo?: AgentId, ): Promise { return invoke('bttask_create', { title, description, priority, groupId, createdBy, assignedTo }); } diff --git a/v2/src/lib/adapters/groups-bridge.ts b/v2/src/lib/adapters/groups-bridge.ts index 1782563..72551f9 100644 --- a/v2/src/lib/adapters/groups-bridge.ts +++ b/v2/src/lib/adapters/groups-bridge.ts @@ -1,5 +1,6 @@ import { invoke } from '@tauri-apps/api/core'; import type { GroupsFile, ProjectConfig, GroupConfig } from '../types/groups'; +import type { SessionId, ProjectId } from '../types/ids'; export type { GroupsFile, ProjectConfig, GroupConfig }; @@ -11,8 +12,8 @@ export interface MdFileEntry { export interface AgentMessageRecord { id: number; - session_id: string; - project_id: string; + session_id: SessionId; + project_id: ProjectId; sdk_session_id: string | null; message_type: string; content: string; @@ -21,8 +22,8 @@ export interface AgentMessageRecord { } export interface ProjectAgentState { - project_id: string; - last_session_id: string; + project_id: ProjectId; + last_session_id: SessionId; sdk_session_id: string | null; status: string; cost_usd: number; @@ -51,8 +52,8 @@ export async function discoverMarkdownFiles(cwd: string): Promise // --- Agent message persistence --- export async function saveAgentMessages( - sessionId: string, - projectId: string, + sessionId: SessionId, + projectId: ProjectId, sdkSessionId: string | undefined, messages: AgentMessageRecord[], ): Promise { @@ -64,7 +65,7 @@ export async function saveAgentMessages( }); } -export async function loadAgentMessages(projectId: string): Promise { +export async function loadAgentMessages(projectId: ProjectId): Promise { return invoke('agent_messages_load', { projectId }); } @@ -74,7 +75,7 @@ export async function saveProjectAgentState(state: ProjectAgentState): Promise { +export async function loadProjectAgentState(projectId: ProjectId): Promise { return invoke('project_agent_state_load', { projectId }); } @@ -82,8 +83,8 @@ export async function loadProjectAgentState(projectId: string): Promise): Prom return invoke('session_metric_save', { metric: { id: 0, ...metric } }); } -export async function loadSessionMetrics(projectId: string, limit = 20): Promise { +export async function loadSessionMetrics(projectId: ProjectId, limit = 20): Promise { return invoke('session_metrics_load', { projectId, limit }); }