Repository navigation
feat: Command system implementation - #382
Conversation
|
Cursor Agent can help with this pull request. Just |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a command system (registry, adapters, tooling, zod-backed input schemas) integrated into store slices and AI tools, and a new RoomShell.CommandPalette UI (with Button). Updates examples, docs, package exports, and adds tests and portable schema utilities. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant UI as RoomShell.CommandPalette (UI)
participant Store as CommandSlice (Registry)
participant Cmd as Command Handler
participant Norm as Normalizer
User->>UI: Press Ctrl/Cmd+K or click Button
UI->>Store: listCommands(options)
Store->>Store: Filter visible/enabled, export schemas
Store->>UI: Return descriptors
alt command requires input
UI->>User: Show input dialog (custom or Monaco)
User->>UI: Submit input
UI->>Store: invokeCommand(id, input, {surface:'palette'})
else no input
UI->>Store: invokeCommand(id, undefined, {surface:'palette'})
end
Store->>Store: validate input (zod) and check enabled
Store->>Cmd: Execute(commandContext, input)
Cmd->>Norm: Return raw result or throw
Norm->>Store: Normalize to RoomCommandResult
Store->>UI: Return RoomCommandResult
UI->>User: Display success or error
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Note Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it. Generating unit tests... This may take up to 20 minutes. |
There was a problem hiding this comment.
Pull request overview
This pull request implements a comprehensive command system for the SQL Rooms application, enabling slices to register custom commands that can be executed via a command palette UI or other surfaces.
Changes:
- Added a command registry infrastructure in
room-storewith owner-based command management - Implemented a
RoomShell.CommandPalettecomponent with keyboard shortcut support (Cmd/Ctrl+K) - Integrated command registration in SQL Editor, Layout, DuckDB, and AI slices with lifecycle management
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| packages/room-store/src/CommandSlice.ts | Core command registry implementation with owner-based management and execution context |
| packages/room-store/src/index.ts | Exports command-related types and functions from CommandSlice |
| packages/room-shell/src/RoomShellCommandPalette.tsx | UI component for command palette with search, filtering, and keyboard navigation |
| packages/room-shell/src/RoomShell.tsx | Exposes CommandPalette as RoomShell.CommandPalette |
| packages/room-shell/src/RoomShellSlice.ts | Integrates CommandSlice into the room shell state |
| packages/room-shell/src/index.ts | Re-exports command types and utilities |
| packages/sql-editor/src/SqlEditorSlice.tsx | Registers SQL editor commands (run query, abort query, create tab, clear results) with lifecycle hooks |
| packages/layout/src/LayoutSlice.ts | Dynamically registers panel toggle commands with subscription to panel changes |
| packages/duckdb/src/DuckDbSlice.ts | Registers database commands (refresh table schemas) |
| packages/ai-core/src/AiSlice.ts | Registers AI commands (create session, cancel analysis) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Note Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it. Generating unit tests... This may take up to 20 minutes. |
1 similar comment
|
Note Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it. Generating unit tests... This may take up to 20 minutes. |
|
✅ Created PR with unit tests: #383 |
Signed-off-by: Ilya Boyandin <[email protected]>
…nd-system-implementation-96b3
Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (6)
examples/query-motherduck/src/Room.tsx (1)
16-24:⚠️ Potential issue | 🟠 Major
<RoomShell.CommandPalette />is missing — the palette dialog is never rendered.
<RoomShell.CommandPalette.Button />is added to the sidebar but the palette overlay component itself is absent. Every other example in this PR (query,cosmos-embedding,ai,kepler) renders both the button in the sidebar and<RoomShell.CommandPalette />as a direct child ofRoomShell. Without the latter, clicking the button does nothing.🐛 Proposed fix
<RoomShell className="h-screen" roomStore={roomStoreRef.current}> <RoomShell.Sidebar> <RoomShell.CommandPalette.Button /> <ThemeSwitch /> </RoomShell.Sidebar> <RoomShell.LayoutComposer /> + <RoomShell.CommandPalette /> </RoomShell>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/query-motherduck/src/Room.tsx` around lines 16 - 24, RoomShell currently renders only RoomShell.CommandPalette.Button in the Sidebar so the overlay never appears; add the missing overlay component by rendering RoomShell.CommandPalette as a direct child of RoomShell (alongside RoomShell.LayoutComposer) so the button can open the palette; update the JSX in Room (function/component) to include RoomShell.CommandPalette (ensure you reference the existing RoomShell and RoomShell.CommandPalette.Button symbols).docs/upgrade-guide.md (1)
426-433:⚠️ Potential issue | 🟡 Minor
<RoomShell.CommandPalette />in the 0.17.0 "New" example may mislead version-targeted upgraders.
RoomShell.CommandPaletteis introduced in this PR, not in 0.17.0. A developer upgrading specifically to 0.17.0 using this guide as a reference will encounter a missing component. Consider either adding a dedicated entry for the current release version that documents<RoomShell.CommandPalette />, or annotating the snippet (e.g., a comment noting it was added in a later release).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/upgrade-guide.md` around lines 426 - 433, The example in the 0.17.0 "New" snippet references RoomShell.CommandPalette which was added after 0.17.0; update the upgrade guide so upgraders aren't misled by either (a) remove RoomShell.CommandPalette from the 0.17.0 example, or (b) add an inline annotation to the snippet stating "RoomShell.CommandPalette added in a later release" (or specify the release), or (c) add a separate short entry that documents RoomShell.CommandPalette in the current release notes—locate and edit the snippet that contains RoomShell, RoomShell.Sidebar, RoomShell.LayoutComposer, RoomShell.LoadingProgress and RoomShell.CommandPalette and apply one of the above fixes.examples/deckgl-mosaic/src/room.tsx (1)
7-23:⚠️ Potential issue | 🟠 MajorCall
room.initialize()on mount.
This Room component does not initialize the room store, so lifecycle hooks may not run. Please add a mount effect.✅ Suggested fix
import {RoomShell} from '@sqlrooms/room-shell'; import {SqlEditorModal} from '@sqlrooms/sql-editor'; import {ThemeSwitch, useDisclosure} from '@sqlrooms/ui'; import {TerminalIcon} from 'lucide-react'; +import {useEffect} from 'react'; import {roomStore} from './store'; export const Room = () => { const sqlEditorDisclosure = useDisclosure(); + useEffect(() => { + void roomStore.getState().room.initialize?.(); + }, []); return (As per coding guidelines, "Always call
room.initialize()during component mount to orchestrate automatic slice initialization with lifecycle hooks".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/deckgl-mosaic/src/room.tsx` around lines 7 - 23, The Room component never calls room.initialize() on mount; add a React mount effect (useEffect) inside the Room component that calls roomStore.initialize() once (empty deps) so the store lifecycle hooks run; ensure useEffect is imported and reference the Room component and roomStore identifiers when adding the effect.apps/sqlrooms-cli-ui/src/room.tsx (1)
3-25:⚠️ Potential issue | 🟠 MajorCall
room.initialize()on mount.
This Room component still doesn’t trigger initialization, so slice lifecycle hooks may never run. Please add a mount effect.✅ Suggested fix
import {RoomShell} from '@sqlrooms/room-shell'; import {SqlEditorModal} from '@sqlrooms/sql-editor'; import {ThemeProvider, ThemeSwitch, useDisclosure} from '@sqlrooms/ui'; import {TerminalIcon} from 'lucide-react'; +import {useEffect} from 'react'; import {roomStore} from './store'; export const Room = () => { const sqlEditor = useDisclosure(); + useEffect(() => { + void roomStore.getState().room.initialize?.(); + }, []); return (As per coding guidelines, "Always call
room.initialize()during component mount to orchestrate automatic slice initialization with lifecycle hooks".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/sqlrooms-cli-ui/src/room.tsx` around lines 3 - 25, The Room component currently never calls room.initialize(), so add a mount effect: import useEffect from 'react' and inside the Room function add useEffect(() => { roomStore.room.initialize(); }, []); to ensure the room.initialize() runs on mount (referencing roomStore and the Room component) so slice lifecycle hooks execute; keep the effect with an empty dependency array and no additional cleanup unless initialize returns a disposer.examples/cosmos/src/room.tsx (1)
7-23:⚠️ Potential issue | 🟠 MajorCall
room.initialize()on mount.
This component mounts the store but never triggers initialization, so slice lifecycle hooks (including command registration) may not run. Please add a mount effect.✅ Suggested fix
import {RoomShell} from '@sqlrooms/room-shell'; import {SqlEditorModal} from '@sqlrooms/sql-editor'; import {ThemeSwitch, useDisclosure} from '@sqlrooms/ui'; import {TerminalIcon} from 'lucide-react'; +import {useEffect} from 'react'; import {roomStore} from './store'; export const Room = () => { const sqlEditorDisclosure = useDisclosure(); + useEffect(() => { + void roomStore.getState().room.initialize?.(); + }, []); return (As per coding guidelines, "Always call
room.initialize()during component mount to orchestrate automatic slice initialization with lifecycle hooks".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/cosmos/src/room.tsx` around lines 7 - 23, The Room component mounts roomStore but never calls room.initialize(), so slice lifecycle hooks and command registration won't run; add a mount effect (e.g., useEffect with empty deps) inside the Room component that calls roomStore.initialize() (and optionally returns cleanup calling roomStore.shutdown() if available) to ensure initialization runs when Room mounts; locate this change in the Room function where roomStore is passed into <RoomShell roomStore={roomStore}> and add the effect there.examples/ai-rag/src/room.tsx (1)
8-33:⚠️ Potential issue | 🟠 MajorCall
room.initialize()on mount.
This Room component never initializes the room store, so slice hooks (including commands) may not run. Add a mount effect.✅ Suggested fix
import {RoomShell} from '@sqlrooms/room-shell'; import {SqlEditorModal} from '@sqlrooms/sql-editor'; import {ThemeSwitch, useDisclosure} from '@sqlrooms/ui'; import {SearchIcon, TerminalIcon} from 'lucide-react'; +import {useEffect} from 'react'; import {RagSearchDialog} from './components/RagSearchDialog'; import {roomStore} from './store'; export const Room = () => { const sqlEditor = useDisclosure(); const ragTest = useDisclosure(); + useEffect(() => { + void roomStore.getState().room.initialize?.(); + }, []); return (As per coding guidelines, "Always call
room.initialize()during component mount to orchestrate automatic slice initialization with lifecycle hooks".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@examples/ai-rag/src/room.tsx` around lines 8 - 33, The Room component never calls room.initialize(), so add a React mount effect in the Room component that invokes roomStore.room.initialize() once on mount (useEffect(() => { roomStore.room.initialize(); }, [])); place the effect near the top of the Room function (alongside existing hooks like useDisclosure) so slice hooks and commands initialize correctly; if the room API exposes a teardown method, optionally return a cleanup from the same effect to dispose on unmount.
🧹 Nitpick comments (1)
packages/room-store/src/CommandAdapters.ts (1)
145-148:resolveCommandIdrebuilds the full tool list on every call.
callTool→resolveCommandId→listTools→listDescriptorsevaluates all commands, maps descriptors, and scans the list linearly every invocation. For typical command counts this is fine, but if it becomes a hot path, consider caching the name→ID mapping and invalidating on registry changes.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/room-store/src/CommandAdapters.ts` around lines 145 - 148, resolveCommandId currently calls listTools() (which calls listDescriptors()) on every invocation, causing the full command list to be rebuilt each time; change resolveCommandId to use a cached name→commandId map (e.g. a module-scoped Map) and consult that map instead of calling listTools() every time. Populate the cache lazily on first lookup and update or invalidate it when the registry changes (hook into the same places that modify descriptors/registry or add explicit invalidation in the functions that register/unregister tools), and update callTool to use resolveCommandId as before so callers are unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/ai-core/src/AiSlice.ts`:
- Around line 1313-1322: The execute handler in AiSlice returns undefined when
no current session exists; update the execute function so that instead of early
returning it always returns a RoomCommand-style result: when currentSession is
present call getState().ai.cancelAnalysis(currentSession.id) and then return a
success RoomCommand result, and when currentSession is absent return a failure
RoomCommand result (e.g., success: false with an error message like "no active
session"). Reference the execute function and the currentSession /
getState().ai.cancelAnalysis symbols to locate and implement the change.
In `@packages/ai/src/tools/commandTools.ts`:
- Around line 126-129: The execute_command tool description hardcodes
"list_commands" which can be wrong when consumers set a custom list tool name
via options?.listToolName; update the description construction (where
executeToolName is defined) to reference the configured list tool name
dynamically (use options?.listToolName || 'list_commands') so the text reads
"Call <listToolName> first…" and ensure executeToolName's description string
uses that variable instead of the literal "list_commands".
In `@packages/room-store/src/CommandSlice.ts`:
- Around line 569-576: The current isRoomCommandResult duck-type check
misclassifies arbitrary TData objects that contain a boolean success field;
update isRoomCommandResult to require a more distinctive sentinel alongside
success (e.g., require both 'success' and 'commandId' properties) so only actual
RoomCommandResult shapes match, and ensure normalizeCommandExecuteResult uses
the updated isRoomCommandResult to correctly route real data into the data field
rather than treating it as a RoomCommandResult; adjust type guards and any tests
that assume the old behavior accordingly.
- Around line 668-806: toPortableSchema reads schema.def and internal fields
(values, entries, shape, options, innerType, defaultValue), which is fragile
across Zod minor updates; add a focused unit test suite for the toPortableSchema
function that constructs Zod schemas for each handled branch (primitive types,
literal, enum, array, object with optional/required children, union, optional,
nullable, default, and an unknown/fallback case) and asserts the produced
RoomCommandPortableSchema shape and flags (type, const, enum, items, properties,
required, anyOf, optional, nullable, default); alternatively, if you prefer a
maintained conversion, replace the custom logic in toPortableSchema with
zod-to-json-schema and add tests verifying parity for a representative set of
schemas.
---
Outside diff comments:
In `@apps/sqlrooms-cli-ui/src/room.tsx`:
- Around line 3-25: The Room component currently never calls room.initialize(),
so add a mount effect: import useEffect from 'react' and inside the Room
function add useEffect(() => { roomStore.room.initialize(); }, []); to ensure
the room.initialize() runs on mount (referencing roomStore and the Room
component) so slice lifecycle hooks execute; keep the effect with an empty
dependency array and no additional cleanup unless initialize returns a disposer.
In `@docs/upgrade-guide.md`:
- Around line 426-433: The example in the 0.17.0 "New" snippet references
RoomShell.CommandPalette which was added after 0.17.0; update the upgrade guide
so upgraders aren't misled by either (a) remove RoomShell.CommandPalette from
the 0.17.0 example, or (b) add an inline annotation to the snippet stating
"RoomShell.CommandPalette added in a later release" (or specify the release), or
(c) add a separate short entry that documents RoomShell.CommandPalette in the
current release notes—locate and edit the snippet that contains RoomShell,
RoomShell.Sidebar, RoomShell.LayoutComposer, RoomShell.LoadingProgress and
RoomShell.CommandPalette and apply one of the above fixes.
In `@examples/ai-rag/src/room.tsx`:
- Around line 8-33: The Room component never calls room.initialize(), so add a
React mount effect in the Room component that invokes
roomStore.room.initialize() once on mount (useEffect(() => {
roomStore.room.initialize(); }, [])); place the effect near the top of the Room
function (alongside existing hooks like useDisclosure) so slice hooks and
commands initialize correctly; if the room API exposes a teardown method,
optionally return a cleanup from the same effect to dispose on unmount.
In `@examples/cosmos/src/room.tsx`:
- Around line 7-23: The Room component mounts roomStore but never calls
room.initialize(), so slice lifecycle hooks and command registration won't run;
add a mount effect (e.g., useEffect with empty deps) inside the Room component
that calls roomStore.initialize() (and optionally returns cleanup calling
roomStore.shutdown() if available) to ensure initialization runs when Room
mounts; locate this change in the Room function where roomStore is passed into
<RoomShell roomStore={roomStore}> and add the effect there.
In `@examples/deckgl-mosaic/src/room.tsx`:
- Around line 7-23: The Room component never calls room.initialize() on mount;
add a React mount effect (useEffect) inside the Room component that calls
roomStore.initialize() once (empty deps) so the store lifecycle hooks run;
ensure useEffect is imported and reference the Room component and roomStore
identifiers when adding the effect.
In `@examples/query-motherduck/src/Room.tsx`:
- Around line 16-24: RoomShell currently renders only
RoomShell.CommandPalette.Button in the Sidebar so the overlay never appears; add
the missing overlay component by rendering RoomShell.CommandPalette as a direct
child of RoomShell (alongside RoomShell.LayoutComposer) so the button can open
the palette; update the JSX in Room (function/component) to include
RoomShell.CommandPalette (ensure you reference the existing RoomShell and
RoomShell.CommandPalette.Button symbols).
---
Nitpick comments:
In `@packages/room-store/src/CommandAdapters.ts`:
- Around line 145-148: resolveCommandId currently calls listTools() (which calls
listDescriptors()) on every invocation, causing the full command list to be
rebuilt each time; change resolveCommandId to use a cached name→commandId map
(e.g. a module-scoped Map) and consult that map instead of calling listTools()
every time. Populate the cache lazily on first lookup and update or invalidate
it when the registry changes (hook into the same places that modify
descriptors/registry or add explicit invalidation in the functions that
register/unregister tools), and update callTool to use resolveCommandId as
before so callers are unchanged.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (38)
README.mdapps/sqlrooms-cli-ui/src/room.tsxdocs/getting-started.mddocs/upgrade-guide.mdexamples/ai-rag/src/room.tsxexamples/ai/src/room.tsxexamples/canvas-sync/src/room.tsxexamples/canvas/src/room.tsxexamples/cosmos-embedding/src/room.tsxexamples/cosmos/src/room.tsxexamples/deckgl-discuss/src/room.tsxexamples/deckgl-mosaic/src/room.tsxexamples/deckgl/src/room.tsxexamples/get-started/src/room.tsxexamples/kepler/src/room.tsxexamples/mosaic/src/room.tsxexamples/nextjs/components/app-shell.tsxexamples/query-motherduck/src/Room.tsxexamples/query-pwa/src/App.tsxexamples/query-websocket/src/App.tsxexamples/query/src/App.tsxpackages/ai-core/src/AiSlice.tspackages/ai/src/index.tspackages/ai/src/tools/commandTools.tspackages/ai/src/tools/defaultTools.tspackages/duckdb/src/DuckDbSlice.tspackages/layout/src/LayoutSlice.tspackages/room-shell/README.mdpackages/room-shell/package.jsonpackages/room-shell/src/RoomShell.tsxpackages/room-shell/src/RoomShellCommandPalette.tsxpackages/room-shell/src/RoomShellSlice.tspackages/room-shell/src/index.tspackages/room-store/src/CommandAdapters.tspackages/room-store/src/CommandSlice.tspackages/room-store/src/index.tspackages/sql-editor/src/SqlEditorSlice.tsxpackages/ui/src/components/theme-switch.tsx
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/room-shell/src/RoomShellCommandPalette.tsx`:
- Around line 120-210: The component never calls room.initialize(), so slice
lifecycle hooks may not run; add a mount effect that invokes
roomStore.initialize() when the component mounts (use a React useEffect with
empty deps or an existing mount helper) and ensure you reference the roomStore
acquired via useRoomStoreApi<RoomShellSliceState>() so the call happens only on
mount and not on every render; place this near where roomStore is defined to
guarantee automatic slice initialization.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (2)
packages/room-shell/package.jsonpackages/room-shell/src/RoomShellCommandPalette.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/room-shell/package.json
| const roomStore = useRoomStoreApi<RoomShellSliceState>(); | ||
| const commandRegistry = useBaseRoomShellStore( | ||
| (state) => state.commands.registry, | ||
| ); | ||
|
|
||
| const commandContext = useMemo( | ||
| () => createRoomCommandExecutionContext(roomStore, {surface: 'palette'}), | ||
| [roomStore], | ||
| ); | ||
|
|
||
| const commands = useMemo(() => { | ||
| const nextCommands: PaletteCommand[] = []; | ||
| for (const command of Object.values(commandRegistry)) { | ||
| if (!resolveCommandVisibility(command, commandContext)) { | ||
| continue; | ||
| } | ||
| nextCommands.push({ | ||
| ...command, | ||
| enabled: resolveCommandEnabled(command, commandContext), | ||
| }); | ||
| } | ||
|
|
||
| nextCommands.sort((first, second) => first.name.localeCompare(second.name)); | ||
| return nextCommands; | ||
| }, [commandContext, commandRegistry]); | ||
|
|
||
| const groupedCommands = useMemo(() => { | ||
| const groups = new Map<string, PaletteCommand[]>(); | ||
| for (const command of commands) { | ||
| const groupName = command.group ?? GENERAL_COMMAND_GROUP; | ||
| const existingGroup = groups.get(groupName); | ||
| if (existingGroup) { | ||
| existingGroup.push(command); | ||
| } else { | ||
| groups.set(groupName, [command]); | ||
| } | ||
| } | ||
|
|
||
| return Array.from(groups.entries()).sort(([firstGroup], [secondGroup]) => { | ||
| if (firstGroup === GENERAL_COMMAND_GROUP) { | ||
| return -1; | ||
| } | ||
| if (secondGroup === GENERAL_COMMAND_GROUP) { | ||
| return 1; | ||
| } | ||
| return firstGroup.localeCompare(secondGroup); | ||
| }); | ||
| }, [commands]); | ||
|
|
||
| const activeInputCommand = useMemo(() => { | ||
| if (!activeInputCommandId) { | ||
| return undefined; | ||
| } | ||
| return commands.find((command) => command.id === activeInputCommandId); | ||
| }, [activeInputCommandId, commands]); | ||
| const ActiveInputComponent = activeInputCommand | ||
| ? getCommandInputComponent(activeInputCommand) | ||
| : undefined; | ||
| const activeInputCommandJsonSchema = useMemo(() => { | ||
| if (!activeInputCommand?.inputSchema) { | ||
| return undefined; | ||
| } | ||
| const portableInputSchema = exportCommandInputSchema( | ||
| activeInputCommand.inputSchema, | ||
| ); | ||
| const jsonSchema = toJsonSchema(portableInputSchema); | ||
| if (!jsonSchema) { | ||
| return undefined; | ||
| } | ||
| return { | ||
| $schema: 'http://json-schema.org/draft-07/schema#', | ||
| title: activeInputCommand.name, | ||
| ...jsonSchema, | ||
| } satisfies JsonSchema; | ||
| }, [activeInputCommand]); | ||
| const activeInputEditorPath = useMemo(() => { | ||
| if (!activeInputCommand) { | ||
| return undefined; | ||
| } | ||
| return `${commandInputEditorPathPrefixRef.current}/${encodeURIComponent(activeInputCommand.id)}.json`; | ||
| }, [activeInputCommand]); | ||
|
|
||
| const setPaletteOpen = useCallback( | ||
| (nextOpen: boolean) => { | ||
| if (controlledOpen === undefined) { | ||
| setUncontrolledOpen(nextOpen); | ||
| } | ||
| onOpenChange?.(nextOpen); | ||
| }, | ||
| [controlledOpen, onOpenChange], | ||
| ); |
There was a problem hiding this comment.
Call room.initialize() on mount
This component doesn’t initialize the room, so slice lifecycle hooks may never run. Add a mount effect to call room.initialize() via the store. As per coding guidelines, Always call room.initialize() during component mount to orchestrate automatic slice initialization with lifecycle hooks.
✅ Suggested fix
const roomStore = useRoomStoreApi<RoomShellSliceState>();
const commandRegistry = useBaseRoomShellStore(
(state) => state.commands.registry,
);
+ useEffect(() => {
+ roomStore.getState().room.initialize();
+ }, [roomStore]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/room-shell/src/RoomShellCommandPalette.tsx` around lines 120 - 210,
The component never calls room.initialize(), so slice lifecycle hooks may not
run; add a mount effect that invokes roomStore.initialize() when the component
mounts (use a React useEffect with empty deps or an existing mount helper) and
ensure you reference the roomStore acquired via
useRoomStoreApi<RoomShellSliceState>() so the call happens only on mount and not
on every render; place this near where roomStore is defined to guarantee
automatic slice initialization.
Signed-off-by: Ilya Boyandin <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
packages/room-shell/src/RoomShellCommandPalette.tsx (1)
120-123:room.initialize()not called on mount.As per coding guidelines,
**/{room,Room}*.{ts,tsx}files should always callroom.initialize()during component mount to orchestrate automatic slice initialization with lifecycle hooks.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/room-shell/src/RoomShellCommandPalette.tsx` around lines 120 - 123, The component in RoomShellCommandPalette.tsx obtains the room store via useRoomStoreApi<RoomShellSliceState>() but never calls room.initialize(); add a mount effect that calls room.initialize() (via the store object returned from useRoomStoreApi) when the component mounts so automatic slice initialization runs; implement this using a useEffect inside RoomShellCommandPalette that calls room.initialize() (or the appropriate initialize method on the object returned by useRoomStoreApi) and runs only on mount.
🧹 Nitpick comments (2)
packages/room-shell/src/RoomShellCommandPalette.tsx (2)
374-377:getCommandShortcutcalled twice per command item — extract to a variable.♻️ Proposed refactor
+ {(() => { + const shortcut = getCommandShortcut(command); + return shortcut ? ( + <CommandShortcut>{shortcut}</CommandShortcut> + ) : null; + })()} - {getCommandShortcut(command) ? ( - <CommandShortcut> - {getCommandShortcut(command)} - </CommandShortcut> - ) : null}Or more readably, compute it earlier in the outer
groupCommands.mapcallback:- {groupCommands.map((command) => ( + {groupCommands.map((command) => { + const shortcut = getCommandShortcut(command); + return ( <CommandItem ... > ... - {getCommandShortcut(command) ? ( - <CommandShortcut> - {getCommandShortcut(command)} - </CommandShortcut> + {shortcut ? ( + <CommandShortcut>{shortcut}</CommandShortcut> ) : null} </CommandItem> - ))} + ); + })}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/room-shell/src/RoomShellCommandPalette.tsx` around lines 374 - 377, getCommandShortcut is being invoked twice for each command item; compute its result once and reuse it. In the groupCommands.map callback (the code that renders each command item), call getCommandShortcut(command) once into a local const (e.g., shortcut) and then use that variable for the conditional render and as the child of <CommandShortcut> instead of calling getCommandShortcut a second time; update the JSX that currently references getCommandShortcut(command) in both the ternary check and inside <CommandShortcut> to use the new local variable.
663-669:anyOftemplate always resolves to the first variant — theundefinedguard is never triggered.
createInputTemplateFromSchemaalways returns a non-undefinedvalue (its fallback is{}), so the loop'svalue !== undefinedguard is always true and the loop exits on the first variant unconditionally. Thefor+ early-return is effectively equivalent tocreateInputTemplateFromSchema(schema.anyOf[0]).This isn't a correctness bug for the intended use-case (generating a UI hint), but the defensive loop logic is misleading. Consider either:
- Simplifying to always pick the first variant directly, or
- Actually returning
undefinedfrom the function for types that have no meaningful template (e.g., purenullschemas), so later variants in the union get a chance.♻️ Simplified variant (pick first non-null-only schema)
if (schema.anyOf && schema.anyOf.length > 0) { - for (const variant of schema.anyOf) { - const value = createInputTemplateFromSchema(variant); - if (value !== undefined) { - return value; - } - } + // Pick first non-null-only variant for a meaningful template hint. + const preferredVariant = + schema.anyOf.find((v) => resolveSchemaType(v.type) !== 'null') ?? + schema.anyOf[0]; + if (preferredVariant) { + return createInputTemplateFromSchema(preferredVariant); + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/room-shell/src/RoomShellCommandPalette.tsx` around lines 663 - 669, The loop over schema.anyOf always returns the first variant because createInputTemplateFromSchema never returns undefined; either simplify by selecting the first anyOf variant directly (replace the for+early-return with using schema.anyOf[0] and creating the template once), or change createInputTemplateFromSchema so it can return undefined for schemas that produce no meaningful template (e.g., pure null-only schemas) so the current loop can correctly continue to later variants; update the call site in RoomShellCommandPalette to match the chosen approach and adjust any related logic that assumed a possible undefined result.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/room-shell/src/RoomShellCommandPalette.tsx`:
- Around line 395-472: The Dialog renders two DialogDescription nodes when
ActiveInputComponent is absent and activeInputCommand.description exists; fix by
only rendering the JSON input hint when there is no
activeInputCommand.description (or merge them into one string) in the fallback
branch where ActiveInputComponent is falsy, and when rendering DialogContent
with an ActiveInputComponent ensure you explicitly pass
aria-describedby={undefined} if activeInputCommand?.description is falsy to
avoid a dangling pointer; locate the relevant JSX around ActiveInputComponent,
activeInputCommand, DialogDescription and DialogContent in
RoomShellCommandPalette and adjust the conditional rendering and DialogContent
prop accordingly.
---
Duplicate comments:
In `@packages/room-shell/src/RoomShellCommandPalette.tsx`:
- Around line 120-123: The component in RoomShellCommandPalette.tsx obtains the
room store via useRoomStoreApi<RoomShellSliceState>() but never calls
room.initialize(); add a mount effect that calls room.initialize() (via the
store object returned from useRoomStoreApi) when the component mounts so
automatic slice initialization runs; implement this using a useEffect inside
RoomShellCommandPalette that calls room.initialize() (or the appropriate
initialize method on the object returned by useRoomStoreApi) and runs only on
mount.
---
Nitpick comments:
In `@packages/room-shell/src/RoomShellCommandPalette.tsx`:
- Around line 374-377: getCommandShortcut is being invoked twice for each
command item; compute its result once and reuse it. In the groupCommands.map
callback (the code that renders each command item), call
getCommandShortcut(command) once into a local const (e.g., shortcut) and then
use that variable for the conditional render and as the child of
<CommandShortcut> instead of calling getCommandShortcut a second time; update
the JSX that currently references getCommandShortcut(command) in both the
ternary check and inside <CommandShortcut> to use the new local variable.
- Around line 663-669: The loop over schema.anyOf always returns the first
variant because createInputTemplateFromSchema never returns undefined; either
simplify by selecting the first anyOf variant directly (replace the
for+early-return with using schema.anyOf[0] and creating the template once), or
change createInputTemplateFromSchema so it can return undefined for schemas that
produce no meaningful template (e.g., pure null-only schemas) so the current
loop can correctly continue to later variants; update the call site in
RoomShellCommandPalette to match the chosen approach and adjust any related
logic that assumed a possible undefined result.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
packages/ai-core/src/components/tools/ToolErrorMessage.tsxpackages/room-shell/src/RoomShellCommandPalette.tsx
| <DialogHeader> | ||
| <DialogTitle> | ||
| {activeInputCommand?.name ?? 'Command input'} | ||
| </DialogTitle> | ||
| {activeInputCommand?.description ? ( | ||
| <DialogDescription> | ||
| {activeInputCommand.description} | ||
| </DialogDescription> | ||
| ) : null} | ||
| </DialogHeader> | ||
|
|
||
| {ActiveInputComponent && activeInputCommand ? ( | ||
| <ActiveInputComponent | ||
| commandId={activeInputCommand.id} | ||
| commandName={activeInputCommand.name} | ||
| isSubmitting={isSubmittingInput} | ||
| error={inputError} | ||
| onSubmit={(input) => { | ||
| void submitCommandInput(input); | ||
| }} | ||
| onCancel={closeCommandInputDialog} | ||
| /> | ||
| ) : ( | ||
| <> | ||
| <DialogDescription> | ||
| {activeInputCommand?.inputDescription ?? | ||
| 'Provide command input as a JSON value. Example: {"panelId":"sql-editor"}'} | ||
| </DialogDescription> | ||
| <div className="relative h-56 w-full rounded-md border"> | ||
| <JsonMonacoEditor | ||
| className="absolute inset-0 h-full w-full" | ||
| schema={activeInputCommandJsonSchema} | ||
| value={inputJsonValue} | ||
| path={activeInputEditorPath} | ||
| readOnly={isSubmittingInput} | ||
| onChange={(value) => { | ||
| setInputJsonValue(value ?? ''); | ||
| setInputError(undefined); | ||
| }} | ||
| onValidate={handleJsonEditorValidate} | ||
| options={{ | ||
| lineNumbers: 'off', | ||
| glyphMargin: false, | ||
| folding: false, | ||
| wordWrap: 'on', | ||
| tabSize: 2, | ||
| fixedOverflowWidgets: false, | ||
| automaticLayout: true, | ||
| }} | ||
| /> | ||
| </div> | ||
| {hasJsonValidationErrors ? ( | ||
| <p className="text-destructive text-sm"> | ||
| JSON input has syntax or schema validation errors. | ||
| </p> | ||
| ) : null} | ||
| {inputError ? ( | ||
| <p className="text-destructive text-sm">{inputError}</p> | ||
| ) : null} | ||
| <DialogFooter> | ||
| <Button | ||
| variant="outline" | ||
| onClick={closeCommandInputDialog} | ||
| disabled={isSubmittingInput} | ||
| > | ||
| Cancel | ||
| </Button> | ||
| <Button | ||
| onClick={() => { | ||
| void submitJsonCommandInput(); | ||
| }} | ||
| disabled={isSubmittingInput || hasJsonValidationErrors} | ||
| > | ||
| Run command | ||
| </Button> | ||
| </DialogFooter> | ||
| </> | ||
| )} |
There was a problem hiding this comment.
Two overlapping DialogDescription nodes when ActiveInputComponent is absent and a command description exists.
When activeInputCommand.description is truthy AND no ActiveInputComponent is resolved, both DialogDescription elements render:
- Lines 399–402: the command's description
- Lines 419–422: the JSON input hint
Radix DialogContent auto-wires aria-describedby to the first DialogDescription by generated ID. The second one becomes a visually redundant label that is not announced by screen readers.
Consider rendering the JSON input hint description only when the command has no description, or merge them. Additionally, when ActiveInputComponent is used and there is no command description, DialogContent ends up with a dangling aria-describedby pointer; pass aria-describedby={undefined} to DialogContent in that case per Radix guidance.
🛡️ Sketch of fix
- {activeInputCommand?.description ? (
- <DialogDescription>
- {activeInputCommand.description}
- </DialogDescription>
- ) : null}
+ {/* Move description into the body so it's co-located with the JSON editor hint */}In the fallback (JSON) path, show description if present, then always show the input hint:
+ {activeInputCommand?.description && (
+ <DialogDescription>{activeInputCommand.description}</DialogDescription>
+ )}
<DialogDescription>
{activeInputCommand?.inputDescription ??
'Provide command input as a JSON value. Example: {"panelId":"sql-editor"}'}
</DialogDescription>For the ActiveInputComponent path without a description, suppress the dangling attribute:
- <DialogContent>
+ <DialogContent aria-describedby={!activeInputCommand?.description ? undefined : undefined}>(Use aria-describedby={undefined} explicitly on DialogContent if no description can ever be guaranteed for custom input commands, per Radix docs recommendation.)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/room-shell/src/RoomShellCommandPalette.tsx` around lines 395 - 472,
The Dialog renders two DialogDescription nodes when ActiveInputComponent is
absent and activeInputCommand.description exists; fix by only rendering the JSON
input hint when there is no activeInputCommand.description (or merge them into
one string) in the fallback branch where ActiveInputComponent is falsy, and when
rendering DialogContent with an ActiveInputComponent ensure you explicitly pass
aria-describedby={undefined} if activeInputCommand?.description is falsy to
avoid a dangling pointer; locate the relevant JSX around ActiveInputComponent,
activeInputCommand, DialogDescription and DialogContent in
RoomShellCommandPalette and adjust the conditional rendering and DialogContent
prop accordingly.
Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
packages/ai/src/tools/commandTools.ts (2)
85-86:listToolNameForDescriptionis a redundant variable.
listToolName(line 85, using??) already resolves to'list_commands'for all valid tool names. The only distinction fromlistToolNameForDescription(line 86, using||) is for an empty-string input, which is an invalid tool name anyway. ReplacelistToolNameForDescriptionwithlistToolNamedirectly in the description.♻️ Proposed fix
- const listToolNameForDescription = options?.listToolName || 'list_commands';- description: `Execute a room command by ID. -Call ${listToolNameForDescription} first to discover valid command IDs and input requirements.`, + description: `Execute a room command by ID. +Call ${listToolName} first to discover valid command IDs and input requirements.`,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ai/src/tools/commandTools.ts` around lines 85 - 86, The variable listToolNameForDescription is redundant because listToolName already defaults to 'list_commands' and should be used for descriptions; remove listToolNameForDescription and replace its usages with listToolName, ensuring you keep the existing default behavior (listToolName uses ?? with DEFAULT_LIST_TOOL_NAME) and delete the declaration of listToolNameForDescription from commandTools.ts.
112-115: Dead-code??fallbacks on Zod-defaulted parameters.
params.includeInvisible,params.includeDisabled, andparams.includeInputSchemaall carry Zod.default()values, so their types are non-nullablebooleanafter schema parsing. The?? includeInvisibleDefault,?? includeDisabledCommandsInList, and?? truefallbacks are never reached.♻️ Proposed simplification
- includeInvisible: params.includeInvisible ?? includeInvisibleDefault, - includeDisabled: - params.includeDisabled ?? includeDisabledCommandsInList, - includeInputSchema: params.includeInputSchema ?? true, + includeInvisible: params.includeInvisible, + includeDisabled: params.includeDisabled, + includeInputSchema: params.includeInputSchema,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/ai/src/tools/commandTools.ts` around lines 112 - 115, The nullish-coalescing fallbacks are dead code because Zod defaults make params.includeInvisible, params.includeDisabled, and params.includeInputSchema always boolean; update the object construction to use the parsed values directly (e.g., set includeInvisible: params.includeInvisible, includeDisabled: params.includeDisabled, includeInputSchema: params.includeInputSchema) and remove the unnecessary ?? includeInvisibleDefault, ?? includeDisabledCommandsInList and ?? true; ensure any references to includeInvisibleDefault/includeDisabledCommandsInList remain only where genuinely needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/ai-core/src/AiSlice.ts`:
- Around line 1161-1213: AiCreateSessionInput currently rejects undefined which
makes doesCommandRequireInput return true and parseCommandInput throw when
invoked without a payload; change the schema declaration for
AiCreateSessionInput to provide a default empty object (e.g. use .default({}) on
the z.object) so safeParse(undefined) succeeds, the command is treated as not
requiring input, and invokeCommand('ai.create-session', undefined) works; verify
related code paths in createAiCommands (execute handler) continue to handle the
parsed result (the existing defensive (input as AiCreateSessionInput |
undefined) ?? {} can remain).
In `@packages/room-store/package.json`:
- Around line 39-41: package.json's devDependencies is missing
`@sqlrooms/preset-jest` even though jest.config.js imports
'@sqlrooms/preset-jest/node.js'; add "@sqlrooms/preset-jest":
"<appropriate-version>" to the devDependencies object (next to "ts-jest") in the
package.json for the room-store package and then install dependencies so tests
work in strict pnpm workspace mode. Ensure the dependency name matches the
import string used in jest.config.js.
---
Nitpick comments:
In `@packages/ai/src/tools/commandTools.ts`:
- Around line 85-86: The variable listToolNameForDescription is redundant
because listToolName already defaults to 'list_commands' and should be used for
descriptions; remove listToolNameForDescription and replace its usages with
listToolName, ensuring you keep the existing default behavior (listToolName uses
?? with DEFAULT_LIST_TOOL_NAME) and delete the declaration of
listToolNameForDescription from commandTools.ts.
- Around line 112-115: The nullish-coalescing fallbacks are dead code because
Zod defaults make params.includeInvisible, params.includeDisabled, and
params.includeInputSchema always boolean; update the object construction to use
the parsed values directly (e.g., set includeInvisible: params.includeInvisible,
includeDisabled: params.includeDisabled, includeInputSchema:
params.includeInputSchema) and remove the unnecessary ??
includeInvisibleDefault, ?? includeDisabledCommandsInList and ?? true; ensure
any references to includeInvisibleDefault/includeDisabledCommandsInList remain
only where genuinely needed.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
packages/ai-core/src/AiSlice.tspackages/ai/src/tools/commandTools.tspackages/room-store/__tests__/CommandSlice.toPortableSchema.test.tspackages/room-store/jest.config.jspackages/room-store/package.jsonpackages/room-store/src/CommandSlice.tspackages/room-store/src/toPortableSchema.ts
| const AiCreateSessionInput = z.object({ | ||
| name: z.string().optional().describe('Optional session name.'), | ||
| modelProvider: z.string().optional().describe('Optional model provider ID.'), | ||
| model: z.string().optional().describe('Optional model ID.'), | ||
| }); | ||
| type AiCreateSessionInput = z.infer<typeof AiCreateSessionInput>; | ||
|
|
||
| const AiSessionIdInput = z.object({ | ||
| sessionId: z.string().describe('Target AI session ID.'), | ||
| }); | ||
| type AiSessionIdInput = z.infer<typeof AiSessionIdInput>; | ||
|
|
||
| const AiRenameSessionInput = z.object({ | ||
| sessionId: z.string().describe('Target AI session ID.'), | ||
| name: z.string().min(1).describe('New session name.'), | ||
| }); | ||
| type AiRenameSessionInput = z.infer<typeof AiRenameSessionInput>; | ||
|
|
||
| function createAiCommands(): RoomCommand<AiCommandStoreState>[] { | ||
| const ensureSessionExists = ( | ||
| state: AiCommandStoreState, | ||
| sessionId: string, | ||
| ) => { | ||
| if (!state.ai.config.sessions.some((session) => session.id === sessionId)) { | ||
| throw new Error(`Unknown AI session "${sessionId}".`); | ||
| } | ||
| }; | ||
|
|
||
| return [ | ||
| { | ||
| id: 'ai.create-session', | ||
| name: 'Create AI session', | ||
| description: 'Start a new AI chat session', | ||
| group: 'AI', | ||
| keywords: ['ai', 'chat', 'session', 'new'], | ||
| inputSchema: AiCreateSessionInput, | ||
| inputDescription: | ||
| 'Optionally provide name, modelProvider, and model for the new session.', | ||
| metadata: { | ||
| readOnly: false, | ||
| idempotent: false, | ||
| riskLevel: 'low', | ||
| }, | ||
| execute: ({getState}, input) => { | ||
| const {name, modelProvider, model} = | ||
| (input as AiCreateSessionInput | undefined) ?? {}; | ||
| getState().ai.createSession(name, modelProvider, model); | ||
| return { | ||
| success: true, | ||
| commandId: 'ai.create-session', | ||
| message: 'Created AI session.', | ||
| }; | ||
| }, |
There was a problem hiding this comment.
ai.create-session blocks invocation without explicit input.
AiCreateSessionInput is a plain z.object schema, so safeParse(undefined) fails (an object schema never accepts undefined as the top-level input). Two concrete consequences:
doesCommandRequireInputreturnstrue, causing the command palette to display an unnecessary input form for a command whose fields are all optional.- From the AI surface, calling
invokeCommand('ai.create-session', undefined)hitsparseCommandInput, which throws"Invalid command input: Expected object, received undefined"— the LLM must always supply{}explicitly.
Adding .default({}) resolves both:
🔧 Proposed fix
-const AiCreateSessionInput = z.object({
- name: z.string().optional().describe('Optional session name.'),
- modelProvider: z.string().optional().describe('Optional model provider ID.'),
- model: z.string().optional().describe('Optional model ID.'),
-});
+const AiCreateSessionInput = z.object({
+ name: z.string().optional().describe('Optional session name.'),
+ modelProvider: z.string().optional().describe('Optional model provider ID.'),
+ model: z.string().optional().describe('Optional model ID.'),
+}).default({});With .default({}), parse(undefined) short-circuits and the default {} is eagerly returned, so safeParse(undefined) succeeds, doesCommandRequireInput returns false, and the AI can invoke the command without a payload. The defensive ?? {} cast in the execute handler then also becomes clearly correct (though unreachable after parsing).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const AiCreateSessionInput = z.object({ | |
| name: z.string().optional().describe('Optional session name.'), | |
| modelProvider: z.string().optional().describe('Optional model provider ID.'), | |
| model: z.string().optional().describe('Optional model ID.'), | |
| }); | |
| type AiCreateSessionInput = z.infer<typeof AiCreateSessionInput>; | |
| const AiSessionIdInput = z.object({ | |
| sessionId: z.string().describe('Target AI session ID.'), | |
| }); | |
| type AiSessionIdInput = z.infer<typeof AiSessionIdInput>; | |
| const AiRenameSessionInput = z.object({ | |
| sessionId: z.string().describe('Target AI session ID.'), | |
| name: z.string().min(1).describe('New session name.'), | |
| }); | |
| type AiRenameSessionInput = z.infer<typeof AiRenameSessionInput>; | |
| function createAiCommands(): RoomCommand<AiCommandStoreState>[] { | |
| const ensureSessionExists = ( | |
| state: AiCommandStoreState, | |
| sessionId: string, | |
| ) => { | |
| if (!state.ai.config.sessions.some((session) => session.id === sessionId)) { | |
| throw new Error(`Unknown AI session "${sessionId}".`); | |
| } | |
| }; | |
| return [ | |
| { | |
| id: 'ai.create-session', | |
| name: 'Create AI session', | |
| description: 'Start a new AI chat session', | |
| group: 'AI', | |
| keywords: ['ai', 'chat', 'session', 'new'], | |
| inputSchema: AiCreateSessionInput, | |
| inputDescription: | |
| 'Optionally provide name, modelProvider, and model for the new session.', | |
| metadata: { | |
| readOnly: false, | |
| idempotent: false, | |
| riskLevel: 'low', | |
| }, | |
| execute: ({getState}, input) => { | |
| const {name, modelProvider, model} = | |
| (input as AiCreateSessionInput | undefined) ?? {}; | |
| getState().ai.createSession(name, modelProvider, model); | |
| return { | |
| success: true, | |
| commandId: 'ai.create-session', | |
| message: 'Created AI session.', | |
| }; | |
| }, | |
| const AiCreateSessionInput = z.object({ | |
| name: z.string().optional().describe('Optional session name.'), | |
| modelProvider: z.string().optional().describe('Optional model provider ID.'), | |
| model: z.string().optional().describe('Optional model ID.'), | |
| }).default({}); | |
| type AiCreateSessionInput = z.infer<typeof AiCreateSessionInput>; | |
| const AiSessionIdInput = z.object({ | |
| sessionId: z.string().describe('Target AI session ID.'), | |
| }); | |
| type AiSessionIdInput = z.infer<typeof AiSessionIdInput>; | |
| const AiRenameSessionInput = z.object({ | |
| sessionId: z.string().describe('Target AI session ID.'), | |
| name: z.string().min(1).describe('New session name.'), | |
| }); | |
| type AiRenameSessionInput = z.infer<typeof AiRenameSessionInput>; | |
| function createAiCommands(): RoomCommand<AiCommandStoreState>[] { | |
| const ensureSessionExists = ( | |
| state: AiCommandStoreState, | |
| sessionId: string, | |
| ) => { | |
| if (!state.ai.config.sessions.some((session) => session.id === sessionId)) { | |
| throw new Error(`Unknown AI session "${sessionId}".`); | |
| } | |
| }; | |
| return [ | |
| { | |
| id: 'ai.create-session', | |
| name: 'Create AI session', | |
| description: 'Start a new AI chat session', | |
| group: 'AI', | |
| keywords: ['ai', 'chat', 'session', 'new'], | |
| inputSchema: AiCreateSessionInput, | |
| inputDescription: | |
| 'Optionally provide name, modelProvider, and model for the new session.', | |
| metadata: { | |
| readOnly: false, | |
| idempotent: false, | |
| riskLevel: 'low', | |
| }, | |
| execute: ({getState}, input) => { | |
| const {name, modelProvider, model} = | |
| (input as AiCreateSessionInput | undefined) ?? {}; | |
| getState().ai.createSession(name, modelProvider, model); | |
| return { | |
| success: true, | |
| commandId: 'ai.create-session', | |
| message: 'Created AI session.', | |
| }; | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/ai-core/src/AiSlice.ts` around lines 1161 - 1213,
AiCreateSessionInput currently rejects undefined which makes
doesCommandRequireInput return true and parseCommandInput throw when invoked
without a payload; change the schema declaration for AiCreateSessionInput to
provide a default empty object (e.g. use .default({}) on the z.object) so
safeParse(undefined) succeeds, the command is treated as not requiring input,
and invokeCommand('ai.create-session', undefined) works; verify related code
paths in createAiCommands (execute handler) continue to handle the parsed result
(the existing defensive (input as AiCreateSessionInput | undefined) ?? {} can
remain).
| "devDependencies": { | ||
| "ts-jest": "^29.4.4" | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# 1. Check if `@sqlrooms/preset-jest` exists as a package
echo "=== Checking for `@sqlrooms/preset-jest` package ==="
fd 'package.json' --type f | xargs grep -l '"name": "@sqlrooms/preset-jest"' 2>/dev/null
# 2. Check current devDependencies in packages/room-store/package.json
echo ""
echo "=== Current packages/room-store/package.json devDependencies ==="
sed -n '/devDependencies/,/^ }/p' packages/room-store/package.json
# 3. Check if jest.config.js exists and what it imports
echo ""
echo "=== jest.config.js in packages/room-store ==="
if [ -f packages/room-store/jest.config.js ]; then
cat -n packages/room-store/jest.config.js
else
echo "jest.config.js not found"
fi
# 4. Check how other packages reference `@sqlrooms/preset-jest`
echo ""
echo "=== Other packages using `@sqlrooms/preset-jest` ==="
rg '@sqlrooms/preset-jest' --type json -B 2 -A 1 | head -30Repository: sqlrooms/sqlrooms
Length of output: 948
Add missing @sqlrooms/preset-jest devDependency.
jest.config.js imports from @sqlrooms/preset-jest/node.js, but this package is absent from devDependencies. In strict pnpm workspace mode this will cause tests to fail when the package is installed in isolation.
🔧 Proposed fix
"devDependencies": {
+ "@sqlrooms/preset-jest": "workspace:*",
"ts-jest": "^29.4.4"
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "devDependencies": { | |
| "ts-jest": "^29.4.4" | |
| } | |
| "devDependencies": { | |
| "@sqlrooms/preset-jest": "workspace:*", | |
| "ts-jest": "^29.4.4" | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/room-store/package.json` around lines 39 - 41, package.json's
devDependencies is missing `@sqlrooms/preset-jest` even though jest.config.js
imports '@sqlrooms/preset-jest/node.js'; add "@sqlrooms/preset-jest":
"<appropriate-version>" to the devDependencies object (next to "ts-jest") in the
package.json for the room-store package and then install dependencies so tests
work in strict pnpm workspace mode. Ensure the dependency name matches the
import string used in jest.config.js.
Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>
Adds a command pattern, a command registry to
room-store, and aRoomShell.CommandPalettecomponent to enable slices to register custom commands and support multi-surface dispatch.Kapture.2026-02-24.at.12.31.35.mp4
here's how AI assistant picks them up:
Kapture.2026-02-24.at.16.22.05.mp4
Summary by CodeRabbit
New Features
Documentation
Tests