feat(web): add slash command for quick access to Skills/Projects/Agents - #104
Conversation
- Add `/` command in WelcomePage PromptComposer to quickly access Skills, Projects, and Code Agents - Add color-coded chips in input: GitHub PR/Issue (muted), files (blue), skills (yellow) - Fix slash popover not closing after agent/project selection - Adjust Settings sections order (Shortcuts before About) - Add puzzle.svg icon for skill chips Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Replace incorrect puzzle icon with the correct Lucide puzzle icon shape. Convert from stroke-based to fill-based for CSS mask compatibility. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis PR adds a "/" command to the workspace composer: PromptComposer gains slash contracts, token parsing/serialization and caret-range helpers, and WelcomePage implements a Fuse-searchable slash popover (Skills, Projects, Code Agents) with keyboard navigation and selection wiring back into the composer. ChangesSlash Command for Skills, Projects, and Code Agents
Sequence Diagram(s)sequenceDiagram
participant User
participant PromptComposer
participant WelcomePage
participant skillsApi
participant Editor
User->>PromptComposer: press "/" key
PromptComposer->>PromptComposer: readSlashContextFromSelection -> SlashTriggerContext(caretRect, query, slashOffset)
PromptComposer->>WelcomePage: onSlashTrigger(SlashTriggerContext)
WelcomePage->>skillsApi: list() / Fuse search (on query)
skillsApi-->>WelcomePage: skills list
WelcomePage->>WelcomePage: filter via Fuse -> visibleSlashItems
User->>WelcomePage: navigate/select item (Enter)
WelcomePage->>PromptComposer: applySlashAtRange(slashOffset, queryLength, mention)
PromptComposer->>Editor: replace /query with /skill:<path> token and move caret
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
There was a problem hiding this comment.
1 issue found across 5 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/web/src/components/welcome/PromptComposer.tsx">
<violation number="1" location="apps/web/src/components/welcome/PromptComposer.tsx:358">
P2: `readSlashContextFromSelection` can compute `slashOffset` for the wrong occurrence when the same `/<query>` exists later in the text, causing `applySlashAtRange` to replace the wrong segment.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/components/welcome/WelcomePage.tsx (1)
150-157:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
/skill:chips are never resolved before submission.
resolvePromptPlaceholdersdoes not map/skill:<absolutePath>to its path, so the backend receives the token string instead of the intended skill path.Suggested fix
function resolvePromptPlaceholders(text: string, atts: ComposerAttachment[]): string { return text .replace(/@(?:issue|pr)#\d+/g, () => ".atmos/context/requirement.md") .replace(/@file:([^\s]+)/g, (_match, relativePath: string) => relativePath) + .replace(/\/skill:([^\s]+)/g, (_match, absolutePath: string) => absolutePath) .replace(/\[`#img-`(\d+)\]/g, (match, n: string) => { const att = atts.find((a) => a.number === Number(n)); return att ? `.atmos/attachments/${att.filename}` : match; }); }🤖 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 `@apps/web/src/components/welcome/WelcomePage.tsx` around lines 150 - 157, The resolvePromptPlaceholders function currently leaves /skill:<absolutePath> tokens untouched; add a replacement step in resolvePromptPlaceholders (alongside the existing .replace chains) that matches /skill:([^\s]+) and returns the captured path (the first capture group) so the token is replaced with the actual skill path before submission; reference resolvePromptPlaceholders to locate where to insert the .replace and ensure the regex is global and returns the captured path string.
🤖 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 `@apps/web/src/components/welcome/PromptComposer.tsx`:
- Around line 122-123: The span.className mutation currently appends hardcoded
blue classes ("border-blue-500/30 bg-blue-500/10 text-blue-600
dark:text-blue-400") (and the similar hardcoded yellow/blue classes around lines
~137-138) which breaks theme adaptation; replace those literal color utility
classes with semantic theme tokens (e.g. use border-border, bg-background or a
semantic chip/bg and text-primary/text-muted-foreground with appropriate opacity
tokens) so chips use the app's CSS variables; update the span.className
concatenation and any other places (search for the other hardcoded class usage
near where iconProps is created and the lines flagged) to use those semantic
class names instead of hardcoded palette classes.
- Around line 348-353: The slash trigger currently matches any "/" in
beforeDomText (domAtIndex = lastIndexOf("/")) and aborts if any space exists in
the remainder, which both picks up URLs/paths and forbids commands with args
(e.g., "/project my-app"). Change the parsing in PromptComposer.tsx so you find
a "/" that is either at the start or preceded by whitespace (check
charAt(domAtIndex - 1) or use a regex like /(^|\s)\/.../), then allow spaces
after the slash by extracting the command token only (e.g., let raw =
beforeDomText.slice(domAtIndex + 1); let command = raw.split(/\s/)[0]) instead
of returning null on /\s/. Use these variables (domAtIndex, beforeDomText,
query/raw/command) to implement the stricter trigger detection and permissive
argument handling.
In `@apps/web/src/components/welcome/WelcomePage.tsx`:
- Around line 847-857: The selectSlashProject callback closes the popover and
sets the project but doesn't remove the typed "/<query>" from the
PromptComposer; update selectSlashProject to also call the PromptComposer's
clearSlashAtRange handler (via the composer ref) after closing the popover and
before/after setProjectId so the slash fragment is consumed, making sure to
null-check the composer ref/handler; apply the same change to the analogous
selectSlashAgent handler so both actions clear the slash text from the composer
input.
- Around line 715-727: The current filtering uses the raw debouncedSlashQuery
for every section (see filteredSkills, filteredProjects and the similar
filteredAgents block) and doesn't support subcommand scopes like "/skills" or
"/project"; update the logic to parse the debouncedSlashQuery for a scope prefix
(e.g., detect "/skills", "/project" or "/agent"), strip that prefix to produce
the actual search term, and then: for each memoized filter (filteredSkills,
filteredProjects, filteredAgents) only run the Fuse search when the scope is
either "all" or matches that section; otherwise return the original unfiltered
array (or an empty array if you prefer hiding non-target sections). Also ensure
the UI rendering respects scope !== "all" by showing only the targeted section
when a scope is present.
- Around line 879-881: The explicit any on the slash nav entry (the property
"item?: any" in the inline type that also has "section?: 'skills' | 'projects' |
'agents'") is causing the lint failure; replace it with a concrete interface
(e.g., define an interface SlashNavItem { /* fields used by the component such
as id: string; title: string; description?: string; ... */ } and then change
"item?: any" to "item?: SlashNavItem") or, if the shape is not yet known, use
"unknown" and narrow it where accessed with type guards; update any usages of
"item" to match the new type (or add type guards/casts) so the component
compiles without `@typescript-eslint/no-explicit-any` errors.
---
Outside diff comments:
In `@apps/web/src/components/welcome/WelcomePage.tsx`:
- Around line 150-157: The resolvePromptPlaceholders function currently leaves
/skill:<absolutePath> tokens untouched; add a replacement step in
resolvePromptPlaceholders (alongside the existing .replace chains) that matches
/skill:([^\s]+) and returns the captured path (the first capture group) so the
token is replaced with the actual skill path before submission; reference
resolvePromptPlaceholders to locate where to insert the .replace and ensure the
regex is global and returns the captured path string.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 7ebdeedc-5821-465d-bb41-d4b807e983b6
⛔ Files ignored due to path filters (1)
apps/web/public/icons/puzzle.svgis excluded by!**/*.svg
📒 Files selected for processing (4)
AGENTS.mdapps/web/src/components/dialogs/SettingsModal.tsxapps/web/src/components/welcome/PromptComposer.tsxapps/web/src/components/welcome/WelcomePage.tsx
| span.className += " border-blue-500/30 bg-blue-500/10 text-blue-600 dark:text-blue-400"; | ||
| const iconProps = getFileIconProps({ name: filename, isDir, className: "size-3.5" }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use semantic theme tokens instead of hardcoded blue/yellow classes for chips.
Line 122 and Line 137 hardcode palette values, which makes theme adaptation brittle.
As per coding guidelines: "ALWAYS use semantic CSS variables (bg-background, text-muted-foreground, border-border) instead of hardcoded colors for theme adaptation".
Also applies to: 137-138
🤖 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 `@apps/web/src/components/welcome/PromptComposer.tsx` around lines 122 - 123,
The span.className mutation currently appends hardcoded blue classes
("border-blue-500/30 bg-blue-500/10 text-blue-600 dark:text-blue-400") (and the
similar hardcoded yellow/blue classes around lines ~137-138) which breaks theme
adaptation; replace those literal color utility classes with semantic theme
tokens (e.g. use border-border, bg-background or a semantic chip/bg and
text-primary/text-muted-foreground with appropriate opacity tokens) so chips use
the app's CSS variables; update the span.className concatenation and any other
places (search for the other hardcoded class usage near where iconProps is
created and the lines flagged) to use those semantic class names instead of
hardcoded palette classes.
| const domAtIndex = beforeDomText.lastIndexOf("/"); | ||
| if (domAtIndex < 0) return null; | ||
|
|
||
| const query = beforeDomText.slice(domAtIndex + 1); | ||
| if (/\s/.test(query)) return null; | ||
|
|
There was a problem hiding this comment.
Slash trigger parsing is too broad and too strict at the same time.
Line 348 triggers on any / (including URLs/paths), and Line 352 cancels as soon as a space appears, which blocks command patterns like /project my-app.
Suggested fix
const beforeDomText = beforeRange.toString();
const domAtIndex = beforeDomText.lastIndexOf("/");
if (domAtIndex < 0) return null;
+ // Only treat "/" as a command trigger at start-of-text or after whitespace.
+ if (domAtIndex > 0 && !/\s/.test(beforeDomText[domAtIndex - 1] ?? "")) return null;
const query = beforeDomText.slice(domAtIndex + 1);
- if (/\s/.test(query)) return null;📝 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 domAtIndex = beforeDomText.lastIndexOf("/"); | |
| if (domAtIndex < 0) return null; | |
| const query = beforeDomText.slice(domAtIndex + 1); | |
| if (/\s/.test(query)) return null; | |
| const domAtIndex = beforeDomText.lastIndexOf("/"); | |
| if (domAtIndex < 0) return null; | |
| // Only treat "/" as a command trigger at start-of-text or after whitespace. | |
| if (domAtIndex > 0 && !/\s/.test(beforeDomText[domAtIndex - 1] ?? "")) return null; | |
| const query = beforeDomText.slice(domAtIndex + 1); |
🤖 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 `@apps/web/src/components/welcome/PromptComposer.tsx` around lines 348 - 353,
The slash trigger currently matches any "/" in beforeDomText (domAtIndex =
lastIndexOf("/")) and aborts if any space exists in the remainder, which both
picks up URLs/paths and forbids commands with args (e.g., "/project my-app").
Change the parsing in PromptComposer.tsx so you find a "/" that is either at the
start or preceded by whitespace (check charAt(domAtIndex - 1) or use a regex
like /(^|\s)\/.../), then allow spaces after the slash by extracting the command
token only (e.g., let raw = beforeDomText.slice(domAtIndex + 1); let command =
raw.split(/\s/)[0]) instead of returning null on /\s/. Use these variables
(domAtIndex, beforeDomText, query/raw/command) to implement the stricter trigger
detection and permissive argument handling.
| const filteredSkills = React.useMemo(() => { | ||
| const query = debouncedSlashQuery; | ||
| if (!query) return skills; | ||
| const results = skillsFuse.search(query); | ||
| return results.map((r) => r.item); | ||
| }, [debouncedSlashQuery, skills, skillsFuse]); | ||
|
|
||
| const filteredProjects = React.useMemo(() => { | ||
| const query = debouncedSlashQuery; | ||
| if (!query) return projects; | ||
| const results = projectsFuse.search(query); | ||
| return results.map((r) => r.item); | ||
| }, [debouncedSlashQuery, projects, projectsFuse]); |
There was a problem hiding this comment.
Subcommand targeting (/skills, /project, /agent) is not implemented yet.
All sections are filtered by the same raw query; this misses the issue objective requiring command-scoped filtering.
Suggested direction
+function parseSlashQuery(q: string): { scope: "all" | "skills" | "projects" | "agents"; term: string } {
+ const m = q.trim().match(/^(skills?|project|agent)\b\s*(.*)$/i);
+ if (!m) return { scope: "all", term: q.trim() };
+ const scopeMap = { skill: "skills", skills: "skills", project: "projects", agent: "agents" } as const;
+ return { scope: scopeMap[m[1].toLowerCase() as keyof typeof scopeMap] ?? "all", term: m[2] ?? "" };
+}Then filter/render only the targeted section when scope !== "all".
Also applies to: 770-775
🤖 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 `@apps/web/src/components/welcome/WelcomePage.tsx` around lines 715 - 727, The
current filtering uses the raw debouncedSlashQuery for every section (see
filteredSkills, filteredProjects and the similar filteredAgents block) and
doesn't support subcommand scopes like "/skills" or "/project"; update the logic
to parse the debouncedSlashQuery for a scope prefix (e.g., detect "/skills",
"/project" or "/agent"), strip that prefix to produce the actual search term,
and then: for each memoized filter (filteredSkills, filteredProjects,
filteredAgents) only run the Fuse search when the scope is either "all" or
matches that section; otherwise return the original unfiltered array (or an
empty array if you prefer hiding non-target sections). Also ensure the UI
rendering respects scope !== "all" by showing only the targeted section when a
scope is present.
| const selectSlashProject = React.useCallback( | ||
| (project: { id: string }) => { | ||
| const popover = slashPopover; | ||
| if (!popover) return; | ||
| // Close popover immediately to prevent re-opening from side effects | ||
| setSlashPopover(null); | ||
| // Then switch project | ||
| setProjectId(project.id); | ||
| }, | ||
| [slashPopover], | ||
| ); |
There was a problem hiding this comment.
Selecting Project/Agent from slash menu should also consume the typed /<query> fragment.
Currently it closes the popover and switches project/agent, but the command text remains in the composer input.
Suggested direction
const selectSlashProject = React.useCallback(
(project: { id: string }) => {
const popover = slashPopover;
if (!popover) return;
+ composerRef.current?.clearSlashAtRange?.(popover.slashOffset, popover.query.length);
setSlashPopover(null);
setProjectId(project.id);
},
[slashPopover],
);You’d need the corresponding clearSlashAtRange handle method in PromptComposer.
Also applies to: 859-869
🤖 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 `@apps/web/src/components/welcome/WelcomePage.tsx` around lines 847 - 857, The
selectSlashProject callback closes the popover and sets the project but doesn't
remove the typed "/<query>" from the PromptComposer; update selectSlashProject
to also call the PromptComposer's clearSlashAtRange handler (via the composer
ref) after closing the popover and before/after setProjectId so the slash
fragment is consumed, making sure to null-check the composer ref/handler; apply
the same change to the analogous selectSlashAgent handler so both actions clear
the slash text from the composer input.
| item?: any; | ||
| section?: "skills" | "projects" | "agents"; | ||
| }> = []; |
There was a problem hiding this comment.
Replace any in slash nav item typing (CI is already failing on this line).
Line 879 violates @typescript-eslint/no-explicit-any and matches the lint failure.
Suggested fix
- const items: Array<{
- type: "skill" | "project" | "agent" | "show-more";
- item?: any;
- section?: "skills" | "projects" | "agents";
- }> = [];
+ type SlashNavItem =
+ | { type: "skill"; item: SkillInfo }
+ | { type: "project"; item: { id: string; name: string } }
+ | { type: "agent"; item: AgentMenuOption }
+ | { type: "show-more"; section: "skills" | "projects" | "agents" };
+ const items: SlashNavItem[] = [];📝 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.
| item?: any; | |
| section?: "skills" | "projects" | "agents"; | |
| }> = []; | |
| type SlashNavItem = | |
| | { type: "skill"; item: SkillInfo } | |
| | { type: "project"; item: { id: string; name: string } } | |
| | { type: "agent"; item: AgentMenuOption } | |
| | { type: "show-more"; section: "skills" | "projects" | "agents" }; | |
| const items: SlashNavItem[] = []; |
🧰 Tools
🪛 GitHub Actions: CI - Web / 2_Lint.txt
[error] 879-879: @typescript-eslint/no-explicit-any: Unexpected any. Specify a different type.
🪛 GitHub Actions: CI - Web / Lint
[error] 879-879: @typescript-eslint/no-explicit-any: Unexpected any. Specify a different type
🤖 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 `@apps/web/src/components/welcome/WelcomePage.tsx` around lines 879 - 881, The
explicit any on the slash nav entry (the property "item?: any" in the inline
type that also has "section?: 'skills' | 'projects' | 'agents'") is causing the
lint failure; replace it with a concrete interface (e.g., define an interface
SlashNavItem { /* fields used by the component such as id: string; title:
string; description?: string; ... */ } and then change "item?: any" to "item?:
SlashNavItem") or, if the shape is not yet known, use "unknown" and narrow it
where accessed with type guards; update any usages of "item" to match the new
type (or add type guards/casts) so the component compiles without
`@typescript-eslint/no-explicit-any` errors.
…ized text Previously, readSlashContextFromSelection and readAtContextFromSelection computed offsets by searching the entire serialized text for the last occurrence of "/<query>" or "@<query>". If the same query appeared later in the document, this could point to the wrong occurrence, causing applySlashAtRange/applyMentionAtRange to replace the wrong segment. Fix: serialize only the content up to the caret and find the trigger character within that truncated text, ensuring the offset matches the active selection. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Summary
Add
/command support in WelcomePage PromptComposer to quickly access Skills, Projects, and Code Agents. Users can now type/in the composer input to see a searchable dropdown with:Additionally, color-coded chips in the input editor for visual distinction:
The slash popover supports keyboard navigation (Arrow Up/Down, Enter) and "Show more" functionality for sections with more than 3 items.
Related Issue
Closes #102
Type of Change
Validation
just lintjust testjust fmtbun typecheck)Checklist
Generated with Devin
Summary by cubic
Adds a “/” quick command in the Welcome page composer for fast access to Skills, Projects, and Code Agents, with a searchable, keyboard-friendly popover. Implements the quick-access workflow requested in Linear #102.
New Features
/skill:token support.Bug Fixes
/or@token by using caret-scoped offsets, preventing wrong-segment replacements when duplicate queries exist.Written for commit 4501f96. Summary will update on new commits.
Summary by CodeRabbit
New Features
Updates
Documentation