Skip to content
Merged
18 changes: 5 additions & 13 deletions apps/web/docs/CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ References:

### Organize by domain, not technical layer

Group code by what it does (messages, conversations, streaming,
Group code by what it does (messages, conversations, voice,
interactions), not by what it is (hooks, utils, components). The
top-level folder for domain modules is called **`domains/`**.

Expand All @@ -181,15 +181,6 @@ src/
conversation-queries.ts
use-conversation-loader.ts
types.ts
streaming/ # SSE transport, event parsing
stream-store.ts
stream-transport.ts
event-parser.ts
event-types.ts
handlers/
message-handlers.ts
interaction-handlers.ts
types.ts
chat/ # chat feature module
turn-store.ts # turn-level state machine
turn-coordinator.ts # atomic turn-store + conversation-store transitions
Expand All @@ -207,6 +198,7 @@ src/
auth/ # allauth client, CSRF, auth middleware
feature-flags/ # feature flag provider
sync/ # server state sync (tag registry, router)
streaming/ # SSE transport, event parsing, debug tracking
api-client.ts # HeyAPI configured client + interceptors
telemetry/ # client identity for daemon registration
runtime/ # framework adapters, platform bridges
Expand All @@ -220,7 +212,7 @@ src/
This app uses `domains/` over the more common `features/` because
"features" implies product-level concepts (like "chat" or
"settings") that contain multiple domains. `messages`,
`conversations`, and `streaming` are business domains with distinct
`conversations`, and `voice` are business domains with distinct
data models and lifecycles — not features. `domains/` is more precise
for a DDD-influenced architecture and signals that each folder
represents a bounded context.
Expand Down Expand Up @@ -306,7 +298,7 @@ Examples of correct splits:
- `messages/` vs `conversations/`: messages are created, streamed,
delta-updated, and compacted — different lifecycle from conversation
CRUD and grouping.
- `streaming/` vs `messages/`: SSE transport and reconnection logic
- `lib/streaming/` vs `messages/`: SSE transport and reconnection logic
changes for different reasons than message state management.
- `chat/interaction-store` vs `chat/turn-store`: user-facing prompts
(secrets, confirmations) have their own state machine, independent
Expand Down Expand Up @@ -384,7 +376,7 @@ owns it.
| `hooks/` | Cross-domain React hooks | `use-is-mobile.ts`, `use-visible-viewport.ts`, `use-feature-flag-bus-sync.ts` |
| `utils/` | Pure utility functions (no side effects, no third-party SDKs) | `format.ts`, `browser.ts`, `network-status.ts`, `stable-id.ts` |
| `types/` | Shared type definitions | `window.d.ts`, `api-types.ts` |
| `lib/` | Third-party integrations and infrastructure wrappers (have side effects, configure SDK instances, manage lifecycle) | `sentry/` (error reporting), `auth/` (allauth + CSRF), `feature-flags/` (catalog + registry), `sync/` (state sync), `api-client.ts` (HeyAPI) |
| `lib/` | Third-party integrations and infrastructure wrappers (have side effects, configure SDK instances, manage lifecycle) | `sentry/` (error reporting), `auth/` (allauth + CSRF), `feature-flags/` (catalog + registry), `sync/` (state sync), `streaming/` (SSE transport), `diagnostics.ts` (session ring buffer), `api-client.ts` (HeyAPI) |
| `runtime/` | Framework adapters and native platform bridges | `route-adapter.ts`, `native-auth.ts`, `native-deep-link.ts`, `app-bridge.ts` |
| `components/` | Cross-domain shared UI | `error-boundary.tsx`, `sign-in-gate.tsx`, `providers.tsx` |

Expand Down
3 changes: 2 additions & 1 deletion apps/web/docs/STYLE_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,14 @@ src/
chat/ # chat feature (turn, subagent, interaction stores)
messages/ # message lifecycle
conversations/ # conversation CRUD, grouping, selection
streaming/ # SSE transport, event parsing
voice/ # STT, TTS, PTT
...
hooks/ # cross-domain shared hooks
utils/ # cross-domain shared utilities (pure functions)
types/ # cross-domain shared types
lib/ # configured third-party wrappers (API client, Sentry, CSRF)
streaming/ # SSE transport, event parsing, debug tracking
diagnostics.ts # session diagnostics ring buffer
runtime/ # framework adapters, platform bridges
components/ # cross-domain shared UI
generated/ # auto-generated code (HeyAPI, catalogs)
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/share-feedback-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { feedbackCreateMutation } from "@/generated/api/@tanstack/react-query.ge
import type { ClassificationEnum } from "@/generated/api/types.gen";
import { buildVellumMutatingHeaders } from "@/lib/auth/request-headers";
import type { ChatDebugApi } from "@/domains/chat/utils/debug-api";
import { buildChatDiagnosticsSnapshot } from "@/domains/chat/utils/diagnostics";
import { buildDiagnosticsSnapshot } from "@/lib/diagnostics";
import { isElectron } from "@/runtime/is-electron";
import { useAuthStore } from "@/stores/auth-store";
import { VELLUM_COMMUNITY_URL } from "@/utils/external-urls";
Expand Down Expand Up @@ -208,7 +208,7 @@ async function buildClientLogsFile(
} catch {
currentChatState = null;
}
const chatDiagnostics = buildChatDiagnosticsSnapshot(currentChatState);
const chatDiagnostics = buildDiagnosticsSnapshot(currentChatState);
const payload = {
collected_at: now.toISOString(),
time_range: timeRange,
Expand Down
19 changes: 0 additions & 19 deletions apps/web/src/domains/chat/api/client.ts

This file was deleted.

8 changes: 3 additions & 5 deletions apps/web/src/domains/chat/api/debug-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@
* namespace, so both halves of the debug API mount/unmount together.
*/

import type {
SseDebugClient,
SseDebugEventEntry,
} from "@/domains/chat/api/stream-debug";
import {
type SseDebugClient,
type SseDebugEventEntry,
getSseClients,
getSseEvents,
} from "@/domains/chat/api/stream-debug";
} from "@/lib/streaming/stream-debug";

export interface ChatDebugEventsApi {
/** Snapshot of currently-live SSE clients. */
Expand Down
10 changes: 4 additions & 6 deletions apps/web/src/domains/chat/api/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@ import {
assertHasResponse,
extractErrorMessage,
} from "@/utils/api-errors";
import {
recordChatDiagnostic,
summarizeDisplayMessages,
} from "@/domains/chat/utils/diagnostics";
import { recordDiagnostic } from "@/lib/diagnostics";
import { summarizeDisplayMessages } from "@/domains/chat/utils/diagnostics";

import { mapRuntimeToDisplayMessage } from "@/domains/chat/utils/map-runtime-message";
import { dedupeDisplayMessages } from "@/domains/chat/utils/reconcile";
Expand Down Expand Up @@ -119,7 +117,7 @@ async function fetchPaginatedHistory(

assertHasResponse(response, error, "Failed to fetch history");
if (!response.ok) {
recordChatDiagnostic("history_page_fetch_error", {
recordDiagnostic("history_page_fetch_error", {
assistantId,
query,
status: response.status,
Expand All @@ -133,7 +131,7 @@ async function fetchPaginatedHistory(
}

const result = parsePaginatedResponse(data ?? {});
recordChatDiagnostic("history_page_fetch", {
recordDiagnostic("history_page_fetch", {
assistantId,
query,
status: response.status,
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/domains/chat/api/interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@

import type { ConfirmationDecision } from "@/types/event-types";
import type { QuestionSubmission } from "@/domains/chat/api/event-types";
import { client } from "@/generated/api/client.gen";
import {
assertHasResponse,
client,
extractErrorMessage,
SDK_BASE_OPTIONS,
} from "@/domains/chat/api/client";
} from "@/utils/api-errors";

export async function getPendingInteractions(
assistantId: string,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/domains/chat/api/messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*/

import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { client } from "@/domains/chat/api/client";
import { client } from "@/generated/api/client.gen";
import { getChatHistory, normalizeContentOrder, normalizeTextSegments, postChatMessage } from "@/domains/chat/api/messages";

// ---------------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/domains/chat/api/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import type {
SlackRuntimeMessage,
Surface,
} from "@/domains/chat/types/types";
import { client } from "@/generated/api/client.gen";
import {
assertHasResponse,
client,
extractErrorMessage,
SDK_BASE_OPTIONS,
} from "@/domains/chat/api/client";
} from "@/utils/api-errors";
import {
normalizePreChatOnboardingContext,
type PreChatOnboardingContext,
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/domains/chat/api/slack-channel-name.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { client, SDK_BASE_OPTIONS } from "@/domains/chat/api/client";
import { client } from "@/generated/api/client.gen";
import { SDK_BASE_OPTIONS } from "@/utils/api-errors";

export interface SlackChannelNameResolution {
channelId: string;
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/domains/chat/api/surfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
* Surface action submission, content fetching, and artifact download.
*/

import { client } from "@/generated/api/client.gen";
import {
assertHasResponse,
client,
extractErrorMessage,
SDK_BASE_OPTIONS,
} from "@/domains/chat/api/client";
} from "@/utils/api-errors";

export async function submitSurfaceAction(
assistantId: string,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/domains/chat/api/threshold-api.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, mock, test } from "bun:test";

import { client } from "@/domains/chat/api/client";
import { client } from "@/generated/api/client.gen";
import { getConversationOverride } from "@/lib/threshold-api";

// ---------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/domains/chat/chat-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ import {
import { getEditChatConversationId, setEditChatConversationId } from "@/domains/chat/utils/edit-chat-session";
import { routes } from "@/utils/routes";
import { haptic } from "@/utils/haptics";
import type { ChatEventStream } from "@/domains/chat/api/stream";
import type { ChatEventStream } from "@/lib/streaming/stream-transport";
import {
ChatRouteContent,
type ChatRouteContentProps,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ import { DiskPressureBanner, type DiskPressureBannerMode } from "@/domains/chat/
import type { VoiceInputButtonHandle } from "@/domains/chat/components/voice-input-button";
import type { Conversation } from "@/types/conversation-types";
import { submitQuestionResponse } from "@/domains/chat/api/interactions";
import type { ChatEventStream } from "@/domains/chat/api/stream";
import type { ChatEventStream } from "@/lib/streaming/stream-transport";

// ---------------------------------------------------------------------------
// Types
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { cleanup, render, screen, waitFor } from "@testing-library/react";

import { client } from "@/domains/chat/api/client";
import { client } from "@/generated/api/client.gen";
import { SlackChannelFooter } from "@/domains/chat/components/slack-channel-footer";

describe("SlackChannelFooter lazy channel name resolution", () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/domains/chat/hooks/stream-message-updaters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import type { DisplayMessage } from "@/domains/chat/utils/reconcile";
import type { Surface } from "@/domains/chat/types/types";
import { toDisplayAttachments } from "@/domains/chat/api/event-parser";
import { toDisplayAttachments } from "@/lib/streaming/event-parser";
import type { AllowlistOption, DirectoryScopeOption, ScopeOption } from "@/types/interaction-ui-types";
import type { ChatMessageToolCall } from "@/domains/chat/api/event-types";
import type { MessageCompleteEvent } from "@vellumai/assistant-api";
Expand Down
12 changes: 5 additions & 7 deletions apps/web/src/domains/chat/hooks/use-conversation-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,8 @@ import {
reconcileDisplayMessagesWithLatestHistory,
} from "@/domains/chat/utils/reconcile";
import { filterDismissedSurfaces } from "@/domains/chat/utils/dismissed-surfaces-storage";
import {
recordChatDiagnostic,
summarizeDisplayMessages,
} from "@/domains/chat/utils/diagnostics";
import { recordDiagnostic } from "@/lib/diagnostics";
import { summarizeDisplayMessages } from "@/domains/chat/utils/diagnostics";
import type { TranscriptPaginationState } from "@/domains/chat/transcript/types";
import type { ContextWindowUsage } from "@/domains/chat/components/context-window-indicator";
import { useConversationStore } from "@/stores/conversation-store";
Expand Down Expand Up @@ -179,7 +177,7 @@ export function useConversationHistory({
const isFreshSwitch = switchResetRef.current;
switchResetRef.current = false;

recordChatDiagnostic("history_tq_data_apply", {
recordDiagnostic("history_tq_data_apply", {
assistantId,
conversationId: activeConversationId,
isFreshSwitch,
Expand All @@ -193,7 +191,7 @@ export function useConversationHistory({
dismissedSurfaceIdsRef.current,
);

recordChatDiagnostic("history_tq_set_messages", {
recordDiagnostic("history_tq_set_messages", {
assistantId,
conversationId: activeConversationId,
isFreshSwitch,
Expand Down Expand Up @@ -256,7 +254,7 @@ export function useConversationHistory({
}
}
} else {
recordChatDiagnostic("history_tq_empty", {
recordDiagnostic("history_tq_empty", {
assistantId,
conversationId: activeConversationId,
});
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/domains/chat/hooks/use-conversation-switch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
import { useTurnStore } from "@/domains/chat/turn-store";
import { useInteractionStore } from "@/domains/chat/interaction-store";
import { useConversationStore } from "@/stores/conversation-store";
import { recordChatDiagnostic } from "@/domains/chat/utils/diagnostics";
import { recordDiagnostic } from "@/lib/diagnostics";
import { loadDismissedSurfaceIds } from "@/domains/chat/utils/dismissed-surfaces-storage";
import type { DisplayMessage } from "@/domains/chat/utils/reconcile";
import type { TranscriptPaginationState } from "@/domains/chat/transcript/types";
Expand Down Expand Up @@ -128,7 +128,7 @@ export function useConversationSwitch({
}
previousConversationIdRef.current = activeConversationId;

recordChatDiagnostic("conversation_switch_reset", {
recordDiagnostic("conversation_switch_reset", {
assistantId,
conversationId: activeConversationId,
outgoingConversationId: outgoingConversationId ?? null,
Expand Down
7 changes: 2 additions & 5 deletions apps/web/src/domains/chat/hooks/use-empty-state-greeting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,8 @@

import { useQuery } from "@tanstack/react-query";

import {
client,
assertHasResponse,
SDK_BASE_OPTIONS,
} from "@/domains/chat/api/client";
import { client } from "@/generated/api/client.gen";
import { assertHasResponse, SDK_BASE_OPTIONS } from "@/utils/api-errors";
import { DEFAULT_EMPTY_STATE_GREETING } from "@/domains/chat/utils/empty-state-constants";

const STALE_TIME_MS = 5 * 60 * 1000;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { cleanup, renderHook } from "@testing-library/react";
import { useRef, type MutableRefObject } from "react";

import type { AssistantEvent } from "@/types/event-types";
import type { ChatEventStream } from "@/domains/chat/api/stream";
import type { ChatEventStream } from "@/lib/streaming/stream-transport";
import {
__resetEventBusForTesting,
useEventBusStore,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { act } from "react";
import { useRef, type MutableRefObject } from "react";

import type { AssistantEvent } from "@/types/event-types";
import type { ChatEventStream } from "@/domains/chat/api/stream";
import type { ChatEventStream } from "@/lib/streaming/stream-transport";
import {
__resetEventBusForTesting,
useEventBusStore,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
import { cleanup, renderHook } from "@testing-library/react";
import { useRef, type MutableRefObject } from "react";

import type { ChatEventStream } from "@/domains/chat/api/stream";
import type { ChatEventStream } from "@/lib/streaming/stream-transport";
import {
__resetEventBusForTesting,
useEventBusStore,
Expand Down
Loading