Skip to content

Commit 3f45fa6

Browse files
committed
feat(web): wire new session tools + update system prompt
1 parent c6574af commit 3f45fa6

6 files changed

Lines changed: 437 additions & 111 deletions

File tree

apps/web/src/app/api/sessions/chat/route.ts

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
1-
import { anthropic } from "@ai-sdk/anthropic";
2-
import {
3-
streamText,
4-
convertToModelMessages,
5-
stepCountIs,
6-
type UIMessage,
7-
} from "ai";
81
import { getSessionFromRequest } from "@/lib/api";
2+
import { buildSessionInstructions } from "@/lib/sessions/instructions";
93
import {
104
createChatSession,
115
listRecentSessions,
126
persistMessages,
137
updateSessionSummary,
148
} from "@/lib/sessions/persistence";
159
import { extractSessionSummary } from "@/lib/sessions/summary";
16-
import { buildSessionInstructions } from "@/lib/sessions/instructions";
1710
import {
1811
createSessionTools,
1912
fetchAccountResources,
2013
} from "@/lib/sessions/tools";
14+
import { anthropic } from "@ai-sdk/anthropic";
15+
import {
16+
type UIMessage,
17+
convertToModelMessages,
18+
stepCountIs,
19+
streamText,
20+
} from "ai";
2121
import { after } from "next/server";
2222
import { NextResponse } from "next/server";
2323

@@ -47,7 +47,7 @@ export async function POST(req: Request) {
4747
listRecentSessions(sessionToken, 5),
4848
]);
4949
const system = buildSessionInstructions(resources, recentSessions);
50-
const tools = createSessionTools(sessionToken);
50+
const tools = createSessionTools(sessionToken, resources);
5151

5252
const result = streamText({
5353
model: anthropic("claude-sonnet-4-20250514"),
@@ -75,6 +75,7 @@ export async function POST(req: Request) {
7575
"lookup_docs",
7676
"recall_sessions",
7777
"diagnose",
78+
"show_code",
7879
],
7980
};
8081
}
@@ -108,15 +109,9 @@ export async function POST(req: Request) {
108109
chatSessionId,
109110
allMessages as UIMessage[],
110111
);
111-
const summary = extractSessionSummary(
112-
allMessages as UIMessage[],
113-
);
112+
const summary = extractSessionSummary(allMessages as UIMessage[]);
114113
if (summary.toolCalls.length > 0) {
115-
await updateSessionSummary(
116-
sessionToken,
117-
chatSessionId,
118-
summary,
119-
);
114+
await updateSessionSummary(sessionToken, chatSessionId, summary);
120115
}
121116
} catch (e) {
122117
console.error("[sessions/chat] Persist error:", e);

apps/web/src/components/sessions/message-list.tsx

Lines changed: 114 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
"use client";
22

33
import {
4-
isTextUIPart,
5-
isToolUIPart,
6-
getToolName,
7-
type UIMessage,
4+
type ChatStatus,
85
type UIDataTypes,
6+
type UIMessage,
97
type UITools,
10-
type ChatStatus,
8+
getToolName,
9+
isTextUIPart,
10+
isToolUIPart,
1111
} from "ai";
1212
import { useEffect, useMemo, useRef } from "react";
1313
import { ToolPartRenderer } from "./tool-part-renderer";
14+
import { SessionCodeBlock } from "./tool-parts/session-code-block";
1415
import { StepFlow, type StepInfo } from "./tool-parts/step-flow";
1516

1617
interface MessageListProps {
@@ -75,6 +76,9 @@ const VISIBLE_TOOLS = new Set([
7576
"manage_subgraphs",
7677
"scaffold_subgraph",
7778
"recall_sessions",
79+
"query_subgraph",
80+
"diagnose",
81+
"show_code",
7882
]);
7983

8084
const TOOL_STEP_LABELS: Record<string, string> = {
@@ -91,6 +95,7 @@ const TOOL_STEP_LABELS: Record<string, string> = {
9195
query_subgraph: "Querying subgraph data",
9296
manage_keys: "Managing API keys",
9397
manage_subgraphs: "Managing subgraphs",
98+
show_code: "Generating code example",
9499
};
95100

96101
function MessageBubble({
@@ -135,26 +140,18 @@ function MessageBubble({
135140
{segments?.map((seg) => {
136141
if (seg.type === "text") {
137142
if (!seg.text.trim()) return null;
138-
return (
139-
<div
140-
key={seg.key}
141-
className="msg-content"
142-
dangerouslySetInnerHTML={{
143-
__html: formatMarkdown(seg.text),
144-
}}
145-
/>
146-
);
143+
return <MessageTextContent key={seg.key} text={seg.text} />;
147144
}
148145
if (seg.type === "step-flow") {
149-
return (
150-
<StepFlow key={seg.key} steps={seg.steps} />
151-
);
146+
return <StepFlow key={seg.key} steps={seg.steps} />;
152147
}
153148
if (seg.type === "tool") {
154149
return (
155150
<ToolPartRenderer
156151
key={seg.key}
157-
part={seg.part as Parameters<typeof ToolPartRenderer>[0]["part"]}
152+
part={
153+
seg.part as Parameters<typeof ToolPartRenderer>[0]["part"]
154+
}
158155
addToolOutput={addToolOutput}
159156
/>
160157
);
@@ -184,7 +181,9 @@ function groupPartsIntoSegments(
184181
if (toolBuffer.length >= 2) {
185182
// Multiple tool calls → render as step flow
186183
const totalVisible = toolBuffer.filter((t) =>
187-
VISIBLE_TOOLS.has(getToolName(t.part as Parameters<typeof getToolName>[0])),
184+
VISIBLE_TOOLS.has(
185+
getToolName(t.part as Parameters<typeof getToolName>[0]),
186+
),
188187
).length;
189188

190189
if (totalVisible >= 2) {
@@ -317,31 +316,105 @@ function InlineToolCard({ part }: { part: UIMessage["parts"][number] }) {
317316
function MiniLogo() {
318317
return (
319318
<svg viewBox="4 7 40 28" width="18" height="12" fill="none">
320-
<polygon
321-
points="8,25 28,17 42,25 22,33"
322-
fill="rgba(255,255,255,0.15)"
323-
/>
324-
<polygon
325-
points="8,19 28,11 42,19 22,27"
326-
fill="rgba(255,255,255,0.4)"
327-
/>
319+
<polygon points="8,25 28,17 42,25 22,33" fill="rgba(255,255,255,0.15)" />
320+
<polygon points="8,19 28,11 42,19 22,27" fill="rgba(255,255,255,0.4)" />
328321
</svg>
329322
);
330323
}
331324

325+
/** Split text into alternating prose and fenced code block chunks */
326+
function parseTextWithCodeBlocks(
327+
text: string,
328+
): Array<
329+
| { type: "prose"; content: string }
330+
| { type: "code"; code: string; lang: string }
331+
> {
332+
const parts: Array<
333+
| { type: "prose"; content: string }
334+
| { type: "code"; code: string; lang: string }
335+
> = [];
336+
const fenceRegex = /^```(\w*)\s*\n([\s\S]*?)^```\s*$/gm;
337+
let lastIndex = 0;
338+
let match: RegExpExecArray | null;
339+
340+
while ((match = fenceRegex.exec(text)) !== null) {
341+
// Prose before this code block
342+
if (match.index > lastIndex) {
343+
parts.push({
344+
type: "prose",
345+
content: text.slice(lastIndex, match.index),
346+
});
347+
}
348+
parts.push({
349+
type: "code",
350+
code: match[2].trimEnd(),
351+
lang: match[1] || "text",
352+
});
353+
lastIndex = match.index + match[0].length;
354+
}
355+
356+
// Remaining prose after last code block
357+
if (lastIndex < text.length) {
358+
parts.push({ type: "prose", content: text.slice(lastIndex) });
359+
}
360+
361+
return parts;
362+
}
363+
364+
/** Renders text with fenced code blocks as SessionCodeBlock components */
365+
function MessageTextContent({ text }: { text: string }) {
366+
const chunks = useMemo(() => parseTextWithCodeBlocks(text), [text]);
367+
368+
// No code blocks — fast path
369+
if (chunks.length === 1 && chunks[0].type === "prose") {
370+
return (
371+
<div
372+
className="msg-content"
373+
dangerouslySetInnerHTML={{ __html: formatMarkdown(chunks[0].content) }}
374+
/>
375+
);
376+
}
377+
378+
return (
379+
<>
380+
{chunks.map((chunk, i) => {
381+
if (chunk.type === "code") {
382+
return (
383+
<SessionCodeBlock
384+
key={`code-${i}`}
385+
code={chunk.code}
386+
lang={chunk.lang}
387+
/>
388+
);
389+
}
390+
if (!chunk.content.trim()) return null;
391+
return (
392+
<div
393+
key={`prose-${i}`}
394+
className="msg-content"
395+
dangerouslySetInnerHTML={{ __html: formatMarkdown(chunk.content) }}
396+
/>
397+
);
398+
})}
399+
</>
400+
);
401+
}
402+
332403
function formatMarkdown(text: string): string {
333-
return text
334-
.replace(/&/g, "&amp;")
335-
.replace(/</g, "&lt;")
336-
.replace(/>/g, "&gt;")
337-
// Headers
338-
.replace(/^### (.+)$/gm, '<h4 class="msg-h4">$1</h4>')
339-
.replace(/^## (.+)$/gm, '<h3 class="msg-h3">$1</h3>')
340-
// Bold + code
341-
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
342-
.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
343-
// List items
344-
.replace(/^- (.+)$/gm, '<div class="msg-li">$1</div>')
345-
// Line breaks (but not after block elements)
346-
.replace(/\n(?!<)/g, "<br>");
404+
return (
405+
text
406+
.replace(/&/g, "&amp;")
407+
.replace(/</g, "&lt;")
408+
.replace(/>/g, "&gt;")
409+
// Headers
410+
.replace(/^### (.+)$/gm, '<h4 class="msg-h4">$1</h4>')
411+
.replace(/^## (.+)$/gm, '<h3 class="msg-h3">$1</h3>')
412+
// Bold + code
413+
.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
414+
.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
415+
// List items
416+
.replace(/^- (.+)$/gm, '<div class="msg-li">$1</div>')
417+
// Line breaks (but not after block elements)
418+
.replace(/\n(?!<)/g, "<br>")
419+
);
347420
}

0 commit comments

Comments
 (0)