Jminnetian/askskills#1410
Conversation
… for the organization
Any org member can create a skill into the shared workspace catalog; members opt in by adopting it, while only owners can feature or auto-enroll a skill for everyone. Personal and org skills may share a slug since commands resolve by source and id, so duplicates from different sources coexist.
The Ask model can now discover opt-in skills from an <agent_skills> catalog in the system prompt and load their instructions on demand through a single load_skill tool, reusing the existing auth-scope helpers and argument substitution. Auto-invocation is gated per skill by a new autoInvocationEnabled flag and a global env kill-switch; manual slash-command invocation is unchanged.
Surface load_skill tool calls in the Ask details card via a new tool-part type and component so users can see which skill the model loaded and its instructions. Also folds in review hardening: sanitize every skill catalog field injected into the system prompt, make load_skill fail closed on transient lookup errors, and consolidate the manual and auto skill catalogs onto one shared query helper. Includes the migration for the autoInvocationEnabled column.
… personal skills Skills are now always model-invocable: removed the per-skill autoInvocationEnabled flag, its UI toggle, and the env kill-switch. Skills no longer take arguments, so the substitution engine and argumentNames are gone. Personal skills are bound to the current org, and the ORG visibility is renamed to SHARED across schema, server, and UI. The five skill migrations are collapsed into one.
Extract personal and shared skill management out of the account Ask Sourcebot settings into a routed /settings/skills two-pane page with inline editing. The account page now manages connectors only; existing skill actions are reused, with the shared catalog surfacing instructions and creator email for the detail view.
Make the instructions editor flush with the name/command bars, let split view use the full pane, and keep the view toggle and counter stationary across mode switches. Replace the misleading Shared toggle on other members' shared skills with an enable toggle in the list, color the Auto/Featured badges instead of using icons, add a Skills entry to the chat + menu, and stop the table-of-contents focus ring from lingering after a click.
Drop the `featured` flag entirely from agent skills: the schema column and shared-catalog index, the server selects/sort/flag-input schema, the Featured badge and admin toggle, and all related tests. Shared skills now sort by recency then name; the remaining auto-enroll flag is unchanged. Includes a migration dropping the column.
The Ask Sourcebot tooltip stayed permanently visible on the chat landing page because the search mode tooltips force their open state from the current mode. The recent tooltip Portal change let that force-open content escape the dropdown's DOM subtree. Gate the tooltips on the Select's open state so they only render while the dropdown is open.
The Skills page '+' now opens a menu offering manual create or import from markdown. Importing parses front matter for name, command, and description and the body for instructions, then drops into the create form pre-populated for review before saving.
Adds an 'Import from repository' option to the Skills page '+' menu that browses your indexed repos, filters to markdown files, and populates the create form from the selected file's front matter and body. Reuses the existing markdown parser and create flow.
Import a skill from an indexed repository file; its description and instructions stay synced to the file (refreshed via Update from source, keyed on the git blob OID) while name and command stay editable local labels. Synced skills can be published to the org and stay synced, and are exposed only to users who can access the source repo — across the command catalog, instruction materialization, load_skill, and the shared catalog.
Replace the workspace shared-skills card list with a searchable, filterable table (Synced/Manual filter, Added by, Auto toggle), backed by source and creator fields on the management item. Sharing a synced skill now goes through a confirmation dialog warning about owner/member repo visibility, shown only when permission syncing is on. Add cross-navigation between the account and admin skill views, and drop the redundant Make personal menu item in favor of the Shared toggle.
Remove the unused OrgSkillFlagToggle export (and its now-unused Switch import), which had no consumers after the shared-skills admin table redesign. Simplify the delete button's onClick, which is already disabled while a flag mutation is pending.
WalkthroughThis PR introduces "Ask Sourcebot Skills" — a full feature enabling personal and shared reusable chat instructions with database models, CRUD/publish/adopt server actions, an auto-invocation ChangesAgent Skills Feature
Estimated code review effort: 5 (Critical) | ~150 minutes Sequence Diagram(s)sequenceDiagram
participant Model
participant AgentStream as createAgentStream
participant LoadSkillTool as load_skill tool
participant Registry as resolveAutoInvocableSkill
participant Prisma
AgentStream->>AgentStream: buildSkillRegistry(userId, orgId)
AgentStream->>Model: prompt includes agent_skills catalog
Model->>LoadSkillTool: call load_skill(skill_id)
LoadSkillTool->>Registry: resolveAutoInvocableSkill(skillId)
Registry->>Prisma: agentSkill.findFirst (visibility scope)
Prisma-->>Registry: skill row or null
Registry-->>LoadSkillTool: ResolvedAutoInvocableSkill or null
LoadSkillTool-->>Model: instructions or generic error
sequenceDiagram
participant ChatRoute as ee/chat route
participant Materializer as materializeCommandMessageTexts
participant Prisma
participant Stream as createMessageStream
ChatRoute->>Materializer: messages, prisma, userId, orgId
Materializer->>Prisma: agentSkill.findMany (command ids)
Prisma-->>Materializer: matching skill instructions
Materializer->>Materializer: inject expandedText and data-source parts
Materializer-->>ChatRoute: messagesWithMaterializedCommands
ChatRoute->>Stream: createMessageStream(materialized messages)
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
License Audit
Weak Copyleft Packages (informational)
Resolved Packages (12)
|
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/web/src/app/(app)/settings/layout.tsx (1)
86-99: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winNew "Skills" nav item is unreachable for exactly the users it's meant to reach.
The new "Skills" entry (and the pre-existing "Ask Sourcebot" entry) is only added to the Account nav group when
hasAskEntitlementis true — non-entitled orgs get no entry at all. ButSkillsEntitlementMessage(rendered by/settings/skillswhen!hasEntitlement('ask')) is explicitly built as "the upsell surface for free-plan users" so it can render for free-plan users. Since the sidebar never surfaces/settings/skillsfor such users, and the deep-link source (workspace admin table) is likewise gated behindisAskAgentAvailable, this upsell page is only reachable by manually typing the URL — defeating its purpose.Compare with the Workspace group's "Ask Sourcebot" item (line 133-137 below), which stays visible with
requiredEntitlement: 'ask'so the Nav component can show an upgrade badge instead of hiding the link entirely. Consider applying the same pattern to "Skills" so free-plan users can actually discover and reach the upsell.♻️ Proposed fix
+ { + title: "Skills", + href: `/settings/skills`, + hrefRegex: `/settings/skills(/.*)?$`, + icon: "sparkles" as const, + requiredEntitlement: 'ask', + }, ...(hasAskEntitlement ? [ { title: "Ask Sourcebot", href: `/settings/accountAskAgent`, hrefRegex: `/settings/accountAskAgent(/.*)?$`, icon: "bot" as const, }, - { - title: "Skills", - href: `/settings/skills`, - hrefRegex: `/settings/skills(/.*)?$`, - icon: "sparkles" as const, - } ] : []),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/app/`(app)/settings/layout.tsx around lines 86 - 99, The “Skills” nav entry in the settings sidebar is gated behind hasAskEntitlement, which hides the upsell page from the free-plan users it is meant for. Update the Account nav items in the layout component so “Skills” is always present, similar to the Workspace “Ask Sourcebot” item, and use requiredEntitlement: 'ask' or equivalent entitlement metadata instead of conditionally removing it. Keep the existing route symbols like settings/accountAskAgent, settings/skills, and the nav item definitions in layout.tsx aligned so Nav can render the link with an upgrade state rather than hiding it entirely.packages/web/src/app/api/(server)/ee/chat/route.ts (1)
93-105: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winPer-turn budget check should run after command materialization. The current guard only measures the raw user text, so a short
/skillcan expand into much larger instructions and bypassATTACHMENT_MAX_TURN_TEXT_BYTESbefore the message is sent and persisted. MovematerializeCommandMessageTextsahead of the size check, or measure the materialized message text instead.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/app/api/`(server)/ee/chat/route.ts around lines 93 - 105, The per-turn budget guard in the chat route is checking the raw user text too early, so command expansion can bypass the limit. In the route handler around latestMessage handling, move materializeCommandMessageTexts before the ATTACHMENT_MAX_TURN_TEXT_BYTES check, or change the size check to use the materialized message text instead of the original input. Keep the enforcement in the same request path so the persisted message and prompt both reflect the final expanded content.
🧹 Nitpick comments (18)
packages/web/src/features/git/getFileSourceApi.ts (2)
121-143: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate repo/path/ref validation logic with
getFileSourceForRepo.The repo lookup,
isPathValid/isGitRefValidchecks, and git-instance/gitRefsetup (lines 121-143) are copy-pasted fromgetFileSourceForRepo(lines 30-48). Consider extracting a shared helper (e.g.resolveRepoAndGitRef) returning either the validated{ repo, git, gitRef }or the appropriateServiceError, used by both functions, to avoid future divergence.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/features/git/getFileSourceApi.ts` around lines 121 - 143, The repo lookup, path/ref validation, and git setup logic in resolveFileBlobShaForRepo is duplicated from getFileSourceForRepo, so extract the shared repo resolution flow into a helper such as resolveRepoAndGitRef that performs the prisma.repo lookup, isPathValid/isGitRefValid checks, and builds the simpleGit().cwd(repoPath) plus gitRef value. Update both getFileSourceForRepo and resolveFileBlobShaForRepo to call the shared helper and return or propagate the same ServiceError results to keep the two paths consistent.
64-73: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winCompute
blobShafromfileContentinstead of a secondrev-parsecall. Since this endpoint already returns text content as a string, hashing the fetched body is sufficient here; it also removes an extra git subprocess on a broad path and avoids a mismatch ifgitRefmoves between reads.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/features/git/getFileSourceApi.ts` around lines 64 - 73, The blobSha calculation in getFileSourceApi should be derived from the already-fetched fileContent instead of calling git.raw with rev-parse again. Update the blobSha assignment logic in the getFileSourceApi path so it hashes the returned text body locally, preserves the current best-effort/undefined-on-failure behavior, and removes the extra git subprocess while keeping the existing filePath/gitRef-based lookup flow intact.packages/web/src/features/chat/components/chatBox/suggestionsBox.tsx (1)
133-172: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider collapsing the two near-identical
titleDetailbranches.The
titleDetail-present andtitleDetail-absent branches only differ by one extra<span>; the outer wrapper, title span, and badge logic are duplicated. Could be simplified to a single block that conditionally renders thetitleDetailspan inline.♻️ Proposed simplification
- {titleDetail ? ( - <div className="flex flex-row items-baseline gap-2 min-w-0"> - <span className="text-sm font-medium truncate"> - {title} - </span> - <span className="text-sm text-muted-foreground truncate"> - {titleDetail} - </span> - {badge && ( - <SourceLabelBadge className="ml-auto">{badge}</SourceLabelBadge> - )} - </div> - ) : ( - <div className="flex flex-row items-baseline gap-2 min-w-0"> - <span className="text-sm font-medium truncate"> - {title} - </span> - {badge && ( - <SourceLabelBadge className="ml-auto">{badge}</SourceLabelBadge> - )} - </div> - )} + <div className="flex flex-row items-baseline gap-2 min-w-0"> + <span className="text-sm font-medium truncate"> + {title} + </span> + {titleDetail && ( + <span className="text-sm text-muted-foreground truncate"> + {titleDetail} + </span> + )} + {badge && ( + <SourceLabelBadge className="ml-auto">{badge}</SourceLabelBadge> + )} + </div>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/features/chat/components/chatBox/suggestionsBox.tsx` around lines 133 - 172, The SuggestionListItem component duplicates nearly the same JSX in the titleDetail and non-titleDetail branches. Refactor the SuggestionListItem return so the shared wrapper, title span, and badge rendering live in one block, and render the titleDetail span conditionally inline only when present. Use the SuggestionListItem and titleDetail symbols to locate the duplicated branch logic.packages/web/src/ee/features/chat/useUnsavedChangesGuard.test.tsx (1)
38-128: 📐 Maintainability & Code Quality | 🔵 TrivialMissing coverage for
bypass()and the one-shotresolve()latch.Good coverage of the
beforeunloadand controlled/confirm push paths, but there's no test exercisingbypass()(used for save-then-redirect flows) or thedecisionMadeRefone-shot guard inresolve(). Givenbypass()has a state-reset bug (seeuseUnsavedChangesGuard.tscomment), a test here would have caught it.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/ee/features/chat/useUnsavedChangesGuard.test.tsx` around lines 38 - 128, Add test coverage in useUnsavedChangesGuard test suite for the bypass() path and the one-shot resolve() behavior. Create a case that triggers the guard, calls bypass(), and verifies the next navigation is allowed without re-prompting or leaving stale active state, then add a case that exercises resolve() twice to confirm decisionMadeRef only lets the first decision take effect. Use the existing useUnsavedChangesGuard hook and Probe/Noop patterns to locate and validate these flows.packages/db/prisma/migrations/20260624202848_refactor_agent_skills/migration.sql (1)
44-53: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winRedundant index alongside unique constraint.
AgentSkill_orgId_visibility_scopeId_idx(orgId, visibility, scopeId) is a strict column-prefix of the unique indexAgentSkill_orgId_visibility_scopeId_slug_key(orgId, visibility, scopeId, slug). Postgres can already satisfy prefix-column lookups using the unique btree index, so the plain index adds write/storage overhead without a query benefit.♻️ Proposed fix
--- CreateIndex -CREATE INDEX "AgentSkill_orgId_visibility_scopeId_idx" ON "AgentSkill"("orgId", "visibility", "scopeId"); - -- CreateIndex CREATE INDEX "AgentSkill_orgId_visibility_scopeId_idx" ON "AgentSkill"("orgId", "visibility", "scopeId", "enabled", "featured" DESC, "updatedAt" DESC, "name");Alternatively, simply drop the standalone index in a follow-up migration and rely on
AgentSkill_orgId_visibility_scopeId_slug_keyfor prefix lookups.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/db/prisma/migrations/20260624202848_refactor_agent_skills/migration.sql` around lines 44 - 53, Remove the redundant AgentSkill_orgId_visibility_scopeId_idx index from the migration because AgentSkill_orgId_visibility_scopeId_slug_key already covers the same orgId, visibility, and scopeId prefix. Update the migration.sql block that creates AgentSkill indexes so only the unique index remains for prefix lookups, and keep the existing unique constraint creation intact.packages/web/src/ee/features/chat/skills/actions.ts (4)
358-360: 📐 Maintainability & Code Quality | 🔵 TrivialInconsistent ServiceError narrowing idiom.
These sites narrow
requireManageableSharedSkill's result with"errorCode" in x, while the rest of the file (e.g. lines 516, 547, 643) uses theisServiceError()helper already imported at the top. UsingisServiceError()consistently avoids having two different type-narrowing idioms for the same union type.♻️ Example
- if ("errorCode" in manageableSkill) { + if (isServiceError(manageableSkill)) { return manageableSkill; }Also applies to: 682-684, 1054-1056, 1110-1112
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/ee/features/chat/skills/actions.ts` around lines 358 - 360, Replace the ad hoc ServiceError check in requireManageableSharedSkill call sites with the existing isServiceError() helper for consistent narrowing. Update the branches in actions.ts that currently use "errorCode" in manageableSkill to use isServiceError(manageableSkill) instead, matching the patterns already used elsewhere in the file. Make the same consistency change in the other listed call sites so all ServiceError unions are narrowed through the same helper.
63-67: 📐 Maintainability & Code Quality | 🔵 Trivial
skillSourceInvalidandskillNotSyncedshare the sameerrorCode.Both errors use
ErrorCode.INVALID_REQUEST_BODYdespite representing distinct conditions (missing source link vs. now-invalid source content) and different HTTP status codes (400 vs 422). Clients branching onerrorCodealone can't distinguish them. Since two new codes (AGENT_SKILL_ALREADY_EXISTS,AGENT_SKILL_NOT_FOUND) were already added for this feature, consider adding dedicated codes here too.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/ee/features/chat/skills/actions.ts` around lines 63 - 67, The skill error helpers currently reuse ErrorCode.INVALID_REQUEST_BODY for two different failure cases, so update the chat skills error definitions to use distinct error codes for skillNotSynced and skillSourceInvalid. Add dedicated codes alongside the existing feature-specific codes, then wire each helper in actions.ts to the appropriate new code while keeping their current status codes and messages. Reference the skillNotSynced and skillSourceInvalid functions so the separation is clear and clients can branch on errorCode reliably.
340-381: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winDuplicate fetch pattern for manageable shared skills.
getSharedAgentSkillandupdateSharedAgentSkillFromSourceboth callrequireManageableSharedSkill(which already does afindFirstwithenabled: true) and then immediately re-fetch the same row with a secondfindFirstto pull additional columns.updateSharedAgentSkillavoids this by using the fields already returned fromrequireManageableSharedSkill. Consider making the select inrequireManageableSharedSkillgeneric/parameterizable so callers can request the extra columns they need in one round trip.Also applies to: 664-724
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/ee/features/chat/skills/actions.ts` around lines 340 - 381, In getSharedAgentSkill, avoid the extra prisma.agentSkill.findFirst after requireManageableSharedSkill by making requireManageableSharedSkill return the needed fields up front, similar to updateSharedAgentSkill. Refactor requireManageableSharedSkill to accept a caller-provided select/include (or equivalent generic selection) so getSharedAgentSkill can request the columns needed by canAccessSkillSource and toAgentSkillListItem in one query. Apply the same one-round-trip pattern to updateSharedAgentSkillFromSource as well.
876-909: 🗄️ Data Integrity & Integration | 🔵 TrivialUn-sharing a skill silently drops it for all other adopters.
When the creator calls
makeSharedAgentSkillPersonal, the sharedAgentSkillrow is deleted (line 894-898). Per the migration,AgentSkillAdoptionrows cascade-delete onAgentSkillremoval, so every other user who had adopted or auto-enrolled this shared skill loses it immediately with no warning. If this is intended (converting a shared skill to personal necessarily un-shares it for everyone), consider surfacing adopter counts in the UI so the creator can make an informed decision before converting.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/ee/features/chat/skills/actions.ts` around lines 876 - 909, The personal-conversion flow in makeSharedAgentSkillPersonal is deleting the shared AgentSkill record when sharedSkill.createdById matches user.id, which cascades removal for every AgentSkillAdoption. Update this branch so converting a creator-owned shared skill to personal does not silently unshare it for all adopters, or otherwise make the behavior explicit before deletion. Use the prisma.$transaction block, tx.agentSkill.delete, and removeSharedSkillForUser as the key spots to adjust, and keep the creator’s personal copy creation separate from any global unshare behavior.packages/web/src/ee/features/chat/skills/commands.server.ts (1)
5-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
console.errorinstead of the project's structured logger.These three functions log failures with
console.error, while the surrounding chat/skills tests mockcreateLoggerfrom@sourcebot/shared, suggesting other modules in this feature use the structured logger. Usingconsole.errorhere bypasses whatever log level/formatting/observability pipeline the structured logger provides.#!/bin/bash # Check whether sibling skills files use createLogger instead of console.error rg -n 'createLogger|console\.error' packages/web/src/ee/features/chat/skills🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/ee/features/chat/skills/commands.server.ts` around lines 5 - 33, The three command-loading helpers currently use console.error, which bypasses the feature’s structured logging path. Update listPersonalAgentSkillCommandsOrEmpty, listSharedSkillCommandsOrEmpty, and listAgentSkillCommandsOrEmpty to use the shared logger from `@sourcebot/shared` via createLogger instead of direct console calls, keeping the same failure context and return-[] fallback behavior.packages/db/prisma/schema.prisma (1)
807-812: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueRedundant index:
@@index([orgId, visibility, scopeId])is a prefix of the unique constraint.
@@unique([orgId, visibility, scopeId, slug])(Line 807) already supports lookups filtered by(orgId, visibility, scopeId)as a leftmost-prefix scan, making the separate@@index([orgId, visibility, scopeId])(Line 811) largely redundant and adding write overhead with no read benefit.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/db/prisma/schema.prisma` around lines 807 - 812, Remove the redundant `@@index([orgId, visibility, scopeId])` from the Prisma model since `@@unique([orgId, visibility, scopeId, slug])` in the same schema already covers that prefix for lookups. Keep the other indexes intact, and verify the remaining `AgentSkill` indexing still supports the intended query patterns without duplicating write cost.packages/web/src/ee/features/chat/skills/components/personalSkillEditorPage.tsx (1)
183-211: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueNon-null assertions on
skill!.idfor edit modes.
saveModeguaranteesskill !== nullwheneditShared/editPersonalexecute, but TypeScript can't infer that through the map-lookup pattern, hence the!assertions. Functionally safe given the currentsaveModederivation, but fragile to future refactors ofsaveModeConfig/saveMode.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/ee/features/chat/skills/components/personalSkillEditorPage.tsx` around lines 183 - 211, The save handler in handleSubmit uses non-null assertions on skill.id for editShared and editPersonal, which is fragile even though saveMode currently implies a non-null skill. Refactor the saveMode dispatch in personalSkillEditorPage so the edit paths are only constructed or called after an explicit skill null check, or otherwise narrow skill before accessing its id; keep the createPersonalAgentSkill/createSharedAgentSkill branches unchanged and avoid relying on skill! inside the map lookup.packages/web/src/ee/features/chat/skills/components/skillsPage.tsx (1)
903-903: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueCurly-brace bodies should be on their own line.
Both
onOpenChangehandlers place theifbody on the same line, unlike the rest of the file which consistently formats multi-line.As per coding guidelines: "Always use curly braces for
ifstatements, with the body on a new line — even for single-line bodies."🎨 Suggested formatting
- onOpenChange={(open) => { if (!open) { setPendingDiscard(null); } }} + onOpenChange={(open) => { + if (!open) { + setPendingDiscard(null); + } + }}- onOpenChange={(open) => { if (!open) { navGuard.resolve(false); } }} + onOpenChange={(open) => { + if (!open) { + navGuard.resolve(false); + } + }}Also applies to: 930-930
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/ee/features/chat/skills/components/skillsPage.tsx` at line 903, The onOpenChange handlers in skillsPage.tsx use a one-line if body inside the arrow function, which breaks the file’s brace-and-newline formatting convention. Update both onOpenChange callbacks so the if statement keeps curly braces and places the body on its own line, matching the surrounding style in this component and the related handler near the other reported location.Source: Coding guidelines
packages/web/src/ee/features/chat/skills/components/skillsPage.test.tsx (1)
159-187: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winMissing coverage for the owner-(non-creator) "make personal" path.
All make-personal tests use
sharedSkill/syncedSkill, which are treated as user-created (isCreatedByUsernot overridden tofalsefor an owner acting on someone else's skill). This gap would have caught the stale-state issue flagged inskillsPage.tsx'shandleMakePersonal.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/ee/features/chat/skills/components/skillsPage.test.tsx` around lines 159 - 187, Add test coverage for the owner-but-not-creator make-personal flow in skillsPage.test.tsx, since the current make-personal cases only use sharedSkill/syncedSkill and never exercise the non-user-created path. Update the relevant test setup around renderSkillsPage and handleMakePersonal so the selected skill has isCreatedByUser set to false while still belonging to the owner, then verify the confirmation dialog and make-personal action behave correctly. Use the existing makeSharedAgentSkillPersonal and skillsPage.tsx handleMakePersonal flow as the anchors for locating the path.packages/web/src/ee/features/chat/skills/components/importFromRepoDialog.tsx (2)
143-143: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueCurly-brace body should be on its own line.
The inline
if (!isOpen) { resetAndClose(); }violates the project's if-statement formatting rule.As per coding guidelines: "Always use curly braces for
ifstatements, with the body on a new line — even for single-line bodies."🎨 Suggested formatting
- <Dialog open={open} onOpenChange={(isOpen) => { if (!isOpen) { resetAndClose(); } }} modal={true}> + <Dialog + open={open} + onOpenChange={(isOpen) => { + if (!isOpen) { + resetAndClose(); + } + }} + modal={true} + >🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/ee/features/chat/skills/components/importFromRepoDialog.tsx` at line 143, The inline if statement inside the Dialog onOpenChange handler in importFromRepoDialog should follow the project’s brace formatting rule. Update the onOpenChange callback so the if (!isOpen) block uses curly braces with the body on its own line, matching the style used elsewhere in the component and keeping the resetAndClose call inside that properly formatted block.Source: Coding guidelines
48-62: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winRepo search fires a request on every keystroke.
repoQuerydrives theuseQuerykey directly with no debounce, so each keystroke issues a newlistReposcall (confirmed by the accompanying test asserting multiple calls for"wid"/"widg"). Debouncing the input would meaningfully cut backend load with minimal code.⚡ Suggested debounce
- const [repoQuery, setRepoQuery] = useState(""); + const [repoQuery, setRepoQuery] = useState(""); + const [debouncedRepoQuery, setDebouncedRepoQuery] = useState(""); + + useEffect(() => { + const id = setTimeout(() => setDebouncedRepoQuery(repoQuery), 250); + return () => clearTimeout(id); + }, [repoQuery]); ... - queryKey: ["skillImportRepos", repoQuery], + queryKey: ["skillImportRepos", debouncedRepoQuery], queryFn: () => unwrapServiceError(listRepos({ page: 1, perPage: MAX_RESULTS, sort: "name", direction: "asc", - query: repoQuery, + query: debouncedRepoQuery, })),🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/ee/features/chat/skills/components/importFromRepoDialog.tsx` around lines 48 - 62, The repo search in importFromRepoDialog’s useQuery is firing listRepos on every keystroke because repoQuery is used directly in the query key and queryFn. Add a debounce layer for the search input so the query only updates after typing pauses, while keeping the existing selectedRepo/open gating and keepPreviousData behavior intact. Update the repoQuery flow in importFromRepoDialog and any related input state so the debounced value is what drives the query.packages/web/src/ee/features/chat/skills/components/workspaceSkillMutations.ts (1)
24-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
flagparam is accepted but not used to build the update payload.
data: { autoEnrolled: checked }is hardcoded, ignoringflag. Works today sinceOrgSkillFlagKeyhas a single member, but the signature implies genericity that the body doesn't honor — a future second flag key would silently write to the wrong field.♻️ Proposed fix
export async function updateWorkspaceSkillFlag({ skillId, + flag, checked, updateOrgSkills, }: { skillId: string; flag: OrgSkillFlagKey; checked: boolean; updateOrgSkills: (updater: (skills: SharedAgentSkillManagementItem[]) => SharedAgentSkillManagementItem[]) => void; }): Promise<ServiceError | null> { const result = await setSharedSkillFlag({ skillId, - data: { autoEnrolled: checked }, + data: { [flag]: checked } as Record<OrgSkillFlagKey, boolean>, });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/ee/features/chat/skills/components/workspaceSkillMutations.ts` around lines 24 - 46, The updateWorkspaceSkillFlag function accepts a flag parameter but always sends data.autoEnrolled, so the payload is not actually driven by the selected flag. Update the setSharedSkillFlag call to build its data from the flag argument inside updateWorkspaceSkillFlag, so the function stays aligned with OrgSkillFlagKey and will update the correct field if more flag keys are added later.packages/web/src/ee/features/chat/agent.ts (1)
605-659: 🚀 Performance & Scalability | 🔵 TrivialSkill catalog gating and load_skill wiring look correct.
Entitlement/identity gating mirrors the existing MCP-tool pattern, and
alwaysActiveToolsis defined once so both the initialactiveToolsand everyprepareSteprebuild stay in sync automatically. Reasonable defense-in-depth against the registry snapshot going stale (re-resolved inresolveAutoInvocableSkill).One minor unbounded-growth consideration worth keeping in mind operationally:
buildSkillRegistryloads every visible personal+shared skill on every request without any cap. As orgs accumulate many skills, this could inflate the dynamic prompt size and per-message tool-call description tokens. Not a defect introduced here (it mirrors the manual command catalog), but worth monitoring/pagination if catalog sizes grow.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/web/src/ee/features/chat/agent.ts` around lines 605 - 659, The skill catalog can grow without bound because buildSkillRegistry loads every visible personal/shared skill on each request, which can bloat the dynamic prompt and tool descriptions. Update the ask-agent flow around buildSkillRegistry/createPrompt to either cap the catalog size or add pagination/selection for large orgs, and keep the load_skill wiring and alwaysActiveTools behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CHANGELOG.md`:
- Line 23: The Unreleased changelog item for the Ask Sourcebot skills entry is
missing the trailing GitHub PR link, unlike the other sibling entries. Update
the `CHANGELOG.md` Unreleased entry so the line ends with the pull request
reference in the required `[#<id>](<url>)` format, matching the existing pattern
used by the nearby changelog items.
In `@packages/db/prisma/schema.prisma`:
- Around line 773-780: The AgentSkill audit relation is too destructive:
`createdBy` currently cascades on user deletion, which can delete shared/org
skills and their adopters. Update the `AgentSkill` model’s
`createdBy`/`createdById` relation in the Prisma schema to stop using cascade
delete and allow the creator reference to be cleared instead, matching the
intent of audit metadata. Then update any `createdById` callers and mappings
that assume a required user, including `toSharedAgentSkillCatalogItem` and any
`Pick<AgentSkill, ... "createdById" ...>` usages, so they handle a nullable
creator.
In `@packages/web/src/components/ui/tooltip.tsx`:
- Around line 18-28: The TooltipContent in tooltip.tsx is being portaled, which
moves the nested Docs link outside SelectContent’s dismissable tree and can
close the select before navigation. Update the TooltipPrimitive.Content usage in
the tooltip component so this tooltip renders inline for the Select/docs case,
or gate the Portal wrapper behind a prop/branch that keeps the click target
inside the SelectContent subtree; use TooltipPrimitive.Content and
TooltipPrimitive.Portal as the key symbols to adjust.
In `@packages/web/src/ee/features/chat/agent.ts`:
- Around line 181-187: The sanitizeSkillCatalogText helper in agent.ts only
collapses whitespace, so markup-like values can still break the <agent_skills>
block. Update this helper (and the call sites that build the catalog line) to
escape or encode prompt-sensitive characters such as <, >, and & before
interpolation, or switch the skill catalog serialization to a safe non-markup
format so names, hints, and descriptions cannot be reinterpreted as tags.
In `@packages/web/src/ee/features/chat/skills/actions.ts`:
- Around line 579-609: The shared-skill lookup in getAgentSkillSourceStatus is
too permissive because it uses sharedAgentSkillAuthScope(org.id) with enabled:
true instead of the same visibility checks used by getSharedAgentSkill and
adoptSharedSkill. Update the Prisma findFirst filter to scope shared skills
through canAccessSkillSource or sharedAgentSkillVisibleToUserWhere so only
skills visible to the caller can have their sync status queried, while keeping
the personalAgentSkillAuthScope path unchanged.
In `@packages/web/src/ee/features/chat/skills/commandResolution.ts`:
- Around line 292-304: The manual skill invocation event path in
commandResolution.ts is not preserving the request source, so all events fall
back to the default attribution. Thread the source value from route.ts through
materializeCommandMessageTexts into buildAskSkillInvokedEvent, and use that
value when emitting ask_skill_invoked for the manual invocation branch so
web/MCP/API-originated calls are attributed correctly.
In
`@packages/web/src/ee/features/chat/skills/components/personalSkillEditorPage.tsx`:
- Around line 82-113: `SkillEditor` still allows editing description and
instructions for synced skills even though those fields are ignored on save.
Update the `personalSkillEditorPage.tsx` flow in `SkillEditor` to detect synced
skills from `skill.source` (or equivalent existing skill metadata) and make the
description/instructions inputs read-only/disabled, or replace them with a
synced-from-repo notice. Keep the existing create/edit logic intact, but ensure
the UI reflects that only supported fields are editable for synced skills.
In `@packages/web/src/ee/features/chat/skills/components/skillsPage.tsx`:
- Around line 588-617: The shared-skill state update in handleMakePersonal
leaves a stale entry behind for the non-creator owner path. Update the
make-personal flow so that, after a successful makeSharedAgentSkillPersonal
call, the skill is removed from sharedSkills in both branches rather than only
toggling flags; use the existing handleMakePersonal logic and the
sharedSkills/setSharedSkills updates to locate it. Keep the personalSkills
update and selection/toast behavior unchanged, but ensure the shared catalog no
longer renders the converted skill after the destructive operation.
In
`@packages/web/src/ee/features/chat/skills/components/workspaceSharedSkillsManager.tsx`:
- Around line 228-238: The Switch in workspaceSharedSkillsManager currently uses
the same static aria-label for every row, so each auto-enroll toggle is
indistinguishable to screen readers. Update the Switch’s aria-label in the skill
row render to include the current skill name, following the same pattern used by
the delete button, and keep the change localized around the `Switch` inside the
workspaceSharedSkillsManager component.
In `@packages/web/src/ee/features/chat/useUnsavedChangesGuard.ts`:
- Around line 22-26: The bypass in useUnsavedChangesGuard is effectively
permanent because bypassRef.current is set true in bypass() and never cleared,
which disables the enabled() guard for all future unsaved changes. Update
useUnsavedChangesGuard so bypass() only suppresses the next navigation attempt,
then resets bypassRef.current back to false as part of the guard flow (for
example, when enabled() observes and consumes the bypass flag). Make sure the
logic around enabled, bypassRef, and bypass preserves normal guarding after the
deliberate programmatic navigation.
---
Outside diff comments:
In `@packages/web/src/app/`(app)/settings/layout.tsx:
- Around line 86-99: The “Skills” nav entry in the settings sidebar is gated
behind hasAskEntitlement, which hides the upsell page from the free-plan users
it is meant for. Update the Account nav items in the layout component so
“Skills” is always present, similar to the Workspace “Ask Sourcebot” item, and
use requiredEntitlement: 'ask' or equivalent entitlement metadata instead of
conditionally removing it. Keep the existing route symbols like
settings/accountAskAgent, settings/skills, and the nav item definitions in
layout.tsx aligned so Nav can render the link with an upgrade state rather than
hiding it entirely.
In `@packages/web/src/app/api/`(server)/ee/chat/route.ts:
- Around line 93-105: The per-turn budget guard in the chat route is checking
the raw user text too early, so command expansion can bypass the limit. In the
route handler around latestMessage handling, move materializeCommandMessageTexts
before the ATTACHMENT_MAX_TURN_TEXT_BYTES check, or change the size check to use
the materialized message text instead of the original input. Keep the
enforcement in the same request path so the persisted message and prompt both
reflect the final expanded content.
---
Nitpick comments:
In
`@packages/db/prisma/migrations/20260624202848_refactor_agent_skills/migration.sql`:
- Around line 44-53: Remove the redundant
AgentSkill_orgId_visibility_scopeId_idx index from the migration because
AgentSkill_orgId_visibility_scopeId_slug_key already covers the same orgId,
visibility, and scopeId prefix. Update the migration.sql block that creates
AgentSkill indexes so only the unique index remains for prefix lookups, and keep
the existing unique constraint creation intact.
In `@packages/db/prisma/schema.prisma`:
- Around line 807-812: Remove the redundant `@@index([orgId, visibility,
scopeId])` from the Prisma model since `@@unique([orgId, visibility, scopeId,
slug])` in the same schema already covers that prefix for lookups. Keep the
other indexes intact, and verify the remaining `AgentSkill` indexing still
supports the intended query patterns without duplicating write cost.
In `@packages/web/src/ee/features/chat/agent.ts`:
- Around line 605-659: The skill catalog can grow without bound because
buildSkillRegistry loads every visible personal/shared skill on each request,
which can bloat the dynamic prompt and tool descriptions. Update the ask-agent
flow around buildSkillRegistry/createPrompt to either cap the catalog size or
add pagination/selection for large orgs, and keep the load_skill wiring and
alwaysActiveTools behavior unchanged.
In `@packages/web/src/ee/features/chat/skills/actions.ts`:
- Around line 358-360: Replace the ad hoc ServiceError check in
requireManageableSharedSkill call sites with the existing isServiceError()
helper for consistent narrowing. Update the branches in actions.ts that
currently use "errorCode" in manageableSkill to use
isServiceError(manageableSkill) instead, matching the patterns already used
elsewhere in the file. Make the same consistency change in the other listed call
sites so all ServiceError unions are narrowed through the same helper.
- Around line 63-67: The skill error helpers currently reuse
ErrorCode.INVALID_REQUEST_BODY for two different failure cases, so update the
chat skills error definitions to use distinct error codes for skillNotSynced and
skillSourceInvalid. Add dedicated codes alongside the existing feature-specific
codes, then wire each helper in actions.ts to the appropriate new code while
keeping their current status codes and messages. Reference the skillNotSynced
and skillSourceInvalid functions so the separation is clear and clients can
branch on errorCode reliably.
- Around line 340-381: In getSharedAgentSkill, avoid the extra
prisma.agentSkill.findFirst after requireManageableSharedSkill by making
requireManageableSharedSkill return the needed fields up front, similar to
updateSharedAgentSkill. Refactor requireManageableSharedSkill to accept a
caller-provided select/include (or equivalent generic selection) so
getSharedAgentSkill can request the columns needed by canAccessSkillSource and
toAgentSkillListItem in one query. Apply the same one-round-trip pattern to
updateSharedAgentSkillFromSource as well.
- Around line 876-909: The personal-conversion flow in
makeSharedAgentSkillPersonal is deleting the shared AgentSkill record when
sharedSkill.createdById matches user.id, which cascades removal for every
AgentSkillAdoption. Update this branch so converting a creator-owned shared
skill to personal does not silently unshare it for all adopters, or otherwise
make the behavior explicit before deletion. Use the prisma.$transaction block,
tx.agentSkill.delete, and removeSharedSkillForUser as the key spots to adjust,
and keep the creator’s personal copy creation separate from any global unshare
behavior.
In `@packages/web/src/ee/features/chat/skills/commands.server.ts`:
- Around line 5-33: The three command-loading helpers currently use
console.error, which bypasses the feature’s structured logging path. Update
listPersonalAgentSkillCommandsOrEmpty, listSharedSkillCommandsOrEmpty, and
listAgentSkillCommandsOrEmpty to use the shared logger from `@sourcebot/shared`
via createLogger instead of direct console calls, keeping the same failure
context and return-[] fallback behavior.
In
`@packages/web/src/ee/features/chat/skills/components/importFromRepoDialog.tsx`:
- Line 143: The inline if statement inside the Dialog onOpenChange handler in
importFromRepoDialog should follow the project’s brace formatting rule. Update
the onOpenChange callback so the if (!isOpen) block uses curly braces with the
body on its own line, matching the style used elsewhere in the component and
keeping the resetAndClose call inside that properly formatted block.
- Around line 48-62: The repo search in importFromRepoDialog’s useQuery is
firing listRepos on every keystroke because repoQuery is used directly in the
query key and queryFn. Add a debounce layer for the search input so the query
only updates after typing pauses, while keeping the existing selectedRepo/open
gating and keepPreviousData behavior intact. Update the repoQuery flow in
importFromRepoDialog and any related input state so the debounced value is what
drives the query.
In
`@packages/web/src/ee/features/chat/skills/components/personalSkillEditorPage.tsx`:
- Around line 183-211: The save handler in handleSubmit uses non-null assertions
on skill.id for editShared and editPersonal, which is fragile even though
saveMode currently implies a non-null skill. Refactor the saveMode dispatch in
personalSkillEditorPage so the edit paths are only constructed or called after
an explicit skill null check, or otherwise narrow skill before accessing its id;
keep the createPersonalAgentSkill/createSharedAgentSkill branches unchanged and
avoid relying on skill! inside the map lookup.
In `@packages/web/src/ee/features/chat/skills/components/skillsPage.test.tsx`:
- Around line 159-187: Add test coverage for the owner-but-not-creator
make-personal flow in skillsPage.test.tsx, since the current make-personal cases
only use sharedSkill/syncedSkill and never exercise the non-user-created path.
Update the relevant test setup around renderSkillsPage and handleMakePersonal so
the selected skill has isCreatedByUser set to false while still belonging to the
owner, then verify the confirmation dialog and make-personal action behave
correctly. Use the existing makeSharedAgentSkillPersonal and skillsPage.tsx
handleMakePersonal flow as the anchors for locating the path.
In `@packages/web/src/ee/features/chat/skills/components/skillsPage.tsx`:
- Line 903: The onOpenChange handlers in skillsPage.tsx use a one-line if body
inside the arrow function, which breaks the file’s brace-and-newline formatting
convention. Update both onOpenChange callbacks so the if statement keeps curly
braces and places the body on its own line, matching the surrounding style in
this component and the related handler near the other reported location.
In
`@packages/web/src/ee/features/chat/skills/components/workspaceSkillMutations.ts`:
- Around line 24-46: The updateWorkspaceSkillFlag function accepts a flag
parameter but always sends data.autoEnrolled, so the payload is not actually
driven by the selected flag. Update the setSharedSkillFlag call to build its
data from the flag argument inside updateWorkspaceSkillFlag, so the function
stays aligned with OrgSkillFlagKey and will update the correct field if more
flag keys are added later.
In `@packages/web/src/ee/features/chat/useUnsavedChangesGuard.test.tsx`:
- Around line 38-128: Add test coverage in useUnsavedChangesGuard test suite for
the bypass() path and the one-shot resolve() behavior. Create a case that
triggers the guard, calls bypass(), and verifies the next navigation is allowed
without re-prompting or leaving stale active state, then add a case that
exercises resolve() twice to confirm decisionMadeRef only lets the first
decision take effect. Use the existing useUnsavedChangesGuard hook and
Probe/Noop patterns to locate and validate these flows.
In `@packages/web/src/features/chat/components/chatBox/suggestionsBox.tsx`:
- Around line 133-172: The SuggestionListItem component duplicates nearly the
same JSX in the titleDetail and non-titleDetail branches. Refactor the
SuggestionListItem return so the shared wrapper, title span, and badge rendering
live in one block, and render the titleDetail span conditionally inline only
when present. Use the SuggestionListItem and titleDetail symbols to locate the
duplicated branch logic.
In `@packages/web/src/features/git/getFileSourceApi.ts`:
- Around line 121-143: The repo lookup, path/ref validation, and git setup logic
in resolveFileBlobShaForRepo is duplicated from getFileSourceForRepo, so extract
the shared repo resolution flow into a helper such as resolveRepoAndGitRef that
performs the prisma.repo lookup, isPathValid/isGitRefValid checks, and builds
the simpleGit().cwd(repoPath) plus gitRef value. Update both
getFileSourceForRepo and resolveFileBlobShaForRepo to call the shared helper and
return or propagate the same ServiceError results to keep the two paths
consistent.
- Around line 64-73: The blobSha calculation in getFileSourceApi should be
derived from the already-fetched fileContent instead of calling git.raw with
rev-parse again. Update the blobSha assignment logic in the getFileSourceApi
path so it hashes the returned text body locally, preserves the current
best-effort/undefined-on-failure behavior, and removes the extra git subprocess
while keeping the existing filePath/gitRef-based lookup flow intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: d888b4a8-037d-40ce-b0f4-199f0fb4fb6b
⛔ Files ignored due to path filters (1)
yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (98)
CHANGELOG.mddocs/api-reference/sourcebot-public.openapi.jsonpackages/db/prisma/migrations/20260624202848_refactor_agent_skills/migration.sqlpackages/db/prisma/migrations/20260626120000_remove_featured_from_agent_skill/migration.sqlpackages/db/prisma/migrations/20260627223058_add_agent_skill_source_provenance/migration.sqlpackages/db/prisma/schema.prismapackages/db/src/index.tspackages/web/package.jsonpackages/web/src/app/(app)/@sidebar/components/settingsSidebar/nav.tsxpackages/web/src/app/(app)/askgh/[owner]/[repo]/components/landingPage.tsxpackages/web/src/app/(app)/askgh/[owner]/[repo]/page.tsxpackages/web/src/app/(app)/chat/[id]/page.tsxpackages/web/src/app/(app)/chat/chatLandingPage.tsxpackages/web/src/app/(app)/chat/components/landingPageChatBox.tsxpackages/web/src/app/(app)/chat/layout.tsxpackages/web/src/app/(app)/components/searchModeSelector.tsxpackages/web/src/app/(app)/layout.tsxpackages/web/src/app/(app)/settings/accountAskAgent/skills/[skillId]/page.tsxpackages/web/src/app/(app)/settings/accountAskAgent/skills/new/page.tsxpackages/web/src/app/(app)/settings/accountAskAgent/workspaceSkills/[skillId]/page.tsxpackages/web/src/app/(app)/settings/components/settingsContentFrame.test.tsxpackages/web/src/app/(app)/settings/components/settingsContentFrame.tsxpackages/web/src/app/(app)/settings/layout.tsxpackages/web/src/app/(app)/settings/skills/page.tsxpackages/web/src/app/(app)/settings/skills/skillsEntitlementMessage.tsxpackages/web/src/app/(app)/settings/workspaceAskAgent/page.tsxpackages/web/src/app/(app)/settings/workspaceAskAgent/workspaceAskAgentPage.test.tsxpackages/web/src/app/(app)/settings/workspaceAskAgent/workspaceAskAgentPage.tsxpackages/web/src/app/api/(client)/client.tspackages/web/src/app/api/(server)/ee/askmcp/configuration/[serverId]/tools/route.test.tspackages/web/src/app/api/(server)/ee/chat/route.tspackages/web/src/app/api/(server)/ee/skills/sourceStatus/route.tspackages/web/src/components/ui/tooltip.tsxpackages/web/src/ee/features/chat/agent.tspackages/web/src/ee/features/chat/components/chatThread/chatThread.tsxpackages/web/src/ee/features/chat/components/chatThread/detailsCard.test.tsxpackages/web/src/ee/features/chat/components/chatThread/detailsCard.tsxpackages/web/src/ee/features/chat/components/chatThread/tableOfContents.tsxpackages/web/src/ee/features/chat/components/chatThread/tools/loadSkillToolComponent.tsxpackages/web/src/ee/features/chat/components/chatThreadPanel.test.tsxpackages/web/src/ee/features/chat/components/chatThreadPanel.tsxpackages/web/src/ee/features/chat/mcp/components/accountAskAgentPage.test.tsxpackages/web/src/ee/features/chat/mcp/components/accountAskAgentPage.tsxpackages/web/src/ee/features/chat/mcp/components/connectorsMenu.tsxpackages/web/src/ee/features/chat/mcp/mcpToolSets.tspackages/web/src/ee/features/chat/skills/actions.test.tspackages/web/src/ee/features/chat/skills/actions.tspackages/web/src/ee/features/chat/skills/commandCatalog.tspackages/web/src/ee/features/chat/skills/commandResolution.test.tspackages/web/src/ee/features/chat/skills/commandResolution.tspackages/web/src/ee/features/chat/skills/commands.server.tspackages/web/src/ee/features/chat/skills/components/importFromRepoDialog.test.tsxpackages/web/src/ee/features/chat/skills/components/importFromRepoDialog.tsxpackages/web/src/ee/features/chat/skills/components/personalSkillEditorPage.tsxpackages/web/src/ee/features/chat/skills/components/skillInstructionsEditor.test.tspackages/web/src/ee/features/chat/skills/components/skillInstructionsEditor.tsxpackages/web/src/ee/features/chat/skills/components/skillsPage.test.tsxpackages/web/src/ee/features/chat/skills/components/skillsPage.tsxpackages/web/src/ee/features/chat/skills/components/workspaceSharedSkillsManager.test.tsxpackages/web/src/ee/features/chat/skills/components/workspaceSharedSkillsManager.tsxpackages/web/src/ee/features/chat/skills/components/workspaceSkillMutations.tspackages/web/src/ee/features/chat/skills/components/workspaceSkillShared.tsxpackages/web/src/ee/features/chat/skills/loadSkillTool.test.tspackages/web/src/ee/features/chat/skills/loadSkillTool.tspackages/web/src/ee/features/chat/skills/registry.test.tspackages/web/src/ee/features/chat/skills/registry.tspackages/web/src/ee/features/chat/skills/skillAnalytics.test.tspackages/web/src/ee/features/chat/skills/skillAnalytics.tspackages/web/src/ee/features/chat/skills/sourceRepoAccess.test.tspackages/web/src/ee/features/chat/skills/sourceRepoAccess.tspackages/web/src/ee/features/chat/skills/types.test.tspackages/web/src/ee/features/chat/skills/types.tspackages/web/src/ee/features/chat/useTOCItems.tspackages/web/src/ee/features/chat/useUnsavedChangesGuard.test.tsxpackages/web/src/ee/features/chat/useUnsavedChangesGuard.tspackages/web/src/ee/features/oauth/server.tspackages/web/src/features/chat/commands/types.tspackages/web/src/features/chat/commands/utils.test.tspackages/web/src/features/chat/commands/utils.tspackages/web/src/features/chat/components/chatBox/chatBox.tsxpackages/web/src/features/chat/components/chatBox/keyboard.test.tspackages/web/src/features/chat/components/chatBox/keyboard.tspackages/web/src/features/chat/components/chatBox/sourceLabelBadge.tsxpackages/web/src/features/chat/components/chatBox/suggestionsBox.tsxpackages/web/src/features/chat/components/chatBox/types.tspackages/web/src/features/chat/components/chatBox/useSuggestionModeAndQuery.tspackages/web/src/features/chat/components/chatBox/useSuggestionsData.tspackages/web/src/features/chat/mcpOAuthDraft.test.tspackages/web/src/features/chat/mcpOAuthDraft.tspackages/web/src/features/chat/types.tspackages/web/src/features/chat/utils.test.tspackages/web/src/features/chat/utils.tspackages/web/src/features/git/getFileSourceApi.tspackages/web/src/features/git/schemas.tspackages/web/src/lib/errorCodes.tspackages/web/src/lib/posthogEvents.tspackages/web/src/lib/prismaErrors.tspackages/web/tailwind.config.ts
| - [EE] Added DPoP sender-constrained OAuth tokens for MCP clients. [#1395](https://github.com/sourcebot-dev/sourcebot/pull/1395) | ||
| - [EE] Added text file attachments to Ask Sourcebot, letting users attach text/code/config files to a chat message via the paperclip button, drag-and-drop, or paste, with large pastes auto-converted to attachments. [#1374](https://github.com/sourcebot-dev/sourcebot/pull/1374) | ||
| - [EE] Added image attachments to Ask Sourcebot, letting users attach images to a chat message when the selected model supports image input. [#1375](https://github.com/sourcebot-dev/sourcebot/pull/1375) | ||
| - [EE] Added Ask Sourcebot skills, letting users create, import, share, sync, and auto-invoke reusable chat instructions across personal and workspace scopes. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add missing PR link to changelog entry.
This is the only [Unreleased] entry without a trailing PR link; every sibling entry (lines 16-22) ends with [#<id>](<url>).
📝 Proposed fix
-- [EE] Added Ask Sourcebot skills, letting users create, import, share, sync, and auto-invoke reusable chat instructions across personal and workspace scopes.
+- [EE] Added Ask Sourcebot skills, letting users create, import, share, sync, and auto-invoke reusable chat instructions across personal and workspace scopes. [#<PR>](https://github.com/sourcebot-dev/sourcebot/pull/<PR>)As per path instructions, "Update CHANGELOG.md with an entry under [Unreleased] linking to the new PR" and "entries must include the GitHub pull request id at the end of the line, formatted as [#<id>](<url>)".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - [EE] Added Ask Sourcebot skills, letting users create, import, share, sync, and auto-invoke reusable chat instructions across personal and workspace scopes. | |
| [EE] Added Ask Sourcebot skills, letting users create, import, share, sync, and auto-invoke reusable chat instructions across personal and workspace scopes. [#<PR>](https://github.com/sourcebot-dev/sourcebot/pull/<PR>) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@CHANGELOG.md` at line 23, The Unreleased changelog item for the Ask Sourcebot
skills entry is missing the trailing GitHub PR link, unlike the other sibling
entries. Update the `CHANGELOG.md` Unreleased entry so the line ends with the
pull request reference in the required `[#<id>](<url>)` format, matching the
existing pattern used by the nearby changelog items.
Source: Path instructions
| // The user who authored the skill. This is audit metadata, separate from | ||
| // the namespace that owns the slash-command slug. | ||
| createdBy User @relation("AgentSkillCreatedBy", fields: [createdById], references: [id], onDelete: Cascade) | ||
| createdById String | ||
|
|
||
| updatedBy User? @relation("AgentSkillUpdatedBy", fields: [updatedById], references: [id], onDelete: SetNull) | ||
| updatedById String? | ||
|
|
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
createdBy cascade delete can silently wipe shared/org skills.
createdBy uses onDelete: Cascade (Line 775) while updatedBy uses onDelete: SetNull (Line 778) for what the comment (Lines 773-774) calls audit metadata, "separate from the namespace that owns the slash-command slug." For PERSONAL skills this is harmless since scopeId == createdById anyway. But for SHARED skills, deleting the creator's User row (e.g., offboarding an employee) cascades to delete the entire org-wide shared skill — along with every other user's AgentSkillAdoption rows for it — even though the skill is meant to persist as an org resource independent of its original author.
This contradicts the stated intent and risks unexpected data loss on a routine admin action (user removal).
🛠️ Suggested fix
- createdBy User `@relation`("AgentSkillCreatedBy", fields: [createdById], references: [id], onDelete: Cascade)
- createdById String
+ createdBy User? `@relation`("AgentSkillCreatedBy", fields: [createdById], references: [id], onDelete: SetNull)
+ createdById String?Note: this requires updating all createdById: userId call sites and Pick<AgentSkill, ... "createdById" ...> mapping code (e.g. toSharedAgentSkillCatalogItem in types.ts) to tolerate null.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // The user who authored the skill. This is audit metadata, separate from | |
| // the namespace that owns the slash-command slug. | |
| createdBy User @relation("AgentSkillCreatedBy", fields: [createdById], references: [id], onDelete: Cascade) | |
| createdById String | |
| updatedBy User? @relation("AgentSkillUpdatedBy", fields: [updatedById], references: [id], onDelete: SetNull) | |
| updatedById String? | |
| // The user who authored the skill. This is audit metadata, separate from | |
| // the namespace that owns the slash-command slug. | |
| createdBy User? `@relation`("AgentSkillCreatedBy", fields: [createdById], references: [id], onDelete: SetNull) | |
| createdById String? | |
| updatedBy User? `@relation`("AgentSkillUpdatedBy", fields: [updatedById], references: [id], onDelete: SetNull) | |
| updatedById String? |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/db/prisma/schema.prisma` around lines 773 - 780, The AgentSkill
audit relation is too destructive: `createdBy` currently cascades on user
deletion, which can delete shared/org skills and their adopters. Update the
`AgentSkill` model’s `createdBy`/`createdById` relation in the Prisma schema to
stop using cascade delete and allow the creator reference to be cleared instead,
matching the intent of audit metadata. Then update any `createdById` callers and
mappings that assume a required user, including `toSharedAgentSkillCatalogItem`
and any `Pick<AgentSkill, ... "createdById" ...>` usages, so they handle a
nullable creator.
There was a problem hiding this comment.
As of current this was actually intentional, but it's unclear if it should be this way or if a shared skill truly becomes an org resource (but then it's weird that the creator is the sole person who can then remove everybodies access to it).
There was a problem hiding this comment.
@jsourcebot Thanks for the context — that's a fair tension to flag. A few ways I'd think about resolving the ambiguity, depending on which mental model you want for SHARED skills:
- Skill is an org resource, ownership decoupled from a single user — switch
createdBy/createdByIdtoSetNull(matchingupdatedBy), and introduce an explicit notion of "current owner/maintainer" that's independent from the original author (e.g., allow any orgOWNERto delete/manage aSHAREDskill, not just the creator). This is the cleanest mental model for "shared skills survive offboarding," but requires updating the delete/update authorization logic (currently gated to the creator inactions.ts) plus makingcreatedByIdnullable everywhere it's consumed (e.g.,toSharedAgentSkillCatalogItem). - Keep cascade, but guard the destructive path — leave the schema as-is, but before deleting a
User, check forSHAREDskills they authored and either block the deletion or prompt to reassign/transfer ownership first. This avoids a schema/type migration but adds friction to user offboarding flows. - Status quo — accept that removing a user prunes their shared skills too, treating "shared" as more like "shared while the author remains a member" rather than a durable org asset. Simplest, but as you noted, has surprising blast radius (also deletes all
AgentSkillAdoptionrows for every other member who adopted it).
Given this is a genuine product/design decision rather than a clear-cut bug, I'll leave it to you and the team to decide the intended semantics. If you'd like, I can open a follow-up issue to track "clarify/rework AgentSkill ownership semantics for org offboarding" so it doesn't get lost — just let me know.
✏️ Learnings added
Learnt from: jsourcebot
Repo: sourcebot-dev/sourcebot PR: 1410
File: packages/db/prisma/schema.prisma:773-780
Timestamp: 2026-07-02T02:23:27.352Z
Learning: In Sourcebot's Prisma schema (packages/db/prisma/schema.prisma), the `AgentSkill.createdBy` relation intentionally uses `onDelete: Cascade` (as of PR `#1410`), meaning deleting a `User` who authored a `SHARED` (org-wide) skill also deletes that skill and all associated `AgentSkillAdoption` rows for every other user who adopted it. The team (jsourcebot) has acknowledged this is a known tradeoff/open design question — it's unclear whether shared skills should become independent org resources decoupled from their creator (which would require reworking ownership/authorization, e.g., allowing any org owner to manage a shared skill) or whether the current author-tied-deletion behavior is intended. Do not flag this as a definitive bug in future reviews unless the schema or stated intent changes; treat it as an open question already known to the maintainers.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
🧠 Learnings used
Learnt from: fatmcgav
Repo: sourcebot-dev/sourcebot PR: 1143
File: packages/db/prisma/schema.prisma:640-660
Timestamp: 2026-04-22T07:09:48.693Z
Learning: In Sourcebot’s Prisma schema, treat junction/relationship tables (e.g., AgentConfigToRepo, AgentConfigToConnection) as single-tenant and do not require a denormalized `orgId` column for tenant isolation. Tenant isolation is enforced by foreign keys/cascades from `AgentConfig` (which contains `orgId`). When reviewing queries/relations involving these junction tables, avoid raising multi-tenant isolation concerns as long as access is gated through `AgentConfig` filtered by `orgId`.
| <TooltipPrimitive.Portal> | ||
| <TooltipPrimitive.Content | ||
| ref={ref} | ||
| sideOffset={sideOffset} | ||
| className={cn( | ||
| "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]", | ||
| className | ||
| )} | ||
| {...props} | ||
| /> | ||
| </TooltipPrimitive.Portal> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Radix UI DismissableLayer treats portaled content outside the layer as outside click
💡 Result:
The Radix UI DismissableLayer detects outside interactions by checking whether the target of a pointer or focus event exists within its DOM tree [1]. When content is portaled (rendered outside the main DOM hierarchy of the component), it may not be recognized as "inside" the layer, leading the component to incorrectly trigger an "outside" click and dismiss itself [2][3]. To resolve this, Radix UI provides the DismissableLayer.Branch component, which marks specific elements or containers as part of the layer's "inside" tree, regardless of their actual DOM location [1]. Wrapping your portaled content in a DismissableLayer.Branch will prevent it from triggering an outside dismissal [1]. Key points regarding this behavior: - Scope: DismissableLayer.Branch works globally across all layers, ensuring that portaled content, nested popovers, or tooltips are correctly identified as part of the interface [1]. - Common Pitfalls: In some cases, version mismatches of the @radix-ui/react-dismissable-layer dependency across different components can cause the layer to fail to recognize other Radix components as "inside," leading to premature dismissal [3]. Ensuring all Radix primitives are using the same version of the underlying layer package can resolve these conflicts [3]. - Event Handling: If your portaled content includes elements that call event.stopPropagation (e.g., specific button interactions), the DismissableLayer may fail to register the interaction, sometimes requiring multiple clicks to dismiss [4]. This occurs because the layer relies on bubbling to track interaction state [4]. - Custom Logic: If you need to programmatically prevent dismissal, you can use the onPointerDownOutside or onInteractOutside callbacks and call event.preventDefault to keep the layer open [1][5][6].
Citations:
- 1: https://radix-ui-primitives.mintlify.app/api/dismissable-layer
- 2: Popover portal with forceMount inside a dialog closes unexpectedly radix-ui/primitives#3612
- 3: [Popover] [Dialog] When nesting
PopoverinDialog, clicks in thePopoverare closing it radix-ui/primitives#2121 - 4: [DismissableLayer] After clicking on element that stops propagation, 2 outside clicks are required to close the layer radix-ui/primitives#2782
- 5: Dialog | Is it possible to disable 'click outside' ? radix-ui/primitives#3011
- 6: Why does the modal dialog allow dismissal by clicking outside by default? radix-ui/primitives#1994
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== tooltip.tsx outline ==\n'
ast-grep outline packages/web/src/components/ui/tooltip.tsx --view expanded || true
printf '\n== searchModeSelector.tsx outline ==\n'
ast-grep outline packages/web/src/components/searchModeSelector.tsx --view expanded || true
printf '\n== tooltip.tsx lines ==\n'
cat -n packages/web/src/components/ui/tooltip.tsx | sed -n '1,220p'
printf '\n== searchModeSelector.tsx lines ==\n'
cat -n packages/web/src/components/searchModeSelector.tsx | sed -n '1,260p'
printf '\n== search for DismissableLayer.Branch / tooltip docs link ==\n'
rg -n "DismissableLayer\.Branch|TooltipContent|Docs|Select\.Content|SelectPrimitive\.Content|overflow-visible|portal" packages/web/src/components packages/web/src -g '!**/*.map'Repository: sourcebot-dev/sourcebot
Length of output: 2202
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== locate search mode selector / Docs link ==\n'
rg -n "SearchModeSelector|search mode|Docs|TooltipContent" packages/web/src -g '!**/*.map'
printf '\n== locate select content usages near tooltip ==\n'
rg -n "Select\.Content|SelectPrimitive\.Content|overflow-visible|relative" packages/web/src -g '!**/*.map'
printf '\n== file list candidates ==\n'
fd -HI ".*search.*selector.*\.(ts|tsx|js|jsx)$|.*selector.*\.(ts|tsx|js|jsx)$" packages/web/srcRepository: sourcebot-dev/sourcebot
Length of output: 49815
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== searchModeSelector.tsx slice ==\n'
cat -n 'packages/web/src/app/(app)/components/searchModeSelector.tsx' | sed -n '1,240p'
printf '\n== select.tsx outline ==\n'
ast-grep outline packages/web/src/components/ui/select.tsx --view expanded || trueRepository: sourcebot-dev/sourcebot
Length of output: 9744
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== select.tsx lines ==\n'
cat -n packages/web/src/components/ui/select.tsx | sed -n '1,220p'Repository: sourcebot-dev/sourcebot
Length of output: 7949
Keep the docs tooltip inside the Select dismissable tree. The portaled TooltipContent puts the nested Link outside SelectContent’s DOM subtree, so clicking “Docs” can close the select before navigation. Keep this tooltip inline here or wrap the portal branch so the click stays inside the select.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/web/src/components/ui/tooltip.tsx` around lines 18 - 28, The
TooltipContent in tooltip.tsx is being portaled, which moves the nested Docs
link outside SelectContent’s dismissable tree and can close the select before
navigation. Update the TooltipPrimitive.Content usage in the tooltip component
so this tooltip renders inline for the Select/docs case, or gate the Portal
wrapper behind a prop/branch that keeps the click target inside the
SelectContent subtree; use TooltipPrimitive.Content and TooltipPrimitive.Portal
as the key symbols to adjust.
| // Collapse whitespace so any skill-authored catalog field (name, argument hint, | ||
| // or description) renders as a single catalog line and cannot break out of the | ||
| // `<agent_skills>` block structure (e.g. an interior newline closing the block | ||
| // early). Every value interpolated into the catalog line must pass through this. | ||
| const sanitizeSkillCatalogText = (value: string): string => | ||
| value.replace(/\s+/g, ' ').trim(); | ||
|
|
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n "export const escapeMarkupMetacharacters" -A5 packages/web/src/lib/utils.tsRepository: sourcebot-dev/sourcebot
Length of output: 161
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== repo matches for escapeMarkupMetacharacters =="
rg -n "escapeMarkupMetacharacters" .
echo
echo "== candidate utils files =="
git ls-files 'packages/web/src/**/utils*' 'packages/web/src/**/lib*' | sed -n '1,120p'
echo
echo "== agent.ts excerpt around the sanitizer =="
sed -n '160,205p' packages/web/src/ee/features/chat/agent.tsRepository: sourcebot-dev/sourcebot
Length of output: 211
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== utils-like exports mentioning escape or markup =="
rg -n "export const .*escape|export function .*escape|escapeMarkup|escapeHtml|escapeXml|escape" packages/web/src/lib packages/web/src -g '!**/*.map' | sed -n '1,200p'
echo
echo "== agent.ts around the sanitizer and prompt assembly =="
sed -n '170,240p' packages/web/src/ee/features/chat/agent.ts
echo
echo "== any agent_skills markup handling in repo =="
rg -n "<agent_skills>|agent_skills" packages/web/src | sed -n '1,200p'Repository: sourcebot-dev/sourcebot
Length of output: 11384
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '930,970p' packages/web/src/ee/features/chat/agent.ts
echo
echo "== skill catalog builders =="
rg -n "sanitizeSkillCatalogText|agent_skills|skill catalog|description" packages/web/src/ee/features/chat/agent.ts -n -A4 -B4Repository: sourcebot-dev/sourcebot
Length of output: 6835
Escape skill metadata before embedding it in <agent_skills>.
Whitespace collapsing still leaves values like </agent_skills> or <system> intact, so a skill name/description can break the block structure and inject new tags. Encode markup-sensitive characters here, or serialize the catalog in a format that can’t be reinterpreted as prompt markup.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/web/src/ee/features/chat/agent.ts` around lines 181 - 187, The
sanitizeSkillCatalogText helper in agent.ts only collapses whitespace, so
markup-like values can still break the <agent_skills> block. Update this helper
(and the call sites that build the catalog line) to escape or encode
prompt-sensitive characters such as <, >, and & before interpolation, or switch
the skill catalog serialization to a safe non-markup format so names, hints, and
descriptions cannot be reinterpreted as tags.
Source: Learnings
| export const getAgentSkillSourceStatus = async ( | ||
| skillId: string, | ||
| ): Promise<{ status: AgentSkillSourceStatus } | ServiceError> => sew(() => | ||
| withAuth(async ({ org, user, prisma }) => { | ||
| const askError = await checkAskEntitlement(); | ||
| if (askError) { | ||
| return askError; | ||
| } | ||
|
|
||
| const skill = await prisma.agentSkill.findFirst({ | ||
| where: { | ||
| id: skillId, | ||
| OR: [ | ||
| personalAgentSkillAuthScope(user.id, org.id), | ||
| { ...sharedAgentSkillAuthScope(org.id), enabled: true }, | ||
| ], | ||
| }, | ||
| select: { | ||
| sourceRepoName: true, | ||
| sourceFilePath: true, | ||
| sourceRevision: true, | ||
| sourceBlobSha: true, | ||
| }, | ||
| }); | ||
|
|
||
| if (!skill) { | ||
| return skillNotFound(); | ||
| } | ||
|
|
||
| return resolveSourceStatus(skill, { org, prisma }); | ||
| })); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Sync-status lookup isn't scoped to visible shared skills.
getAgentSkillSourceStatus allows any authenticated org member to query the freshness status of any enabled shared skill (sharedAgentSkillAuthScope(org.id) + enabled: true), regardless of whether that skill is auto-enrolled, adopted by the caller, or passes canAccessSkillSource (unlike getSharedAgentSkill/adoptSharedSkill, which both gate on canAccessSkillSource). While no skill content is returned, a caller who somehow obtains a skillId not otherwise visible to them (e.g. a manager-only shared skill they aren't subscribed to) can still probe its sync status. Consider reusing canAccessSkillSource/sharedAgentSkillVisibleToUserWhere here for consistency with the other read paths.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/web/src/ee/features/chat/skills/actions.ts` around lines 579 - 609,
The shared-skill lookup in getAgentSkillSourceStatus is too permissive because
it uses sharedAgentSkillAuthScope(org.id) with enabled: true instead of the same
visibility checks used by getSharedAgentSkill and adoptSharedSkill. Update the
Prisma findFirst filter to scope shared skills through canAccessSkillSource or
sharedAgentSkillVisibleToUserWhere so only skills visible to the caller can have
their sync status queried, while keeping the personalAgentSkillAuthScope path
unchanged.
| // Symmetric observability with auto-invocation (load_skill). Only fires | ||
| // for newly-materialized commands — already-expanded ones are filtered | ||
| // out above — so it counts each manual invocation exactly once. | ||
| if (skill) { | ||
| void captureEvent('ask_skill_invoked', buildAskSkillInvokedEvent({ | ||
| activationMethod: 'manual', | ||
| skillId: command.commandId, | ||
| success: true, | ||
| slug: command.slug, | ||
| name: command.name, | ||
| sourceLabel: command.sourceLabel, | ||
| })); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm whether buildAskSkillInvokedEvent already injects a source field.
rg -n -A 20 'buildAskSkillInvokedEvent' packages/web/src/ee/features/chat/skills/skillAnalytics.tsRepository: sourcebot-dev/sourcebot
Length of output: 810
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== skillAnalytics builder ==\n'
sed -n '1,140p' packages/web/src/ee/features/chat/skills/skillAnalytics.ts
printf '\n== commandResolution materializeCommandMessageTexts ==\n'
rg -n -A 80 -B 20 'materializeCommandMessageTexts|ask_skill_invoked|buildAskSkillInvokedEvent' packages/web/src/ee/features/chat/skills/commandResolution.ts
printf '\n== route header capture / threading ==\n'
rg -n -A 40 -B 20 'X-Sourcebot-Client-Source|ask_message_sent|source:' packages/web/src/ee/features/chat -g 'route.ts' -g '*.ts'Repository: sourcebot-dev/sourcebot
Length of output: 50380
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== materializeCommandMessageTexts call sites ==\n'
rg -n -A 4 -B 4 'materializeCommandMessageTexts\(' packages/web/src
printf '\n== ask_skill_invoked event type ==\n'
rg -n -A 40 -B 10 "ask_skill_invoked" packages/web/src/lib/posthogEvents.ts packages/web/src -g 'posthogEvents.ts' -g '*.ts'
printf '\n== ask_message_sent capture and route source header ==\n'
rg -n -A 30 -B 20 'ask_message_sent|X-Sourcebot-Client-Source|sourcebot-web-client|sourcebot-mcp-server|sourcebot-ask-agent' packages/web/src/ee/features/chat packages/web/src/app -g '*.ts' -g '*.tsx'Repository: sourcebot-dev/sourcebot
Length of output: 50380
Thread source through manual skill invocation events. buildAskSkillInvokedEvent already fills the field, but this path always falls back to sourcebot-ask-agent, so web/MCP/API-originated invocations get the same attribution. Pass the request source from route.ts into materializeCommandMessageTexts and on to buildAskSkillInvokedEvent.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/web/src/ee/features/chat/skills/commandResolution.ts` around lines
292 - 304, The manual skill invocation event path in commandResolution.ts is not
preserving the request source, so all events fall back to the default
attribution. Thread the source value from route.ts through
materializeCommandMessageTexts into buildAskSkillInvokedEvent, and use that
value when emitting ask_skill_invoked for the manual invocation branch so
web/MCP/API-originated calls are attributed correctly.
Source: Coding guidelines
| function SkillEditor({ skill }: PersonalSkillEditorPageProps) { | ||
| const router = useRouter(); | ||
| const { toast } = useToast(); | ||
| const markdownFileInputRef = useRef<HTMLInputElement>(null); | ||
| const initialForm: AgentSkillInput = skill | ||
| ? { | ||
| name: skill.name, | ||
| slug: skill.slug, | ||
| description: skill.description, | ||
| instructions: skill.instructions, | ||
| } | ||
| : emptySkillForm; | ||
| const [form, setForm] = useState<AgentSkillInput>(initialForm); | ||
| const [isSlugTouched, setIsSlugTouched] = useState(skill !== null); | ||
| const [isSaving, setIsSaving] = useState(false); | ||
| const [isDetailsCollapsed, setIsDetailsCollapsed] = useState(false); | ||
| const [instructionsEditorKey, setInstructionsEditorKey] = useState(0); | ||
| const [publishToShared, setPublishToShared] = useState(false); | ||
| const isEditing = skill !== null; | ||
| const isEditingSharedSkill = skill?.scope === "SHARED"; | ||
| const saveMode: SaveMode = isEditing | ||
| ? isEditingSharedSkill ? "editShared" : "editPersonal" | ||
| : publishToShared ? "createShared" : "createPersonal"; | ||
| const saveConfig = saveModeConfig[saveMode]; | ||
| const SaveIcon = saveConfig.icon; | ||
|
|
||
| const isDirty = | ||
| form.name !== initialForm.name || | ||
| form.slug !== initialForm.slug || | ||
| form.description !== initialForm.description || | ||
| form.instructions !== initialForm.instructions || | ||
| (!isEditing && publishToShared); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether AgentSkillListItem exposes source/provenance info, and whether
# personalSkillEditorPage.tsx or its callers ever check it.
rg -n "AgentSkillListItem" packages/web/src/ee/features/chat/skills/types.ts -A 15
rg -n "source" packages/web/src/ee/features/chat/skills/components/personalSkillEditorPage.tsxRepository: sourcebot-dev/sourcebot
Length of output: 2386
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the editor, the update action, and the list item type shape.
ast-grep outline packages/web/src/ee/features/chat/skills/components/personalSkillEditorPage.tsx --view expanded
printf '\n--- actions.ts outline ---\n'
ast-grep outline packages/web/src/ee/features/chat/skills/actions.ts --view expanded
printf '\n--- type definition around AgentSkillListItem ---\n'
sed -n '1,120p' packages/web/src/ee/features/chat/skills/types.ts
printf '\n--- updatePersonalAgentSkill and nearby logic ---\n'
sed -n '1,260p' packages/web/src/ee/features/chat/skills/actions.tsRepository: sourcebot-dev/sourcebot
Length of output: 16385
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Look for any UI handling of source/provenance in the skill editor page or nearby components.
rg -n "sourceRepoName|sourceFilePath|sourceRevision|source:" packages/web/src/ee/features/chat/skills -g '!**/node_modules/**'Repository: sourcebot-dev/sourcebot
Length of output: 27438
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find the exact save/update flow used by the editor and whether it passes description/instructions for existing skills.
rg -n "updatePersonalAgentSkill|editShared|editPersonal|createShared|createPersonal|description|instructions" packages/web/src/ee/features/chat/skills/components/personalSkillEditorPage.tsx packages/web/src/ee/features/chat/skills/actions.ts -A 8 -B 8Repository: sourcebot-dev/sourcebot
Length of output: 50379
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Confirm whether the editor receives AgentSkillListItem or a narrower type from its caller.
rg -n "personalSkillEditorPage|SkillEditor|AgentSkillListItem|PersonalSkillEditorPageProps" packages/web/src/ee/features/chat/skills -A 6 -B 6Repository: sourcebot-dev/sourcebot
Length of output: 50379
Make synced skill description/instructions read-only.
SkillEditor already receives skill.source, but both editor flows still let users change description/instructions even though synced-skill updates ignore those fields. Disable those inputs or show a synced-from-repo notice so the UI matches what can actually be saved.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@packages/web/src/ee/features/chat/skills/components/personalSkillEditorPage.tsx`
around lines 82 - 113, `SkillEditor` still allows editing description and
instructions for synced skills even though those fields are ignored on save.
Update the `personalSkillEditorPage.tsx` flow in `SkillEditor` to detect synced
skills from `skill.source` (or equivalent existing skill metadata) and make the
description/instructions inputs read-only/disabled, or replace them with a
synced-from-repo notice. Keep the existing create/edit logic intact, but ensure
the UI reflects that only supported fields are editable for synced skills.
| const handleMakePersonal = async (skill: DetailSkill) => { | ||
| setScopePendingId(skill.id); | ||
| try { | ||
| const result = await makeSharedAgentSkillPersonal(skill.id); | ||
| if (isServiceError(result)) { | ||
| toast({ title: "Error", description: result.message, variant: "destructive" }); | ||
| return; | ||
| } | ||
| setPersonalSkills((current) => sortAgentSkillListItems([ | ||
| result, | ||
| ...current.filter((item) => item.id !== result.id), | ||
| ])); | ||
| if (skill.isCreatedByUser) { | ||
| setSharedSkills((current) => current.filter((item) => item.id !== skill.id)); | ||
| } else { | ||
| setSharedSkills((current) => sortSharedAgentSkillCatalogItems(current.map((item) => | ||
| item.id === skill.id | ||
| ? { ...item, isAdopted: false, isRemoved: item.autoEnrolled, isVisibleToUser: false } | ||
| : item, | ||
| ))); | ||
| } | ||
| setConfirmMakePersonal(null); | ||
| setSelectedId(result.id); | ||
| toast({ description: "Skill made personal." }); | ||
| } catch { | ||
| toast({ title: "Error", description: "Failed to make skill personal.", variant: "destructive" }); | ||
| } finally { | ||
| setScopePendingId(null); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Stale shared-skill entry left in state when a non-creator owner "makes it personal".
handleMakePersonal's creator branch removes the item from sharedSkills, but the non-creator (owner) branch only flips flags and keeps it in the array. Since filteredShared/the master list render every item regardless of isVisibleToUser, the skill remains visible in the "Shared" section even though the confirmation dialog states it "removes the /{slug} command from the shared catalog for everyone." This produces a stale, ghost list row after a destructive server-side operation.
🐛 Proposed fix
- if (skill.isCreatedByUser) {
- setSharedSkills((current) => current.filter((item) => item.id !== skill.id));
- } else {
- setSharedSkills((current) => sortSharedAgentSkillCatalogItems(current.map((item) =>
- item.id === skill.id
- ? { ...item, isAdopted: false, isRemoved: item.autoEnrolled, isVisibleToUser: false }
- : item,
- )));
- }
+ setSharedSkills((current) => current.filter((item) => item.id !== skill.id));Also applies to: 963-964
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/web/src/ee/features/chat/skills/components/skillsPage.tsx` around
lines 588 - 617, The shared-skill state update in handleMakePersonal leaves a
stale entry behind for the non-creator owner path. Update the make-personal flow
so that, after a successful makeSharedAgentSkillPersonal call, the skill is
removed from sharedSkills in both branches rather than only toggling flags; use
the existing handleMakePersonal logic and the sharedSkills/setSharedSkills
updates to locate it. Keep the personalSkills update and selection/toast
behavior unchanged, but ensure the shared catalog no longer renders the
converted skill after the destructive operation.
| <Switch | ||
| checked={skill.autoEnrolled} | ||
| disabled={isFlagPending} | ||
| onCheckedChange={(checked) => void handleFlagChange(skill, "autoEnrolled", checked)} | ||
| aria-label="Auto" | ||
| className={cn( | ||
| "data-[state=unchecked]:bg-muted-foreground/40 data-[state=unchecked]:border-muted-foreground/70", | ||
| "data-[state=checked]:bg-green-600 data-[state=checked]:border-green-600", | ||
| "[&>span]:bg-foreground", | ||
| )} | ||
| /> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Switch aria-label is identical across all skill rows.
Every row's Switch uses the static aria-label="Auto", so screen readers announce the same accessible name for every skill's auto-enroll toggle, making rows indistinguishable to assistive tech. The adjacent delete button already differentiates via aria-label={Delete ${skill.name}} — apply the same pattern here.
♿ Proposed fix
<Switch
checked={skill.autoEnrolled}
disabled={isFlagPending}
onCheckedChange={(checked) => void handleFlagChange(skill, "autoEnrolled", checked)}
- aria-label="Auto"
+ aria-label={`Auto-enroll ${skill.name}`}
className={cn(📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Switch | |
| checked={skill.autoEnrolled} | |
| disabled={isFlagPending} | |
| onCheckedChange={(checked) => void handleFlagChange(skill, "autoEnrolled", checked)} | |
| aria-label="Auto" | |
| className={cn( | |
| "data-[state=unchecked]:bg-muted-foreground/40 data-[state=unchecked]:border-muted-foreground/70", | |
| "data-[state=checked]:bg-green-600 data-[state=checked]:border-green-600", | |
| "[&>span]:bg-foreground", | |
| )} | |
| /> | |
| <Switch | |
| checked={skill.autoEnrolled} | |
| disabled={isFlagPending} | |
| onCheckedChange={(checked) => void handleFlagChange(skill, "autoEnrolled", checked)} | |
| aria-label={`Auto-enroll ${skill.name}`} | |
| className={cn( | |
| "data-[state=unchecked]:bg-muted-foreground/40 data-[state=unchecked]:border-muted-foreground/70", | |
| "data-[state=checked]:bg-green-600 data-[state=checked]:border-green-600", | |
| "[&>span]:bg-foreground", | |
| )} | |
| /> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@packages/web/src/ee/features/chat/skills/components/workspaceSharedSkillsManager.tsx`
around lines 228 - 238, The Switch in workspaceSharedSkillsManager currently
uses the same static aria-label for every row, so each auto-enroll toggle is
indistinguishable to screen readers. Update the Switch’s aria-label in the skill
row render to include the current skill name, following the same pattern used by
the delete button, and keep the change localized around the `Switch` inside the
workspaceSharedSkillsManager component.
| // Suppress the guard for a deliberate programmatic navigation (e.g. a | ||
| // save-then-redirect, where the form is still "dirty" versus its initial | ||
| // values at the moment we navigate away). | ||
| bypass: () => void; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
bypass() permanently disables the guard — bypassRef is never reset.
bypass() sets bypassRef.current = true (Line 102) but nothing ever sets it back to false. Since the enabled callback short-circuits to false whenever bypassRef.current is truthy (Lines 44-46), calling bypass() once permanently disables navigation guarding for the remaining lifetime of the component instance — including for future, unrelated unsaved-changes states — rather than suppressing just the single deliberate navigation described in the docstring ("Suppress the guard for a deliberate programmatic navigation").
This only "self-heals" if the component unmounts right after the bypassed navigation (fresh useRef on remount). If the hook's owning component persists across the programmatic navigation (e.g. same layout/component tree reused across routes), later edits would no longer be protected.
🛡️ Proposed fix: make bypass single-use
const navGuard = useNavigationGuard({
enabled: ({ type }) => {
if (bypassRef.current) {
+ bypassRef.current = false;
return false;
}
// A "refresh" here means a client-side `router.refresh()` (not the
// user pressing the refresh key, which is a "beforeunload"); those
// don't lose unsaved changes. Full-page unloads are handled by the
// native `beforeunload` listener below, so neither goes through the
// in-app guard.
if (type === 'refresh' || type === 'beforeunload') {
return false;
}
return enabled;
},
...(confirm ? { confirm } : {}),
});Also applies to: 40-58, 98-104
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/web/src/ee/features/chat/useUnsavedChangesGuard.ts` around lines 22
- 26, The bypass in useUnsavedChangesGuard is effectively permanent because
bypassRef.current is set true in bypass() and never cleared, which disables the
enabled() guard for all future unsaved changes. Update useUnsavedChangesGuard so
bypass() only suppresses the next navigation attempt, then resets
bypassRef.current back to false as part of the guard flow (for example, when
enabled() observes and consumes the bypass flag). Make sure the logic around
enabled, bypassRef, and bypass preserves normal guarding after the deliberate
programmatic navigation.
There was a problem hiding this comment.
could we compress into a single migration?
| // (activationMethod: 'auto', via the load_skill tool) or manually by the | ||
| // user (activationMethod: 'manual', via a slash command). Multi-source, so | ||
| // no wa_ prefix; source identifies the origin. | ||
| ask_skill_invoked: { |
There was a problem hiding this comment.
are we sending posthog events for other instances (ex. create skill, edit skill, share skill, etc) @jsourcebot ? we should add events for every interaction



Added Ask Sourcebot skills, letting users create, import, share, sync, and auto-invoke reusable chat instructions across personal and workspace scopes.
Summary by CodeRabbit
New Features
Bug Fixes