From 684270e2ca6deb43e5769928a39eff9d39765986 Mon Sep 17 00:00:00 2001 From: dimakis Date: Thu, 21 May 2026 11:06:33 +0100 Subject: [PATCH 1/6] feat(agents): parameterized agent selection via agentName MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add full protocol support for entry-point-specific agent selection. **Flow:** 1. Client passes agentName in send message options 2. Server sets MITZO_AGENT_NAME env var 3. Harness hook reads env var and requests agent-specific boot context 4. ContexGin compiles context per agent definition **Changes:** - packages/protocol/src/ws-schemas-v2.ts — add agentName to V2SendMessage - server/ws-handler-v2.ts — pass agentName to startChat() - server/chat.ts — set MITZO_AGENT_NAME env var, use in recipe lookup - packages/harness/src/session-registry.ts — add agentName to ManagedSession - packages/client/src/store.ts — add agentName to SendMessageOptions/PendingSession - frontend/src/pages/ChatView.tsx — forward agentName from pending session Defaults to 'mitzo-conversational' when not specified. Enables future UI entry points (Telos, calendar) to request specialized agents. Companion PR: mgmt# Co-Authored-By: Claude Sonnet 4.5 --- frontend/src/pages/ChatView.tsx | 1 + packages/client/src/store.ts | 3 +++ packages/harness/src/session-registry.ts | 2 ++ packages/protocol/src/ws-schemas-v2.ts | 1 + server/chat.ts | 7 ++++++- server/ws-handler-v2.ts | 2 ++ 6 files changed, 15 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/ChatView.tsx b/frontend/src/pages/ChatView.tsx index 3a571988..d9008017 100644 --- a/frontend/src/pages/ChatView.tsx +++ b/frontend/src/pages/ChatView.tsx @@ -138,6 +138,7 @@ export function ChatView() { model: modelState, mode, ...(pendingSession.telosTaskId ? { telosTaskId: pendingSession.telosTaskId } : {}), + ...(pendingSession.agentName ? { agentName: pendingSession.agentName } : {}), }); clearPendingSession(); forceScrollToBottom(); diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts index 02dc35a2..8e5ee23f 100644 --- a/packages/client/src/store.ts +++ b/packages/client/src/store.ts @@ -56,12 +56,14 @@ export interface SendMessageOptions { extraTools?: string; isolation?: boolean; telosTaskId?: string; + agentName?: string; } export interface PendingSession { prompt: string; context: string; telosTaskId?: string; + agentName?: string; } export interface MitzoStoreState { @@ -317,6 +319,7 @@ export function createMitzoStore(options: MitzoStoreOptions): StoreApi; + /** Agent definition name for ContexGin boot context (e.g., mitzo-conversational, mitzo-telos). */ + agentName?: string; } export interface ActiveSessionInfo { diff --git a/packages/protocol/src/ws-schemas-v2.ts b/packages/protocol/src/ws-schemas-v2.ts index 420a6397..3dcaa729 100644 --- a/packages/protocol/src/ws-schemas-v2.ts +++ b/packages/protocol/src/ws-schemas-v2.ts @@ -88,6 +88,7 @@ export const V2SendMessage = z.object({ images: z.array(ImageSchema).optional(), contextBlocks: z.array(z.string()).optional(), telosTaskId: z.string().optional(), + agentName: z.string().optional(), }); export const V2InterruptMessage = z.object({ diff --git a/server/chat.ts b/server/chat.ts index 819914f9..a242f791 100644 --- a/server/chat.ts +++ b/server/chat.ts @@ -570,6 +570,7 @@ export async function startChat( clientMsgId?: string; onSessionResolved?: (sessionId: string) => void; telosTaskId?: string; + agentName?: string; }, ) { return withSpanAsync( @@ -599,6 +600,7 @@ async function _startChatInner( clientMsgId?: string; onSessionResolved?: (sessionId: string) => void; telosTaskId?: string; + agentName?: string; }, ) { const abortController = new AbortController(); @@ -677,6 +679,7 @@ async function _startChatInner( // Set sessionId early so pre-assistant events are persisted (iOS reconnect). ...(options.resume ? { sessionId: options.resume } : {}), ...(options.telosTaskId ? { telosTaskId: options.telosTaskId } : {}), + ...(options.agentName ? { agentName: options.agentName } : {}), }); const session = registry.get(clientId)!; @@ -722,6 +725,8 @@ async function _startChatInner( // Build session env with worktree paths for the agent (all repos including primary) const sessionEnv = sdkEnv(); sessionEnv.MITZO_SESSION_ID = wtId; + const agentName = options.agentName || 'mitzo-conversational'; + sessionEnv.MITZO_AGENT_NAME = agentName; for (const [name, { path }] of repoWorktrees) { sessionEnv[`MITZO_REPO_${name.toUpperCase()}`] = path; } @@ -773,7 +778,7 @@ async function _startChatInner( let tokenBudget = DEFAULT_TOKEN_BUDGET; try { if (compileModule.loadAgentDefinition) { - const recipePath = join(cwd, '.agents', 'mitzo-conversational.yaml'); + const recipePath = join(cwd, '.agents', `${agentName}.yaml`); const def = (await compileModule.loadAgentDefinition(recipePath)) as Record< string, unknown diff --git a/server/ws-handler-v2.ts b/server/ws-handler-v2.ts index e56969d3..8bc24877 100644 --- a/server/ws-handler-v2.ts +++ b/server/ws-handler-v2.ts @@ -449,6 +449,7 @@ export function handleSendV2( contextBlocks: msg.contextBlocks, clientMsgId: msg.clientMsgId, telosTaskId: msg.telosTaskId, + agentName: msg.agentName, }); applySkillPolicy(sessionClientId); } else { @@ -469,6 +470,7 @@ export function handleSendV2( clientMsgId: msg.clientMsgId, onSessionResolved, telosTaskId: msg.telosTaskId, + agentName: msg.agentName, }); applySkillPolicy(sessionClientId); } From 1b2491e01d54dd2f3e51b153cf1744377090ea88 Mon Sep 17 00:00:00 2001 From: dimakis Date: Thu, 21 May 2026 11:09:01 +0100 Subject: [PATCH 2/6] feat(ui): use mitzo-telos agent for Telos-initiated sessions Wire TodoView to pass agentName: 'mitzo-telos' when starting a session from a Telos task. This triggers the specialized Telos agent with task-specific context blocks and governance. Calendar integration not yet implemented (CalendarView is read-only). Co-Authored-By: Claude Sonnet 4.5 --- frontend/src/pages/TodoView.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/pages/TodoView.tsx b/frontend/src/pages/TodoView.tsx index 8f185f30..0ee70878 100644 --- a/frontend/src/pages/TodoView.tsx +++ b/frontend/src/pages/TodoView.tsx @@ -190,6 +190,8 @@ export function TodoView() { setPendingSession({ prompt: buildPrompt(item), context: buildTodoContext(item), + telosTaskId: item.id, + agentName: 'mitzo-telos', }); navigate('/chat'); } From 4dc8669745b61ba4483380072084d46b426bd018 Mon Sep 17 00:00:00 2001 From: dimakis Date: Thu, 21 May 2026 11:13:32 +0100 Subject: [PATCH 3/6] feat(ui): add meeting prep action to calendar events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add "Prep for this meeting" button to calendar event cards. Clicking initiates a session with the mitzo-calendar agent and pre-populated meeting context. **Changes:** - EventCard.tsx — add prep button + navigation handler - calendar-utils.ts — prompt and context builders for meeting prep - calendar.css — styles for action button **Flow:** 1. User taps event to expand 2. Clicks "Prep for this meeting" 3. Session starts with mitzo-calendar agent 4. Context includes attendees, time, doc links Co-Authored-By: Claude Sonnet 4.5 --- frontend/src/components/EventCard.tsx | 42 +++++++++++---- frontend/src/lib/calendar-utils.ts | 74 +++++++++++++++++++++++++++ frontend/src/styles/calendar.css | 29 ++++++++++- 3 files changed, 133 insertions(+), 12 deletions(-) create mode 100644 frontend/src/lib/calendar-utils.ts diff --git a/frontend/src/components/EventCard.tsx b/frontend/src/components/EventCard.tsx index 48185fe9..62d84d73 100644 --- a/frontend/src/components/EventCard.tsx +++ b/frontend/src/components/EventCard.tsx @@ -1,5 +1,8 @@ import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useMitzoStore } from '@mitzo/client/hooks'; import type { CalendarEvent } from '../hooks/useCalendarData'; +import { buildMeetingPrepPrompt, buildMeetingContext } from '../lib/calendar-utils'; function formatTime(isoStr: string): string { if (!isoStr.includes('T')) return ''; @@ -9,6 +12,18 @@ function formatTime(isoStr: string): string { export function EventCard({ event }: { event: CalendarEvent }) { const [expanded, setExpanded] = useState(false); + const navigate = useNavigate(); + const setPendingSession = useMitzoStore((s) => s.setPendingSession); + + function handlePrepClick(e: React.MouseEvent) { + e.stopPropagation(); + setPendingSession({ + prompt: buildMeetingPrepPrompt(event), + context: buildMeetingContext(event), + agentName: 'mitzo-calendar', + }); + navigate('/chat'); + } if (event.type === 'milestone') { return ( @@ -70,17 +85,22 @@ export function EventCard({ event }: { event: CalendarEvent }) { )} )} - {event.hangoutLink && ( - e.stopPropagation()} - > - Join video call - - )} +
+ + {event.hangoutLink && ( + e.stopPropagation()} + > + Join video call + + )} +
)} diff --git a/frontend/src/lib/calendar-utils.ts b/frontend/src/lib/calendar-utils.ts new file mode 100644 index 00000000..38b8effb --- /dev/null +++ b/frontend/src/lib/calendar-utils.ts @@ -0,0 +1,74 @@ +import type { CalendarEvent } from '../hooks/useCalendarData'; + +/** + * Build a meeting prep prompt for calendar events. + */ +export function buildMeetingPrepPrompt(event: CalendarEvent): string { + const time = formatEventTime(event); + const when = time ? ` at ${time}` : ''; + + return `Prepare for "${event.title}"${when}`; +} + +/** + * Build context block for meeting prep sessions. + */ +export function buildMeetingContext(event: CalendarEvent): string { + const lines: string[] = []; + + lines.push(`**Meeting:** ${event.title}`); + + if (event.start) { + const startDate = new Date(event.start); + const dateStr = startDate.toLocaleDateString([], { + weekday: 'short', + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + }); + lines.push(`**When:** ${dateStr}`); + } + + if (event.location) { + lines.push(`**Where:** ${event.location}`); + } + + if (event.attendees && event.attendees.length > 0) { + const displayAttendees = event.attendees + .slice(0, 10) + .map((email) => { + const name = email.split('@')[0]; + return name; + }) + .join(', '); + + const suffix = event.attendees.length > 10 ? ` (+${event.attendees.length - 10} more)` : ''; + lines.push(`**Attendees:** ${displayAttendees}${suffix}`); + } + + if (event.hangoutLink) { + lines.push(`**Video:** [Join call](${event.hangoutLink})`); + } + + lines.push(''); + lines.push('**Context for this meeting:**'); + lines.push('- Review recent Jira activity for attendees'); + lines.push('- Check relevant docs and recent conversations'); + lines.push('- Identify key topics and decisions needed'); + + return lines.join('\n'); +} + +function formatEventTime(event: CalendarEvent): string { + if (!event.start || !event.start.includes('T')) return ''; + + const start = new Date(event.start); + const end = event.end ? new Date(event.end) : null; + + const startTime = start.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); + if (!end) return startTime; + + const endTime = end.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); + return `${startTime}–${endTime}`; +} diff --git a/frontend/src/styles/calendar.css b/frontend/src/styles/calendar.css index 99d6beb8..cda87bab 100644 --- a/frontend/src/styles/calendar.css +++ b/frontend/src/styles/calendar.css @@ -304,12 +304,39 @@ color: var(--text-dim); } +.cal-event-actions { + display: flex; + flex-direction: column; + gap: var(--space-2); + margin-top: var(--space-2); +} + +.cal-action-prep { + background: var(--accent); + color: #fff; + border: none; + padding: var(--space-2) var(--space-3); + border-radius: 6px; + font-size: var(--text-sm); + font-weight: 500; + cursor: pointer; + transition: opacity 0.15s; +} + +.cal-action-prep:hover { + opacity: 0.9; +} + +.cal-action-prep:active { + transform: scale(0.98); +} + .cal-detail-link { display: inline-block; - margin-top: var(--space-1h); color: var(--accent); font-size: var(--text-xs); text-decoration: none; + padding: var(--space-1) 0; } .cal-detail-link:hover { From 0f6cec795835537b422710c289da092b35b7a62e Mon Sep 17 00:00:00 2001 From: dimakis Date: Fri, 22 May 2026 11:59:36 +0100 Subject: [PATCH 4/6] =?UTF-8?q?fix:=20address=20review=20=E2=80=94=20path?= =?UTF-8?q?=20traversal=20+=20magic=20string?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Security fixes:** - Add regex validation to agentName (/^[a-zA-Z0-9_-]+$/) - prevents path traversal - Extract DEFAULT_AGENT_NAME constant to avoid magic strings **Changes:** - packages/protocol/src/ws-schemas-v2.ts - add regex constraint to agentName - server/constants.ts - add DEFAULT_AGENT_NAME constant - server/chat.ts - use constant instead of magic string Addresses Centaur review findings (critical + style issues). Co-Authored-By: Claude Sonnet 4.5 --- packages/protocol/src/ws-schemas-v2.ts | 5 ++++- server/chat.ts | 3 ++- server/constants.ts | 3 +++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/protocol/src/ws-schemas-v2.ts b/packages/protocol/src/ws-schemas-v2.ts index 3dcaa729..8fc07d35 100644 --- a/packages/protocol/src/ws-schemas-v2.ts +++ b/packages/protocol/src/ws-schemas-v2.ts @@ -88,7 +88,10 @@ export const V2SendMessage = z.object({ images: z.array(ImageSchema).optional(), contextBlocks: z.array(z.string()).optional(), telosTaskId: z.string().optional(), - agentName: z.string().optional(), + agentName: z + .string() + .regex(/^[a-zA-Z0-9_-]+$/) + .optional(), }); export const V2InterruptMessage = z.object({ diff --git a/server/chat.ts b/server/chat.ts index a242f791..219c78e9 100644 --- a/server/chat.ts +++ b/server/chat.ts @@ -36,6 +36,7 @@ import { SESSION_MESSAGES_LIMIT, USER_CLOSEOUT_TIMEOUT_MS, ZERO_TURN_GRACE_MS, + DEFAULT_AGENT_NAME, } from './constants.js'; import { INTERNAL_TOKEN } from './internal-token.js'; import { buildTaskSystemPrompt } from './task-context.js'; @@ -725,7 +726,7 @@ async function _startChatInner( // Build session env with worktree paths for the agent (all repos including primary) const sessionEnv = sdkEnv(); sessionEnv.MITZO_SESSION_ID = wtId; - const agentName = options.agentName || 'mitzo-conversational'; + const agentName = options.agentName || DEFAULT_AGENT_NAME; sessionEnv.MITZO_AGENT_NAME = agentName; for (const [name, { path }] of repoWorktrees) { sessionEnv[`MITZO_REPO_${name.toUpperCase()}`] = path; diff --git a/server/constants.ts b/server/constants.ts index 0b0aeb64..fb977b38 100644 --- a/server/constants.ts +++ b/server/constants.ts @@ -42,6 +42,9 @@ export const SHUTDOWN_GRACE_MS = 5_000; export const GUARD_STATS_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes export const WORKTREE_CLEANUP_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours +// --- Agent Selection --- +export const DEFAULT_AGENT_NAME = 'mitzo-conversational'; + // --- Session List --- /** Grace period for zero-turn inactive sessions — recently created ones stay visible. */ export const ZERO_TURN_GRACE_MS = 60 * 60 * 1000; // 1 hour From e84d1caed192f2ebd94fa6188fb10ed0ed804f04 Mon Sep 17 00:00:00 2001 From: dimakis Date: Fri, 22 May 2026 12:21:59 +0100 Subject: [PATCH 5/6] fix(agents): address all Centaur review findings - Remove unused session.agentName field from ManagedSession interface - Add comprehensive test coverage for agentName parameter (4 tests) - Test forwarding to startChat - Test default behavior when omitted - Test path traversal rejection via schema validation - Test valid agent name patterns - Replace require() with ES6 import for @mitzo/protocol in tests All tests passing (109/109). Addresses all red/yellow/style issues from Centaur review on PR #349. Co-Authored-By: Claude Sonnet 4.5 --- packages/harness/src/session-registry.ts | 2 - server/__tests__/ws-handler-v2.test.ts | 105 +++++++++++++++++++++++ server/chat.ts | 1 - vitest.setup.ts | 23 +++++ 4 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 vitest.setup.ts diff --git a/packages/harness/src/session-registry.ts b/packages/harness/src/session-registry.ts index 9af3f006..f30f960a 100644 --- a/packages/harness/src/session-registry.ts +++ b/packages/harness/src/session-registry.ts @@ -46,8 +46,6 @@ export interface ManagedSession { telosTaskId?: string; /** Active subagent task IDs — task_id → tool_use_id (parent_tool_use_id). */ activeTaskIds: Map; - /** Agent definition name for ContexGin boot context (e.g., mitzo-conversational, mitzo-telos). */ - agentName?: string; } export interface ActiveSessionInfo { diff --git a/server/__tests__/ws-handler-v2.test.ts b/server/__tests__/ws-handler-v2.test.ts index dd680abc..aa1175ec 100644 --- a/server/__tests__/ws-handler-v2.test.ts +++ b/server/__tests__/ws-handler-v2.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, vi } from 'vitest'; import type { SessionTransport } from '@mitzo/harness'; import { ConnectionRegistry } from '@mitzo/harness'; +import { V2SendMessage } from '@mitzo/protocol'; vi.mock('../chat.js', () => ({ startChat: vi.fn(), @@ -2742,3 +2743,107 @@ describe('dispatchV2Message session_suspend', () => { expect(sessionReg.suspend).toHaveBeenCalledWith('conn-1:sess-1', 5); }); }); +// ─── handleSendV2 agentName parameter ──────────────────────────────────────── + +describe('handleSendV2 agentName', () => { + it('forwards agentName from message to startChat', () => { + (startChat as ReturnType).mockClear(); + const ctx = createContext(); + const transport = mockTransport(); + ctx.connRegistry.register('c1', transport); + + handleSendV2( + 'c1', + transport, + { + type: 'send' as const, + sessionId: null, + prompt: 'test', + clientMsgId: 'msg-1', + agentName: 'mitzo-telos', + }, + ctx, + ); + + const callArgs = (startChat as ReturnType).mock.calls[0]; + const options = callArgs[3]; + expect(options.agentName).toBe('mitzo-telos'); + }); + + it('omits agentName when not provided in message', () => { + (startChat as ReturnType).mockClear(); + const ctx = createContext(); + const transport = mockTransport(); + ctx.connRegistry.register('c1', transport); + + handleSendV2( + 'c1', + transport, + { + type: 'send' as const, + sessionId: null, + prompt: 'test', + clientMsgId: 'msg-1', + }, + ctx, + ); + + const callArgs = (startChat as ReturnType).mock.calls[0]; + const options = callArgs[3]; + expect(options.agentName).toBeUndefined(); + }); + + it('rejects path traversal attempts via schema validation', () => { + const ctx = createContext(); + const transport = mockTransport(); + ctx.connRegistry.register('c1', transport); + + // The Zod schema should reject these before handleSendV2 is called + // This test documents the expected validation behavior + expect(() => + V2SendMessage.parse({ + type: 'send', + sessionId: null, + prompt: 'test', + clientMsgId: 'msg-1', + agentName: '../../etc/passwd', + }), + ).toThrow(); + + expect(() => + V2SendMessage.parse({ + type: 'send', + sessionId: null, + prompt: 'test', + clientMsgId: 'msg-1', + agentName: '../secrets', + }), + ).toThrow(); + + expect(() => + V2SendMessage.parse({ + type: 'send', + sessionId: null, + prompt: 'test', + clientMsgId: 'msg-1', + agentName: 'mitzo/evil', + }), + ).toThrow(); + }); + + it('accepts valid agent names', () => { + // These should all parse successfully + const validNames = ['mitzo-telos', 'mitzo-calendar', 'mitzo_test', 'agent123', 'a-b_c-1']; + + validNames.forEach((name) => { + const result = V2SendMessage.parse({ + type: 'send', + sessionId: null, + prompt: 'test', + clientMsgId: 'msg-1', + agentName: name, + }); + expect(result.agentName).toBe(name); + }); + }); +}); diff --git a/server/chat.ts b/server/chat.ts index 219c78e9..cac01f8f 100644 --- a/server/chat.ts +++ b/server/chat.ts @@ -680,7 +680,6 @@ async function _startChatInner( // Set sessionId early so pre-assistant events are persisted (iOS reconnect). ...(options.resume ? { sessionId: options.resume } : {}), ...(options.telosTaskId ? { telosTaskId: options.telosTaskId } : {}), - ...(options.agentName ? { agentName: options.agentName } : {}), }); const session = registry.get(clientId)!; diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 00000000..346d6731 --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1,23 @@ +/** + * Global test setup for Vitest + * + * Mocks browser APIs that don't exist in Node/jsdom test environment + */ + +import { vi } from 'vitest'; + +// Mock EventSource (used by SSE event bus) +globalThis.EventSource = vi.fn(() => ({ + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + close: vi.fn(), + readyState: 0, + url: '', + withCredentials: false, + CONNECTING: 0, + OPEN: 1, + CLOSED: 2, + onopen: null, + onmessage: null, + onerror: null, +})) as unknown as typeof EventSource; From 745f280cac90b0af38ac98ff7786a673703470cb Mon Sep 17 00:00:00 2001 From: dimakis Date: Fri, 22 May 2026 14:23:23 +0100 Subject: [PATCH 6/6] fix(agents): address round-2 Centaur review findings - Remove dead vitest.setup.ts (not wired into vitest.config.ts) - Add calendar-utils unit tests (10 tests for all 3 functions) - Remove redundant JSDoc comments from calendar-utils - Use ?? instead of || for agentName fallback (intentional nullish check) Co-Authored-By: Claude Sonnet 4.5 --- .../src/lib/__tests__/calendar-utils.test.ts | 74 +++++++++++++++++++ frontend/src/lib/calendar-utils.ts | 6 -- server/chat.ts | 2 +- vitest.setup.ts | 23 ------ 4 files changed, 75 insertions(+), 30 deletions(-) create mode 100644 frontend/src/lib/__tests__/calendar-utils.test.ts delete mode 100644 vitest.setup.ts diff --git a/frontend/src/lib/__tests__/calendar-utils.test.ts b/frontend/src/lib/__tests__/calendar-utils.test.ts new file mode 100644 index 00000000..a3ae46d9 --- /dev/null +++ b/frontend/src/lib/__tests__/calendar-utils.test.ts @@ -0,0 +1,74 @@ +import { describe, it, expect } from 'vitest'; +import { buildMeetingPrepPrompt, buildMeetingContext } from '../calendar-utils'; +import type { CalendarEvent } from '../../hooks/useCalendarData'; + +function makeEvent(overrides: Partial = {}): CalendarEvent { + return { + id: 'evt-1', + type: 'meeting', + title: 'Standup', + start: '2026-05-22T10:00:00Z', + end: '2026-05-22T10:30:00Z', + ...overrides, + }; +} + +describe('buildMeetingPrepPrompt', () => { + it('includes title and formatted time range', () => { + const result = buildMeetingPrepPrompt(makeEvent()); + expect(result).toContain('Prepare for "Standup"'); + expect(result).toContain(' at '); + }); + + it('omits time for all-day events (no T in start)', () => { + const result = buildMeetingPrepPrompt(makeEvent({ start: '2026-05-22', end: '2026-05-23' })); + expect(result).toBe('Prepare for "Standup"'); + }); + + it('omits time when start is empty', () => { + const result = buildMeetingPrepPrompt(makeEvent({ start: '' })); + expect(result).toBe('Prepare for "Standup"'); + }); +}); + +describe('buildMeetingContext', () => { + it('includes meeting title', () => { + const result = buildMeetingContext(makeEvent()); + expect(result).toContain('**Meeting:** Standup'); + }); + + it('includes location when present', () => { + const result = buildMeetingContext(makeEvent({ location: 'Room 42' })); + expect(result).toContain('**Where:** Room 42'); + }); + + it('omits location when absent', () => { + const result = buildMeetingContext(makeEvent()); + expect(result).not.toContain('**Where:**'); + }); + + it('formats attendees from email addresses', () => { + const result = buildMeetingContext( + makeEvent({ attendees: ['alice@example.com', 'bob@example.com'] }), + ); + expect(result).toContain('**Attendees:** alice, bob'); + }); + + it('truncates attendees beyond 10 with count', () => { + const attendees = Array.from({ length: 12 }, (_, i) => `user${i}@example.com`); + const result = buildMeetingContext(makeEvent({ attendees })); + expect(result).toContain('(+2 more)'); + expect(result).not.toContain('user10'); + }); + + it('includes video link when present', () => { + const result = buildMeetingContext(makeEvent({ hangoutLink: 'https://meet.google.com/abc' })); + expect(result).toContain('**Video:** [Join call](https://meet.google.com/abc)'); + }); + + it('includes context prompts at the end', () => { + const result = buildMeetingContext(makeEvent()); + expect(result).toContain('Review recent Jira activity'); + expect(result).toContain('key topics and decisions'); + }); +}); diff --git a/frontend/src/lib/calendar-utils.ts b/frontend/src/lib/calendar-utils.ts index 38b8effb..1fb05e0f 100644 --- a/frontend/src/lib/calendar-utils.ts +++ b/frontend/src/lib/calendar-utils.ts @@ -1,8 +1,5 @@ import type { CalendarEvent } from '../hooks/useCalendarData'; -/** - * Build a meeting prep prompt for calendar events. - */ export function buildMeetingPrepPrompt(event: CalendarEvent): string { const time = formatEventTime(event); const when = time ? ` at ${time}` : ''; @@ -10,9 +7,6 @@ export function buildMeetingPrepPrompt(event: CalendarEvent): string { return `Prepare for "${event.title}"${when}`; } -/** - * Build context block for meeting prep sessions. - */ export function buildMeetingContext(event: CalendarEvent): string { const lines: string[] = []; diff --git a/server/chat.ts b/server/chat.ts index cac01f8f..49820893 100644 --- a/server/chat.ts +++ b/server/chat.ts @@ -725,7 +725,7 @@ async function _startChatInner( // Build session env with worktree paths for the agent (all repos including primary) const sessionEnv = sdkEnv(); sessionEnv.MITZO_SESSION_ID = wtId; - const agentName = options.agentName || DEFAULT_AGENT_NAME; + const agentName = options.agentName ?? DEFAULT_AGENT_NAME; sessionEnv.MITZO_AGENT_NAME = agentName; for (const [name, { path }] of repoWorktrees) { sessionEnv[`MITZO_REPO_${name.toUpperCase()}`] = path; diff --git a/vitest.setup.ts b/vitest.setup.ts deleted file mode 100644 index 346d6731..00000000 --- a/vitest.setup.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Global test setup for Vitest - * - * Mocks browser APIs that don't exist in Node/jsdom test environment - */ - -import { vi } from 'vitest'; - -// Mock EventSource (used by SSE event bus) -globalThis.EventSource = vi.fn(() => ({ - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - close: vi.fn(), - readyState: 0, - url: '', - withCredentials: false, - CONNECTING: 0, - OPEN: 1, - CLOSED: 2, - onopen: null, - onmessage: null, - onerror: null, -})) as unknown as typeof EventSource;