Skip to content

feat: skills page and command#8

Merged
zortos293 merged 12 commits intozortos293:mainfrom
brrock:skills
Mar 11, 2026
Merged

feat: skills page and command#8
zortos293 merged 12 commits intozortos293:mainfrom
brrock:skills

Conversation

@brrock
Copy link
Copy Markdown
Contributor

@brrock brrock commented Mar 9, 2026

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

    • Full Skills management: list, online search, install, uninstall, enable/disable, and view skill content via app UI and native/WebSocket APIs.
  • Composer

    • /skills slash command, skill chips in the editor, prompt insertion, tokenization, and command menu items.
  • UI

    • New Skills route with search, detail view, install/uninstall/toggle controls and sidebar entry.
  • Tests

    • Updated tokenization and runtime tests to cover skills flows.
  • Chores

    • Added client query helpers and public contract schemas for skills.

@brrock
Copy link
Copy Markdown
Contributor Author

brrock commented Mar 9, 2026

Haven't really reviewed the code, but have used it for a while now

@zortos293
Copy link
Copy Markdown
Owner

👍 will look into it

@zortos293
Copy link
Copy Markdown
Owner

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 9, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 9, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds 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)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is largely incomplete. It lacks detailed explanations of what changed, why it was implemented this way, and does not follow the repository's template structure (What Changed, Why, UI Changes sections). Expand the description to include: (1) a clear 'What Changed' section detailing the main changes, (2) a 'Why' section explaining the problem and approach, and (3) a 'UI Changes' section with screenshots if applicable.
Docstring Coverage ⚠️ Warning Docstring coverage is 22.81% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: skills page and command' clearly and concisely describes the main change—adding a new skills page feature and associated command functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 | 🟡 Minor

Use skills-specific menu copy here.

When the /skills picker 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 isActive prop already sets data-[active=true] which triggers data-[active=true]:text-foreground. The conditional ${pathname === "/skills" ? "" : "text-muted-foreground"} duplicates this logic. You can simplify by always including text-muted-foreground as 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.ts return the raw s.installs value 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() calls resolveSkillAgents() 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

📥 Commits

Reviewing files that changed from the base of the PR and between c935657 and d9825fb.

📒 Files selected for processing (18)
  • apps/server/src/index.ts
  • apps/server/src/main.test.ts
  • apps/server/src/skills/SkillsManager.ts
  • apps/server/src/wsServer.test.ts
  • apps/server/src/wsServer.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/ComposerPromptEditor.tsx
  • apps/web/src/components/Sidebar.tsx
  • apps/web/src/composer-editor-mentions.ts
  • apps/web/src/composer-logic.ts
  • apps/web/src/lib/skillsReactQuery.ts
  • apps/web/src/routeTree.gen.ts
  • apps/web/src/routes/_chat.skills.tsx
  • apps/web/src/wsNativeApi.ts
  • packages/contracts/src/index.ts
  • packages/contracts/src/ipc.ts
  • packages/contracts/src/skills.ts
  • packages/contracts/src/ws.ts

Comment on lines +14 to +17
queryFn: async () => {
const api = ensureNativeApi();
return api.skills.list();
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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
@brrock
Copy link
Copy Markdown
Contributor Author

brrock commented Mar 10, 2026

Have adressed the review

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
apps/web/src/routes/_chat.skills.tsx (1)

98-114: Extract the repeated agent badge rendering.

SkillCard and SkillDetailView duplicate 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

📥 Commits

Reviewing files that changed from the base of the PR and between d9825fb and 3bfb78d.

📒 Files selected for processing (7)
  • apps/server/src/skills/SkillsManager.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/Sidebar.tsx
  • apps/web/src/composer-editor-mentions.test.ts
  • apps/web/src/composer-editor-mentions.ts
  • apps/web/src/routes/_chat.skills.tsx
  • packages/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
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/server/src/skills/SkillsManager.ts (2)

280-287: Consider using async file read for consistency.

The code uses synchronous fs.readFileSync inside an async function while fsp.readdir is 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 escapedPath is derived from a filesystem path that's first TOML-escaped, then regex-escaped with replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), which escapes all regex metacharacters. Combined with assertValidSkillSlug validation 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3bfb78d and 493cdbe.

📒 Files selected for processing (2)
  • apps/server/src/skills/SkillsManager.ts
  • apps/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

@brrock
Copy link
Copy Markdown
Contributor Author

brrock commented Mar 10, 2026

@zortos293

@zortos293
Copy link
Copy Markdown
Owner

👍 will look into it in a bit

@brrock
Copy link
Copy Markdown
Contributor Author

brrock commented Mar 10, 2026

Thanks!

@zortos293
Copy link
Copy Markdown
Owner

@brrock can you fix these conflicts i merged upstream to main

@brrock
Copy link
Copy Markdown
Contributor Author

brrock commented Mar 10, 2026

Yep

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

ComposerSkillNode is not registered in Lexical's node configuration.

The nodes array only includes ComposerMentionNode, but the editor now also uses ComposerSkillNode. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 493cdbe and fb6b347.

📒 Files selected for processing (9)
  • apps/server/src/wsServer.test.ts
  • apps/server/src/wsServer.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/ComposerPromptEditor.tsx
  • apps/web/src/components/Sidebar.tsx
  • apps/web/src/composer-logic.ts
  • apps/web/src/wsNativeApi.ts
  • packages/contracts/src/ipc.ts
  • packages/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
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/web/src/components/ChatView.tsx (1)

1814-1815: ⚠️ Potential issue | 🟠 Major

Hide /skills outside Electron or switch this to a web-safe query.

The native guard avoids the old crash, but /skills is still advertised in the browser while skillsQuery can never populate there. In apps/web, selecting /skills now 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/web for 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.readFileSync inside an async context can block the event loop. While SKILL.md files are typically small, using fsp.readFile would 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

📥 Commits

Reviewing files that changed from the base of the PR and between fb6b347 and af7caa7.

📒 Files selected for processing (3)
  • apps/server/src/skills/SkillsManager.ts
  • apps/web/src/components/ChatView.tsx
  • apps/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
@brrock
Copy link
Copy Markdown
Contributor Author

brrock commented Mar 10, 2026

@zortos293 mind having a look

@brrock
Copy link
Copy Markdown
Contributor Author

brrock commented Mar 10, 2026

@CodeRabbit review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 10, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
apps/server/src/skills/SkillsManager.ts (1)

311-316: ⚠️ Potential issue | 🟠 Major

Use the Effect recovery combinator that actually exists for this version.

These handlers are recovering SkillsError into successful result objects, but Effect.catch(...) does not match the usual Effect API surface here. If this repo is on the common v3 API, both pipelines will fail bun 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 lint and bun typecheck must 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, and ComposerMentionBackspacePlugin all handle skills now too. Consider renaming to ComposerChip*Plugin for 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: Use isComposerChipNode helper for consistency.

This local function uses direct instanceof checks while the rest of the codebase now relies on isComposerChipNode. If a third chip type is added, this spot could be missed. The function name removeMentionNode is 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 removeChipNode at 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

📥 Commits

Reviewing files that changed from the base of the PR and between fb6b347 and 7deb527.

📒 Files selected for processing (3)
  • apps/server/src/skills/SkillsManager.ts
  • apps/web/src/components/ChatView.tsx
  • apps/web/src/components/ComposerPromptEditor.tsx

@zortos293
Copy link
Copy Markdown
Owner

pulling changes from upstream then testing this build

@brrock
Copy link
Copy Markdown
Contributor Author

brrock commented Mar 10, 2026

Alr could be merge confs tho

@zortos293
Copy link
Copy Markdown
Owner

ok lets see

@zortos293
Copy link
Copy Markdown
Owner

image does it detect copilot skills?

@brrock
Copy link
Copy Markdown
Contributor Author

brrock commented Mar 11, 2026

Yes the codes logo is hard coded it fully works for me, I'll remove the copilot logo

@zortos293
Copy link
Copy Markdown
Owner

Imma test it out

@zortos293
Copy link
Copy Markdown
Owner

Tested and working

@zortos293
Copy link
Copy Markdown
Owner

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

@brrock
Copy link
Copy Markdown
Contributor Author

brrock commented Mar 11, 2026

Yes ofc I'll do in a min

@brrock
Copy link
Copy Markdown
Contributor Author

brrock commented Mar 11, 2026

@zortos293 conflicts resolved and formatted, ready to merge

@zortos293 zortos293 merged commit 59134a6 into zortos293:main Mar 11, 2026
1 check passed
@brrock
Copy link
Copy Markdown
Contributor Author

brrock commented Mar 12, 2026

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants