Skip to content

Commit 37b1129

Browse files
committed
Reject unsupported SME chat providers in the flow
- Block non-Claude providers before conversation creation - Disable unsupported options in the SME chat dialog - Add coverage for the provider validation path
1 parent 91ad734 commit 37b1129

5 files changed

Lines changed: 92 additions & 48 deletions

File tree

DESIGN.md

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -52,34 +52,34 @@ conversation with the agent. Every pixel must earn its place.
5252

5353
### Typography
5454

55-
| Role | Size | Weight | Tracking | Font Stack |
56-
|------|------|--------|----------|------------|
57-
| Thread title (sidebar) | `text-xs` (0.75rem) | `font-normal` | default | Inter, system-ui, sans-serif |
58-
| Thread subtitle / metadata | `text-[10px]` | `font-normal` | default | Inter, system-ui, sans-serif |
59-
| Badge text | `text-[10px]` | `font-medium` | default | Inter, system-ui, sans-serif |
60-
| Button text | `text-sm` (0.875rem) | `font-medium` | default | Inter, system-ui, sans-serif |
61-
| Heading / dialog title | `text-lg` (1.125rem) | `font-semibold` | `-0.01em` | Inter, system-ui, sans-serif |
62-
| Code / terminal | `text-sm` | `font-normal` | default | SF Mono, Consolas, monospace |
63-
| Project name | `text-xs` | `font-semibold` | default | Inter, system-ui, sans-serif |
55+
| Role | Size | Weight | Tracking | Font Stack |
56+
| -------------------------- | -------------------- | --------------- | --------- | ---------------------------- |
57+
| Thread title (sidebar) | `text-xs` (0.75rem) | `font-normal` | default | Inter, system-ui, sans-serif |
58+
| Thread subtitle / metadata | `text-[10px]` | `font-normal` | default | Inter, system-ui, sans-serif |
59+
| Badge text | `text-[10px]` | `font-medium` | default | Inter, system-ui, sans-serif |
60+
| Button text | `text-sm` (0.875rem) | `font-medium` | default | Inter, system-ui, sans-serif |
61+
| Heading / dialog title | `text-lg` (1.125rem) | `font-semibold` | `-0.01em` | Inter, system-ui, sans-serif |
62+
| Code / terminal | `text-sm` | `font-normal` | default | SF Mono, Consolas, monospace |
63+
| Project name | `text-xs` | `font-semibold` | default | Inter, system-ui, sans-serif |
6464

6565
### Color Semantics
6666

6767
Colors are referenced through CSS custom properties, never hardcoded hex values.
6868

69-
| Token | Usage |
70-
|-------|-------|
71-
| `text-foreground` | Primary text |
72-
| `text-muted-foreground` | Secondary/deemphasized text |
69+
| Token | Usage |
70+
| -------------------------- | ------------------------------------------------- |
71+
| `text-foreground` | Primary text |
72+
| `text-muted-foreground` | Secondary/deemphasized text |
7373
| `text-muted-foreground/50` | Tertiary/metadata text (branch names, timestamps) |
74-
| `bg-background` | Page background |
75-
| `bg-accent` | Hover state, active row highlight |
76-
| `bg-accent/60` | Active sidebar item |
77-
| `bg-accent/40` | Selected sidebar item |
78-
| `text-emerald-600` | Additions / success (green) |
79-
| `text-rose-500` | Deletions / error (red) |
80-
| `text-warning` | Warning states, behind-upstream |
81-
| `text-destructive` | Destructive actions (delete) |
82-
| `border-border/60` | Subtle badge borders |
74+
| `bg-background` | Page background |
75+
| `bg-accent` | Hover state, active row highlight |
76+
| `bg-accent/60` | Active sidebar item |
77+
| `bg-accent/40` | Selected sidebar item |
78+
| `text-emerald-600` | Additions / success (green) |
79+
| `text-rose-500` | Deletions / error (red) |
80+
| `text-warning` | Warning states, behind-upstream |
81+
| `text-destructive` | Destructive actions (delete) |
82+
| `border-border/60` | Subtle badge borders |
8383

8484
### Spacing Rules
8585

@@ -95,13 +95,13 @@ Colors are referenced through CSS custom properties, never hardcoded hex values.
9595

9696
Five premium themes, each with light and dark variants:
9797

98-
| Theme | Vibe |
99-
|-------|------|
100-
| **Iridescent Void** | Futuristic, expensive, slightly alien |
101-
| **Carbon** | Stark, modern, performance-focused |
102-
| **Vapor** | Refined, fluid, purposeful |
103-
| **Cotton Candy** | Sweet, dreamy, pink and blue |
104-
| **Cathedral Circuit** | Sacred machine, techno-gothic |
98+
| Theme | Vibe |
99+
| --------------------- | ------------------------------------- |
100+
| **Iridescent Void** | Futuristic, expensive, slightly alien |
101+
| **Carbon** | Stark, modern, performance-focused |
102+
| **Vapor** | Refined, fluid, purposeful |
103+
| **Cotton Candy** | Sweet, dreamy, pink and blue |
104+
| **Cathedral Circuit** | Sacred machine, techno-gothic |
105105

106106
All themes define the same set of CSS custom properties. Components must use semantic
107107
tokens (`bg-accent`, `text-muted-foreground`) — never theme-specific values.
@@ -249,6 +249,7 @@ a single flow:
249249
```
250250

251251
Quick action resolves automatically based on git state:
252+
252253
- Has changes + no PR → "Commit, push & PR"
253254
- Has changes + existing PR → "Commit & push"
254255
- No changes + ahead → "Push & create PR"

apps/server/src/sme/Layers/SmeChatServiceLive.test.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ describe("SmeChatServiceLive", () => {
123123

124124
const layer = makeSmeChatServiceLive({ sendSmeViaAnthropic: sendClaudeMessage }).pipe(
125125
Layer.provideMerge(Layer.succeed(SmeKnowledgeDocumentRepository, makeDocumentRepository())),
126-
Layer.provideMerge(Layer.succeed(SmeConversationRepository, makeConversationRepository([conversationRow]))),
126+
Layer.provideMerge(
127+
Layer.succeed(SmeConversationRepository, makeConversationRepository([conversationRow])),
128+
),
127129
Layer.provideMerge(Layer.succeed(SmeMessageRepository, messageRepo)),
128130
);
129131

@@ -249,9 +251,13 @@ describe("SmeChatServiceLive", () => {
249251
deletedAt: null,
250252
};
251253
const { repository: messageRepo, rowsByConversation } = makeMessageRepository();
252-
const layer = makeSmeChatServiceLive({ sendSmeViaAnthropic: () => Effect.die("unexpected send") }).pipe(
254+
const layer = makeSmeChatServiceLive({
255+
sendSmeViaAnthropic: () => Effect.succeed("unexpected send"),
256+
}).pipe(
253257
Layer.provideMerge(Layer.succeed(SmeKnowledgeDocumentRepository, makeDocumentRepository())),
254-
Layer.provideMerge(Layer.succeed(SmeConversationRepository, makeConversationRepository([conversationRow]))),
258+
Layer.provideMerge(
259+
Layer.succeed(SmeConversationRepository, makeConversationRepository([conversationRow])),
260+
),
255261
Layer.provideMerge(Layer.succeed(SmeMessageRepository, messageRepo)),
256262
);
257263

@@ -311,4 +317,28 @@ describe("SmeChatServiceLive", () => {
311317
}),
312318
]);
313319
});
320+
321+
it("rejects unsupported SME providers before a conversation can be created", async () => {
322+
const projectId = ProjectId.makeUnsafe("project-3");
323+
const layer = makeSmeChatServiceLive().pipe(
324+
Layer.provideMerge(Layer.succeed(SmeKnowledgeDocumentRepository, makeDocumentRepository())),
325+
Layer.provideMerge(Layer.succeed(SmeConversationRepository, makeConversationRepository([]))),
326+
Layer.provideMerge(Layer.succeed(SmeMessageRepository, makeMessageRepository().repository)),
327+
);
328+
329+
await expect(
330+
Effect.runPromise(
331+
Effect.gen(function* () {
332+
const service = yield* SmeChatService;
333+
yield* service.createConversation({
334+
projectId,
335+
title: "Unsupported provider",
336+
provider: "codex",
337+
authMethod: "chatgpt",
338+
model: "codex-mini",
339+
});
340+
}).pipe(Effect.provide(layer)),
341+
),
342+
).rejects.toThrow("SME Chat only supports Claude Code conversations right now.");
343+
});
314344
});

apps/server/src/sme/Layers/SmeChatServiceLive.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
* @module SmeChatServiceLive
88
*/
99
import type {
10-
ProviderStartOptions,
1110
SmeAuthMethod,
1211
SmeConversation,
1312
SmeKnowledgeDocument,
@@ -25,10 +24,7 @@ import { SmeConversationRepository } from "../../persistence/Services/SmeConvers
2524
import { SmeKnowledgeDocumentRepository } from "../../persistence/Services/SmeKnowledgeDocuments.ts";
2625
import { SmeMessageRepository } from "../../persistence/Services/SmeMessages.ts";
2726
import { isValidSmeAuthMethod } from "../authValidation.ts";
28-
import {
29-
resolveAnthropicClientOptions,
30-
sendSmeViaAnthropic,
31-
} from "../backends/anthropic.ts";
27+
import { resolveAnthropicClientOptions, sendSmeViaAnthropic } from "../backends/anthropic.ts";
3228
import { buildSmeSystemPrompt } from "../promptBuilder.ts";
3329
import {
3430
SmeChatError,
@@ -192,7 +188,8 @@ const makeSmeChatService = (options?: SmeChatServiceLiveOptions) =>
192188
? "Claude SME Chat can use the configured Anthropic API key."
193189
: "Claude SME Chat can use the configured Anthropic auth token.",
194190
resolvedAuthMethod: conversation.authMethod,
195-
resolvedAccountType: clientOptions.apiKey !== null ? "apiKey" : "unknown",
191+
resolvedAccountType:
192+
clientOptions.apiKey !== null ? ("apiKey" as const) : ("unknown" as const),
196193
};
197194
});
198195

@@ -462,8 +459,7 @@ const makeSmeChatService = (options?: SmeChatServiceLiveOptions) =>
462459
resolveAnthropicClientOptions({
463460
providerOptions: input.providerOptions?.claudeAgent,
464461
}),
465-
catch: (cause) =>
466-
new SmeChatError("sendMessage:providerRuntime", String(cause), cause),
462+
catch: (cause) => new SmeChatError("sendMessage:providerRuntime", String(cause), cause),
467463
});
468464

469465
if (!anthropicClientOptions.apiKey && !anthropicClientOptions.authToken) {
@@ -476,13 +472,13 @@ const makeSmeChatService = (options?: SmeChatServiceLiveOptions) =>
476472
}
477473

478474
const systemPrompt = buildSmeSystemPrompt(docs);
479-
const messages: ReadonlyArray<MessageParam> = [
480-
...promptHistory
475+
const messages: Array<MessageParam> = [
476+
...(promptHistory
481477
.filter((message) => message.role === "user" || message.role === "assistant")
482478
.map((message) => ({
483479
role: message.role,
484480
content: message.text,
485-
})) as Array<MessageParam>,
481+
})) as Array<MessageParam>),
486482
{
487483
role: "user",
488484
content: input.text,
@@ -502,13 +498,19 @@ const makeSmeChatService = (options?: SmeChatServiceLiveOptions) =>
502498
abortSignal: abortController.signal,
503499
});
504500

505-
yield* input.setInterruptEffect(
501+
yield* setInterrupt(
502+
input.conversationId,
506503
Effect.sync(() => {
507504
abortController.abort();
508505
}),
509506
);
510507

511508
const responseText = yield* sendEffect.pipe(
509+
Effect.ensuring(
510+
Effect.gen(function* () {
511+
yield* clearInterrupt(input.conversationId);
512+
}),
513+
),
512514
Effect.mapError((cause) =>
513515
cause instanceof SmeChatError
514516
? cause

apps/server/src/sme/backends/anthropic.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function resolveAnthropicClientOptions(
4242
const baseURL = nonEmptyTrimmed(env.ANTHROPIC_BASE_URL ?? env.ANTHROPIC_API_BASE_URL);
4343

4444
return {
45-
apiKey: authToken ? null : explicitApiKey ?? null,
45+
apiKey: authToken ? null : (explicitApiKey ?? null),
4646
authToken: authToken ?? null,
4747
...(baseURL ? { baseURL } : {}),
4848
};
@@ -58,7 +58,7 @@ function createAnthropicClient(options: ResolvedAnthropicClientOptions): Anthrop
5858

5959
export interface SendSmeViaAnthropicInput {
6060
readonly client?: AnthropicMessagesClient;
61-
readonly messages: ReadonlyArray<MessageParam>;
61+
readonly messages: Array<MessageParam>;
6262
readonly conversationId: string;
6363
readonly assistantMessageId: string;
6464
readonly model: string;
@@ -73,7 +73,8 @@ export function sendSmeViaAnthropic(input: SendSmeViaAnthropicInput) {
7373
try: async () => {
7474
let result = "";
7575
const client =
76-
input.client ?? createAnthropicClient(input.clientOptions ?? resolveAnthropicClientOptions());
76+
input.client ??
77+
createAnthropicClient(input.clientOptions ?? resolveAnthropicClientOptions());
7778
const stream = client.messages.stream(
7879
{
7980
model: input.model,

apps/web/src/components/sme/SmeConversationDialog.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import {
3535
SME_PROVIDER_LABELS,
3636
} from "./smeConversationConfig";
3737

38+
const SME_CHAT_SUPPORTED_PROVIDERS = new Set<ProviderKind>(["claudeAgent"]);
39+
3840
interface SmeConversationDialogProps {
3941
open: boolean;
4042
onOpenChange: (open: boolean) => void;
@@ -216,11 +218,19 @@ export function SmeConversationDialog({
216218
className="h-10 rounded-xl border border-border bg-background px-3 text-sm"
217219
>
218220
{(["claudeAgent", "codex", "openclaw"] as const).map((value) => (
219-
<option key={value} value={value}>
221+
<option
222+
key={value}
223+
value={value}
224+
disabled={!SME_CHAT_SUPPORTED_PROVIDERS.has(value)}
225+
>
220226
{SME_PROVIDER_LABELS[value]}
221227
</option>
222228
))}
223229
</select>
230+
<p className="text-xs text-muted-foreground">
231+
Claude Code is the only SME Chat provider that currently supports direct replies
232+
without tool workflows.
233+
</p>
224234
</label>
225235

226236
<label className="grid gap-1.5">

0 commit comments

Comments
 (0)