Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
db62032
add reasoning support
DIodide Mar 1, 2026
a41f4d3
basic tool calls working + dynamic httpstreamable mcps
DIodide Mar 1, 2026
21cf316
persist tool call output and render to screen
DIodide Mar 1, 2026
e406f47
add QoL modes for interactivity
DIodide Mar 1, 2026
6363077
add usage token data, costs, to the developer mode ui
DIodide Mar 1, 2026
3f4dfb4
add include usage to openrouter
DIodide Mar 1, 2026
13087d5
parallelize tool calls
DIodide Mar 1, 2026
4d67fbc
stream tool calls correctly.
DIodide Mar 1, 2026
7cf478b
log openrouter error: set max_tokens: 4096 for non thinking models an…
DIodide Mar 1, 2026
883ae74
working parts stream + persist, + fix conversations css
DIodide Mar 2, 2026
f6727be
Add backend oauth implementation + routes based on MCP standard
DIodide Mar 2, 2026
06e3240
add frontend handling for oauth connectivity + initial ui
DIodide Mar 2, 2026
be0bd76
Add chat interrupts, auto scroll
DIodide Mar 4, 2026
c97104a
Merge pull request #6 from DIodide:feat/chat-interrupts-and-scroll
DIodide Mar 4, 2026
1c1ed1e
Merge branch 'staging' into feat/mcp-implementation
DIodide Mar 4, 2026
e287513
Update apps/web/src/routes/harnesses/index.tsx
DIodide Mar 4, 2026
98b9b35
remove migrations.ts
DIodide Mar 4, 2026
1bb307e
Merge branch 'feat/mcp-implementation' of https://github.com/DIodide/…
DIodide Mar 4, 2026
f92956a
remove migrations
DIodide Mar 4, 2026
85d351c
sign in ui
DIodide Mar 4, 2026
af9df03
default to newly created harness (prefer)
DIodide Mar 4, 2026
398bdac
add in-onboarding auth step
DIodide Mar 4, 2026
3aaccfe
use t3-oss-env validator instead of meta.env
DIodide Mar 4, 2026
ba3aa24
add mcp server status check in the ui
DIodide Mar 4, 2026
81f5685
show mcp error on frontend
DIodide Mar 4, 2026
5db47ac
add ui indicator for api/mcp/health/check
DIodide Mar 4, 2026
0901548
ease connectivity for mcp servers that do not support sessions
DIodide Mar 4, 2026
6ca2e28
indicator
DIodide Mar 5, 2026
a033b4c
linear support + proxies
DIodide Mar 6, 2026
e5dbaed
fix auto scroll issue
DIodide Mar 6, 2026
1f4eaa9
add gpt-5.4
DIodide Mar 7, 2026
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
406 changes: 406 additions & 0 deletions apps/web/src/components/mcp-server-status.tsx

Large diffs are not rendered by default.

108 changes: 108 additions & 0 deletions apps/web/src/components/message-actions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Check, Copy, RefreshCw } from "lucide-react";
import { useCallback, useRef, useState } from "react";
import type { UsageData } from "../lib/use-chat-stream";

export type DisplayMode = "zen" | "standard" | "developer";

interface MessageActionsProps {
content: string;
role: "user" | "assistant";
displayMode: DisplayMode;
onRegenerate?: () => void;
isStreaming?: boolean;
usage?: UsageData;
model?: string;
}

export function MessageActions({
content,
role,
displayMode,
onRegenerate,
isStreaming,
usage,
model,
}: MessageActionsProps) {
if (displayMode === "zen" || isStreaming) return null;

const showCopy = displayMode === "standard" || displayMode === "developer";
const showRegenerate =
displayMode === "developer" && role === "assistant" && onRegenerate;
const showInfo =
displayMode === "developer" && role === "assistant" && (usage || model);

return (
<div className="flex items-center gap-3 pt-1 opacity-0 transition-opacity group-hover:opacity-100">
{showCopy && <CopyMessageButton text={content} />}
{showRegenerate && <RegenerateButton onClick={onRegenerate} />}
{showInfo && <UsageInfo usage={usage} model={model} />}
</div>
);
}

function CopyMessageButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);

const handleCopy = useCallback(() => {
navigator.clipboard.writeText(text);
setCopied(true);
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => setCopied(false), 2000);
}, [text]);

return (
<button
type="button"
onClick={handleCopy}
className="flex items-center gap-1 text-[10px] text-muted-foreground transition-colors hover:text-foreground"
>
{copied ? (
<>
<Check size={12} />
Copied
</>
) : (
<>
<Copy size={12} />
Copy
</>
)}
</button>
);
}

function RegenerateButton({ onClick }: { onClick: () => void }) {
return (
<button
type="button"
onClick={onClick}
className="flex items-center gap-1 text-[10px] text-muted-foreground transition-colors hover:text-foreground"
>
<RefreshCw size={12} />
Regenerate
</button>
);
}

function UsageInfo({ usage, model }: { usage?: UsageData; model?: string }) {
const parts: string[] = [];

if (model) {
parts.push(model);
}

if (usage) {
parts.push(`${usage.promptTokens} in / ${usage.completionTokens} out`);

if (usage.cost != null) {
parts.push(`$${usage.cost.toFixed(4)}`);
}
}

return (
<span className="text-[10px] text-muted-foreground">
{parts.join(" · ")}
</span>
);
}
20 changes: 20 additions & 0 deletions apps/web/src/lib/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const MODELS = [
{ value: "openai/gpt-5.4", label: "GPT-5.4" },
{ value: "gpt-4o", label: "GPT-4o" },
{ value: "gpt-4.1", label: "GPT-4.1" },
{ value: "gpt-4.1-mini", label: "GPT-4.1 Mini" },
{ value: "claude-sonnet-4", label: "Claude Sonnet 4" },
{ value: "claude-sonnet-4-thinking", label: "Claude Sonnet 4 (Thinking)" },
{ value: "claude-opus-4", label: "Claude Opus 4" },
{ value: "claude-opus-4-thinking", label: "Claude Opus 4 (Thinking)" },
{
value: "google/gemini-3.1-flash-lite-preview",
label: "Gemini 3.1 Flash Lite Preview",
},
{ value: "gemini-2.5-pro", label: "Gemini 2.5 Pro" },
{ value: "gemini-2.5-flash", label: "Gemini 2.5 Flash" },
{ value: "deepseek-r1", label: "DeepSeek R1" },
{ value: "deepseek-v3", label: "DeepSeek V3" },
{ value: "grok-3", label: "Grok 3" },
{ value: "grok-3-mini", label: "Grok 3 Mini" },
];
61 changes: 57 additions & 4 deletions apps/web/src/lib/use-chat-stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,66 @@ export interface ToolCallEvent {
result?: string;
}

export interface UsageData {
promptTokens: number;
completionTokens: number;
totalTokens: number;
cost?: number;
}

export interface StreamPart {
type: "text" | "reasoning" | "tool_call";
content?: string;
tool?: string;
arguments?: Record<string, unknown>;
call_id?: string;
result?: string;
}

export interface ConvoStreamState {
content: string | null;
reasoning: string | null;
toolCalls: ToolCallEvent[];
parts: StreamPart[];
pendingDoneContent: string | null;
usage: UsageData | null;
model: string | null;
}

interface UseChatStreamCallbacks {
onToken: (conversationId: string, content: string) => void;
onThinking: (conversationId: string, content: string) => void;
onToolCall: (conversationId: string, event: ToolCallEvent) => void;
onToolResult: (
conversationId: string,
event: { call_id: string; result: string },
) => void;
onDone: (conversationId: string, fullContent: string) => void;
onDone: (
conversationId: string,
fullContent: string,
usage?: UsageData,
model?: string,
) => void;
onMcpError: (
conversationId: string,
event: { server_name: string; server_url: string; reason: string },
) => void;
onError: (conversationId: string, error: string) => void;
onAbort?: (conversationId: string) => void;
}

export interface ChatStreamRequest {
messages: Array<{ role: string; content: string }>;
harness: { model: string; mcps: string[]; name: string };
harness: {
model: string;
mcp_servers: Array<{
name: string;
url: string;
auth_type: "none" | "bearer" | "oauth";
auth_token?: string;
}>;
name: string;
};
conversation_id: string;
}

Expand Down Expand Up @@ -101,14 +141,25 @@ export function useChatStream(callbacks: UseChatStreamCallbacks) {
case "token":
cbRef.current.onToken(convoId, data.content);
break;
case "thinking":
cbRef.current.onThinking(convoId, data.content);
break;
case "tool_call":
cbRef.current.onToolCall(convoId, data);
break;
case "tool_result":
cbRef.current.onToolResult(convoId, data);
break;
case "mcp_error":
cbRef.current.onMcpError(convoId, data);
break;
case "done":
cbRef.current.onDone(convoId, data.content);
cbRef.current.onDone(
convoId,
data.content,
data.usage,
data.model,
);
break;
case "error":
cbRef.current.onError(convoId, data.message);
Expand All @@ -122,7 +173,9 @@ export function useChatStream(callbacks: UseChatStreamCallbacks) {
}
}
} catch (err: unknown) {
if (err instanceof Error && err.name !== "AbortError") {
if (err instanceof Error && err.name === "AbortError") {
cbRef.current.onAbort?.(convoId);
} else if (err instanceof Error) {
cbRef.current.onError(convoId, err.message);
}
} finally {
Expand Down
43 changes: 40 additions & 3 deletions apps/web/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Route as OnboardingRouteImport } from './routes/onboarding'
import { Route as IndexRouteImport } from './routes/index'
import { Route as HarnessesIndexRouteImport } from './routes/harnesses/index'
import { Route as ChatIndexRouteImport } from './routes/chat/index'
import { Route as HarnessesHarnessIdRouteImport } from './routes/harnesses/$harnessId'

const SignInRoute = SignInRouteImport.update({
id: '/sign-in',
Expand All @@ -40,18 +41,25 @@ const ChatIndexRoute = ChatIndexRouteImport.update({
path: '/chat/',
getParentRoute: () => rootRouteImport,
} as any)
const HarnessesHarnessIdRoute = HarnessesHarnessIdRouteImport.update({
id: '/harnesses/$harnessId',
path: '/harnesses/$harnessId',
getParentRoute: () => rootRouteImport,
} as any)

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/onboarding': typeof OnboardingRoute
'/sign-in': typeof SignInRoute
'/harnesses/$harnessId': typeof HarnessesHarnessIdRoute
'/chat/': typeof ChatIndexRoute
'/harnesses/': typeof HarnessesIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/onboarding': typeof OnboardingRoute
'/sign-in': typeof SignInRoute
'/harnesses/$harnessId': typeof HarnessesHarnessIdRoute
'/chat': typeof ChatIndexRoute
'/harnesses': typeof HarnessesIndexRoute
}
Expand All @@ -60,21 +68,42 @@ export interface FileRoutesById {
'/': typeof IndexRoute
'/onboarding': typeof OnboardingRoute
'/sign-in': typeof SignInRoute
'/harnesses/$harnessId': typeof HarnessesHarnessIdRoute
'/chat/': typeof ChatIndexRoute
'/harnesses/': typeof HarnessesIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/onboarding' | '/sign-in' | '/chat/' | '/harnesses/'
fullPaths:
| '/'
| '/onboarding'
| '/sign-in'
| '/harnesses/$harnessId'
| '/chat/'
| '/harnesses/'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/onboarding' | '/sign-in' | '/chat' | '/harnesses'
id: '__root__' | '/' | '/onboarding' | '/sign-in' | '/chat/' | '/harnesses/'
to:
| '/'
| '/onboarding'
| '/sign-in'
| '/harnesses/$harnessId'
| '/chat'
| '/harnesses'
id:
| '__root__'
| '/'
| '/onboarding'
| '/sign-in'
| '/harnesses/$harnessId'
| '/chat/'
| '/harnesses/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
OnboardingRoute: typeof OnboardingRoute
SignInRoute: typeof SignInRoute
HarnessesHarnessIdRoute: typeof HarnessesHarnessIdRoute
ChatIndexRoute: typeof ChatIndexRoute
HarnessesIndexRoute: typeof HarnessesIndexRoute
}
Expand Down Expand Up @@ -116,13 +145,21 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ChatIndexRouteImport
parentRoute: typeof rootRouteImport
}
'/harnesses/$harnessId': {
id: '/harnesses/$harnessId'
path: '/harnesses/$harnessId'
fullPath: '/harnesses/$harnessId'
preLoaderRoute: typeof HarnessesHarnessIdRouteImport
parentRoute: typeof rootRouteImport
}
}
}

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
OnboardingRoute: OnboardingRoute,
SignInRoute: SignInRoute,
HarnessesHarnessIdRoute: HarnessesHarnessIdRoute,
ChatIndexRoute: ChatIndexRoute,
HarnessesIndexRoute: HarnessesIndexRoute,
}
Expand Down
Loading