feat: skills page and command#8
Conversation
|
Haven't really reviewed the code, but have used it for a while now |
|
👍 will look into it |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
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 full "Skills" subsystem: a server-side SkillsManager service and live layer (list/toggle/search/install/uninstall/readContent); new skills contracts and WebSocket RPC mappings; web-side React Query utilities and native API bindings; a /skills route with installed/marketplace/detail UI and sidebar entry; composer/editor support for skill chips and a new slash-skill trigger (parsing, UI, cursor/backspace handling); route/tree updates; tests and type updates across contracts, IPC, and WS integrations. 🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
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/ChatView.tsx (1)
1027-1034:⚠️ Potential issue | 🟡 MinorUse skills-specific menu copy here.
When the
/skillspicker is empty or still loading, this falls back to workspace/command copy, so the new flow looks broken instead of just empty.💡 Suggested copy
- {props.isLoading - ? "Searching workspace files..." - : props.triggerKind === "path" - ? "No matching files or folders." - : "No matching command."} + {props.isLoading + ? props.triggerKind === "slash-skill" + ? "Loading installed skills..." + : "Searching workspace files..." + : props.triggerKind === "path" + ? "No matching files or folders." + : props.triggerKind === "slash-skill" + ? "No matching enabled skills." + : "No matching command."}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/ChatView.tsx` around lines 1027 - 1034, The empty-state text in ChatView.tsx currently falls back to workspace/command copy; update the conditional that renders when props.items.length === 0 to handle props.triggerKind === "skill" explicitly: when props.triggerKind === "skill" show the skills-specific loading and empty copy (separate messages for props.isLoading true and false), otherwise keep the existing "path" and default command messages; modify the ternary/conditional around props.isLoading and props.triggerKind in the block that references props.items, props.isLoading, and props.triggerKind to include the new "skill" branch so the /skills picker shows appropriate copy.
🧹 Nitpick comments (3)
apps/web/src/components/Sidebar.tsx (1)
1103-1121: Simplify redundant active-state className logic.The
isActiveprop already setsdata-[active=true]which triggersdata-[active=true]:text-foreground. The conditional${pathname === "/skills" ? "" : "text-muted-foreground"}duplicates this logic. You can simplify by always includingtext-muted-foregroundas the base style and letting the data-attribute override it when active.♻️ Suggested simplification
<SidebarMenuButton render={<button type="button" />} isActive={pathname === "/skills"} size="sm" - className={`cursor-pointer gap-2 px-2 py-1.5 text-xs font-medium hover:bg-accent hover:text-foreground data-[active=true]:bg-accent data-[active=true]:text-foreground ${ - pathname === "/skills" ? "" : "text-muted-foreground" - }`} + className="cursor-pointer gap-2 px-2 py-1.5 text-xs font-medium text-muted-foreground hover:bg-accent hover:text-foreground data-[active=true]:bg-accent data-[active=true]:text-foreground" onClick={() => void navigate({ to: "/skills" })} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/Sidebar.tsx` around lines 1103 - 1121, The className logic on the SidebarMenuButton duplicates active-state handling; remove the conditional expression `${pathname === "/skills" ? "" : "text-muted-foreground"}` and instead always include "text-muted-foreground" in the base class string so the data-[active=true]:text-foreground rule (driven by isActive prop on SidebarMenuButton) overrides it when pathname === "/skills"; update the SidebarMenuButton instance (render prop on SidebarMenuButton, isActive prop, and its className) to reflect this simplified base style.packages/contracts/src/skills.ts (1)
39-44: Keep install counts numeric in the shared schema.Line 42 turns registry data into preformatted English text. That makes sorting and localization harder for every client consuming this contract.
💡 Suggested contract change
- installs: Schema.String, + installs: Schema.Number,Then have
apps/server/src/skills/SkillsManager.tsreturn the raws.installsvalue and let the UI render"123 installs".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/contracts/src/skills.ts` around lines 39 - 44, The shared schema currently defines SkillSearchResultEntry.installs as a String which forces preformatted text; change the schema so installs is a Number (update Schema.Struct to use Schema.Number for installs) and then update the producer code in apps/server/src/skills/SkillsManager.ts to return the raw numeric s.installs value (instead of a formatted "123 installs" string) so UIs can format, sort, and localize counts themselves.apps/server/src/skills/SkillsManager.ts (1)
203-217: Avoid reparsing the same config for every skill.Line 210 rereads each platform config every time this helper runs. Because
listSkills()callsresolveSkillAgents()inside the directory loop, one list request becomes N×M synchronous file parses as the skills directory grows.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/skills/SkillsManager.ts` around lines 203 - 217, resolveSkillAgents currently calls readSkillsConfigEntries for every platform on each skill, causing N×M reparses; instead pre-read each platform config once and reuse it. Modify the caller (listSkills) to build a Map from platform.configPath (or platform.name) to the entries returned by readSkillsConfigEntries for each SKILLS_PLATFORMS entry, then change resolveSkillAgents to accept that preloaded Map (or use a module-level cache keyed by configPath) and consult entries.get(skillMdPath) instead of calling readSkillsConfigEntries repeatedly; keep function names resolveSkillAgents, listSkills, readSkillsConfigEntries and constant SKILLS_PLATFORMS to locate changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/server/src/skills/SkillsManager.ts`:
- Around line 167-199: writeSkillConfigEntry currently reads and then
unconditionally writes configPath which can race with concurrent toggles; to
fix, serialize updates around configPath: acquire a per-config lock
(process-level mutex or file-based lock like creating/locking a .lock file) at
the start of the update, then re-read the file after acquiring the lock, apply
the existing regex cleanup and enabled/disabled append logic (use the same
cleaned variable approach), write changes to a temp file and atomically rename
it to configPath (or use fs.rename) to avoid partial writes, and finally release
the lock; reference the function writeSkillConfigEntry, the configPath variable,
the entryRe regex, and the writeFile calls that should be replaced by this
read-after-lock, temp-write, atomic-rename pattern.
- Around line 309-311: Add an abortable timeout to the server-side fetch to
prevent indefinite hangs: create an AbortController, pass controller.signal into
the existing fetch(url) call (the symbols involved are url, res, fetch), and set
a short timeout (e.g. 3–10s) that calls controller.abort(); ensure you clear the
timeout after the response arrives and convert aborts into a proper error path
(catch the aborted fetch and throw a descriptive Error or handle it the same way
you handle non-ok responses) so the UI spinner won't remain stuck.
- Around line 196-198: The TOML entry is written using skillMdPath directly,
which can produce invalid TOML on Windows due to unescaped backslashes; before
building entry in SkillsManager.ts (around the variables skillMdPath, entry,
cleaned, configPath) escape backslashes and any double quotes in skillMdPath
(e.g., replace "\" with "\\" and `"` with `\"`) or otherwise serialize it as a
TOML-safe basic string, then use that escaped value when constructing entry so
the written TOML remains valid cross-platform.
- Around line 382-383: The regex that strips YAML frontmatter in
SkillsManager.ts uses raw.replace(...) and only matches LF line endings, so
SKILL.md files with CRLF won't strip; update the frontmatter-stripping regex
used to produce content (the raw.replace call) to accept optional CR characters
by using \r?\n (or equivalent) for all newline matches (both after the opening
--- and before/after the closing ---) so CRLF and LF frontmatter are removed
correctly.
In `@apps/web/src/composer-editor-mentions.ts`:
- Line 15: The CHIP_TOKEN_REGEX currently uses a lookahead `(?=\s)` which
prevents matching chips at end-of-input; update the regex used in
composer-editor-mentions (CHIP_TOKEN_REGEX) so it also matches when the token is
at string end (e.g., change the lookahead to `(?=\s|$)` or equivalent) so that
splitPromptIntoComposerSegments() correctly recognizes trailing `@...` and
`$...` chips and they don't revert to plain text on re-render.
In `@apps/web/src/lib/skillsReactQuery.ts`:
- Around line 14-17: The current query/mutation options call ensureNativeApi()
(e.g., in the queryFn that calls api.skills.list()) which throws when the
desktop bridge is absent; wrap each use of ensureNativeApi()/api.skills.* with a
guard that detects whether the native bridge is available and return a safe
fallback instead of letting ensureNativeApi() throw. Concretely, before calling
ensureNativeApi() in the list/create/update/get/delete helpers (the occurrences
around the shown queryFn and the other ranges), check a bridge-available
predicate (or try/catch around ensureNativeApi()) and return an appropriate safe
response (e.g., Promise.resolve([]) for list queries, a resolved null/undefined
or a controlled error object for gets/mutations) so web surfaces gracefully
hide/disable skills rather than erroring.
In `@apps/web/src/routes/_chat.skills.tsx`:
- Around line 521-524: The loading check currently only compares
installMutation.variables?.source to result.source, causing cards with the same
source to all show as installing; update the isInstalling condition used in the
component (where installMutation is referenced and the row key uses
"source:name") to scope to the exact result by matching both source and name
(e.g., compare installMutation.variables?.source === result.source &&
installMutation.variables?.name === result.name or build and compare the
combined `${source}:${name}` key) so only the specific row with that source:name
shows the pending state.
---
Outside diff comments:
In `@apps/web/src/components/ChatView.tsx`:
- Around line 1027-1034: The empty-state text in ChatView.tsx currently falls
back to workspace/command copy; update the conditional that renders when
props.items.length === 0 to handle props.triggerKind === "skill" explicitly:
when props.triggerKind === "skill" show the skills-specific loading and empty
copy (separate messages for props.isLoading true and false), otherwise keep the
existing "path" and default command messages; modify the ternary/conditional
around props.isLoading and props.triggerKind in the block that references
props.items, props.isLoading, and props.triggerKind to include the new "skill"
branch so the /skills picker shows appropriate copy.
---
Nitpick comments:
In `@apps/server/src/skills/SkillsManager.ts`:
- Around line 203-217: resolveSkillAgents currently calls
readSkillsConfigEntries for every platform on each skill, causing N×M reparses;
instead pre-read each platform config once and reuse it. Modify the caller
(listSkills) to build a Map from platform.configPath (or platform.name) to the
entries returned by readSkillsConfigEntries for each SKILLS_PLATFORMS entry,
then change resolveSkillAgents to accept that preloaded Map (or use a
module-level cache keyed by configPath) and consult entries.get(skillMdPath)
instead of calling readSkillsConfigEntries repeatedly; keep function names
resolveSkillAgents, listSkills, readSkillsConfigEntries and constant
SKILLS_PLATFORMS to locate changes.
In `@apps/web/src/components/Sidebar.tsx`:
- Around line 1103-1121: The className logic on the SidebarMenuButton duplicates
active-state handling; remove the conditional expression `${pathname ===
"/skills" ? "" : "text-muted-foreground"}` and instead always include
"text-muted-foreground" in the base class string so the
data-[active=true]:text-foreground rule (driven by isActive prop on
SidebarMenuButton) overrides it when pathname === "/skills"; update the
SidebarMenuButton instance (render prop on SidebarMenuButton, isActive prop, and
its className) to reflect this simplified base style.
In `@packages/contracts/src/skills.ts`:
- Around line 39-44: The shared schema currently defines
SkillSearchResultEntry.installs as a String which forces preformatted text;
change the schema so installs is a Number (update Schema.Struct to use
Schema.Number for installs) and then update the producer code in
apps/server/src/skills/SkillsManager.ts to return the raw numeric s.installs
value (instead of a formatted "123 installs" string) so UIs can format, sort,
and localize counts themselves.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: e1322458-42c1-41e7-af96-03797efec904
📒 Files selected for processing (18)
apps/server/src/index.tsapps/server/src/main.test.tsapps/server/src/skills/SkillsManager.tsapps/server/src/wsServer.test.tsapps/server/src/wsServer.tsapps/web/src/components/ChatView.tsxapps/web/src/components/ComposerPromptEditor.tsxapps/web/src/components/Sidebar.tsxapps/web/src/composer-editor-mentions.tsapps/web/src/composer-logic.tsapps/web/src/lib/skillsReactQuery.tsapps/web/src/routeTree.gen.tsapps/web/src/routes/_chat.skills.tsxapps/web/src/wsNativeApi.tspackages/contracts/src/index.tspackages/contracts/src/ipc.tspackages/contracts/src/skills.tspackages/contracts/src/ws.ts
| queryFn: async () => { | ||
| const api = ensureNativeApi(); | ||
| return api.skills.list(); | ||
| }, |
There was a problem hiding this comment.
Make this shared skills client safe outside the native runtime.
Every helper here calls ensureNativeApi(), which throws when the desktop bridge is absent. Because these options are reused across web surfaces, a browser session falls straight into query/mutation errors instead of gracefully hiding or disabling skills. Please guard the bridge at this boundary or return a safe fallback before dispatching the request.
Also applies to: 26-29, 38-41, 53-55, 65-67, 77-79
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/lib/skillsReactQuery.ts` around lines 14 - 17, The current
query/mutation options call ensureNativeApi() (e.g., in the queryFn that calls
api.skills.list()) which throws when the desktop bridge is absent; wrap each use
of ensureNativeApi()/api.skills.* with a guard that detects whether the native
bridge is available and return a safe fallback instead of letting
ensureNativeApi() throw. Concretely, before calling ensureNativeApi() in the
list/create/update/get/delete helpers (the occurrences around the shown queryFn
and the other ranges), check a bridge-available predicate (or try/catch around
ensureNativeApi()) and return an appropriate safe response (e.g.,
Promise.resolve([]) for list queries, a resolved null/undefined or a controlled
error object for gets/mutations) so web surfaces gracefully hide/disable skills
rather than erroring.
- time out remote skill searches and store install counts as numbers for formatting - write skill config updates atomically and preserve CRLF/frontmatter parsing - recognize trailing @/$ mentions and refine skills UI loading and install states
|
Have adressed the review |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
apps/web/src/routes/_chat.skills.tsx (1)
98-114: Extract the repeated agent badge rendering.
SkillCardandSkillDetailViewduplicate the same icon/fallback loop. A small shared component will keep the platform mapping, fallback badge styling, and accessibility behavior from drifting.Also applies to: 216-232
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/routes/_chat.skills.tsx` around lines 98 - 114, Extract the duplicated agent icon/fallback loop into a small shared component (e.g., AgentBadges or AgentBadgeList) that accepts agents: string[]; inside it reuse PLATFORM_ICONS to render PlatformIcon when present and otherwise render the same fallback <span> with the exact className, key={agent} and aria-label behavior; then replace the inline map in SkillCard and SkillDetailView with <AgentBadges agents={skill.agents} /> and add appropriate prop typing (agents: string[]) and export so both usages (the block shown and the one around lines 216-232) use the shared component.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/server/src/skills/SkillsManager.ts`:
- Around line 371-388: installSkill (and similarly uninstallSkill) must validate
input.skillName before calling runCommand/npx to avoid CLI flag injection; call
the existing assertValidSkillSlug(input.skillName) at the start of installSkill
and in uninstallSkill, and if it throws return an Effect that resolves to a
failed SkillsInstallResult (success: false, message: error.message) instead of
invoking runCommand; ensure you place the validation before building
agentFlags/runCommand and mirror the same validation+error-to-result behavior
used in the function’s catch handler.
- Around line 177-182: The path regex currently stops at the first raw quote and
fails for escaped quotes; update the path parsing in SkillsManager.ts (the
pathMatch capture) to accept escaped characters inside the TOML basic string
(e.g. use a pattern that captures ((?:\\.|[^"\\])*) instead of (.+?)), then
continue to unescape sequences (backslashes and \" ) as you already do;
specifically modify the pathMatch assignment and keep the unescapedPath logic so
skills with escaped quotes round-trip correctly when using
escapeTomlBasicString().
In `@apps/web/src/routes/_chat.skills.tsx`:
- Around line 305-320: The effect is using raw search which allows trailing
spaces to bypass local matches; normalize the input by trimming before any
checks or setting debouncedSearch. Inside the useEffect that references search,
compute a trimmed value (e.g., const normalized = search.trim()), use
normalized.length for the length check, call setDebouncedSearch(normalized) in
the timeout, and clear timeouts via debounceRef as before; also ensure any
installed-skill filtering logic that reads debouncedSearch (or directly uses
search elsewhere, e.g., the installed-skill filter around the other block) uses
the trimmed value so comparisons use normalized strings.
- Around line 324-356: The three mutation handlers (handleToggle,
handleUninstall, handleInstall) lack onError callbacks so network/API failures
never surface to users; update each to pass an onError option to
toggleMutation.mutate, uninstallMutation.mutate, and installMutation.mutate
respectively that calls toastManager.add with an error toast (include a brief
message and the error.message) and, for uninstallMutation.onError and
installMutation.onError, ensure any UI state adjustments mirror success/error
paths (e.g., do not clear selectedSkillName unless success). Reference the
functions handleToggle, handleUninstall, handleInstall and the mutation objects
toggleMutation, uninstallMutation, installMutation and use toastManager.add to
display the error feedback.
---
Nitpick comments:
In `@apps/web/src/routes/_chat.skills.tsx`:
- Around line 98-114: Extract the duplicated agent icon/fallback loop into a
small shared component (e.g., AgentBadges or AgentBadgeList) that accepts
agents: string[]; inside it reuse PLATFORM_ICONS to render PlatformIcon when
present and otherwise render the same fallback <span> with the exact className,
key={agent} and aria-label behavior; then replace the inline map in SkillCard
and SkillDetailView with <AgentBadges agents={skill.agents} /> and add
appropriate prop typing (agents: string[]) and export so both usages (the block
shown and the one around lines 216-232) use the shared component.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6713f680-c501-4532-a90f-cf24c607c1c1
📒 Files selected for processing (7)
apps/server/src/skills/SkillsManager.tsapps/web/src/components/ChatView.tsxapps/web/src/components/Sidebar.tsxapps/web/src/composer-editor-mentions.test.tsapps/web/src/composer-editor-mentions.tsapps/web/src/routes/_chat.skills.tsxpackages/contracts/src/skills.ts
🚧 Files skipped from review as they are similar to previous changes (3)
- packages/contracts/src/skills.ts
- apps/web/src/components/Sidebar.tsx
- apps/web/src/composer-editor-mentions.ts
- validate skill slugs before install/uninstall and return clear errors - parse escaped TOML skill paths correctly when reading config - trim search input, surface mutation failures, and reuse agent badge rendering
There was a problem hiding this comment.
🧹 Nitpick comments (2)
apps/server/src/skills/SkillsManager.ts (2)
280-287: Consider using async file read for consistency.The code uses synchronous
fs.readFileSyncinside an async function whilefsp.readdiris used asynchronously earlier. For consistency and to avoid blocking the event loop when many skills are installed, consider using the async version.♻️ Suggested refactor
let content = ""; if (skillMdPath) { try { - content = fs.readFileSync(skillMdPath, "utf-8"); + content = await fsp.readFile(skillMdPath, "utf-8"); } catch { // Couldn't read SKILL.md } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/skills/SkillsManager.ts` around lines 280 - 287, The code in SkillsManager.ts currently uses synchronous fs.readFileSync to read SKILL.md into the local variable content (when skillMdPath exists), which blocks the event loop; replace that sync call with the async API used elsewhere (e.g., await fsp.readFile(skillMdPath, "utf-8")) inside the same try/catch so content gets assigned the awaited result, and keep the catch to handle read failures; ensure fsp (fs.promises) is imported/used consistently with the earlier fsp.readdir usage.
206-211: ReDoS risk is mitigated but worth noting.The static analysis flags regex construction from variable input. However, the
escapedPathis derived from a filesystem path that's first TOML-escaped, then regex-escaped withreplace(/[.*+?^${}()|[\]\\]/g, "\\$&"), which escapes all regex metacharacters. Combined withassertValidSkillSlugvalidation on input skill names, the ReDoS risk is acceptably low.If stricter security is needed in the future, consider string-based matching instead of regex.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/skills/SkillsManager.ts` around lines 206 - 211, The code builds a RegExp from variable input (escapedPath) in SkillsManager (see escapeTomlBasicString, tomlSafeSkillMdPath, escapedPath, entryRe), which static analysis flagged for ReDoS; to fix, stop constructing a regex from untrusted/variable content: instead build a literal TOML snippet using tomlSafeSkillMdPath and perform a safe string search (String.prototype.includes or indexOf) to locate `[[skills.config]]\npath = "..."` entries, or if you must use a regex, enforce input validation via assertValidSkillSlug and a strict max-length check before escaping with a dedicated escapeRegExp utility so the RegExp constructor only receives safe, size-limited content.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/server/src/skills/SkillsManager.ts`:
- Around line 280-287: The code in SkillsManager.ts currently uses synchronous
fs.readFileSync to read SKILL.md into the local variable content (when
skillMdPath exists), which blocks the event loop; replace that sync call with
the async API used elsewhere (e.g., await fsp.readFile(skillMdPath, "utf-8"))
inside the same try/catch so content gets assigned the awaited result, and keep
the catch to handle read failures; ensure fsp (fs.promises) is imported/used
consistently with the earlier fsp.readdir usage.
- Around line 206-211: The code builds a RegExp from variable input
(escapedPath) in SkillsManager (see escapeTomlBasicString, tomlSafeSkillMdPath,
escapedPath, entryRe), which static analysis flagged for ReDoS; to fix, stop
constructing a regex from untrusted/variable content: instead build a literal
TOML snippet using tomlSafeSkillMdPath and perform a safe string search
(String.prototype.includes or indexOf) to locate `[[skills.config]]\npath =
"..."` entries, or if you must use a regex, enforce input validation via
assertValidSkillSlug and a strict max-length check before escaping with a
dedicated escapeRegExp utility so the RegExp constructor only receives safe,
size-limited content.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: bb099942-72dd-4487-bd67-10b864160183
📒 Files selected for processing (2)
apps/server/src/skills/SkillsManager.tsapps/web/src/routes/_chat.skills.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/src/routes/_chat.skills.tsx
|
👍 will look into it in a bit |
|
Thanks! |
|
@brrock can you fix these conflicts i merged upstream to main |
|
Yep |
There was a problem hiding this comment.
Actionable comments posted: 1
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/ComposerPromptEditor.tsx (1)
896-909:⚠️ Potential issue | 🔴 Critical
ComposerSkillNodeis not registered in Lexical's node configuration.The
nodesarray only includesComposerMentionNode, but the editor now also usesComposerSkillNode. Lexical requires all custom node types to be registered for serialization, deserialization, and copy/paste to work correctly. Without this registration, skill chips will fail to serialize and may cause runtime errors.Proposed fix
const initialConfig = useMemo<InitialConfigType>( () => ({ namespace: "t3tools-composer-editor", editable: true, - nodes: [ComposerMentionNode], + nodes: [ComposerMentionNode, ComposerSkillNode], editorState: () => { $setComposerEditorPrompt(initialValueRef.current); }, onError: (error) => { throw error; }, }), [], );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/ComposerPromptEditor.tsx` around lines 896 - 909, initialConfig's nodes array only registers ComposerMentionNode but not ComposerSkillNode, so Lexical won't serialize/deserialize or handle copy/paste for skill chips; update the useMemo that builds initialConfig (the InitialConfigType returned by initialConfig in the useMemo) to include ComposerSkillNode alongside ComposerMentionNode in the nodes array so both custom node types (ComposerMentionNode and ComposerSkillNode) are registered with the editor config and available for serialization/deserialization and runtime use.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/src/components/ChatView.tsx`:
- Around line 1814-1815: The skills query wired in ChatView (isSkillTrigger ->
skillsQuery using skillsListQueryOptions) calls ensureNativeApi (from
skillsReactQuery.ts) which breaks in the browser; fix by using a web-safe query
or guarding the query: either replace usage of skillsListQueryOptions with a
web-transport-aware variant provided by the apps/web layer, or change the
enabled flag to isSkillTrigger && ensureNativeApi() (or an apps/web-provided
isNativeAvailable flag) so the query never runs in non-native builds; reference
symbols: isSkillTrigger, skillsQuery, skillsListQueryOptions, and
ensureNativeApi to locate the code to update.
---
Outside diff comments:
In `@apps/web/src/components/ComposerPromptEditor.tsx`:
- Around line 896-909: initialConfig's nodes array only registers
ComposerMentionNode but not ComposerSkillNode, so Lexical won't
serialize/deserialize or handle copy/paste for skill chips; update the useMemo
that builds initialConfig (the InitialConfigType returned by initialConfig in
the useMemo) to include ComposerSkillNode alongside ComposerMentionNode in the
nodes array so both custom node types (ComposerMentionNode and
ComposerSkillNode) are registered with the editor config and available for
serialization/deserialization and runtime use.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1c41ccda-2742-4716-81d4-841e1bccc81a
📒 Files selected for processing (9)
apps/server/src/wsServer.test.tsapps/server/src/wsServer.tsapps/web/src/components/ChatView.tsxapps/web/src/components/ComposerPromptEditor.tsxapps/web/src/components/Sidebar.tsxapps/web/src/composer-logic.tsapps/web/src/wsNativeApi.tspackages/contracts/src/ipc.tspackages/contracts/src/ws.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- apps/server/src/wsServer.test.ts
- apps/web/src/wsNativeApi.ts
- apps/server/src/wsServer.ts
- apps/web/src/components/Sidebar.tsx
- toggle skills by moving their directories between enabled and disabled roots - only fetch skills in Electron and register ComposerSkillNode in the editor
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
apps/web/src/components/ChatView.tsx (1)
1814-1815:⚠️ Potential issue | 🟠 MajorHide
/skillsoutside Electron or switch this to a web-safe query.The native guard avoids the old crash, but
/skillsis still advertised in the browser whileskillsQuerycan never populate there. Inapps/web, selecting/skillsnow drops users into a permanent empty picker instead of a working flow. Either back this with the web transport or only surface the command when skills are actually available.✂️ Minimal safe fix
- { - id: "slash:skills", - type: "slash-command", - command: "skills", - label: "/skills", - description: "Use a skill in this message", - }, + ...(isElectron + ? [ + { + id: "slash:skills", + type: "slash-command" as const, + command: "skills", + label: "/skills", + description: "Use a skill in this message", + }, + ] + : []),As per coding guidelines, "Use
apps/webfor React/Vite UI that owns session UX, conversation/event rendering, and client-side state, connecting to server via WebSocket".Also applies to: 1861-1867
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/ChatView.tsx` around lines 1814 - 1815, The UI currently shows the `/skills` command even when skillsQuery (created via skillsListQueryOptions() in the skillsQuery variable) can never run outside Electron; update the code that determines visibility and data fetching so `/skills` only appears when skills are actually available: either change the enabled flag on skillsQuery to "enabled: isSkillTrigger && isElectron" (preserving the existing isElectron guard) and hide/disable the command when isElectron is false, or implement a web-safe transport for skillsQuery so it can run in apps/web; specifically adjust the logic around composerTriggerKind/isSkillTrigger and skillsQuery (and any UI that lists the `/skills` command) so the command is not surfaced in the browser unless skillsQuery can populate.
🧹 Nitpick comments (1)
apps/server/src/skills/SkillsManager.ts (1)
200-206: Prefer async file read for server resilience.Using synchronous
fs.readFileSyncinside an async context can block the event loop. While SKILL.md files are typically small, usingfsp.readFilewould be more consistent with the server's need for predictable behavior under load.♻️ Proposed fix
let content = ""; if (skillMdPath) { try { - content = fs.readFileSync(skillMdPath, "utf-8"); + content = await fsp.readFile(skillMdPath, "utf-8"); } catch { // Couldn't read SKILL.md } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/skills/SkillsManager.ts` around lines 200 - 206, Replace the blocking fs.readFileSync call in the SkillsManager flow with the async promise-based read to avoid blocking the event loop: where skillMdPath is checked and content is assigned, use fs.promises.readFile (or imported fsp.readFile) with await inside the existing try/catch so content = await fs.promises.readFile(skillMdPath, "utf-8") (or equivalent), preserving the error handling path; ensure the surrounding function is async or properly awaits this operation so the rest of SkillsManager (references to content and SKILL.md processing) continues to work correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/server/src/skills/SkillsManager.ts`:
- Around line 351-358: The Effect.catch usage in the uninstall pipeline (same
pattern as in installSkill) is likely invalid; update the error handler to use
the correct Effect error combinator (use Effect.catchAll instead of
Effect.catch) in the pipeline where the anonymous error handler is registered,
e.g., the uninstall pipeline that returns a SkillsUninstallResult, and keep the
handler signature accepting the error (typed as SkillsError) and returning
Effect.succeed({...} satisfies SkillsUninstallResult); mirror the same fix you
applied for installSkill so the uninstall error path compiles and behaves
consistently.
---
Duplicate comments:
In `@apps/web/src/components/ChatView.tsx`:
- Around line 1814-1815: The UI currently shows the `/skills` command even when
skillsQuery (created via skillsListQueryOptions() in the skillsQuery variable)
can never run outside Electron; update the code that determines visibility and
data fetching so `/skills` only appears when skills are actually available:
either change the enabled flag on skillsQuery to "enabled: isSkillTrigger &&
isElectron" (preserving the existing isElectron guard) and hide/disable the
command when isElectron is false, or implement a web-safe transport for
skillsQuery so it can run in apps/web; specifically adjust the logic around
composerTriggerKind/isSkillTrigger and skillsQuery (and any UI that lists the
`/skills` command) so the command is not surfaced in the browser unless
skillsQuery can populate.
---
Nitpick comments:
In `@apps/server/src/skills/SkillsManager.ts`:
- Around line 200-206: Replace the blocking fs.readFileSync call in the
SkillsManager flow with the async promise-based read to avoid blocking the event
loop: where skillMdPath is checked and content is assigned, use
fs.promises.readFile (or imported fsp.readFile) with await inside the existing
try/catch so content = await fs.promises.readFile(skillMdPath, "utf-8") (or
equivalent), preserving the error handling path; ensure the surrounding function
is async or properly awaits this operation so the rest of SkillsManager
(references to content and SKILL.md processing) continues to work correctly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 663c4fcd-1eb9-4bb1-8a64-cd28101e13f6
📒 Files selected for processing (3)
apps/server/src/skills/SkillsManager.tsapps/web/src/components/ChatView.tsxapps/web/src/components/ComposerPromptEditor.tsx
- only show the /skills slash command in Electron chat views - switch skill markdown loading to async file reads in SkillsManager
|
@zortos293 mind having a look |
|
@CodeRabbit review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
apps/server/src/skills/SkillsManager.ts (1)
311-316:⚠️ Potential issue | 🟠 MajorUse the Effect recovery combinator that actually exists for this version.
These handlers are recovering
SkillsErrorinto successful result objects, butEffect.catch(...)does not match the usual Effect API surface here. If this repo is on the common v3 API, both pipelines will failbun typecheck.Suggested fix
- Effect.catch((error: SkillsError) => + Effect.catchAll((error: SkillsError) => Effect.succeed({ success: false, message: error.message, } satisfies SkillsInstallResult), ), @@ - Effect.catch((error: SkillsError) => + Effect.catchAll((error: SkillsError) => Effect.succeed({ success: false, message: error.message, } satisfies SkillsUninstallResult), ),Does the Effect TypeScript library expose `Effect.catch(...)` as a valid combinator in current versions, or should recovery here use `Effect.catchAll(...)` / `Effect.catchTag(...)` instead?As per coding guidelines, "Both
bun lintandbun typecheckmust pass before considering tasks completed".Also applies to: 352-357
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/server/src/skills/SkillsManager.ts` around lines 311 - 316, The recovery currently uses a non-existent Effect.catch combinator; replace it with the correct Effect recovery combinator for this codepath—use Effect.catchAll to handle any error and map it to Effect.succeed({ success: false, message: error.message } as SkillsInstallResult), or if you need to narrow by error type use Effect.catchTag("SkillsError", ...). Update both occurrences around the SkillsManager handlers (the Effect pipeline that returns SkillsInstallResult at the shown sites) so the TypeScript types align and bun typecheck/lint pass.
🧹 Nitpick comments (2)
apps/web/src/components/ComposerPromptEditor.tsx (2)
578-642: Plugin names reference "Mention" but now handle both chip types.
ComposerMentionArrowPlugin,ComposerMentionSelectionNormalizePlugin, andComposerMentionBackspacePluginall handle skills now too. Consider renaming toComposerChip*Pluginfor clarity. This is a minor naming nit — the behavior is correct.Also applies to: 644-670, 672-731
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/ComposerPromptEditor.tsx` around lines 578 - 642, The plugin names still use "Mention" but the logic also handles "skill" chips; rename ComposerMentionArrowPlugin, ComposerMentionSelectionNormalizePlugin, and ComposerMentionBackspacePlugin to ComposerChipArrowPlugin, ComposerChipSelectionNormalizePlugin, and ComposerChipBackspacePlugin respectively (update their function declarations and any imports/exports/usages) so names accurately reflect handling of both chip types and ensure all references in the file (and other modules) are updated to the new identifiers.
685-694: UseisComposerChipNodehelper for consistency.This local function uses direct
instanceofchecks while the rest of the codebase now relies onisComposerChipNode. If a third chip type is added, this spot could be missed. The function nameremoveMentionNodeis also now misleading since it handles skills too.♻️ Proposed refactor for consistency
- const removeMentionNode = (candidate: unknown): boolean => { - if (!((candidate instanceof ComposerMentionNode) || (candidate instanceof ComposerSkillNode))) { + const removeChipNode = (candidate: unknown): boolean => { + if (!(candidate instanceof LexicalNode) || !isComposerChipNode(candidate)) { return false; } - const mentionStart = getAbsoluteOffsetForPoint(candidate, 0); + const chipStart = getAbsoluteOffsetForPoint(candidate, 0); candidate.remove(); - $setSelectionAtComposerOffset(mentionStart); + $setSelectionAtComposerOffset(chipStart); event?.preventDefault(); return true; }; - if (removeMentionNode(anchorNode)) { + if (removeChipNode(anchorNode)) { return true; }Then update the remaining calls to
removeChipNodeat lines 704, 710, and 719.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/src/components/ComposerPromptEditor.tsx` around lines 685 - 694, The local function removeMentionNode should be replaced with a consistent helper that uses isComposerChipNode instead of instanceof checks and be renamed to removeChipNode; keep the existing logic of computing mentionStart via getAbsoluteOffsetForPoint(candidate, 0), calling candidate.remove(), restoring selection with $setSelectionAtComposerOffset(mentionStart) and event?.preventDefault(), and return true/false as before. Update all callers that referenced removeMentionNode to call removeChipNode so the chip-typing logic (ComposerMentionNode/ComposerSkillNode and any future chip types) is handled via isComposerChipNode uniformly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/server/src/skills/SkillsManager.ts`:
- Around line 302-305: The code passes raw input.source into runCommand invoking
"npx" "skills add" which lets values like "--help" be treated as flags; validate
and sanitize input.source before calling runCommand: in SkillsManager (where
input.source and runCommand are used), implement a whitelist/regex check for
allowed source patterns (owner/repo, HTTP/HTTPS/git URLs, absolute/local file
paths) and reject or normalize any value that looks like an option (starts with
"-" ) or fails the patterns; if supporting local paths, normalize them to safe
forms (e.g., resolve to absolute path) or explicitly prefix with "./" when
appropriate, and return/throw a clear validation error instead of invoking
runCommand with an unsafe source.
---
Duplicate comments:
In `@apps/server/src/skills/SkillsManager.ts`:
- Around line 311-316: The recovery currently uses a non-existent Effect.catch
combinator; replace it with the correct Effect recovery combinator for this
codepath—use Effect.catchAll to handle any error and map it to Effect.succeed({
success: false, message: error.message } as SkillsInstallResult), or if you need
to narrow by error type use Effect.catchTag("SkillsError", ...). Update both
occurrences around the SkillsManager handlers (the Effect pipeline that returns
SkillsInstallResult at the shown sites) so the TypeScript types align and bun
typecheck/lint pass.
---
Nitpick comments:
In `@apps/web/src/components/ComposerPromptEditor.tsx`:
- Around line 578-642: The plugin names still use "Mention" but the logic also
handles "skill" chips; rename ComposerMentionArrowPlugin,
ComposerMentionSelectionNormalizePlugin, and ComposerMentionBackspacePlugin to
ComposerChipArrowPlugin, ComposerChipSelectionNormalizePlugin, and
ComposerChipBackspacePlugin respectively (update their function declarations and
any imports/exports/usages) so names accurately reflect handling of both chip
types and ensure all references in the file (and other modules) are updated to
the new identifiers.
- Around line 685-694: The local function removeMentionNode should be replaced
with a consistent helper that uses isComposerChipNode instead of instanceof
checks and be renamed to removeChipNode; keep the existing logic of computing
mentionStart via getAbsoluteOffsetForPoint(candidate, 0), calling
candidate.remove(), restoring selection with
$setSelectionAtComposerOffset(mentionStart) and event?.preventDefault(), and
return true/false as before. Update all callers that referenced
removeMentionNode to call removeChipNode so the chip-typing logic
(ComposerMentionNode/ComposerSkillNode and any future chip types) is handled via
isComposerChipNode uniformly.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: e4b489ee-6d8e-410f-8813-0866c75c5d75
📒 Files selected for processing (3)
apps/server/src/skills/SkillsManager.tsapps/web/src/components/ChatView.tsxapps/web/src/components/ComposerPromptEditor.tsx
|
pulling changes from upstream then testing this build |
|
Alr could be merge confs tho |
|
ok lets see |
|
Yes the codes logo is hard coded it fully works for me, I'll remove the copilot logo |
- Remove OpenAI import from Icons - Remove Codex from PLATFORM_ICONS mapping
This reverts commit 30f7c8e.
|
Imma test it out |
|
Tested and working |
|
https://github.com/zortos293/t3code-copilot/actions/runs/22941586137/job/66591475498?pr=8 can you fix these ci issues? so it merges correctly and builds correctly |
|
Yes ofc I'll do in a min |
|
@zortos293 conflicts resolved and formatted, ready to merge |
|
Thanks! |

Adds a nice skills page, sourced an early pr in the t3code repo that got closed (294), have merged and run typecheck.
Summary by CodeRabbit
New Features
Composer
UI
Tests
Chores