Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 7 additions & 6 deletions docs/THEMING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,13 @@ Defined under `.g-root`, applied regardless of theme.

### Colors

| Variable | Default | Description |
| -------------------------------------- | ------------------------------------------------------ | ----------------------------------- |
| `--g-aikit-color-bg-primary` | `var(--g-aikit-bg-primary, var(--g-color-base-float))` | Primary background |
| `--g-aikit-color-bg-secondary` | `#f5f5f5` | Secondary background |
| `--g-aikit-color-bg-message-user` | `#0077ff` | User message bubble background |
| `--g-aikit-color-bg-message-assistant` | `#f0f0f0` | Assistant message bubble background |
| Variable | Default | Description |
| -------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------- |
| `--g-aikit-color-bg-primary` | `var(--g-aikit-bg-primary, var(--g-color-base-float))` | Primary background |
| `--g-aikit-color-bg-secondary` | `#f5f5f5` | Secondary background |
| `--g-aikit-color-bg-message-user` | `#0077ff` | User message bubble background |
| `--g-aikit-color-bg-message-assistant` | `#f0f0f0` | Assistant message bubble background |
| `--g-aikit-color-bg-code` | `rgba(107, 132, 153, 0.12)` | Fenced code-block background inside `MarkdownRenderer` (overrides `yfm` default). |

### Layout

Expand Down
14 changes: 13 additions & 1 deletion src/adapters/openai/helpers/applyContentUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,21 @@ function applyToolAdd(
update: StreamEventToolAdd,
): TMessageContentUnion[] {
const status = update.status ?? 'loading';
const data: {toolName: string; status: typeof status; headerContent?: string} = {
const data: {
toolName: string;
status: typeof status;
headerContent?: string;
mcpRequest?: string;
} = {
toolName: update.toolName,
status,
};
if (update.headerContent !== undefined) {
data.headerContent = update.headerContent;
}
if (update.mcpRequest !== undefined) {
data.mcpRequest = update.mcpRequest;
}
return [
...parts,
{
Expand Down Expand Up @@ -104,6 +112,10 @@ function applyToolUpdate(
...(update.toolName && {toolName: update.toolName}),
status: update.status,
...(bodyText ? {bodyContent: bodyText} : {}),
...(update.mcpRequest === undefined ? {} : {mcpRequest: update.mcpRequest}),
...(update.mcpResponse === undefined
? {}
: {mcpResponse: update.mcpResponse}),
},
}
: p,
Expand Down
18 changes: 18 additions & 0 deletions src/adapters/openai/helpers/getStreamEventContentUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {TToolStatus} from '../../../types';
import {OpenAIStreamEventLike, OpenAIStreamOutputItemLike} from '../types/openAiTypes';

import {getTextDeltaFromStreamEvent} from './getTextDeltaFromStreamEvent';
import {prettyPrintJson} from './prettyPrintJson';

type EventRecord = Record<string, unknown>;

Expand All @@ -15,6 +16,8 @@ export type StreamEventToolAdd = {
status?: TToolStatus;
/** Optional payload to show in tool card (e.g. approval request arguments). */
headerContent?: string;
/** MCP request arguments, pretty-printed JSON. Surfaced for AIStudioChat's tool renderer. */
mcpRequest?: string;
};
export type StreamEventToolUpdate = {
kind: 'tool_update';
Expand All @@ -23,6 +26,10 @@ export type StreamEventToolUpdate = {
toolName?: string;
output?: string;
error?: string;
/** MCP request arguments, pretty-printed JSON. */
mcpRequest?: string;
/** MCP response or error, pretty-printed JSON. */
mcpResponse?: string;
};
export type StreamEventThinkingAdd = {kind: 'thinking_add'; item_id: string};
export type StreamEventThinkingDelta = {kind: 'thinking_delta'; item_id: string; delta: string};
Expand Down Expand Up @@ -89,11 +96,14 @@ function addUpdateForMcpCall(
): StreamEventContentUpdate | null {
const name = typeof item.name === 'string' ? item.name : undefined;
const serverLabel = typeof item.server_label === 'string' ? item.server_label : undefined;
const args = typeof item.arguments === 'string' ? item.arguments : undefined;
const mcpRequest = prettyPrintJson(args);
return {
kind: 'tool_add',
id,
toolName: name ?? serverLabel ?? 'MCP',
serverLabel,
...(mcpRequest ? {mcpRequest} : {}),
};
}

Expand Down Expand Up @@ -197,12 +207,14 @@ function getMcpCallDoneUpdate(item: OpenAIStreamOutputItemLike): StreamEventCont
name?: string;
server_label?: string;
status?: string;
arguments?: string;
output?: string;
error?: string;
};
const st = mcp.status ?? '';
const outputStr = typeof mcp.output === 'string' ? mcp.output : undefined;
const errStr = typeof mcp.error === 'string' ? mcp.error : undefined;
const argsStr = typeof mcp.arguments === 'string' ? mcp.arguments : undefined;

let status: TToolStatus = 'loading';
if (st === 'failed' || st === 'incomplete') {
Expand All @@ -218,13 +230,19 @@ function getMcpCallDoneUpdate(item: OpenAIStreamOutputItemLike): StreamEventCont
if (typeof mcp.name === 'string') toolName = mcp.name;
else if (typeof mcp.server_label === 'string') toolName = mcp.server_label;
else toolName = undefined;

const mcpRequest = prettyPrintJson(argsStr);
const mcpResponse = prettyPrintJson(errStr ?? outputStr);

return {
kind: 'tool_update',
item_id: mcp.id,
status,
toolName,
output: outputStr,
error: errStr,
...(mcpRequest ? {mcpRequest} : {}),
...(mcpResponse ? {mcpResponse} : {}),
};
}

Expand Down
6 changes: 6 additions & 0 deletions src/adapters/openai/helpers/mapOutputToContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
OpenAIResponseReasoningItem,
} from '../types/openAiTypes';

import {prettyPrintJson} from './prettyPrintJson';

function pushBlocksForMessage(
msg: OpenAIResponseOutputMessage,
blocks: TMessageContentUnion[],
Expand Down Expand Up @@ -64,13 +66,17 @@ function pushBlocksForMcpCall(
} else if (typeof mcp.output === 'string' && mcp.output.length > 0) {
bodyText = mcp.output;
}
const mcpRequest = prettyPrintJson(mcp.arguments);
const mcpResponse = prettyPrintJson(bodyText);
blocks.push({
type: 'tool',
id: mcp.id,
data: {
toolName: mcp.name ?? mcp.server_label ?? 'MCP',
status,
...(bodyText ? {bodyContent: bodyText} : {}),
...(mcpRequest ? {mcpRequest} : {}),
...(mcpResponse ? {mcpResponse} : {}),
},
});
}
Expand Down
12 changes: 12 additions & 0 deletions src/adapters/openai/helpers/prettyPrintJson.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Pretty-print a string that may contain JSON. Falls back to the original string
* if it doesn't parse as JSON, so non-JSON payloads render unchanged.
*/
export function prettyPrintJson(s: string | undefined): string | undefined {
if (!s) return undefined;
try {
return JSON.stringify(JSON.parse(s), null, 2);
} catch {
return s;
}
}
2 changes: 2 additions & 0 deletions src/components/atoms/MarkdownRenderer/MarkdownRenderer.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
$block: '.#{variables.$ns}markdown-renderer';

#{$block} {
--yfm-color-hljs-background: var(--g-aikit-color-bg-code);

max-width: 100%;
overflow-x: auto;
color: var(--g-color-text-primary);
Expand Down
32 changes: 28 additions & 4 deletions src/components/pages/AIStudioChat/AIStudioChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,17 @@ import type {
TUserMessage,
UserRating,
} from '../../../types';
import type {ToolMessageContent} from '../../../types/messages';
import {
createMessageRendererRegistry,
mergeMessageRendererRegistries,
registerMessageRenderer,
} from '../../../utils/messageTypeRegistry';
import {InputContextProvider, useInputContext} from '../../molecules/InputContext';
import type {MessageListConfig} from '../ChatContainer';
import {ChatContainer} from '../ChatContainer';

import {McpToolRenderer} from './McpToolRenderer';
import {normalizeMcpCallIds, omitMcpListToolsEvents} from './transforms';
import type {AIStudioChatProps} from './types';

Expand Down Expand Up @@ -458,14 +465,30 @@ function AIStudioChatInner(props: AIStudioChatInnerProps) {
}, []);

/**
* Wrap consumer-provided Like/Unlike actions so the library toggles `userRating`
* automatically before delegating to the original `onClick`. Other actions and
* non-default (ReactNode) entries pass through unchanged.
* Build the final messageListConfig:
* - Wrap consumer-provided Like/Dislike actions so the library toggles `userRating`
* automatically before delegating to the original `onClick`. Other actions and
* non-default (ReactNode) entries pass through unchanged.
* - Inject a custom 'tool' renderer that renders MCP request/response sections when
* `mcpRequest`/`mcpResponse` are present in the tool content. A consumer-provided
* `messageRendererRegistry` wins on overlap.
*/
const messageListConfig = useMemo<MessageListConfig | undefined>(() => {
const original = chatContainerProps.messageListConfig;

const baseRegistry = createMessageRendererRegistry();
registerMessageRenderer<ToolMessageContent>(baseRegistry, 'tool', {
component: McpToolRenderer,
});
const messageRendererRegistry = mergeMessageRendererRegistries(
baseRegistry,
original?.messageRendererRegistry ?? {},
);

const originalAssistantActions = original?.assistantActions;
if (!originalAssistantActions) return original;
if (!originalAssistantActions) {
return {...original, messageRendererRegistry};
}

const wrappedAssistantActions = originalAssistantActions.map((action) => {
const typed = action as DefaultMessageAction<TAssistantMessage>;
Expand All @@ -492,6 +515,7 @@ function AIStudioChatInner(props: AIStudioChatInnerProps) {
return {
...original,
assistantActions: wrappedAssistantActions,
messageRendererRegistry,
};
}, [chatContainerProps.messageListConfig, setUserRating]);

Expand Down
20 changes: 20 additions & 0 deletions src/components/pages/AIStudioChat/McpToolRenderer.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@use '../../../styles/variables';

$block: '.#{variables.$ns}aistudio-mcp-tool';

#{$block} {
display: flex;
flex-direction: column;
gap: var(--g-spacing-2);

&__section {
display: flex;
flex-direction: column;
gap: var(--g-spacing-1);
}

&__section-label {
color: var(--g-color-text-secondary);
font-weight: 500;
}
}
38 changes: 38 additions & 0 deletions src/components/pages/AIStudioChat/McpToolRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type {ToolMessageContent} from '../../../types/messages';
import {block} from '../../../utils/cn';
import {MarkdownRenderer} from '../../atoms/MarkdownRenderer';
import {ToolMessage} from '../../organisms/ToolMessage';

import {i18n} from './i18n';

import './McpToolRenderer.scss';

const b = block('aistudio-mcp-tool');

export function McpToolRenderer({part}: {part: ToolMessageContent}) {
const {mcpRequest, mcpResponse, ...rest} = part.data;
const hasMcp = Boolean(mcpRequest || mcpResponse);

if (!hasMcp) {
return <ToolMessage {...rest} />;
}

const body = (
<div className={b()}>
{mcpRequest && (
<div className={b('section')}>
<div className={b('section-label')}>{i18n('mcp_request')}</div>
<MarkdownRenderer content={`\`\`\`json\n${mcpRequest}\n\`\`\``} />
</div>
)}
{mcpResponse && (
<div className={b('section')}>
<div className={b('section-label')}>{i18n('mcp_response')}</div>
<MarkdownRenderer content={`\`\`\`json\n${mcpResponse}\n\`\`\``} />
</div>
)}
</div>
);

return <ToolMessage {...rest} bodyContent={body} />;
}
Loading
Loading