(MOT-4173) feat(console): composer prompt picker + slash-command prompts - #565
(MOT-4173) feat(console): composer prompt picker + slash-command prompts#565rohitg00 wants to merge 1 commit into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe console adds a prompts route and editor backed by directory-worker prompt APIs. Real-backend chats can select per-session system prompts, while slash commands dynamically load and inject command prompt bodies. ChangesConsole prompt functionality
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant PromptPicker
participant PromptStore
participant ChatView
participant RealBackend
User->>PromptPicker: choose system prompt
PromptPicker->>PromptStore: getPrompt(name)
PromptStore-->>PromptPicker: prompt body
PromptPicker->>ChatView: update session prompt
ChatView->>RealBackend: send systemPrompt override
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
skill-check — worker0 verified, 49 skipped (no docs/).
Four for four. Nicely done. |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@console/web/src/components/chat/ChatView.tsx`:
- Around line 161-163: Replace the single ChatView-level sessionPrompt state
with prompt selections keyed by conversation.id, preferably in the existing
conversation state. Derive the selected override for the active conversation
before both send paths, so switching conversations neither leaks the previous
selection nor loses each conversation’s prior selection; update the
prompt-switching logic to write to the active conversation’s entry.
In `@console/web/src/components/chat/lexical/SlashCommandsPlugin.tsx`:
- Around line 100-113: Update the async callback in the SlashCommandsPlugin
prompt-loading flow to replace the node only if it still contains the original
`${entry.command} ` placeholder. Preserve the node and all user-entered text
when its content has changed while getPrompt(name) is pending.
In `@console/web/src/components/chat/PromptPicker.tsx`:
- Around line 74-81: Update the PromptPicker onChange handler to invalidate any
in-flight getPrompt request on every selection, including DEFAULT, and track the
latest selection so stale responses cannot call onChange. Handle getPrompt
failures without applying a result, and ensure only the current successful
request updates the selected prompt.
In `@console/web/src/lib/prompts.ts`:
- Around line 17-33: Update the Rust implementations of directory::prompts::list
and directory::prompts::get, including PromptEntry and PromptGetOutput
serialization, to emit each prompt’s actual kind and source fields. Ensure the
values are populated before promptRowSchema or promptDetailSchema defaults are
applied so system prompts retain their true kind and user library prompts retain
their true source and remain editable/deletable.
In `@console/web/src/pages/Prompts/index.tsx`:
- Around line 78-89: Update the open callback to handle both getPrompt failures
and null results through the existing error state: catch rejected requests and
set a useful error message, and set an appropriate error when no prompt detail
is returned instead of returning silently. Preserve the existing draft
population for successful results.
- Around line 132-136: Update onDelete to require explicit user confirmation
before calling deletePrompt, preserving the existing early return and clearing
draft only after successful deletion. Ensure every prompt-deletion entry point,
including the additional onDelete occurrence, uses the same confirmation flow
rather than relying on deletePrompt’s hardcoded yes option.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 6722d1bd-48c0-4aec-9e31-38642143ba1f
📒 Files selected for processing (16)
console/web/src/App.tsxconsole/web/src/components/chat/ChatView.tsxconsole/web/src/components/chat/Composer.tsxconsole/web/src/components/chat/PromptPicker.tsxconsole/web/src/components/chat/lexical/SlashCommandsPlugin.tsxconsole/web/src/hooks/use-hash-route.tsconsole/web/src/hooks/use-prompts-status.tsconsole/web/src/lib/backend/harness-send.tsconsole/web/src/lib/backend/real.tsconsole/web/src/lib/backend/types.tsconsole/web/src/lib/conversations-context.tsxconsole/web/src/lib/nav-options.test.tsconsole/web/src/lib/nav-options.tsconsole/web/src/lib/prompts.tsconsole/web/src/lib/slash-commands.tsconsole/web/src/pages/Prompts/index.tsx
| // System prompt override for this conversation (prompt store, kind: | ||
| // system). Applied to every send until switched back to default. | ||
| const [sessionPrompt, setSessionPrompt] = useState<SessionPrompt | null>(null) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Scope the selected prompt by conversation.id.
This state can leak the previous chat’s override into the next selected conversation; if ChatView remounts instead, it loses the prior conversation’s selection. Store prompt selection keyed by conversation id (preferably in conversation state) and derive the current override from that mapping before both send paths.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@console/web/src/components/chat/ChatView.tsx` around lines 161 - 163, Replace
the single ChatView-level sessionPrompt state with prompt selections keyed by
conversation.id, preferably in the existing conversation state. Derive the
selected override for the active conversation before both send paths, so
switching conversations neither leaks the previous selection nor loses each
conversation’s prior selection; update the prompt-switching logic to write to
the active conversation’s entry.
| const promptRowSchema = z.object({ | ||
| name: z.string(), | ||
| description: z.string().default(''), | ||
| kind: z.string().default('command'), | ||
| source: z.string().default('worker'), | ||
| modified_at: z.string().default(''), | ||
| }) | ||
| export type PromptRow = z.infer<typeof promptRowSchema> | ||
|
|
||
| const promptListSchema = z.object({ | ||
| prompts: z.array(z.unknown()).default([]), | ||
| }) | ||
|
|
||
| const promptDetailSchema = promptRowSchema.extend({ | ||
| body: z.string().default(''), | ||
| }) | ||
| export type PromptDetail = z.infer<typeof promptDetailSchema> |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm whether directory::prompts::list/get currently emit `kind`/`source`.
fd prompts.rs --full-path iii-directory
rg -n -A5 'struct PromptEntry|struct PromptGetOutput' iii-directory/src/functions/prompts.rsRepository: iii-hq/workers
Length of output: 580
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== prompts schema/usages =="
sed -n '1,80p' console/web/src/lib/prompts.ts
echo
rg -n "getPrompt|open\\(|existing:|readonly:|kind|source" console/web/src/pages/Prompts/console/web/src -S || true
echo
echo "== iii-directory prompts.rs relevant sections =="
sed -n '1,180p' iii-directory/src/functions/prompts.rs
echo
sed -n '1,140p' iii-directory/tests/steps/prompts.rs
echo
echo "== zod package reference =="
fd -i package.json . -x sh -c 'echo "--- $1"; rg -n "\"zod\"|zod@|zod:" "$1" || true' sh {}Repository: iii-hq/workers
Length of output: 14413
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== console/prompts page files =="
fd -a "index.tsx|.*\.ts(x)?$" console/web/src/pages 2>/dev/null | grep -i "prompt" || true
echo
if [ -d console/web/src/pages/Prompts ]; then
rg -n "getPrompt|listPrompts|open\\(|existing:|readonly:|kind|source|delete|editable" console/web/src/pages/Prompts -S
fi
echo
if [ -f console/web/src/lib/prompts.ts ]; then
sed -n '1,120p' console/web/src/lib/prompts.ts
fi
echo
echo "== zod v4 optional/unknown/object behavior =="
node - <<'JS'
const schema = require('zod').z.object({
kind: require('zod').z.string().default('command'),
source: require('zod').z.string().default('worker'),
});
for (const input of [{}, {kind:'system'}, {kind:'system', source:'user'}]) {
const parsed = schema.safeParse(input);
console.log(JSON.stringify(input), parsed.success, parsed.success ? parsed.data : parsed.error.errors.map(e => ({path: e.path, message: e.message})));
}
JSRepository: iii-hq/workers
Length of output: 5997
🌐 Web query:
zod v4 object default undefined input field default only when key is undefined
💡 Result:
In Zod v4, the behavior of .default() is designed to handle the absence of a value by short-circuiting the parsing process and returning the specified default value [1][2]. Key behaviors in Zod v4: 1. Short-circuiting: When input is undefined, .default() returns the provided value immediately, skipping the inner schema's parsing [1][3]. Consequently, the output type of a schema with .default() loses optionality (e.g., it becomes string instead of string | undefined) because the parser guarantees a non-undefined result [4][5]. 2. Defining "Only When Undefined": Because .default() treats any undefined input as an absence to be filled, it naturally applies only when the key is undefined (or absent). If you pass an explicit value, Zod validates that value against the inner schema [6]. 3. Preserving Optionality: If your goal is for the field to remain optional (retaining | undefined in the output type) while providing a default value only at runtime, .default() is generally not the correct tool as it forces a non-optional output type [4][5]. Instead, you should use .prefault(), which allows you to substitute a value into the input before parsing while preserving the schema's optional nature [1][2][3]. If you explicitly need to define a default only when a key is undefined while maintaining an optional type, consider the following pattern using .prefault() [1][3]: const schema = z.string.prefault("your-default").optional; type Schema = z.infer; // string | undefined This approach ensures that undefined inputs receive the default value during parsing, but the type signature correctly reflects that the resulting field can still be undefined [3].
Citations:
- 1: https://zod.dev/v4/changelog
- 2: https://github.com/colinhacks/zod/blob/c7805073/packages/docs/content/v4/changelog.mdx
- 3: v4: defaults in zod object are no longer optional when defining a default for wrapped zod object colinhacks/zod#4140
- 4:
.default(undefined)removes optionality colinhacks/zod#4815 - 5: [v4] Prevent
.default(undefined)colinhacks/zod#5213 - 6: https://github.com/colinhacks/zod/blob/912f0f51/packages/zod/src/v4/classic/tests/default.test.ts
Return kind and source from directory::prompts::list/get.
PromptEntry and PromptGetOutput currently serialize only name, description, body, and modified_at, while promptRowSchema defaults missing fields to kind: 'command' and source: 'worker'. This makes system prompts appear as command in the list and forces every detail opened via getPrompt into readonly: true, hiding delete and disabling edits for user library prompts. Emit the actual kind/source in the Rust responses or store/detect it before applying these defaults.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@console/web/src/lib/prompts.ts` around lines 17 - 33, Update the Rust
implementations of directory::prompts::list and directory::prompts::get,
including PromptEntry and PromptGetOutput serialization, to emit each prompt’s
actual kind and source fields. Ensure the values are populated before
promptRowSchema or promptDetailSchema defaults are applied so system prompts
retain their true kind and user library prompts retain their true source and
remain editable/deletable.
Adds in-chat prompt selection surfaces that read the directory prompt store (directory::prompts::*, kind system|command): - a system-prompt picker beside the composer model/memory controls: pick a kind:system entry to override the system prompt for the conversation. - the slash-command menu surfaces kind:command prompts, injecting the body. Depends on the directory prompt-library backend (save/delete, kind, source).
85fb613 to
c7a9a80
Compare
What
In-chat prompt selection surfaces that read the directory prompt store, replacing the earlier standalone console page (that browse/edit surface now lives in the injectable directory tab, #564).
kind: systementry to override the system prompt for the conversation, applied per send (system_prompt+system_prompt_strategy: override).defaultdefers to the harness identity chain. Mid-conversation switches apply from the next turn./menu surfaceskind: commandprompts, injecting the body into the message on select.Depends on
The directory prompt-library backend (
directory::prompts::save/delete,kind,source) in #564. Read-only listing works against any directory worker; save, fork, and delete need the backend.Verification
tsc -b) and biome clean on all touched files.prompt:dropdown lists the store'skind: systementries; selecting one drives the turn under that prompt.claude-codeproviders. The prompt reaches the model verbatim: console toharness::sendoptions.system_prompt, toassemble_context, to the context-managerrender_system_prompt(verbatim), to the provider.codex/gpt-5.6-sol: ablog-writerprompt makes the agent identify as a blog writer (web + state only); a Hindi-instructed prompt returns Devanagari output.claude-sonnet-5(anthropic API): same, adopts theblog-writeridentity.Known limitation
On
claude-code/*(Max subscription) models the override is effectively lost.provider-claude-codemust send the Claude Code identity as the firstsystemblock (a subscription wire requirement, seeprovider-claude-code/src/request.rs), and the caller prompt lands as a second block after the full agent identity, which dominates. This is a provider constraint, not a picker bug. A follow-up can surface a note in the picker when aclaude-code/*model is active.