Skip to content
Merged
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
65 changes: 51 additions & 14 deletions apps/sqlrooms-cli-ui/src/components/AssistantChatContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import {Chat, isChatSessionEmpty} from '@sqlrooms/ai';
import {isAiSessionVisibleForArtifact} from '@sqlrooms/artifacts/ai';
import {Button, SkeletonPane} from '@sqlrooms/ui';
import {
Button,
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
SkeletonPane,
} from '@sqlrooms/ui';
import {PlusIcon} from 'lucide-react';
import React, {useCallback, useMemo, useState} from 'react';
import {useRoomStore} from '../store';
Expand All @@ -16,10 +22,14 @@ interface AssistantChatContainerProps {
canAccept: (data: unknown) => boolean;
onDrop: (data: unknown) => void;
};
beforeCreateSessionAction?: React.ReactNode;
debugPanel?: React.ReactNode;
}

export const AssistantChatContainer: React.FC<AssistantChatContainerProps> = ({
contextDropTarget,
beforeCreateSessionAction,
debugPanel,
}) => {
const currentSessionId = useRoomStore(
(s) => s.ai.getCurrentSession()?.id || null,
Expand Down Expand Up @@ -80,6 +90,19 @@ export const AssistantChatContainer: React.FC<AssistantChatContainerProps> = ({
);
}, [aiSessionArtifacts, currentArtifactId, currentSession, sessions]);

const messagesPane = (
<div className="print-container h-full min-h-0 grow overflow-auto">
{isDataAvailable ? (
<Chat.Messages key={currentSessionId} hoistedRenderers={['chart']} />
) : (
<div className="flex h-full w-full flex-col items-center justify-center">
<SkeletonPane className="p-4" />
<p className="text-muted-foreground mt-4">Loading database...</p>
</div>
)}
</div>
);

return (
<Chat.Root>
<div className="flex min-h-0 flex-1 flex-col gap-0 overflow-hidden">
Expand All @@ -104,7 +127,8 @@ export const AssistantChatContainer: React.FC<AssistantChatContainerProps> = ({
onCreateSession={handleCreateSession}
createSessionDisabled={createSessionDisabled}
historyIsRunning={historyIsRunning}
className="mb-4"
beforeCreateSessionAction={beforeCreateSessionAction}
className={debugPanel ? 'mb-2' : 'mb-4'}
/>
)}
{showHistory ? (
Expand All @@ -123,19 +147,32 @@ export const AssistantChatContainer: React.FC<AssistantChatContainerProps> = ({
className="flex-1"
/>
) : (
<div className="print-container grow overflow-auto">
{isDataAvailable ? (
<Chat.Messages
key={currentSessionId}
hoistedRenderers={['chart']}
/>
<div className="min-h-0 flex-1">
{debugPanel ? (
<ResizablePanelGroup
orientation="vertical"
className="h-full min-h-0"
>
<ResizablePanel
id="ai-debug-panel"
defaultSize={50}
minSize={20}
className="min-h-0 overflow-hidden"
>
{debugPanel}
</ResizablePanel>
<ResizableHandle withHandle className="my-1" />
<ResizablePanel
id="ai-chat-panel"
defaultSize={50}
minSize={20}
className="min-h-0 pt-2"
>
{messagesPane}
</ResizablePanel>
</ResizablePanelGroup>
) : (
<div className="flex h-full w-full flex-col items-center justify-center">
<SkeletonPane className="p-4" />
<p className="text-muted-foreground mt-4">
Loading database...
</p>
</div>
messagesPane
)}
</div>
)}
Expand Down
69 changes: 64 additions & 5 deletions apps/sqlrooms-cli-ui/src/components/AssistantPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,56 @@
import {RoomPanelHeader} from '@sqlrooms/room-shell';
import {Button, useDisclosure} from '@sqlrooms/ui';
import {XIcon} from 'lucide-react';
import React, {useEffect} from 'react';
import {useRoomStore} from '../store';
import {Button, cn, useDisclosure} from '@sqlrooms/ui';
import {BugIcon, XIcon} from 'lucide-react';
import React, {Suspense, useEffect, useState} from 'react';
import {aiDevtoolsEnabled, useRoomStore} from '../store';
import {AssistantChatContainer} from './AssistantChatContainer';
import {AssistantSettingsDialog} from './AssistantSettingsDialog';
import {useAssistantContextDropTarget} from './assistantUtils';

const ChatSessionDebugView = React.lazy(() =>
import('@sqlrooms/ai/devtools').then((mod) => ({
default: mod.ChatSessionDebugView,
})),
);

const AssistantDebugButton: React.FC<{
isOpen: boolean;
onToggle: () => void;
}> = ({isOpen, onToggle}) => (
<Button
variant="ghost"
size="sm"
className={cn('h-8 px-2', isOpen && 'bg-muted text-foreground')}
title="AI session debug view"
aria-label="AI session debug view"
aria-pressed={isOpen}
onClick={onToggle}
>
<BugIcon className="h-4 w-4" />
</Button>
);

export const AssistantPanel: React.FC = () => {
const currentSessionId = useRoomStore(
(s) => s.ai.getCurrentSession()?.id || null,
);
const toggleCollapsed = useRoomStore((s) => s.layout.toggleCollapsed);
const settingsPanelOpen = useDisclosure();
const contextDropTarget = useAssistantContextDropTarget();
const [debugOpen, setDebugOpen] = useState(false);

useEffect(() => {
if (!currentSessionId && settingsPanelOpen.isOpen) {
settingsPanelOpen.onClose();
}
}, [currentSessionId, settingsPanelOpen.isOpen, settingsPanelOpen.onClose]);

useEffect(() => {
if (!currentSessionId || !aiDevtoolsEnabled) {
setDebugOpen(false);
}
}, [currentSessionId]);

return (
<div className="flex h-full flex-col overflow-visible p-2">
<RoomPanelHeader>
Expand Down Expand Up @@ -49,7 +79,36 @@ export const AssistantPanel: React.FC = () => {
</Button>
</div>
</RoomPanelHeader>
<AssistantChatContainer contextDropTarget={contextDropTarget} />
<div className="flex min-h-0 flex-1 flex-col">
<AssistantChatContainer
contextDropTarget={contextDropTarget}
beforeCreateSessionAction={
aiDevtoolsEnabled && currentSessionId ? (
<AssistantDebugButton
isOpen={debugOpen}
onToggle={() => setDebugOpen((open) => !open)}
/>
) : null
}
debugPanel={
aiDevtoolsEnabled && currentSessionId && debugOpen ? (
<Suspense
fallback={
<div className="text-muted-foreground flex h-full items-center justify-center text-sm">
Loading debug view...
</div>
}
>
<ChatSessionDebugView
sessionId={currentSessionId}
className="h-full"
onClose={() => setDebugOpen(false)}
/>
</Suspense>
) : null
}
/>
</div>
</div>
);
};
1 change: 1 addition & 0 deletions apps/sqlrooms-cli-ui/src/runtimeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type RuntimeConfig = {
llmModel?: string;
apiKey?: string;
configWritable?: boolean;
aiDevtools?: boolean;
syncEnabled?: boolean;
crdtWsUrl?: string;
crdtRoomId?: string;
Expand Down
6 changes: 6 additions & 0 deletions apps/sqlrooms-cli-ui/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ const WORKSHEET_BLOCK_DOCUMENT_OPTIONS = {
} as const;

export const runtimeConfig = await fetchRuntimeConfig();
export const aiDevtoolsEnabled =
import.meta.env.DEV || Boolean(runtimeConfig.aiDevtools);
const defaultWorkspaceTitle = getDefaultWorkspaceTitle(runtimeConfig);
const runtimeAiSettings = runtimeConfig.aiSettings || {};
const runtimeAiProviders =
Expand Down Expand Up @@ -718,6 +720,10 @@ export const {roomStore, useRoomStore} = createRoomStore<RoomState>(
...webContainerToolkit.toolRenderers,
chart: VegaChartToolResult,
},
devtools: {
captureAgentSnapshots: aiDevtoolsEnabled,
persistAgentSnapshots: aiDevtoolsEnabled,
},
})(set, get, store);
})(),
};
Expand Down
7 changes: 7 additions & 0 deletions packages/ai-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ Use `uiMessages` for current chat state.
by target session id. Fork metadata uses chat/message terminology and keeps
legacy analysis-result ids only as compatibility fields when needed.

`ChatSessionSchema.agentProgress` stores persisted sub-agent tool-call trees
keyed by the parent tool call id. `ChatSessionSchema.agentSnapshots` optionally
stores persisted devtools metadata for those agent calls, such as available tool
names, descriptions, capability flags, approval hints, and bounded settings.
Snapshots are intended for debugging and should not contain executable tools,
closures, secrets, or unbounded prompt/output content.

## Basic usage

### Validate and load AI session config
Expand Down
26 changes: 26 additions & 0 deletions packages/ai-config/src/schema/ChatSessionSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,30 @@ export function setAiRunContextPrimaryItem(
};
}

const AgentSnapshotSchema = z
.object({
agentName: z.string().optional(),
parentToolCallId: z.string(),
availableTools: z.array(
z.object({
name: z.string(),
description: z.string().optional(),
hasExecute: z.boolean().optional(),
hasRenderer: z.boolean().optional(),
needsApproval: z.boolean().optional(),
}),
),
settings: z
.object({
maxSteps: z.number().int().optional(),
model: z.string().optional(),
provider: z.string().optional(),
})
.optional(),
startedAt: z.number(),
})
.passthrough();

const ChatSessionBaseSchema = z.object({
id: z.string(),
name: z.string(),
Expand All @@ -163,6 +187,8 @@ const ChatSessionBaseSchema = z.object({
runContext: AiRunContextSchema.optional(),
/** Persisted sub-agent tool call trees, keyed by parent toolCallId */
agentProgress: z.record(z.string(), z.array(z.unknown())).optional(),
/** Optional persisted agent devtools snapshots, keyed by parent toolCallId */
agentSnapshots: z.record(z.string(), AgentSnapshotSchema).optional(),
Comment on lines +190 to +191

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Document the ai-config snapshot field

The root AGENTS.md says that public API changes in @sqlrooms/* packages must update that package's README.md. This adds agentSnapshots to the exported @sqlrooms/ai-config ChatSessionSchema, but packages/ai-config/README.md still only documents the older session shape, so consumers of the schema do not get package-local docs for the new persisted devtools field.

Useful? React with 👍 / 👎.

});

/**
Expand Down
54 changes: 54 additions & 0 deletions packages/ai-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,64 @@ const blocks: ChatSearchBlock[] = [
const matches = findChatSearchMatches(blocks, query);
```

## Devtools

`@sqlrooms/ai-core/devtools` exposes development-only inspection helpers and UI
for session debugging without adding the debug surface to the main
`@sqlrooms/ai-core` barrel.

```tsx
import {ChatSessionDebugView} from '@sqlrooms/ai-core/devtools';

function DebugPanel({
sessionId,
onClose,
}: {
sessionId: string;
onClose?: () => void;
}) {
return <ChatSessionDebugView sessionId={sessionId} onClose={onClose} />;
}
```

`ChatSessionDebugView` reads the current AI store context and renders a tabbed
debug view for one chat session: the chronological timeline, registered tools,
run context, raw session JSON, model settings, tool calls, nested agent work,
and optional agent snapshots.

Agent snapshot capture is opt-in on `createAiSlice`:

```ts
createAiSlice({
tools,
getInstructions,
devtools: {
captureAgentSnapshots: true,
persistAgentSnapshots: true,
maxAgentSnapshotBytes: 64_000,
},
});
```

Snapshots are bounded serializable metadata only: agent/tool names,
descriptions, capability flags, approval hints, and settings. They must not
store executable tools, closures, secrets, or unbounded prompt/output content.
Persist snapshots only for debugging workflows where cross-tab or post-mortem
inspection is useful.

Captured snapshots and snapshot controls live under the AI state's devtools
namespace:

```ts
const snapshots = useRoomStore((state) => state.ai.devtools.agentSnapshots);
state.ai.devtools.clearAgentSnapshots();
```

## Useful exports

- Slice/hooks: `createAiSlice`, `useStoreWithAi`, `generateSessionTitle`, `useGenerateSessionTitle`, `AiSliceState`
- Chat UI: `Chat`, `ChatMessagesContainer`, `ChatTurnView`, `MessageContent`, `ModelSelector`, `QueryControls`, `PromptSuggestions`
- Devtools subexport: `@sqlrooms/ai-core/devtools`
- Legacy/compat components: `AnalysisResultsContainer`, `AnalysisResult`, `AnalysisAnswer`, `ErrorMessage`
- Session helpers: `ChatSessionSchema`, `isChatSessionEmpty`, `getChatTurnsFromUiMessages`
- Forking: `ai.forkSessionFromMessage()`, `AiSessionForkOrigin`, `ForkSessionFromMessageArgs`
Expand Down
Loading
Loading