Skip to content

Onboarding updates - #55

Merged
khaliqgant merged 46 commits into
mainfrom
onboarding-updates
Mar 2, 2026
Merged

Onboarding updates#55
khaliqgant merged 46 commits into
mainfrom
onboarding-updates

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Feb 24, 2026

Copy link
Copy Markdown
Member

Summary

Large-scale refactor and feature update that restructures the dashboard into a clean, modular architecture while adding new onboarding and session management capabilities.

Architecture Refactors

  • Dashboard as pure UI kit: Removed all direct cloud API dependencies (billing, sessions, workspace status), making the dashboard a standalone UI that receives configuration via an adapter pattern (DashboardConfigProvider)

    • Deleted: CloudSessionProvider, BillingResult, SessionExpiredModal, WorkspaceStatusIndicator, useSession, useWorkspaceMembers, useWorkspaceStatus
    • Net: ~12,600 lines removed, ~2,500 lines added
  • Split App.tsx into focused providers: Extracted the 3,500-line App.tsx into four dedicated context providers:

    • SettingsProvider — theme, display, notification preferences
    • CloudWorkspaceProvider — workspace list/selection, cloud user state
    • AgentProvider — agent merging, spawn/release, fleet, decisions
    • MessageProvider — channels, threads, DM, send ops, presence
  • Server route extraction: Broke monolithic server.ts into modular route files under routes/ (agents, channels, data, health, reactions, broker-proxy) with a proxy route table

  • Relaycast provider migration: Refactored relaycast-provider.ts from 832 → 366 lines by adopting SDK-only transport, removing custom envelope parsing and manual cache invalidation

  • Extract business logic to proxy pass-throughs: Metrics, needs-attention, health, and spawned-agents endpoints now proxy to broker/cloud instead of containing business logic

New Features

  • Hybrid WebSocket mode: Dashboard connects to broker's /ws endpoint for real-time event streaming instead of polling Relaycast REST API every 3 seconds. Falls back to snapshot polling if broker WS is unavailable. Includes exponential backoff reconnect.

  • Resume Previous Session toggle: Added continueFrom support to SpawnModal UI so agents can resume prior sessions

  • Centralized model registry: Removed hardcoded model options from SpawnModal — now imports CLAUDE_MODEL_OPTIONS, CURSOR_MODEL_OPTIONS, etc. from agent-relay/broker SDK (source of truth: cli-registry.yaml)

  • Onboarding page overhaul: Significantly expanded the onboarding page (~870 lines added, 170 removed) with improved provider setup flows

Server Infrastructure

  • New utility modules: cli-auth.ts, file-search.ts, log-reader.ts, spawned-agents.ts, relay-client.ts (1,417 lines)
  • WebSocket handlers split into: logs.ts, mock.ts, proxy.ts, standalone.ts
  • Snapshot-before-stream ordering: initial snapshot is sent before broker event stream connects so the client always has non-null data
  • Dual-send for spawned agents: messages sent via both Relaycast (observer visibility) and broker /api/send (reliable delivery)

Tests

  • Added tests for: SpawnModal toggle states, proxy-server, relaycast-provider, spawned-agents, file-search, log-reader, proxy-route-table, useWebSocket, useMessages, utils
  • Aligned relaycast-provider mocks with SDK readers
  • Simplified metrics and needs-attention test suites to match new proxy architecture

84 files changed across packages/dashboard/ and packages/dashboard-server/


Open with Devin

khaliqgant and others added 27 commits February 19, 2026 11:56
Add continueFrom support to SpawnModal UI with toggle switch.
Wire continueFrom through App.tsx to the spawn API call.
Add tests for toggle enabled/disabled states.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove hardcoded model options from SpawnModal.tsx
- Import CLAUDE_MODEL_OPTIONS, CURSOR_MODEL_OPTIONS, etc. from agent-relay/broker
- Use DefaultModels for fallback values
- SettingsPage.tsx also imports from SDK

Source of truth is now packages/shared/cli-registry.yaml in relay repo.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…endencies

Remove all direct cloud API dependencies from the dashboard package,
making it a standalone UI kit that receives configuration via adapters.

- Delete cloud-specific components: CloudSessionProvider, BillingResult,
  SessionExpiredModal, WorkspaceStatusIndicator
- Delete cloud hooks: useSession, useWorkspaceMembers, useWorkspaceStatus
- Delete settings panels: BillingSettingsPanel, TeamSettingsPanel,
  WorkspaceSettingsPanel, CredentialAssignmentSection
- Delete monolithic server.ts (6.5k lines), replace with proxy-server
- Add adapters layer (DashboardConfigProvider, types) for host integration
- Simplify App.tsx, SettingsPage, Header, Sidebar to use adapter pattern
- Remove cloudApi.ts dependency from lib/

Net: -12,652 lines removed, +2,507 lines added

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
In proxy mode, the dashboard now connects to the broker's /ws endpoint
for real-time event streaming instead of polling Relaycast REST API
every 3 seconds. Falls back to snapshot polling if broker WS is down.

- Add handleHybridWebSocket with exponential backoff reconnect
- Route proxy mode WS connections through hybrid handler
- Add BrokerEvent type and applyBrokerEvent state patching
- Handle relay_inbound, agent lifecycle, and worker status events

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract business logic from the 3,515-line App.tsx into four dedicated
context providers, reducing it to a ~1,230-line layout shell.

- SettingsProvider: theme, display, notification preferences (247 lines)
- CloudWorkspaceProvider: workspace list/selection, cloud user state (402 lines)
- AgentProvider: agent merging, spawn/release, fleet, decisions (799 lines)
- MessageProvider: channels, threads, DM, send ops, presence (1,208 lines)

Each provider uses React.createContext + useContext with proper TypeScript
types. App.tsx now contains only the provider tree and layout JSX.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- metrics.ts → fetchCloudMetrics() proxy
- needs-attention.ts → fetchCloudNeedsAttention() proxy
- health-worker-manager.ts → fetchBrokerHealth() proxy
- spawned-agents.ts → fetchBrokerSpawnedAgents() proxy
- Added proxy-route-table.ts with explicit route mapping
- Updated server.ts to consume proxy helpers
- Refactored relaycast-provider.ts from 832 to 366 lines
- Removed custom workspaceGet transport, manual envelope parsing
- Removed DM participant extraction, local cache invalidation
- Added relaycast-provider-helpers.ts and relaycast-provider-types.ts
- Uses SDK clients for all reads + shared contracts isBrokerIdentity()
# Conflicts:
#	packages/dashboard-server/src/lib/spawned-agents.ts
#	packages/dashboard-server/src/server.ts
#	packages/dashboard-server/src/services/index.ts
…for spawned agents

Reorder hybrid WS to send the initial snapshot before connecting the
broker stream so the client always has non-null data. Dual-send messages
to spawned agents via both Relaycast (observer visibility) and broker
/api/send (reliable delivery). Handle raw broker events on the client
and bootstrap empty state so events before the snapshot aren't lost.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

View 12 additional findings in Devin Review.

Open in Devin Review

Comment thread packages/dashboard-server/src/routes/agents.ts Outdated
Comment on lines +6 to +31
app.get('/api/relay-config', (_req: Request, res: Response) => {
const config = ctx.resolveRelaycastConfig();
if (!config) {
res.status(503).json({
success: false,
error: `Relaycast credentials not found in ${path.join(ctx.dataDir, 'relaycast.json')}`,
});
return;
}

if (!config.agentToken) {
res.status(503).json({
success: false,
error: 'Relaycast agent token is missing from relaycast.json',
});
return;
}

res.json({
success: true,
baseUrl: config.baseUrl,
apiKey: config.apiKey,
agentToken: config.agentToken,
agentName: config.agentName ?? path.basename(path.resolve(ctx.dataDir, '..')),
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Relaycast API key exposed to browser clients via unauthenticated /api/relay-config endpoint

The /api/relay-config endpoint at packages/dashboard-server/src/routes/relay-config.ts:24-31 returns the Relaycast apiKey and agentToken directly to any HTTP client without authentication. The frontend RelayConfigProvider.tsx fetches this endpoint to initialize the @relaycast/react SDK in the browser.

Security Impact

The apiKey is the workspace-level Relaycast API key (rk_...), and agentToken is the agent-scoped token. Both are returned in plaintext JSON to any caller. While the dashboard is typically served on localhost, in cloud deployments or when exposed on a network, any client that can reach the dashboard server can obtain these credentials and use them to:

  1. Read all messages in the Relaycast workspace
  2. Send messages as the dashboard agent
  3. Manage channels

The endpoint has no authentication middleware — no session check, no workspace token validation, no CSRF protection. This contrasts with the auth routes which use validateWorkspaceToken middleware.

The .claude/rules/security-auth-middleware.md rule explicitly states: "All mutation endpoints that modify credentials, tokens, or sensitive configuration must be covered by workspace-token validation middleware." While this is a GET endpoint, it exposes sensitive tokens.

Prompt for agents
In packages/dashboard-server/src/routes/relay-config.ts, add authentication or access control to the GET /api/relay-config endpoint. At minimum, when WORKSPACE_TOKEN is set (cloud deployments), require a valid session cookie or workspace token before returning the apiKey and agentToken. For local-only deployments (no WORKSPACE_TOKEN), the current behavior may be acceptable since the server is on localhost. Consider adding a check like: if (process.env.WORKSPACE_TOKEN && !isValidSession(req)) return res.status(401).json({ error: 'Unauthorized' }).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

devin-ai-integration[bot]

This comment was marked as resolved.

@khaliqgant

Copy link
Copy Markdown
Member Author

Also includes #54

});
return;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was in this file but we should support interrupt

  // Agent interrupt — broker does not have this endpoint yet.
  app.post('/api/agents/by-name/:name/interrupt', (req: Request, res: Response) => {
    const name = typeof req.params.name === 'string' ? decodeURIComponent(req.params.name) : '';
    res.status(501).json({
      success: false,
      error: 'Agent interrupt is not yet supported by the broker HTTP API.',
      name,
    });
  });

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 20 additional findings in Devin Review.

Open in Devin Review

Comment on lines 277 to +285
if (result.success) {
// Success! The optimistic message will be cleaned up when
// the real message arrives via WebSocket
// If the server returned a canonical message ID, update the optimistic
// message so dedup logic can match it when the real event arrives.
const canonicalId = result.data?.messageId;
if (canonicalId) {
setOptimisticMessages((prev) =>
prev.map((m) => (m.id === optimisticId ? { ...m, id: canonicalId } : m)),
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Optimistic messages with updated canonical IDs are never cleaned up, causing duplicates

When sendMessage succeeds and the server returns a messageId, the optimistic message's ID is updated to the canonical ID. However, the optimistic message is never removed from optimisticMessages state — it stays in the sending status forever. When the real message arrives via WebSocket with the same canonical ID, the dedup logic in mergedMessages should catch it, but the optimistic message remains with status: 'sending' while the real message has no status or status: 'read'.

Root Cause

At packages/dashboard/src/components/hooks/useMessages.ts:277-285, on successful send, the code updates the optimistic message's ID to the canonical one but never transitions its status from 'sending' to 'sent' or removes it. The comment at the old line 275-276 said "The optimistic message will be cleaned up when the real message arrives via WebSocket" — but after updating the ID to match the canonical one, the dedup logic in mergedMessages (which filters optimistic messages whose IDs match real messages) should work. However, the optimistic message retains status: 'sending' indefinitely, which means the UI shows a perpetual "sending" indicator on the message even after it was successfully delivered.

The test at useMessages.test.ts:36 confirms this: expect(result.current.messages[0]?.status).toBe('sending') — this is the expected behavior per the test, but it means the message never transitions to a delivered state.

Impact: Messages appear stuck in "sending" state in the UI until the WebSocket delivers the same message with the canonical ID, at which point dedup kicks in. If the WebSocket snapshot doesn't include the message (e.g., it was a DM not in the current view), the sending indicator persists forever.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

khaliqgant and others added 12 commits February 24, 2026 23:16
Replace the inline /api/history/stats handler and remove the broken
/api/history proxy (which returned HTML for unimplemented routes).

New routes in history-relaycast.ts:
- GET /api/history/stats — agent/message counts from Relaycast
- GET /api/history/messages — filterable message history from all
  channels and DMs via Relaycast SDK
- GET /api/history/conversations — channel + DM conversation list
  with participant info, message counts, and last message preview

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove file:// references to @agent-relay/contracts and @agent-relay/sdk
- Inline isBrokerIdentity function since @agent-relay/contracts is not on npm
- Update to latest npm versions:
  - @agent-relay/sdk: ^3.0.1
  - @agent-relay/config: ^2.4.7
  - @agent-relay/protocol: ^2.3.14
  - @agent-relay/storage: ^2.3.14
  - @agent-relay/trajectory: ^2.4.7
  - @agent-relay/utils: ^2.4.7
  - @relaycast/sdk: ^0.4.2
  - @relaycast/types: ^0.4.2
- Update createRelaycastClient to use new RelayCast API
- Fix TypeScript errors from SDK API changes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ndle

The @agent-relay/config package includes Node.js dependencies (child_process,
crypto, fs, os, path) that cannot be bundled for browser environments.

- Inline RegistryModelOptions in SpawnModal.tsx instead of importing from
  @agent-relay/config
- Remove transpilePackages for @agent-relay/config from next.config.js
- Remove @agent-relay/config path alias from tsconfig.json

The model options data was previously accessed via a path alias pointing to
a local file. With npm packages, the full package gets imported including
Node.js modules, causing webpack build failures.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add fallback implementation in relaycast-provider-helpers.ts for
  when SDK's createRelaycastClient is not exported (test compatibility)
- Create simple fetch-based HTTP client for reader operations to avoid
  registration requirements
- Handle both snake_case and camelCase response fields from SDK
- Skip WORKSPACE_TOKEN auth check in standalone mode for /api/relay-config
  to allow proper 503 response when credentials are missing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add @testing-library/jest-dom for toHaveTextContent matcher
- Create vitest.setup.ts to import jest-dom matchers
- Update vitest.config.ts to use setup file
- Add [broker] Run pattern to TELEMETRY_NOISE_PATTERN for noise filtering

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The dashboard server does not support --team-dir option. In mock mode,
the test data directory is sufficient.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ints

WebSocket endpoints like /ws require a protocol upgrade. Regular HTTP
GET requests should return 426 to indicate the endpoint exists but
requires WebSocket upgrade.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@khaliqgant
khaliqgant merged commit 3710d5a into main Mar 2, 2026
1 check passed
@khaliqgant
khaliqgant deleted the onboarding-updates branch March 2, 2026 05:31
github-actions Bot added a commit that referenced this pull request Mar 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant