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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ relayfile-mount/
/.agentworkforce/relay
.claude/projects
.claude/settings.local.json

# Playwright stress harness artifacts (run-time output)
test-results/
tests/playwright/screenshots/
64 changes: 64 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
"verify:mcp-resources-drift": "node scripts/generate-mcp-extraResources.mjs --check",
"dev": "electron-vite dev",
"build": "npm run generate:mcp-resources && node scripts/install-relayfile-mount.mjs --optional && electron-vite build",
"build:web": "vite build --config vite.web.config.ts",
"preview": "electron-vite preview",
"dist:mac": "npm run icons:macos && npm run relayfile-mount:install:release && RELAYFILE_MOUNT_INSTALL_SOURCE=release npm run build && electron-builder --mac --publish never",
"verify:mcp-spawn": "node scripts/verify-mcp-spawn.mjs",
"release:mac": "npm run icons:macos && npm run relayfile-mount:install:release && RELAYFILE_MOUNT_INSTALL_SOURCE=release npm run build && electron-builder --mac --publish always",
"test": "node --experimental-strip-types --no-warnings --test 'src/main/__tests__/*.test.ts'",
"test:stress": "npm run build:web && playwright test --config playwright.stress.config.ts",
"personas:refresh": "npx --yes agentworkforce install @agentworkforce/persona-autonomous-actor --overwrite && npx --yes agentworkforce install @agentworkforce/persona-slack-comms --overwrite"
},
"dependencies": {
Expand Down Expand Up @@ -56,6 +58,7 @@
"protobufjs": "8.5.0"
},
"devDependencies": {
"@playwright/test": "^1.57.0",
"@tailwindcss/vite": "^4.0.0",
"@testing-library/react": "^16.3.2",
"@types/react": "^19.0.0",
Expand Down
23 changes: 23 additions & 0 deletions playwright.stress.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { defineConfig, devices } from '@playwright/test'

export default defineConfig({
testDir: './tests/playwright',
testMatch: /stress-1000-agents\.spec\.ts/,
timeout: 120_000,
expect: {
timeout: 15_000
},
webServer: {
command: 'npx vite preview --config vite.web.config.ts --host 127.0.0.1 --port 4174',
url: 'http://127.0.0.1:4174',
reuseExistingServer: !process.env.CI,
timeout: 30_000
},
use: {
...devices['Desktop Chrome'],
baseURL: 'http://127.0.0.1:4174',
viewport: { width: 1440, height: 960 },
trace: 'retain-on-failure'
},
reporter: [['list']]
})
10 changes: 9 additions & 1 deletion src/renderer/src/components/chat/ChatMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,12 @@ function ChatMessageInner({

if (message.kind === 'notice') {
return (
<div className="flex justify-center px-2 py-2">
<div
className="flex justify-center px-2 py-2"
data-testid="chat-message"
data-message-id={message.id}
data-message-kind={message.kind || 'message'}
>
<div className="flex max-w-full items-center gap-2 rounded-md border border-[var(--pear-border-subtle)] bg-[var(--pear-bg)]/45 px-2.5 py-1 text-xs text-[var(--pear-text-faint)]">
<span className="truncate">{message.body}</span>
<span className="shrink-0 text-[10px]">{formatClockTime(message.timestamp)}</span>
Expand All @@ -269,6 +274,9 @@ function ChatMessageInner({

return (
<div
data-testid="chat-message"
data-message-id={message.id}
data-message-kind={message.kind || 'message'}
className={`group relative flex gap-3 rounded-md px-2 py-1.5 ${
activeThread ? 'bg-[var(--pear-bg-overlay)]/70' : 'hover:bg-[var(--pear-bg-surface-hover)]/45'
}`}
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/components/common/StatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export function StatusBar(): React.ReactNode {
cancelled = true
window.clearInterval(interval)
}
}, [projectRootPathKey, projectSummary?.branch])
}, [projectRootPathKey])

const statusColor =
brokerStatus === 'connected'
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/components/diff/DiffPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1752,7 +1752,7 @@ export function DiffPane(): React.ReactNode {
const historyFilesListRef = useRef<HTMLDivElement>(null)
const windowFocused = useWindowFocused()
const activeProject = useProjectStore((s) => s.getActiveProject())
const root = useProjectStore((s) => s.getActiveRoot())
const root = useProjectStore((s) => s.getActiveRoot()) as ProjectRoot
const activeProjectId = useProjectStore((s) => s.activeProjectId)
const setActiveRoot = useProjectStore((s) => s.setActiveRoot)
const agents = useAgentStore((s) => s.agents)
Expand Down
11 changes: 5 additions & 6 deletions src/renderer/src/components/graph/GraphView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,24 +139,23 @@ export function GraphView(): React.ReactNode {
const initialEdges: Edge[] = useMemo(() => {
const agentKeys = new Set(agents.map(getAgentKeyForAgent))
const hierarchyEdges: Edge[] = agents
.map((agent) => {
if (!agent.parent) return null
.flatMap((agent): Edge[] => {
if (!agent.parent) return []

const source = getAgentKey(agent.projectId, agent.parent)
const target = getAgentKeyForAgent(agent)
if (!agentKeys.has(source)) return null
if (!agentKeys.has(source)) return []

return {
return [{
id: `hierarchy:${source}->${target}`,
source,
target,
type: 'hierarchy',
selectable: false,
focusable: false,
zIndex: 0
} satisfies Edge
}]
})
.filter((edge): edge is Edge => edge !== null)

const edgeMap = new Map<string, { count: number; lastBody: string; lastTimestamp: number }>()
for (const msg of relayMessages) {
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/src/components/settings/AccountSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ export function AccountSettings(): React.ReactNode {
return
}

if (event.projectId === activeProjectId) {
if ('projectId' in event && event.projectId === activeProjectId) {
void loadConnected()
}
})
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/src/components/terminal/TerminalInstance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export function TerminalInstance({ agentName, projectId, visible, active, mode,
<div
ref={containerRef}
tabIndex={0}
data-testid="terminal-instance"
data-agent-name={agentName}
aria-label={`${agentName} ${mode} terminal`}
onFocus={onActivate}
onPointerDown={onActivate}
Expand Down
11 changes: 6 additions & 5 deletions src/renderer/src/hooks/use-message-reconciliation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useMemo } from 'react'
import { getDirectMessageRoomId } from '@/lib/direct-messages'
import { pear } from '@/lib/ipc'
import { useAgentStore, type ChatMessage } from '@/stores/agent-store'
import { useProjectStore } from '@/stores/project-store'
import { type AppTab, useUIStore } from '@/stores/ui-store'
Expand All @@ -8,7 +9,7 @@ import type {
BrokerReconciledChatMessage,
BrokerReconcileMessagesInput,
PearAPI
} from '@shared/types/ipc'
} from '@/lib/ipc'

const DEFAULT_RECONCILE_LIMIT = 50
const DEFAULT_RECONCILE_DEBOUNCE_MS = 750
Expand Down Expand Up @@ -206,7 +207,7 @@ function debugReconciliation(event: MessageReconciliationDebugEvent): void {

function refreshEventStream(reason: string): void {
const projectId = useProjectStore.getState().activeProjectId || undefined
const broker = window.pear?.broker as (PearAPI['broker'] & BrokerWithMessageReconciliation) | undefined
const broker = pear?.broker as (PearAPI['broker'] & BrokerWithMessageReconciliation) | undefined
void broker?.refreshEventStream?.(projectId, reason)?.catch(() => undefined)
}

Expand All @@ -233,7 +234,7 @@ export function useMessageReconciliation(): void {
})
},
reconcileMessages: (input) => {
const broker = window.pear?.broker as (PearAPI['broker'] & BrokerWithMessageReconciliation) | undefined
const broker = pear?.broker as (PearAPI['broker'] & BrokerWithMessageReconciliation) | undefined
return broker?.reconcileMessages?.(input) ?? Promise.resolve([])
},
mergeMessages: mergeReconciledMessages,
Expand Down Expand Up @@ -287,7 +288,7 @@ export function useMessageReconciliation(): void {
}, [reconciler])

useEffect(() => {
return window.pear?.broker?.onStatus?.((status) => {
return pear?.broker?.onStatus?.((status) => {
if (BROKER_CONNECTED_STATUSES.has(status.status)) {
refreshEventStream(`broker:${status.status}`)
reconciler.schedule(`broker:${status.status}`)
Expand All @@ -296,7 +297,7 @@ export function useMessageReconciliation(): void {
}, [reconciler])

useEffect(() => {
const broker = window.pear?.broker as (PearAPI['broker'] & BrokerWithMessageReconciliation) | undefined
const broker = pear?.broker as (PearAPI['broker'] & BrokerWithMessageReconciliation) | undefined
if (!broker?.onEventStreamDiagnostic) return
return broker.onEventStreamDiagnostic((event) => {
if (EVENT_STREAM_RECONCILED_STATUSES.has(event.status)) {
Expand Down
13 changes: 13 additions & 0 deletions src/renderer/src/lib/ipc-electron.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { PearAPI } from '@shared/types/ipc'

declare global {
interface Window {
pear: PearAPI
}
}

// Guard against evaluation in non-browser environments (vitest in node env,
// web preview bundle eval before the selector switches modes, etc.). In
// mock mode `ipc.ts` reads `pearMock` and never touches this value, so
// production callers always run inside Electron with the preload installed.
export const pear: PearAPI = (typeof window !== 'undefined' ? window.pear : undefined) as PearAPI
Loading
Loading